Files
2025-11-30 08:47:54 +08:00

13 KiB

name, description
name description
policyengine-period-patterns PolicyEngine period handling - converting between YEAR, MONTH definition periods and testing patterns

PolicyEngine Period Patterns

Essential patterns for handling different definition periods (YEAR, MONTH) in PolicyEngine.

Quick Reference

From To Method Example
MONTH formula YEAR variable period.this_year age = person("age", period.this_year)
YEAR formula MONTH variable period.first_month person("monthly_rent", period.first_month)
Any Year integer period.start.year year = period.start.year
Any Month integer period.start.month month = period.start.month
Annual → Monthly Divide by 12 / MONTHS_IN_YEAR monthly = annual / 12
Monthly → Annual Multiply by 12 * MONTHS_IN_YEAR annual = monthly * 12

1. Definition Periods in PolicyEngine US

Available Periods

  • YEAR: Annual values (most common - 2,883 variables)
  • MONTH: Monthly values (395 variables)
  • ETERNITY: Never changes (1 variable - structural relationships)

Note: QUARTER is NOT used in PolicyEngine US

Examples

from policyengine_us.model_api import *

class annual_income(Variable):
    definition_period = YEAR  # Annual amount

class monthly_benefit(Variable):
    definition_period = MONTH  # Monthly amount

class is_head(Variable):
    definition_period = ETERNITY  # Never changes

2. The Golden Rule

When accessing a variable with a different definition period than your formula, you must specify the target period explicitly.

# ✅ CORRECT - MONTH formula accessing YEAR variable
def formula(person, period, parameters):
    age = person("age", period.this_year)  # Gets actual age

# ❌ WRONG - Would get age/12
def formula(person, period, parameters):
    age = person("age", period)  # BAD: gives age divided by 12!

3. Common Patterns

Pattern 1: MONTH Formula Accessing YEAR Variable

Use Case: Monthly benefits need annual demographic data

class monthly_benefit_eligible(Variable):
    value_type = bool
    entity = Person
    definition_period = MONTH  # Monthly eligibility

    def formula(person, period, parameters):
        # Age is YEAR-defined, use period.this_year
        age = person("age", period.this_year)  # ✅ Gets full age

        # is_pregnant is MONTH-defined, just use period
        is_pregnant = person("is_pregnant", period)  # ✅ Same period

        return (age < 18) | is_pregnant

Pattern 2: Accessing Stock Variables (Assets)

Stock variables (point-in-time values like assets) are typically YEAR-defined

class tanf_countable_resources(Variable):
    value_type = float
    entity = SPMUnit
    definition_period = MONTH  # Monthly check

    def formula(spm_unit, period, parameters):
        # Assets are stocks (YEAR-defined)
        cash = spm_unit("cash_assets", period.this_year)  # ✅
        vehicles = spm_unit("vehicles_value", period.this_year)  # ✅

        p = parameters(period).gov.tanf.resources
        return cash + max_(0, vehicles - p.vehicle_exemption)

4. Understanding Auto-Conversion: When to Use period vs period.this_year

The Key Question

When accessing a YEAR variable from a MONTH formula, should the value be divided by 12?

  • If YES → Use period (let auto-conversion happen)
  • If NO → Use period.this_year (prevent auto-conversion)

When Auto-Conversion Makes Sense (Use period)

Flow variables where you want the monthly portion:

class monthly_benefit(Variable):
    definition_period = MONTH

    def formula(person, period, parameters):
        # ✅ Use period - want $2,000/month from $24,000/year
        monthly_income = person("employment_income", period)

        # Compare to monthly threshold
        p = parameters(period).gov.program
        return monthly_income < p.monthly_threshold

Why: If annual income is $24,000, you want $2,000/month for monthly eligibility checks.

When Auto-Conversion Breaks Things (Use period.this_year)

Stock variables and counts where division by 12 is nonsensical:

1. Age

# ❌ WRONG - gives age/12
age = person("age", period)  # 30 years → 2.5 "monthly age" ???

# ✅ CORRECT - gives actual age
age = person("age", period.this_year)  # 30 years

