From e70c76cbae6c4375c76ab4cafd5e6b6809399750 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Tue, 6 Oct 2026 16:23:46 -0400 Subject: [PATCH 1/3] Make only an infant's own mother WIC postpartum or breastfeeding wic_category made every woman with her own child in the household postpartum when the family had an infant, so a grandmother living with her daughter's baby was postpartum too. 7 CFR 246.2 defines postpartum and breastfeeding women by the time since their own pregnancy, so each infant can make only one woman postpartum or breastfeeding. Without parent-child links, match each infant to one mother in its tax unit (breastfeeding mothers first, then the youngest), and an infant in a tax unit without a mother to one of the family's remaining mothers. Fixes #9874. Co-Authored-By: Claude Opus 5.5 --- changelog.d/wic-postpartum.fixed.md | 1 + .../baseline/gov/usda/wic/wic_category.yaml | 186 ++++++++++++++++++ .../variables/gov/usda/wic/wic_category.py | 41 +++- 3 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 changelog.d/wic-postpartum.fixed.md diff --git a/changelog.d/wic-postpartum.fixed.md b/changelog.d/wic-postpartum.fixed.md new file mode 100644 index 00000000000..dbd9e51f96b --- /dev/null +++ b/changelog.d/wic-postpartum.fixed.md @@ -0,0 +1 @@ +Make only one mother per infant WIC postpartum or breastfeeding, so a grandmother living with her grandchild no longer qualifies. diff --git a/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml b/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml index c22fe7aebfa..4d8a902c933 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml @@ -119,3 +119,189 @@ age: 5 output: wic_category: NONE + +# 7 CFR 246.2: postpartum women are "women up to six months after termination +# of pregnancy" and breastfeeding women are "women up to one year postpartum +# who are breastfeeding their infants", so an infant makes only its own mother +# postpartum or breastfeeding, not a grandmother who has her own child at home. +- name: Three generations in separate tax units, only the infant's mother is postpartum + period: 2025 + input: + people: + grandmother: + age: 50 + is_female: true + own_children_in_household: 1 + mother: + age: 25 + is_female: true + own_children_in_household: 1 + infant: + age: 0.2 + tax_units: + grandmother_tax_unit: + members: [grandmother] + mother_tax_unit: + members: [mother, infant] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, POSTPARTUM, INFANT] + +- name: Three generations in one tax unit, only the youngest mother is postpartum + period: 2025 + input: + people: + grandmother: + age: 50 + is_female: true + own_children_in_household: 1 + mother: + age: 25 + is_female: true + own_children_in_household: 1 + infant: + age: 0.2 + tax_units: + tax_unit: + members: [grandmother, mother, infant] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, POSTPARTUM, INFANT] + +- name: Mother living alone with her infant is postpartum + period: 2025 + input: + people: + mother: + age: 25 + is_female: true + own_children_in_household: 1 + infant: + age: 0.2 + tax_units: + tax_unit: + members: [mother, infant] + spm_units: + spm_unit: + members: [mother, infant] + families: + family: + members: [mother, infant] + households: + household: + members: [mother, infant] + state_code: TX + output: + wic_category: [POSTPARTUM, INFANT] + +- name: Infant in a tax unit without a mother is matched to the family's youngest mother + period: 2025 + input: + people: + grandmother: + age: 47 + is_female: true + own_children_in_household: 1 + mother: + age: 22 + is_female: true + own_children_in_household: 1 + infant: + age: 0.2 + tax_units: + adults_tax_unit: + members: [grandmother, mother] + infant_tax_unit: + members: [infant] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, POSTPARTUM, INFANT] + +- name: Breastfeeding mother of an infant aged 0.8 is breastfeeding, the grandmother is not + period: 2025 + input: + people: + grandmother: + age: 50 + is_female: true + own_children_in_household: 1 + mother: + age: 25 + is_female: true + own_children_in_household: 1 + is_breastfeeding: true + infant: + age: 0.8 + tax_units: + tax_unit: + members: [grandmother, mother, infant] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, BREASTFEEDING, INFANT] + +- name: Breastfeeding mother of an infant aged 0.2 is breastfeeding, the grandmother is not postpartum + period: 2025 + input: + people: + grandmother: + age: 50 + is_female: true + own_children_in_household: 1 + mother: + age: 25 + is_female: true + own_children_in_household: 1 + is_breastfeeding: true + infant: + age: 0.2 + tax_units: + tax_unit: + members: [grandmother, mother, infant] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, BREASTFEEDING, INFANT] diff --git a/policyengine_us/variables/gov/usda/wic/wic_category.py b/policyengine_us/variables/gov/usda/wic/wic_category.py index 0953e4ee38d..8de83949546 100644 --- a/policyengine_us/variables/gov/usda/wic/wic_category.py +++ b/policyengine_us/variables/gov/usda/wic/wic_category.py @@ -10,6 +10,15 @@ class WICCategory(Enum): NONE = "None" +def _rank_mothers(person, entity, age, breastfeeding, eligible): + # Rank the eligible mothers in each entity: breastfeeding mothers first, + # then from youngest to oldest. Only use the rank together with eligible. + breastfeeding_rank = person.get_rank(entity, age, eligible & breastfeeding) + other_rank = person.get_rank(entity, age, eligible & ~breastfeeding) + breastfeeding_mothers = entity.sum(eligible & breastfeeding) + return where(breastfeeding, breastfeeding_rank, breastfeeding_mothers + other_rank) + + class wic_category(Variable): value_type = Enum entity = Person @@ -18,20 +27,42 @@ class wic_category(Variable): default_value = WICCategory.NONE documentation = "Demographic category for the Special Supplemental Nutrition Program for Women, Infants and Children (WIC)" label = "WIC demographic category" - reference = "https://www.law.cornell.edu/uscode/text/42/1786#b" + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1786#b", + "https://www.ecfr.gov/current/title-7/part-246/section-246.2", + ) def formula(person, period, parameters): pregnant = person("is_pregnant", period) mother = person("is_mother", period) breastfeeding = person("is_breastfeeding", period) age = person("age", period) - # Categorize mothers based on the minimum age of children in the SPM unit. - min_age_family = person.family.min(age) + tax_unit = person.tax_unit + family = person.family + # 7 CFR 246.2 defines postpartum and breastfeeding women by the time + # since their own pregnancy (up to six months and one year), so an + # infant makes only its mother postpartum or breastfeeding. Without + # parent-child links, match each infant to one mother in its tax unit, + # and an infant in a tax unit without a mother to one of the family's + # remaining mothers, taking breastfeeding mothers first, then the + # youngest. + tax_unit_rank = _rank_mothers(person, tax_unit, age, breastfeeding, mother) + tax_unit_has_mother = tax_unit.any(mother) + + def is_matched_to_infant_under(age_limit): + infant = age < age_limit + in_tax_unit = mother & (tax_unit_rank < tax_unit.sum(infant)) + unmatched_infants = family.sum(infant & ~tax_unit_has_mother) + remaining = mother & ~in_tax_unit + family_rank = _rank_mothers(person, family, age, breastfeeding, remaining) + in_family = remaining & (family_rank < unmatched_infants) + return in_tax_unit | in_family + return select( [ pregnant, - mother & breastfeeding & (min_age_family < 1), - mother & (min_age_family < 0.5), + breastfeeding & is_matched_to_infant_under(1), + is_matched_to_infant_under(0.5), age < 1, age < 5, ], From f27d5c781f1169043ec29ab3566dc9333aac8525 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Tue, 6 Oct 2026 19:35:15 -0400 Subject: [PATCH 2/3] Match breastfeeding women before postpartum women in wic_category A breastfeeding mother of a 6-12 month old took the six-month match of another mother's younger infant and left that mother uncategorized. Match breastfeeding women first, then give the remaining infants under six months to the other mothers. Adds regression tests for that case, two mothers in one tax unit, a father-only household and a 0.6-year-old infant. Co-Authored-By: Claude Opus 5.5 --- .../baseline/gov/usda/wic/wic_category.yaml | 94 +++++++++++++++++++ .../variables/gov/usda/wic/wic_category.py | 47 ++++++++-- 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml b/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml index 4d8a902c933..a8d5007633c 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml @@ -305,3 +305,97 @@ state_code: TX output: wic_category: [NONE, BREASTFEEDING, INFANT] + +- name: A breastfeeding mother of an older infant does not take the postpartum match of another mother's younger infant + period: 2025 + input: + people: + grandmother: + age: 38 + is_female: true + own_children_in_household: 2 + is_breastfeeding: true + mother: + age: 17 + is_female: true + own_children_in_household: 1 + aunt: + age: 0.8 + infant: + age: 0.2 + tax_units: + tax_unit: + members: [grandmother, mother, aunt, infant] + spm_units: + spm_unit: + members: [grandmother, mother, aunt, infant] + families: + family: + members: [grandmother, mother, aunt, infant] + households: + household: + members: [grandmother, mother, aunt, infant] + state_code: TX + output: + wic_category: [BREASTFEEDING, POSTPARTUM, INFANT, INFANT] + +- name: Two mothers with their own infants in one tax unit are both postpartum + period: 2025 + input: + people: + mother_a: + age: 24 + is_female: true + own_children_in_household: 1 + infant_a: + age: 0.3 + mother_b: + age: 21 + is_female: true + own_children_in_household: 1 + infant_b: + age: 0.1 + tax_units: + tax_unit: + members: [mother_a, infant_a, mother_b, infant_b] + spm_units: + spm_unit: + members: [mother_a, infant_a, mother_b, infant_b] + families: + family: + members: [mother_a, infant_a, mother_b, infant_b] + households: + household: + members: [mother_a, infant_a, mother_b, infant_b] + state_code: TX + output: + wic_category: [POSTPARTUM, INFANT, POSTPARTUM, INFANT] + +- name: Father with an infant has no postpartum woman + period: 2025 + input: + people: + father: + age: 30 + own_children_in_household: 1 + infant: + age: 0.2 + output: + wic_category: [NONE, INFANT] + +- name: Mother of an infant aged 0.6 who is not breastfeeding is not postpartum + period: 2025 + input: + people: + grandmother: + age: 50 + is_female: true + own_children_in_household: 1 + mother: + age: 25 + is_female: true + own_children_in_household: 1 + infant: + age: 0.6 + output: + wic_category: [NONE, NONE, INFANT] diff --git a/policyengine_us/variables/gov/usda/wic/wic_category.py b/policyengine_us/variables/gov/usda/wic/wic_category.py index 8de83949546..a37b856b8ca 100644 --- a/policyengine_us/variables/gov/usda/wic/wic_category.py +++ b/policyengine_us/variables/gov/usda/wic/wic_category.py @@ -46,23 +46,50 @@ def formula(person, period, parameters): # and an infant in a tax unit without a mother to one of the family's # remaining mothers, taking breastfeeding mothers first, then the # youngest. - tax_unit_rank = _rank_mothers(person, tax_unit, age, breastfeeding, mother) tax_unit_has_mother = tax_unit.any(mother) + in_motherless_tax_unit = ~tax_unit_has_mother - def is_matched_to_infant_under(age_limit): - infant = age < age_limit - in_tax_unit = mother & (tax_unit_rank < tax_unit.sum(infant)) - unmatched_infants = family.sum(infant & ~tax_unit_has_mother) - remaining = mother & ~in_tax_unit + def match(eligible, tax_unit_infants, family_infants): + tax_unit_rank = _rank_mothers( + person, tax_unit, age, breastfeeding, eligible + ) + in_tax_unit = eligible & (tax_unit_rank < tax_unit_infants) + remaining = eligible & ~in_tax_unit family_rank = _rank_mothers(person, family, age, breastfeeding, remaining) - in_family = remaining & (family_rank < unmatched_infants) - return in_tax_unit | in_family + in_family = remaining & (family_rank < family_infants) + return in_tax_unit, in_family + + infant = age < 1 + young_infant = age < 0.5 + older_infant = infant & ~young_infant + bf_tax_unit, bf_family = match( + mother, + tax_unit.sum(infant), + family.sum(infant & in_motherless_tax_unit), + ) + breastfeeding_woman = breastfeeding & (bf_tax_unit | bf_family) + # Breastfeeding women take the infants aged six months to one year + # first; the remaining infants under six months make one other mother + # postpartum each. + young_tax_unit = tax_unit.sum(young_infant) - max_( + 0, + tax_unit.sum(breastfeeding & bf_tax_unit) - tax_unit.sum(older_infant), + ) + young_family = family.sum(young_infant & in_motherless_tax_unit) - max_( + 0, + family.sum(breastfeeding & bf_family) + - family.sum(older_infant & in_motherless_tax_unit), + ) + pp_tax_unit, pp_family = match( + mother & ~breastfeeding_woman, young_tax_unit, young_family + ) + postpartum = pp_tax_unit | pp_family return select( [ pregnant, - breastfeeding & is_matched_to_infant_under(1), - is_matched_to_infant_under(0.5), + breastfeeding_woman, + postpartum, age < 1, age < 5, ], From 33dad79927b073cb9e19bc3cd85930bf8c2ccfbf Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Wed, 7 Oct 2026 14:50:23 -0400 Subject: [PATCH 3/3] Match breastfeeding women to any infant in their family first A woman flagged is_breastfeeding lost her category when the infant was claimed in another tax unit that had a different mother. Breastfeeding women are now matched first: to an infant in their tax unit, else to any remaining infant in their family. The matched infants (oldest first; at family level, infants in tax units without a mother first) leave the pools, and the tax-unit heuristic matches the other mothers to the remaining infants under six months. Without is_breastfeeding inputs the results do not change. Tests: the reviewer's regression case, and one case each for the pregnant-mother, two-breastfeeding-women and two-women-couple limitations. Co-Authored-By: Claude Opus 5.5 --- .../baseline/gov/usda/wic/wic_category.yaml | 130 ++++++++++++++++++ .../variables/gov/usda/wic/wic_category.py | 86 ++++++------ 2 files changed, 170 insertions(+), 46 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml b/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml index a8d5007633c..1b185be2360 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/wic/wic_category.yaml @@ -399,3 +399,133 @@ age: 0.6 output: wic_category: [NONE, NONE, INFANT] + +# A woman reported as breastfeeding is matched before the tax-unit heuristic: +# here the grandmother claims the infant, and the infant's mother files alone. +- name: Breastfeeding mother in her own tax unit keeps her infant claimed by the grandmother + period: 2025 + input: + people: + grandmother: + age: 45 + is_female: true + own_children_in_household: 1 + mother: + age: 20 + is_female: true + is_breastfeeding: true + infant: + age: 0.3 + tax_units: + grandmother_tax_unit: + members: [grandmother, infant] + mother_tax_unit: + members: [mother] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, BREASTFEEDING, INFANT] + +# Limitation: a pregnant mother can take the postpartum match (she stays +# PREGNANT, which comes first), so the other mother is not postpartum. +- name: Pregnant younger mother takes the match and stays pregnant + period: 2025 + input: + people: + grandmother: + age: 50 + is_female: true + own_children_in_household: 1 + mother: + age: 25 + is_female: true + is_pregnant: true + own_children_in_household: 1 + infant: + age: 0.2 + tax_units: + tax_unit: + members: [grandmother, mother, infant] + spm_units: + spm_unit: + members: [grandmother, mother, infant] + families: + family: + members: [grandmother, mother, infant] + households: + household: + members: [grandmother, mother, infant] + state_code: TX + output: + wic_category: [NONE, PREGNANT, INFANT] + +# Limitation: one infant makes one woman breastfeeding (the youngest). +- name: Two breastfeeding women and one infant, only the younger is breastfeeding + period: 2025 + input: + people: + woman_1: + age: 30 + is_female: true + is_breastfeeding: true + woman_2: + age: 25 + is_female: true + is_breastfeeding: true + infant: + age: 0.5 + tax_units: + tax_unit: + members: [woman_1, woman_2, infant] + spm_units: + spm_unit: + members: [woman_1, woman_2, infant] + families: + family: + members: [woman_1, woman_2, infant] + households: + household: + members: [woman_1, woman_2, infant] + state_code: TX + output: + wic_category: [NONE, BREASTFEEDING, INFANT] + +# Limitation: in a couple of two women who are both the infant's parents, only +# one can be postpartum (the younger). +- name: Two mothers and one infant in one tax unit, only the younger is postpartum + period: 2025 + input: + people: + parent_1: + age: 32 + is_female: true + own_children_in_household: 1 + parent_2: + age: 30 + is_female: true + own_children_in_household: 1 + infant: + age: 0.2 + tax_units: + tax_unit: + members: [parent_1, parent_2, infant] + spm_units: + spm_unit: + members: [parent_1, parent_2, infant] + families: + family: + members: [parent_1, parent_2, infant] + households: + household: + members: [parent_1, parent_2, infant] + state_code: TX + output: + wic_category: [NONE, POSTPARTUM, INFANT] diff --git a/policyengine_us/variables/gov/usda/wic/wic_category.py b/policyengine_us/variables/gov/usda/wic/wic_category.py index a37b856b8ca..6f43d0d2855 100644 --- a/policyengine_us/variables/gov/usda/wic/wic_category.py +++ b/policyengine_us/variables/gov/usda/wic/wic_category.py @@ -10,15 +10,6 @@ class WICCategory(Enum): NONE = "None" -def _rank_mothers(person, entity, age, breastfeeding, eligible): - # Rank the eligible mothers in each entity: breastfeeding mothers first, - # then from youngest to oldest. Only use the rank together with eligible. - breastfeeding_rank = person.get_rank(entity, age, eligible & breastfeeding) - other_rank = person.get_rank(entity, age, eligible & ~breastfeeding) - breastfeeding_mothers = entity.sum(eligible & breastfeeding) - return where(breastfeeding, breastfeeding_rank, breastfeeding_mothers + other_rank) - - class wic_category(Variable): value_type = Enum entity = Person @@ -42,48 +33,51 @@ def formula(person, period, parameters): # 7 CFR 246.2 defines postpartum and breastfeeding women by the time # since their own pregnancy (up to six months and one year), so an # infant makes only its mother postpartum or breastfeeding. Without - # parent-child links, match each infant to one mother in its tax unit, - # and an infant in a tax unit without a mother to one of the family's - # remaining mothers, taking breastfeeding mothers first, then the - # youngest. - tax_unit_has_mother = tax_unit.any(mother) - in_motherless_tax_unit = ~tax_unit_has_mother - - def match(eligible, tax_unit_infants, family_infants): - tax_unit_rank = _rank_mothers( - person, tax_unit, age, breastfeeding, eligible - ) - in_tax_unit = eligible & (tax_unit_rank < tax_unit_infants) - remaining = eligible & ~in_tax_unit - family_rank = _rank_mothers(person, family, age, breastfeeding, remaining) - in_family = remaining & (family_rank < family_infants) - return in_tax_unit, in_family - + # parent-child links, match each infant to at most one woman. infant = age < 1 young_infant = age < 0.5 - older_infant = infant & ~young_infant - bf_tax_unit, bf_family = match( - mother, - tax_unit.sum(infant), - family.sum(infant & in_motherless_tax_unit), - ) - breastfeeding_woman = breastfeeding & (bf_tax_unit | bf_family) - # Breastfeeding women take the infants aged six months to one year - # first; the remaining infants under six months make one other mother - # postpartum each. - young_tax_unit = tax_unit.sum(young_infant) - max_( - 0, - tax_unit.sum(breastfeeding & bf_tax_unit) - tax_unit.sum(older_infant), + in_motherless_tax_unit = ~tax_unit.any(mother) + + # 1. A woman reported as breastfeeding is feeding an infant, so match + # her first: to an infant in her tax unit, else to any remaining infant + # in her family (youngest breastfeeding women first). + bf_rank_tax_unit = person.get_rank(tax_unit, age, breastfeeding) + bf_in_tax_unit = breastfeeding & (bf_rank_tax_unit < tax_unit.sum(infant)) + bf_rest = breastfeeding & ~bf_in_tax_unit + bf_rank_family = person.get_rank(family, age, bf_rest) + family_infants_left = family.sum(infant) - family.sum(bf_in_tax_unit) + bf_in_family = bf_rest & (bf_rank_family < family_infants_left) + breastfeeding_woman = bf_in_tax_unit | bf_in_family + + # Take the matched infants out of the pools, oldest first, so that + # breastfeeding women count against infants aged six months to one + # year before younger ones (inputs cannot tell which infant is whose: + # a breastfeeding woman and another mother in one tax unit are assumed + # to have the older and the younger infant). Family matches take + # infants from tax units without a mother first. + infant_rank_tax_unit = person.get_rank(tax_unit, -age, infant) + taken = infant & (infant_rank_tax_unit < tax_unit.sum(bf_in_tax_unit)) + left = infant & ~taken + infant_rank_family = person.get_rank( + family, where(in_motherless_tax_unit, 0, 2) - age, left ) - young_family = family.sum(young_infant & in_motherless_tax_unit) - max_( - 0, - family.sum(breastfeeding & bf_family) - - family.sum(older_infant & in_motherless_tax_unit), + taken = taken | (left & (infant_rank_family < family.sum(bf_in_family))) + free_young_infant = young_infant & ~taken + + # 2. Match each remaining infant under six months to one other mother + # in its tax unit, youngest first, and an infant in a tax unit without + # a mother to one of the family's remaining mothers. + candidate = mother & ~breastfeeding_woman + pp_rank_tax_unit = person.get_rank(tax_unit, age, candidate) + pp_in_tax_unit = candidate & ( + pp_rank_tax_unit < tax_unit.sum(free_young_infant) ) - pp_tax_unit, pp_family = match( - mother & ~breastfeeding_woman, young_tax_unit, young_family + pp_rest = candidate & ~pp_in_tax_unit + pp_rank_family = person.get_rank(family, age, pp_rest) + pp_in_family = pp_rest & ( + pp_rank_family < family.sum(free_young_infant & in_motherless_tax_unit) ) - postpartum = pp_tax_unit | pp_family + postpartum = pp_in_tax_unit | pp_in_family return select( [