| name | policyengine-period-patterns |
| description | 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
class monthly_benefit(Variable):
definition_period = MONTH
class is_head(Variable):
definition_period = ETERNITY
2. The Golden Rule
When accessing a variable with a different definition period than your formula, you must specify the target period explicitly.
def formula(person, period, parameters):
age = person("age", period.this_year)
def formula(person, period, parameters):
age = person("age", period)
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
def formula(person, period, parameters):
age = person("age", period.this_year)
is_pregnant = person("is_pregnant", 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
def formula(spm_unit, period, parameters):
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):
monthly_income = person("employment_income", period)
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
age = person("age", period)
age = person("age", period.this_year)
2. Assets/Resources (Stocks)
assets = spm_unit("spm_unit_assets", period)
assets = spm_unit("spm_unit_assets", period.this_year)
3. Counts (Household Size, Number of Children)
size = spm_unit("household_size", period)
size = spm_unit("household_size", period.this_year)
4. Boolean/Enum Variables
status = person("is_disabled", period)
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 = person("age", period.this_year)
assets = person("assets", period.this_year)
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
Pattern 3: Converting Annual to Monthly
class monthly_income_limit(Variable):
definition_period = MONTH
def formula(household, period, parameters):
annual_limit = parameters(period).gov.program.annual_limit
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):
year = period.start.year
month = period.start.month
if month >= 10:
instant_str = f"{year}-10-01"
else:
instant_str = f"{year - 1}-10-01"
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):
p = parameters(period).gov.program.benefit
return p.amount
Specific Date Access
def formula(entity, period, parameters):
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
input:
people:
person1:
employment_income: 12_000
output:
employment_income: 1_000
Example 2: Mixed Variables
- name: Eligibility with age and income
period: 2024-01
input:
age: 30
employment_income: 24_000
output:
age: 30
employment_income: 2_000
monthly_eligible: true
Example 3: YEAR Period Test
- name: Annual calculation
period: 2024
input:
employment_income: 18_000
output:
employment_income: 18_000
annual_tax: 2_000
Testing Best Practices
- Always specify period explicitly
- Input YEAR variables as annual amounts
- Expect monthly output for YEAR variables in MONTH tests
- Use underscore separators:
12_000 not 12000
- Add calculation comments in integration tests
7. Common Mistakes and Solutions
❌ Mistake 1: Not Using period.this_year
def formula(person, period, parameters):
age = person("age", period)
def formula(person, period, parameters):
age = person("age", period.this_year)
❌ Mistake 2: Redundant period.this_year / MONTHS_IN_YEAR for Flow Variables
fpg = spm_unit("spm_unit_fpg", period.this_year) / MONTHS_IN_YEAR
fpg = spm_unit("spm_unit_fpg", period)
This applies to all YEAR-defined flow variables (income, FPG, dollar amounts). Using period from a MONTH formula auto-divides by 12. Using period.this_year / MONTHS_IN_YEAR is equivalent but redundant — just use period.
❌ Mistake 3: Mixing annual and monthly
monthly_income = person("monthly_income", period)
annual_limit = parameters(period).gov.limit
if monthly_income < annual_limit:
monthly_income = person("monthly_income", period)
annual_limit = parameters(period).gov.limit
monthly_limit = annual_limit / MONTHS_IN_YEAR
if monthly_income < monthly_limit:
❌ Mistake 4: Wrong test expectations
period: 2024-01
input:
employment_income: 12_000
output:
employment_income: 12_000
period: 2024-01
input:
employment_income: 12_000
output:
employment_income: 1_000
8. Real-World Example
class tanf_income_eligible(Variable):
value_type = bool
entity = SPMUnit
definition_period = MONTH
def formula(spm_unit, period, parameters):
household_size = spm_unit("spm_unit_size", period.this_year)
state = spm_unit.household("state_code", period.this_year)
gross_income = spm_unit("tanf_gross_income", period)
p = parameters(period).gov.states[state].tanf
annual_limit = p.income_limit[household_size]
monthly_limit = annual_limit / MONTHS_IN_YEAR
return gross_income <= monthly_limit