2. Assets/Resources (Stocks)

# ❌ WRONG - gives assets/12
assets = spm_unit("spm_unit_assets", period)  # $12,000 → $1,000 ???

# ✅ CORRECT - gives point-in-time value
assets = spm_unit("spm_unit_assets", period.this_year)  # $12,000

3. Counts (Household Size, Number of Children)

# ❌ WRONG - gives count/12
size = spm_unit("household_size", period)  # 4 people → 0.33 people ???

# ✅ CORRECT - gives actual count
size = spm_unit("household_size", period.this_year)  # 4 people

4. Boolean/Enum Variables

# ❌ WRONG - weird fractional conversion
status = person("is_disabled", period)

# ✅ CORRECT - actual status
status = person("is_disabled", period.this_year)

Decision Tree

Accessing YEAR variable from MONTH formula?
│
├─ Is it an INCOME or FLOW variable?
│  └─ YES → Use period (auto-convert to monthly) ✅
│           Example: employment_income, self_employment_income
│
└─ Is it AGE, ASSET, COUNT, or BOOLEAN?
   └─ YES → Use period.this_year (prevent conversion) ✅
            Examples: age, assets, household_size, is_disabled

Complete Example

class monthly_tanf_eligible(Variable):
    value_type = bool
    entity = Person
    definition_period = MONTH

    def formula(person, period, parameters):
        # Age: Use period.this_year (don't want age/12)
        age = person("age", period.this_year)  # ✅

        # Assets: Use period.this_year (don't want assets/12)
        assets = person("assets", period.this_year)  # ✅

        # Income: Use period (DO want monthly income from annual)
        monthly_income = person("employment_income", period)  # ✅

        p = parameters(period).gov.tanf.eligibility

        age_eligible = (age >= 18) & (age <= 64)
        asset_eligible = assets <= p.asset_limit
        income_eligible = monthly_income <= p.monthly_income_limit

        return age_eligible & asset_eligible & income_eligible

Quick Reference for Auto-Conversion

Variable Type Use period Use period.this_year Why
Income (flow) Want monthly portion
Age Age/12 is meaningless
Assets/Resources (stock) Point-in-time value
Household size/counts Can't divide people
Boolean/status flags True/12 is nonsense
Demographic attributes Properties don't divide

Rule of thumb: If dividing by 12 makes the value meaningless → use period.this_year

Pattern 3: Converting Annual to Monthly

class monthly_income_limit(Variable):
    definition_period = MONTH

    def formula(household, period, parameters):
        # Get annual parameter
        annual_limit = parameters(period).gov.program.annual_limit

        # Convert to monthly
        monthly_limit = annual_limit / MONTHS_IN_YEAR  # ✅

        return monthly_limit

Pattern 4: Getting Period Components

class federal_poverty_guideline(Variable):
    definition_period = MONTH

    def formula(entity, period, parameters):
        # Get year and month as integers
        year = period.start.year  # e.g., 2024
        month = period.start.month  # e.g., 1-12

        # FPG updates October 1st
        if month >= 10:
            instant_str = f"{year}-10-01"
        else:
            instant_str = f"{year - 1}-10-01"

        # Access parameters at specific date
        p_fpg = parameters(instant_str).gov.hhs.fpg
        return p_fpg.first_person / MONTHS_IN_YEAR

5. Parameter Access

Standard Access

def formula(entity, period, parameters):
    # Parameters use current period
    p = parameters(period).gov.program.benefit
    return p.amount

Specific Date Access

def formula(entity, period, parameters):
    # Access parameters at specific instant
    p = parameters("2024-10-01").gov.hhs.fpg
    return p.amount

Important: Never use parameters(period.this_year) - parameters always use the formula's period


6. Testing with Different Periods

Critical Testing Rules

For MONTH period tests (period: 2025-01):

  • Input YEAR variables as annual amounts
  • Output YEAR variables show monthly values (÷12)

Test Examples

Example 1: Basic MONTH Test

- name: Monthly income test
  period: 2025-01  # MONTH period
  input:
    people:
      person1:
        employment_income: 12_000  # Input: Annual
  output:
    employment_income: 1_000  # Output: Monthly (12_000/12)

