# Policyengine Period Patterns

> PolicyEngine period handling - converting between YEAR, MONTH definition periods and testing patterns

- Skill: `majiayu000/policyengine-period-patterns` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/policyengine-period-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/policyengine-period-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/policyengine-period-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
```python
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.**

```python
# ✅ 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

```python
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

```python
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:

```python
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**
```python
# ❌ 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)**
```python
# ❌ 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)**
```python
# ❌ 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**
```python
# ❌ 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

```python
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

```python
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

```python
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
```python
def formula(entity, period, parameters):
    # Parameters use current period
    p = parameters(period).gov.program.benefit
    return p.amount
```

### Specific Date Access
```python
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**
```yaml
- 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**
```yaml
- 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**
```yaml
- 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
```python
# 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
```python
# 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
```yaml
# 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
```python
# 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

```python
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

---

## Related Skills

- **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