Example 2: Mixed Variables

- name: Eligibility with age and income
  period: 2024-01  # MONTH period
  input:
    age: 30  # Age doesn't convert
    employment_income: 24_000  # Annual input
  output:
    age: 30  # Age stays same
    employment_income: 2_000  # Monthly output
    monthly_eligible: true

Example 3: YEAR Period Test

- name: Annual calculation
  period: 2024  # YEAR period
  input:
    employment_income: 18_000  # Annual
  output:
    employment_income: 18_000  # Annual output
    annual_tax: 2_000

Testing Best Practices

  1. Always specify period explicitly
  2. Input YEAR variables as annual amounts
  3. Expect monthly output for YEAR variables in MONTH tests
  4. Use underscore separators: 12_000 not 12000
  5. Add calculation comments in integration tests

7. Common Mistakes and Solutions

Mistake 1: Not Using period.this_year

# WRONG - From MONTH formula
def formula(person, period, parameters):
    age = person("age", period)  # Gets age/12!

# CORRECT
def formula(person, period, parameters):
    age = person("age", period.this_year)  # Gets actual age

Mistake 2: Mixing Annual and Monthly

# WRONG - Comparing different units
monthly_income = person("monthly_income", period)
annual_limit = parameters(period).gov.limit
if monthly_income < annual_limit:  # BAD comparison

# CORRECT - Convert to same units
monthly_income = person("monthly_income", period)
annual_limit = parameters(period).gov.limit
monthly_limit = annual_limit / MONTHS_IN_YEAR
if monthly_income < monthly_limit:  # Good comparison

Mistake 3: Wrong Test Expectations

# WRONG - Expecting annual in MONTH test
period: 2024-01
input:
  employment_income: 12_000
output:
  employment_income: 12_000  # Wrong!

# CORRECT
period: 2024-01
input:
  employment_income: 12_000  # Annual input
output:
  employment_income: 1_000  # Monthly output

8. Quick Patterns Cheat Sheet

Accessing Variables

Your Formula Target Variable Use
MONTH YEAR period.this_year
YEAR MONTH period.first_month
Any ETERNITY period

Common Variables That Need period.this_year

  • age
  • household_size, spm_unit_size
  • cash_assets, vehicles_value
  • state_name, state_code
  • Any demographic variable

Period Conversion

# Annual to monthly
monthly = annual / MONTHS_IN_YEAR

# Monthly to annual
annual = monthly * MONTHS_IN_YEAR

# Get year/month numbers
year = period.start.year  # 2024
month = period.start.month  # 1-12

9. Real-World Example

class tanf_income_eligible(Variable):
    value_type = bool
    entity = SPMUnit
    definition_period = MONTH  # Monthly eligibility

    def formula(spm_unit, period, parameters):
        # YEAR variables need period.this_year
        household_size = spm_unit("spm_unit_size", period.this_year)
        state = spm_unit.household("state_code", period.this_year)

        # MONTH variables use period
        gross_income = spm_unit("tanf_gross_income", period)

        # Parameters use period
        p = parameters(period).gov.states[state].tanf

        # Convert annual limit to monthly
        annual_limit = p.income_limit[household_size]
        monthly_limit = annual_limit / MONTHS_IN_YEAR

        return gross_income <= monthly_limit

10. Checklist for Period Handling

When writing a formula:

  • Identify your formula's definition_period
  • Check definition_period of accessed variables
  • Use period.this_year for YEAR variables from MONTH formulas
  • Use period for parameters (not period.this_year)
  • Convert units when comparing (annual ↔ monthly)
  • Test with appropriate period values

  • policyengine-aggregation-skill: For summing across entities with period handling
  • policyengine-core-skill: For understanding variable and parameter systems

For Agents

  1. Always check definition_period before accessing variables
  2. Default to period.this_year for demographic/stock variables from MONTH formulas
  3. Test thoroughly - period mismatches cause subtle bugs
  4. Document period conversions in comments
  5. Follow existing patterns in similar variables