diff --git a/changelog.d/date-of-birth-cutoffs.added.md b/changelog.d/date-of-birth-cutoffs.added.md new file mode 100644 index 0000000000..a60cef2e70 --- /dev/null +++ b/changelog.d/date-of-birth-cutoffs.added.md @@ -0,0 +1 @@ +Add `date_of_birth`, each person's day of birth as a YYYYMMDD number from `age` and `months_since_last_birthday`, which can also be set directly (with `age` as the person's age on 6 October) and then sets State Pension age and the cutoffs below; and add date parameters for the 6 April 2017 cutoffs: `gov.dwp.universal_credit.elements.child.limit.born_before`, `gov.dwp.tax_credits.child_tax_credit.limit.born_before` and `gov.dwp.pension_credit.guarantee_credit.child.first.born_before`, and for the Pension Credit mixed-age couple saving, `gov.dwp.pension_credit.mixed_age_couples.saving_latest_birth_date` (5 February 1954). diff --git a/changelog.d/date-of-birth-cutoffs.changed.md b/changelog.d/date-of-birth-cutoffs.changed.md new file mode 100644 index 0000000000..c6f92382e2 --- /dev/null +++ b/changelog.d/date-of-birth-cutoffs.changed.md @@ -0,0 +1 @@ +`birth_year` is now the calendar year of `date_of_birth`. It is unchanged for household situations with whole ages; in survey data, people whose birthday falls from 7 October to 31 December now have the year before the period less their age, and a fractional age no longer truncates to the year before. diff --git a/changelog.d/date-of-birth-cutoffs.fixed.md b/changelog.d/date-of-birth-cutoffs.fixed.md new file mode 100644 index 0000000000..83ce91956a --- /dev/null +++ b/changelog.d/date-of-birth-cutoffs.fixed.md @@ -0,0 +1 @@ +Apply the "born before 6 April 2017" rules by date of birth: the Universal Credit child limit and higher first child element (UC Regs 2013 reg 24A(3); UC (Transitional Provisions) Regs 2014 reg 43), the Child Tax Credit child limit (Tax Credits Act 2002 s.9(3A)), the Pension Credit first child addition (SPC Regs 2002 Sch IIA para 10), and the Pension Credit mixed-age couple saving, which needs the older member born on or before 5 February 1954 to have reached the qualifying age by 14 May 2019 (SI 2019/37 art. 4). The child rules compared a birth year of the period less the whole age with 2017, so in survey data the whole cohort born from October 2016 to October 2017 counted as born after the cutoff, when about half was born before it. Universal Credit and Pension Credit also now order children by date of birth (UC Regs reg 24B), so the first or eldest child is the one born first. For the child rules, household situations with whole ages, which place each person in the middle of their year of age, are unchanged; a fractional age is now read as the exact age on 6 October, as it already is for State Pension age. The mixed-age saving does change for whole ages: a situation's older member aged period less 1954 is placed on 6 April 1954, after the cutoff, where the birth year admitted them. diff --git a/changelog.d/date-of-birth-cutoffs.removed.md b/changelog.d/date-of-birth-cutoffs.removed.md new file mode 100644 index 0000000000..3ff6bc1fb0 --- /dev/null +++ b/changelog.d/date-of-birth-cutoffs.removed.md @@ -0,0 +1 @@ +Remove `gov.dwp.universal_credit.elements.child.limit.start_year` and `gov.dwp.tax_credits.child_tax_credit.limit.start_year`, which held the year 2017, and `gov.dwp.pension_credit.mixed_age_couples.saving_latest_birth_year` (1954). A reform to a cutoff now sets the matching date parameter, written as a YYYYMMDD number: `born_before` (20170406) or `saving_latest_birth_date` (19540205). diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index ed74f0f54f..985576b98a 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -79,7 +79,10 @@ timetable row by row: A person attains State Pension age on the later of the two. The model places each person's date of birth with `age` and `months_since_last_birthday`, which counts months since their last -birthday on 6 October, the middle of the fiscal year: +birthday on 6 October, the middle of the fiscal year. `date_of_birth` holds +the resulting day (as a YYYYMMDD number), which other date-of-birth rules +also read, such as the 6 April 2017 cutoffs in Universal Credit, Child Tax +Credit and Pension Credit: - `state_pension_age` is the person's own State Pension age; - `months_since_state_pension_age` is how long before 6 October they @@ -93,7 +96,12 @@ the exact age on 6 October, overriding `months_since_last_birthday` even when another person supplies birthday months. The default implies a birth date of 6 April: an entered age of 66 is above State Pension age at the 6 October check in 2026-27 and below it in 2027-28. Set a fractional age -or `months_since_last_birthday` to specify a different inferred birth date. +or `months_since_last_birthday` to specify a different inferred birth date, +or set `date_of_birth` directly (a YYYYMMDD number, with `age` as the age on +6 October): `state_pension_age`, `months_since_state_pension_age`, +`is_SP_age`, the State Pension Credit qualifying age and its attainment, and +the Savings Credit age test then all follow that day. A value that is not a +calendar date, or that gives a different age, raises an error. Survey microdata records whole years only, so in simulations built from data, including a region or constituency filtered from it, each single year of age and sex is spread evenly over the year by @@ -126,8 +134,9 @@ Other programmes test State Pension age in two ways: itself is read as over it at the beginning of that year. With default whole-number ages in a household situation, the inferred birthday is 6 April. Entering an integer State Pension age therefore makes this - boundary reading determine the Class 4 result; fractional ages and explicit - birthday-month inputs can place the birthday on another day. Class 1 employee + boundary reading determine the Class 4 result; fractional ages, explicit + birthday-month inputs and a `date_of_birth` input can place the birthday on + another day. Class 1 employee contributions stop at State Pension age itself (SSCBA 1992 s.6(3)), which the annual model reads as `is_SP_age`. diff --git a/docs/book/programs/gov/dwp/tax-credits.md b/docs/book/programs/gov/dwp/tax-credits.md index 284dd56994..eac83f4600 100644 --- a/docs/book/programs/gov/dwp/tax-credits.md +++ b/docs/book/programs/gov/dwp/tax-credits.md @@ -68,11 +68,12 @@ benunit worked. PolicyEngine computes it via `ctc_entitlement`, gated by Since April 2017 the child element has been restricted to the first two qualifying children. The `child_count` parameter under -`child_tax_credit/limit/` and the `start_year/year.yaml` switch encode -this. Children born before the start year and a few specific exceptions -(non-consensual conception, multiple births, kinship care) are exempt; -the exemptions are modelled via `gov/contrib/two_child_limit/` and -related variables. +`child_tax_credit/limit/` encodes this, and `born_before` (6 April 2017, +Tax Credits Act 2002 s.9(3A)) sets which children it applies to: those +born before it are exempt, compared on `date_of_birth`. A few specific +exceptions (non-consensual conception, multiple births, kinship care) +also apply in law but are not modelled; `gov/contrib/two_child_limit/` +holds reform options such as an age-based exemption. ### Qualifying child tests diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/child/first/born_before.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/child/first/born_before.yaml new file mode 100644 index 0000000000..eaf4bdb59d --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/child/first/born_before.yaml @@ -0,0 +1,10 @@ +description: The first child addition replaces the standard child addition in the Pension Credit minimum guarantee for the eldest child or qualifying young person if they were born before this date, written as a YYYYMMDD number. +values: + 0001-01-01: 20170406 +metadata: + unit: date + period: year + label: Pension Credit first child addition cutoff date of birth + reference: + - title: The State Pension Credit Regulations 2002 Schedule IIA paragraph 10 + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IIA diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/mixed_age_couples/saving_latest_birth_date.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/mixed_age_couples/saving_latest_birth_date.yaml new file mode 100644 index 0000000000..c79b30feeb --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/mixed_age_couples/saving_latest_birth_date.yaml @@ -0,0 +1,13 @@ +description: Latest date of birth, written as a YYYYMMDD number, of the older member of a mixed-age couple for the couple to keep Pension Credit under the SI 2019/37 article 4 saving. The older member must have reached the qualifying age for State Pension Credit by 14 May 2019; a woman born on or before 5 February 1954 reached it on 6 May 2019, and one born on 6 February 1954 on 6 July 2019 (Pensions Act 1995 Sch 4 para 1, table 2). +values: + 2019-05-15: 19540205 +metadata: + unit: date + label: Latest date of birth for the mixed-age couple Pension Credit saving + reference: + - title: SI 2019/37 art. 4(1) + href: https://www.legislation.gov.uk/uksi/2019/37/article/4 + - title: Pensions Act 1995 Sch 4 para 1 (pensionable age by date of birth) + href: https://www.legislation.gov.uk/ukpga/1995/26/schedule/4 + - title: State Pension Credit Act 2002 s.1(6) (qualifying age) + href: https://www.legislation.gov.uk/ukpga/2002/16/section/1 diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/mixed_age_couples/saving_latest_birth_year.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/mixed_age_couples/saving_latest_birth_year.yaml deleted file mode 100644 index 9207413045..0000000000 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/mixed_age_couples/saving_latest_birth_year.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: Latest birth year of the older member of a mixed-age couple for the couple to keep Pension Credit under the SI 2019/37 article 4 saving. The older member must have reached the qualifying age for State Pension Credit by 14 May 2019, which means being born on or before 5 February 1954. With whole-year ages the model admits the whole of 1954. -values: - 2019-05-15: 1954 -metadata: - unit: year - label: Latest birth year for the mixed-age couple Pension Credit saving - reference: - - title: SI 2019/37 art. 4(1) - href: https://www.legislation.gov.uk/uksi/2019/37/article/4 - - title: Pensions Act 1995 Sch 4 para 1 (pensionable age by date of birth) - href: https://www.legislation.gov.uk/ukpga/1995/26/schedule/4 - - title: State Pension Credit Act 2002 s.1(6) (qualifying age) - href: https://www.legislation.gov.uk/ukpga/2002/16/section/1 diff --git a/policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/born_before.yaml b/policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/born_before.yaml new file mode 100644 index 0000000000..7044d46ca0 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/born_before.yaml @@ -0,0 +1,12 @@ +description: The Child Tax Credit child limit applies to children and qualifying young persons born on or after this date, written as a YYYYMMDD number; those born before it are exempt. +values: + 0001-01-01: 20170406 +metadata: + unit: date + period: year + label: Child Tax Credit child limit cutoff date of birth + reference: + - title: Tax Credits Act 2002 s. 9(3A) and (3B) + href: https://www.legislation.gov.uk/ukpga/2002/21/section/9 + - title: The Child Tax Credit Regulations 2002 reg. 9 (exceptions to the restriction on numbers) + href: https://www.legislation.gov.uk/uksi/2002/2007/regulation/9 diff --git a/policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/start_year.yaml b/policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/start_year.yaml deleted file mode 100644 index a59e4dd075..0000000000 --- a/policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/start_year.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: Year in which the Child Tax Credit child limit applies. -values: - 0001-01-01: 2017 -metadata: - unit: year - reference: https://www.gov.uk/guidance/child-tax-credit-exceptions-to-the-2-child-limit diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/born_before.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/born_before.yaml new file mode 100644 index 0000000000..2aae8c9e9c --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/born_before.yaml @@ -0,0 +1,12 @@ +description: A child or qualifying young person born before this date, written as a YYYYMMDD number, is transitionally protected from the Universal Credit child limit, and a claimant responsible for one keeps the higher child element for the first child. +values: + 0001-01-01: 20170406 +metadata: + unit: date + period: year + label: Universal Credit child limit and higher first child element cutoff date of birth + reference: + - title: The Universal Credit Regulations 2013 reg. 24A(3) (born before 6 April 2017, as worded from 1 February 2019 until revoked from 6 April 2026; from 6 April 2017 to 31 January 2019 it referred to the transitional circumstances in UC (Transitional Provisions) Regs 2014 reg 40, whose further conditions are not modelled) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/24A/2025-04-06 + - title: The Universal Credit (Transitional Provisions) Regulations 2014 reg. 43 (saving of the higher first child element) + href: https://www.legislation.gov.uk/uksi/2014/1230/regulation/43 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/start_year.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/start_year.yaml deleted file mode 100644 index e96d8f3e00..0000000000 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/start_year.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: A higher Universal Credit child element is payable for the first child if the child is born before this year. -values: - 0001-01-01: 2017 -metadata: - unit: year - period: year - label: Universal Credit child element start year - reference: - - title: The Universal Credit Regulations 2013 reg. 24A (3) - href: https://www.legislation.gov.uk/uksi/2013/376/data.pdf#page=31 diff --git a/policyengine_uk/tests/policy/baseline/demographic/date_of_birth.yaml b/policyengine_uk/tests/policy/baseline/demographic/date_of_birth.yaml new file mode 100644 index 0000000000..da0a7b54d5 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/demographic/date_of_birth.yaml @@ -0,0 +1,114 @@ +# The date of birth is age years and months_since_last_birthday months before +# 6 October of the fiscal year, on a grid whose months start on the 6th; the +# day of birth is the one starting at or after that instant. + +- name: A whole age in a household situation is the middle of the year of age. + period: 2025 + absolute_error_margin: 0 + input: + age: 8 + output: + months_since_last_birthday: 6 + # 6 October 2025 less 8 years and 6 months. + date_of_birth: 20170406 + birth_year: 2017 + +- name: One day more since the last birthday is born the day before. + period: 2025 + absolute_error_margin: 0 + input: + age: 8 + # The grid month from 6 March to 5 April 2017 has 31 days. + months_since_last_birthday: 6.032258064516129 + output: + date_of_birth: 20170405 + birth_year: 2017 + +- name: A birthday in December falls in the year before period less age. + period: 2025 + absolute_error_margin: 0 + input: + age: 8 + months_since_last_birthday: 10 + output: + # 6 October 2025 less 8 years and 10 months. + date_of_birth: 20161206 + birth_year: 2016 + +- name: A fractional age is the exact age on 6 October. + period: 2025 + absolute_error_margin: 0 + input: + age: 8.25 + output: + months_since_last_birthday: 3 + date_of_birth: 20170706 + birth_year: 2017 + +- name: A birthday on 6 October itself. + period: 2025 + absolute_error_margin: 0 + input: + age: 0 + months_since_last_birthday: 0 + output: + date_of_birth: 20251006 + birth_year: 2025 + +- name: A date of birth from the age and birthday places the Universal Credit cutoff exactly, born 5 April 2017. + period: 2025 + absolute_error_margin: 0 + input: + age: 8 + months_since_last_birthday: 6.032258064516129 + output: + uc_is_child_born_before_child_limit: true + is_CTC_child_limit_exempt: true + +- name: A date of birth from the age and birthday places the Universal Credit cutoff exactly, born 6 April 2017. + period: 2025 + absolute_error_margin: 0 + input: + age: 8 + output: + uc_is_child_born_before_child_limit: false + is_CTC_child_limit_exempt: false + +- name: A date of birth set for one person leaves the others placed by age. + period: 2025 + absolute_error_margin: 0.001 + input: + people: + parent: + age: 35 + child: + age: 8 + date_of_birth: 20170405 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + # The parent's date_of_birth input is 0 (not given), so the rules place + # them on 6 April 1990, as with no input at all: State Pension age 68. + birth_year: [1990, 2017] + state_pension_age: [68, 68] + is_SP_age: [false, false] + uc_is_child_born_before_child_limit: [false, true] + +- name: A date of birth sets State Pension age exactly. + period: 2026 + absolute_error_margin: 0.001 + input: + age: 66 + date_of_birth: 19600105 + output: + # Pensions Act 1995 Sch 4 para 1: born 6 October 1954 to 5 April 1960, + # State Pension age is 66, attained on 5 January 2026. From then to 6 + # October 2026 is 9 grid months and the 1 day of the grid month from 6 + # December 2025 to 5 January 2026 that falls after it: 10 - 30/31. + state_pension_age: 66 + is_SP_age: true + months_since_state_pension_age: 9.032 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml index 7725b1e703..85504d160e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml @@ -104,3 +104,63 @@ child_tax_credit_pre_minimum: 26 output: tax_credits: 26 +# Tax Credits Act 2002 s.9(3A): the child limit applies to a child or qualifying +# young person born on or after 6 April 2017. The 2024 child element is 3,455. +- name: CTC child limit, born 5 April 2017 is exempt + period: 2024 + absolute_error_margin: 0 + input: + age: 7 + date_of_birth: 20170405 + output: + is_CTC_child_limit_exempt: true +- name: CTC child limit, born 6 April 2017 is not exempt + period: 2024 + absolute_error_margin: 0 + input: + age: 7 + date_of_birth: 20170406 + output: + is_CTC_child_limit_exempt: false +- name: CTC child element for a third child born 5 April 2017 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + child_1: + age: 10 + date_of_birth: 20140301 + child_2: + age: 9 + date_of_birth: 20150501 + child_3: + age: 7 + date_of_birth: 20170405 + benunits: + benunit: + members: [parent, child_1, child_2, child_3] + output: + CTC_child_element: 10_365 +- name: CTC child element for a third child born 6 April 2017 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + child_1: + age: 10 + date_of_birth: 20140301 + child_2: + age: 9 + date_of_birth: 20150501 + child_3: + age: 7 + date_of_birth: 20170406 + benunits: + benunit: + members: [parent, child_1, child_2, child_3] + output: + CTC_child_element: 6_910 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/uc_is_child_born_before_child_limit.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/uc_is_child_born_before_child_limit.yaml index 53619af66a..65150601f2 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/uc_is_child_born_before_child_limit.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/uc_is_child_born_before_child_limit.yaml @@ -21,3 +21,156 @@ age: 4 output: uc_is_child_born_before_child_limit: true + +# UC Regs 2013 reg 24A(3) (in force 6 April 2017 to 5 April 2026): a child or +# qualifying young person born before 6 April 2017 is transitionally protected. +# In 2025 the child element is 292.81 * 12 = 3,513.72 and the higher amount for +# the first child is 339 * 12 = 4,068. + +- name: A third child born on 5 April 2017 is protected from the child limit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_benunit_head: true + is_parent: true + child_1: + age: 10 + date_of_birth: 20150301 + child_2: + age: 9 + date_of_birth: 20160501 + child_3: + age: 8 + date_of_birth: 20170405 + benunits: + benunit: + members: [parent, child_1, child_2, child_3] + output: + uc_is_child_born_before_child_limit: [false, true, true, true] + uc_child_index: [-1, 1, 2, 3] + uc_individual_child_element: [0, 4_068, 3_513.72, 3_513.72] + +- name: A third child born on 6 April 2017 is not protected from the child limit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_benunit_head: true + is_parent: true + child_1: + age: 10 + date_of_birth: 20150301 + child_2: + age: 9 + date_of_birth: 20160501 + child_3: + age: 8 + date_of_birth: 20170406 + benunits: + benunit: + members: [parent, child_1, child_2, child_3] + output: + uc_is_child_born_before_child_limit: [false, true, true, false] + uc_child_index: [-1, 1, 2, 3] + uc_individual_child_element: [0, 4_068, 3_513.72, 0] + +- name: Children of the same age are ordered by date of birth (reg 24B). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_benunit_head: true + is_parent: true + child_1: + age: 10 + date_of_birth: 20150301 + born_later: + age: 8 + date_of_birth: 20170406 + born_earlier: + age: 8 + date_of_birth: 20170405 + benunits: + benunit: + members: [parent, child_1, born_later, born_earlier] + output: + # The child born on 6 April 2017 is the third child and not protected; the + # one born on 5 April is the second, and protected either way. + uc_child_index: [-1, 1, 3, 2] + uc_individual_child_element: [0, 4_068, 0, 3_513.72] + +# UC (Transitional Provisions) Regs 2014 reg 43 keeps the higher child element +# for the first child where the claimant is responsible for a child born before +# 6 April 2017. In 2026 the child element is 303.94 * 12 = 3,647.28 and the +# higher amount is 351.88 * 12 = 4,222.56. + +- name: An only child born on 5 April 2017 keeps the higher first child amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_benunit_head: true + is_parent: true + child: + age: 9 + date_of_birth: 20170405 + benunits: + benunit: + members: [parent, child] + output: + uc_is_child_born_before_child_limit: [false, true] + uc_individual_child_element: [0, 4_222.56] + +- name: An only child born on 6 April 2017 gets the standard child amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_benunit_head: true + is_parent: true + child: + age: 9 + date_of_birth: 20170406 + benunits: + benunit: + members: [parent, child] + output: + uc_is_child_born_before_child_limit: [false, false] + uc_individual_child_element: [0, 3_647.28] + +- name: Children without a date of birth input are placed by age. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_benunit_head: true + is_parent: true + child_1: + age: 10 + child_2: + age: 8 + child_3: + age: 8 + date_of_birth: 20170405 + benunits: + benunit: + members: [parent, child_1, child_2, child_3] + output: + # child_2 has no input (0), so is placed on 6 April 2017: after child_3, + # and not protected. + uc_is_child_born_before_child_limit: [false, true, false, true] + uc_child_index: [-1, 1, 3, 2] + uc_individual_child_element: [0, 4_068, 0, 3_513.72] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml index a99d4cbda2..a9547e5e7c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml @@ -7,10 +7,8 @@ age: 81 older_child: age: 10 - birth_year: 2016 younger_child: age: 6 - birth_year: 2020 benunits: benunit: members: [claimant, older_child, younger_child] @@ -32,7 +30,6 @@ age_started_or_accepted_current_education_or_training: 18 child: age: 6 - birth_year: 2020 benunits: benunit: members: [claimant, young_person, child] @@ -41,3 +38,58 @@ # (81.07 + 69.98) * 52 = 7,854.60; single guarantee = 238 * 52. child_minimum_guarantee_addition: 7_854.60 minimum_guarantee: 20_230.60 + +# Sch IIA para 10: the eldest child or qualifying young person gets 81.07 a week +# instead of 69.98 if born before 6 April 2017. 81.07 * 52 = 4,215.64 and +# 69.98 * 52 = 3,638.96. +- name: An only child born on 5 April 2017 gets the first child amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + child: + age: 9 + date_of_birth: 20170405 + benunits: + benunit: + members: [claimant, child] + output: + child_minimum_guarantee_addition: 4_215.64 + +- name: An only child born on 6 April 2017 gets the standard child amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + child: + age: 9 + date_of_birth: 20170406 + benunits: + benunit: + members: [claimant, child] + output: + child_minimum_guarantee_addition: 3_638.96 + +- name: The eldest of two children of the same age is the one born first. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + born_later: + age: 9 + date_of_birth: 20170406 + born_earlier: + age: 9 + date_of_birth: 20170405 + benunits: + benunit: + members: [claimant, born_later, born_earlier] + output: + # (81.07 + 69.98) * 52 = 7,854.60. + child_minimum_guarantee_addition: 7_854.60 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/mixed_age_couples.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/mixed_age_couples.yaml index 14846b2ead..83e9f1ca82 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/mixed_age_couples.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/mixed_age_couples.yaml @@ -635,3 +635,49 @@ output: is_mixed_age_couple: true meets_pension_credit_age_conditions: true + +# SI 2019/37 art. 4(1): the older member must have reached the qualifying age +# by 14 May 2019, so been born on or before 5 February 1954 (State Pension age +# 6 May 2019; a birth on 6 February 1954 reaches it on 6 July 2019). Ages are +# the legal ages on 6 October 2026. +- name: Mixed-age saving, older member born 5 February 1954, is set + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 72 + date_of_birth: 19540205 + state_pension: 9_000 + pension_credit_reported: 1_000 + younger: + age: 60 + benunits: + benunit: + members: [older, younger] + households: + household: + members: [older, younger] + output: + has_mixed_age_couple_pension_credit_saving: true + +- name: Mixed-age saving, older member born 6 February 1954, is not set + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 72 + date_of_birth: 19540206 + state_pension: 9_000 + pension_credit_reported: 1_000 + younger: + age: 60 + benunits: + benunit: + members: [older, younger] + households: + household: + members: [older, younger] + output: + has_mixed_age_couple_pension_credit_saving: false diff --git a/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml b/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml index a2c470489a..44d4df7a05 100644 --- a/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml +++ b/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml @@ -8,16 +8,12 @@ child_tax_credit_reported: 8000 child_1: age: 5 - birth_year: 2018 child_2: age: 4 - birth_year: 2019 child_3: age: 2 - birth_year: 2021 child_4: age: 1 - birth_year: 2022 benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] @@ -36,16 +32,12 @@ child_tax_credit_reported: 8000 child_1: age: 5 - birth_year: 2018 child_2: age: 4 - birth_year: 2019 child_3: age: 2 # This child is under the threshold - birth_year: 2021 child_4: age: 1 - birth_year: 2022 benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] @@ -64,16 +56,12 @@ child_tax_credit_reported: 8000 child_1: age: 5 - birth_year: 2018 child_2: age: 4 - birth_year: 2019 child_3: age: 3 # No child below threshold - birth_year: 2020 child_4: age: 3 - birth_year: 2020 benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] @@ -91,16 +79,12 @@ child_tax_credit_reported: 8000 child_1: age: 5 - birth_year: 2018 child_2: age: 4 - birth_year: 2019 child_3: age: 2 # This child is under the threshold - birth_year: 2021 child_4: age: 1 # This child is under the threshold - birth_year: 2022 benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] @@ -119,16 +103,12 @@ child_tax_credit_reported: 8000 child_1: age: 5 - birth_year: 2018 child_2: age: 4 - birth_year: 2019 child_3: age: 2 # This child is under the threshold - birth_year: 2021 child_4: age: 1 # This child is under the threshold - birth_year: 2022 benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] diff --git a/policyengine_uk/tests/test_date_of_birth.py b/policyengine_uk/tests/test_date_of_birth.py new file mode 100644 index 0000000000..dc300f06a8 --- /dev/null +++ b/policyengine_uk/tests/test_date_of_birth.py @@ -0,0 +1,352 @@ +"""Date-of-birth cutoffs in Universal Credit, Child Tax Credit and Pension Credit. + +UC Regs 2013 reg 24A(3), UC (Transitional Provisions) Regs 2014 reg 43, Tax +Credits Act 2002 s.9(3A) and SPC Regs 2002 Sch IIA para 10 all turn on whether a +child was born before 6 April 2017. The model compares date_of_birth with that +date. The reference here reads the date of birth with datetime, sharing no code +with policyengine_uk.utils.dates. +""" + +from datetime import date, timedelta + +import numpy as np +import pandas as pd +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +CUTOFF = date(2017, 4, 6) + + +def ymd(day: date) -> int: + return day.year * 10000 + day.month * 100 + day.day + + +def from_ymd(value: int) -> date: + value = int(value) + return date(value // 10000, value // 100 % 100, value % 100) + + +def grid_months(day: date) -> float: + """Months on a grid whose months start on the 6th.""" + whole = 12 * day.year + day.month - 1 - (day.day < 6) + start = date(whole // 12, whole % 12 + 1, 6) + end = date((whole + 1) // 12, (whole + 1) % 12 + 1, 6) + return whole + (day - start).days / (end - start).days + + +def birth_day_from_grid(months: float) -> date: + """The calendar day starting at or after an instant on the grid, within + about a minute and a half, as the model treats float error.""" + whole = int(np.floor(months)) + start = date(whole // 12, whole % 12 + 1, 6) + end = date((whole + 1) // 12, (whole + 1) % 12 + 1, 6) + offset = int(np.ceil((months - whole) * (end - start).days - 1e-3)) + return start + timedelta(days=offset) + + +def legal_age(birth: date, on: date) -> int: + return on.year - birth.year - ((on.month, on.day) < (birth.month, birth.day)) + + +def situation(people: dict) -> dict: + return { + "people": people, + "benunits": {"benunit": {"members": list(people)}}, + "households": {"household": {"members": list(people)}}, + } + + +def test_household_situations_match_the_birth_year_rule(): + """A whole age in a household situation is the middle of the year of age, + 6 April of the year the person turned that age. So for households with + whole ages the date rule gives what the old birth-year rule (period - age + < 2017) gave, and birth_year is still period - age: only microdata, which + spreads birthdays over the year, and fractional ages move.""" + for year in range(2015, 2036): + ages = range(20) + people = {f"p{age}": {"age": {year: age}} for age in ages} + sim = Simulation(situation=situation(people)) + born = sim.calculate("date_of_birth", year) + birth_year = sim.calculate("birth_year", year) + exempt = sim.calculate("is_CTC_child_limit_exempt", year) + uc = sim.calculate("uc_is_child_born_before_child_limit", year) + old_rule = np.array([year - age < 2017 for age in ages]) + assert np.array_equal(born < ymd(CUTOFF), old_rule), year + assert np.array_equal(exempt, old_rule), year + under_16 = np.array(ages) < 16 + assert np.array_equal(uc[under_16], old_rule[under_16]), year + assert np.array_equal(birth_year, [year - age for age in ages]), year + + +@settings(max_examples=30, deadline=None) +@given( + cases=st.lists( + st.tuples( + st.integers(min_value=0, max_value=15), + st.floats(min_value=0, max_value=12, exclude_max=True), + ), + min_size=1, + max_size=12, + ), + year=st.integers(min_value=2015, max_value=2040), +) +def test_cutoffs_follow_the_date_of_birth(cases, year): + """For any whole age and position in the year, including instants within a + day: date_of_birth is the day the reference places, the person has that + legal age on 6 October, birth_year is its year, and each cutoff holds + exactly when the person was born before 6 April 2017.""" + people = {"parent": {"age": {year: 40}, "months_since_last_birthday": {year: 6}}} + for i, (age, months) in enumerate(cases): + people[f"c{i}"] = { + "age": {year: age}, + "months_since_last_birthday": {year: months}, + } + sim = Simulation(situation=situation(people)) + born = sim.calculate("date_of_birth", year)[1:] + birth_year = sim.calculate("birth_year", year)[1:] + uc = sim.calculate("uc_is_child_born_before_child_limit", year)[1:] + ctc = sim.calculate("is_CTC_child_limit_exempt", year)[1:] + mid_year = date(year, 10, 6) + for i, (age, months) in enumerate(cases): + # The model caps months since the last birthday about four minutes + # short of 12, so the exact age never rounds onto the next birthday. + months = min(months, 12 - 1e-4) + expected = birth_day_from_grid(grid_months(mid_year) - 12 * age - months) + assert from_ymd(born[i]) == expected, (year, age, months) + assert legal_age(expected, mid_year) == age, (year, age, months) + assert birth_year[i] == expected.year + assert uc[i] == (expected < CUTOFF), (year, age, months) + assert ctc[i] == (expected < CUTOFF), (year, age, months) + + +@settings(max_examples=25, deadline=None) +@given( + children=st.lists( + st.tuples( + st.integers(min_value=0, max_value=15), + st.floats(min_value=0, max_value=12, exclude_max=True), + ), + min_size=2, + max_size=6, + ), + year=st.integers(min_value=2017, max_value=2030), +) +def test_children_are_ordered_by_date_of_birth(children, year): + """Universal Credit numbers children eldest first by date of birth (reg + 24B), which refines the order by whole age: an older child always comes + first, and children of the same age come in the order they were born.""" + people = { + "parent": { + "age": {year: 40}, + "months_since_last_birthday": {year: 6}, + "is_benunit_head": {year: True}, + "is_parent": {year: True}, + } + } + for i, (age, months) in enumerate(children): + people[f"c{i}"] = { + "age": {year: age}, + "months_since_last_birthday": {year: months}, + "is_benunit_head": {year: False}, + } + sim = Simulation(situation=situation(people)) + index = sim.calculate("uc_child_index", year)[1:] + born = sim.calculate("date_of_birth", year)[1:] + ages = np.array([age for age, _ in children]) + assert sorted(index) == list(range(1, len(children) + 1)) + for a in range(len(children)): + for b in range(len(children)): + if born[a] < born[b]: + assert index[a] < index[b] + if ages[a] > ages[b]: + assert index[a] < index[b] + + +def cohort_share_born_before_cutoff(year: int, age: int) -> float: + """Share of people aged `age` on 6 October with birthdays spread evenly + over the days of the year who were born before 6 April 2017.""" + mid_year = date(year, 10, 6) + last = date(year - age, 10, 6) + first = date(year - age - 1, 10, 7) + days = [first + timedelta(days=d) for d in range((last - first).days + 1)] + return np.mean([day < CUTOFF for day in days]) + + +def cohort_share_born_before_calendar_year(year: int, age: int) -> float: + last = date(year - age, 10, 6) + first = date(year - age - 1, 10, 7) + days = [first + timedelta(days=d) for d in range((last - first).days + 1)] + return np.mean([day.year < year - age for day in days]) + + +def microdata_simulation(weights: np.ndarray, years): + """A simulation built from data: one child per household, aged 5 to 12 in + turn, alternating sex.""" + from policyengine_uk import Microsimulation + from policyengine_uk.data import UKMultiYearDataset, UKSingleYearDataset + + n = len(weights) + ids = np.arange(n) + ages = 5 + ids % 8 + + def year(y): + person = pd.DataFrame( + { + "person_id": ids * 10 + 3, + "person_benunit_id": ids, + "person_household_id": ids, + "age": ages, + "gender": np.where(ids % 16 < 8, "MALE", "FEMALE"), + } + ) + return UKSingleYearDataset( + person=person, + benunit=pd.DataFrame({"benunit_id": ids}), + household=pd.DataFrame({"household_id": ids, "household_weight": weights}), + fiscal_year=y, + ) + + dataset = UKMultiYearDataset(datasets=[year(y) for y in years]) + return Microsimulation(dataset=dataset), ages + + +def test_microdata_share_born_before_cutoff_matches_statute(): + """In data each single year of age and sex is spread evenly over the year + by weight, so the weighted share of the cohort that straddles 6 April 2017 + born before it is the statutory share, about half, where the birth-year + rule gave none. Every other cohort is wholly before or after.""" + weights = np.random.default_rng(3).lognormal(7, 0.5, 8 * 2 * 150) + years = (2022, 2024, 2025, 2026, 2028) + sim, ages = microdata_simulation(weights, years) + for year in years: + born = np.asarray(sim.calculate("date_of_birth", year)) + birth_year = np.asarray(sim.calculate("birth_year", year)) + uc = np.asarray(sim.calculate("uc_is_child_born_before_child_limit", year)) + ctc = np.asarray(sim.calculate("is_CTC_child_limit_exempt", year)) + assert np.array_equal(uc, born < ymd(CUTOFF)) + assert np.array_equal(ctc, born < ymd(CUTOFF)) + for age in range(5, 13): + cell = ages == age + # Within the largest weight in the cell, plus a day of births for + # where the month grid and the calendar differ. + tolerance = weights[cell].max() / weights[cell].sum() + 1 / 365 + share = np.average(born[cell] < ymd(CUTOFF), weights=weights[cell]) + expected = cohort_share_born_before_cutoff(year, age) + assert abs(share - expected) <= tolerance, (year, age, share) + earlier_year = np.average( + birth_year[cell] < year - age, weights=weights[cell] + ) + expected = cohort_share_born_before_calendar_year(year, age) + assert abs(earlier_year - expected) <= tolerance, (year, age) + + +@st.composite +def births_and_year(draw): + """A year from 2015 (when the State Pension Credit qualifying age still + differs from State Pension age for men born before 6 December 1953) and + people born before its 6 October, of either sex.""" + year = draw(st.integers(min_value=2015, max_value=2040)) + births = draw( + st.lists( + st.tuples( + st.dates(min_value=date(1935, 1, 1), max_value=date(year - 1, 12, 31)), + st.booleans(), + ), + min_size=1, + max_size=8, + ) + ) + return births, year + + +@settings(max_examples=25, deadline=None) +@given(case=births_and_year()) +# Transitional cohorts in the year they reach the qualifying age, where a +# wrong birth instant changes has_attained_state_pension_credit_qualifying_age +# but a uniform draw rarely lands: a woman born 10 December 1952 (qualifying +# age 62 years and about 9 months, reached in 2015-16) and a man born 10 +# November 1960 (66 years and 8 months, reached on 10 July 2027). +@example(case=([(date(1952, 12, 10), False)], 2015)) +@example(case=([(date(1960, 11, 10), True)], 2027)) +def test_a_date_of_birth_input_matches_the_same_birthday_by_age(case): + """Setting date_of_birth (with the legal age on 6 October) gives the same + State Pension age, State Pension Credit qualifying age, statuses, Savings + Credit age test and cutoffs as placing the same day with + months_since_last_birthday. A person left without the input in the same + situation gets what they would with no input at all.""" + people, year = case + births = [birth for birth, _ in people] + mid_year = date(year, 10, 6) + by_date = {"unset": {"age": {year: 40}}} + by_months = {"unset": {"age": {year: 40}, "months_since_last_birthday": {year: 6}}} + for i, (birth, male) in enumerate(people): + age = legal_age(birth, mid_year) + months = grid_months(mid_year) - grid_months(birth) - 12 * age + by_date[f"p{i}"] = { + "age": {year: age}, + "date_of_birth": {year: ymd(birth)}, + "is_male": {year: male}, + } + by_months[f"p{i}"] = { + "age": {year: age}, + "months_since_last_birthday": {year: months}, + "is_male": {year: male}, + } + a = Simulation(situation=situation(by_date)) + b = Simulation(situation=situation(by_months)) + for variable in [ + "birth_year", + "is_SP_age", + "has_attained_state_pension_credit_qualifying_age", + "meets_savings_credit_age_requirement", + "uc_is_child_born_before_child_limit", + "is_CTC_child_limit_exempt", + ]: + assert np.array_equal( + a.calculate(variable, year), b.calculate(variable, year) + ), variable + for variable in [ + "state_pension_age", + "state_pension_credit_qualifying_age", + "months_since_state_pension_age", + ]: + assert np.allclose( + a.calculate(variable, year), b.calculate(variable, year), rtol=0, atol=1e-9 + ), variable + # A woman's qualifying age for State Pension Credit is her State Pension + # age (SPCA 2002 s.1(6)(a)), so on the date path both statuses agree. + women = ~a.calculate("is_male", year) + assert np.array_equal( + a.calculate("has_attained_state_pension_credit_qualifying_age", year)[women], + a.calculate("is_SP_age", year)[women], + ) + assert list(a.calculate("date_of_birth", year)[1:]) == [ymd(d) for d in births] + assert a.calculate("date_of_birth", year)[0] == 0 + + +@pytest.mark.parametrize( + "date_of_birth, age", + [ + (20170229, 8), # 29 February 2017 does not exist + (20170340, 8), # nor does 40 March + (2017, 8), # a birth year, not a date + (-20170405, 8), # 0 means not given; a negative number is no date + ], +) +def test_an_invalid_date_of_birth_is_rejected(date_of_birth, age): + people = {"child": {"age": {2025: age}, "date_of_birth": {2025: date_of_birth}}} + sim = Simulation(situation=situation(people)) + with pytest.raises(ValueError, match="calendar date"): + sim.calculate("uc_is_child_born_before_child_limit", 2025) + + +def test_a_date_of_birth_that_contradicts_age_is_rejected(): + """A child given only a date of birth keeps the default age; the rules + would read them as a 40-year-old born in 2017.""" + people = {"child": {"date_of_birth": {2025: 20170405}}} + sim = Simulation(situation=situation(people)) + with pytest.raises(ValueError, match="aged 8 on 6 October 2025"): + sim.calculate("is_SP_age", 2025) diff --git a/policyengine_uk/tests/test_mixed_age_couple_properties.py b/policyengine_uk/tests/test_mixed_age_couple_properties.py index 2034a45b3e..598bc8b093 100644 --- a/policyengine_uk/tests/test_mixed_age_couple_properties.py +++ b/policyengine_uk/tests/test_mixed_age_couple_properties.py @@ -25,8 +25,8 @@ Universal Credit meets the Pension Credit age conditions, whatever the saving input; one reported on UC meets them only with the saving. P8 The default saving is never set where the older member was born after - 1954 (so had not reached the qualifying age by 14 May 2019), or where - the couple reports Universal Credit. + 5 February 1954 (so had not reached the qualifying age by 14 May 2019), + or where the couple reports Universal Credit. """ import json @@ -277,5 +277,7 @@ def test_mixed_age_couple_invariants(case): assert default["age_conditions"] == (not on_uc), context older = case["people"]["older"] if default["saving"]: # P8 - assert case["year"] - older["age"] <= 1954, context + # A whole age places the birth on 6 April of year - age, which is on + # or before 5 February 1954 only if year - age is 1953 or earlier. + assert case["year"] - older["age"] <= 1953, context assert older["universal_credit_reported"] == 0, context diff --git a/policyengine_uk/utils/dates.py b/policyengine_uk/utils/dates.py index ec08d2b94e..8f3c112cd0 100644 --- a/policyengine_uk/utils/dates.py +++ b/policyengine_uk/utils/dates.py @@ -125,3 +125,76 @@ def exact_age_in_months(age, months_since_last_birthday) -> np.ndarray: np.asarray(months_since_last_birthday, dtype=np.float64), ) return 12 * whole_years + np.clip(months, 0, _LATEST_MONTHS_SINCE_BIRTHDAY) + + +def birth_instant_from_age(year: int, age, months_since_last_birthday) -> np.ndarray: + """The instant of birth in grid months: age years and + months_since_last_birthday months before 6 October of ``year``.""" + return grid_month(year, 10) - exact_age_in_months(age, months_since_last_birthday) + + +def birth_instant(person, period) -> np.ndarray: + """A person's instant of birth in grid months, in float64, for every rule + that turns on the date of birth. + + Where date_of_birth is given as an input and is not the day that age and + months_since_last_birthday place, it is the start of that day. Otherwise + it is the instant from age and months_since_last_birthday. A situation + that sets date_of_birth for some people gives everyone else 0, which + means not given. + """ + from_age = birth_instant_from_age( + period.start.year, + person("age", period), + person("months_since_last_birthday", period), + ) + given = np.asarray(person("date_of_birth", period), dtype=np.int64) + if (given < 0).any(): + raise ValueError( + "date_of_birth must be a calendar date written as YYYYMMDD, such " + f"as 20170405, or 0 for not given; got {sorted(set(given[given < 0].tolist()))[:5]}." + ) + use_given = (given > 0) & (given != grid_months_to_yyyymmdd(from_age)) + # Any valid date stands in where the input is not used. + safe_given = np.where(use_given, given, 19700106) + given_instant = yyyymmdd_to_grid_months(safe_given) + if use_given.any(): + _check_given_dates_of_birth( + given[use_given], + given_instant[use_given], + np.asarray(person("age", period), dtype=np.float64)[use_given], + period.start.year, + ) + return np.where(use_given, given_instant, from_age) + + +def _check_given_dates_of_birth(dates, instants, ages, year: int) -> None: + """Reject a date_of_birth input that is not a calendar date, or whose + legal age on 6 October is not the person's age: silently normalising + either would misplace the person in every date-of-birth rule.""" + invalid = grid_months_to_yyyymmdd(instants) != dates + if invalid.any(): + raise ValueError( + "date_of_birth must be a calendar date written as YYYYMMDD, such " + f"as 20170405; got {sorted(set(dates[invalid].tolist()))[:5]}." + ) + legal_age = np.floor((grid_month(year, 10) - instants) / 12) + mismatch = legal_age != np.floor(ages) + if mismatch.any(): + examples = [ + f"{d} is aged {int(a)} on 6 October {year}, not {int(np.floor(g))}" + for d, a, g in zip( + dates[mismatch][:5], legal_age[mismatch][:5], ages[mismatch][:5] + ) + ] + raise ValueError( + "Set age to the age on 6 October that date_of_birth gives: " + + "; ".join(examples) + + "." + ) + + +def birth_day(person, period) -> np.ndarray: + """A person's day of birth as a YYYYMMDD number: date_of_birth where + given, and otherwise the day age and months_since_last_birthday place.""" + return grid_months_to_yyyymmdd(birth_instant(person, period)) diff --git a/policyengine_uk/utils/state_pension_age.py b/policyengine_uk/utils/state_pension_age.py index 7904e1029b..ce8d19225d 100644 --- a/policyengine_uk/utils/state_pension_age.py +++ b/policyengine_uk/utils/state_pension_age.py @@ -16,25 +16,11 @@ from policyengine_uk.utils.dates import ( add_months_to_yyyymmdd, - exact_age_in_months, grid_month, - grid_months_to_yyyymmdd, yyyymmdd_to_grid_months, ) -def date_of_birth(age, months_since_last_birthday, year: int) -> tuple: - """The instant of birth, in grid months, and the day of birth (YYYYMMDD). - - The person was born age years and months_since_last_birthday months before - 6 October of the fiscal year starting in ``year``. The day of birth is the - one starting at or after that instant, so the person's legal age on 6 - October is age. - """ - birth = grid_month(year, 10) - exact_age_in_months(age, months_since_last_birthday) - return birth, grid_months_to_yyyymmdd(birth) - - def age_attaining_pensionable_age( timetable, birth, birth_date, male_rule ) -> np.ndarray: @@ -63,14 +49,18 @@ def age_attaining_pensionable_age( return (attained - birth) / 12 -def months_since_attaining(age, months_since_last_birthday, attained_age) -> np.ndarray: - """Months between attaining ``attained_age`` and 6 October; negative if - the person attains it later. +def months_since_attaining(birth, year: int, attained_age) -> np.ndarray: + """Months between attaining ``attained_age`` and 6 October of the fiscal + year starting in ``year``; negative if the person attains it later. + + ``birth`` is the instant of birth in grid months, the same instant the + attained age was measured from (policyengine_uk.utils.dates.birth_instant), + so the result is 6 October less the instant of attainment. Rounding to a thousandth of a month (under an hour) removes float error, so attaining the age exactly on 6 October, or exactly on 6 April six months earlier, counts as having attained it then. """ - age_in_months = exact_age_in_months(age, months_since_last_birthday) + age_in_months = grid_month(year, 10) - np.asarray(birth, dtype=np.float64) months_since = age_in_months - 12 * np.asarray(attained_age, dtype=np.float64) return np.round(months_since, 3) diff --git a/policyengine_uk/variables/gov/dwp/is_CTC_child_limit_exempt.py b/policyengine_uk/variables/gov/dwp/is_CTC_child_limit_exempt.py index 9db8d79370..5630d30d41 100644 --- a/policyengine_uk/variables/gov/dwp/is_CTC_child_limit_exempt.py +++ b/policyengine_uk/variables/gov/dwp/is_CTC_child_limit_exempt.py @@ -1,20 +1,23 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day class is_CTC_child_limit_exempt(Variable): value_type = bool entity = Person label = "Exemption from Child Tax Credit child limit" - documentation = "Exemption from Child Tax Credit limit on number of children based on birth year" + documentation = ( + "Whether the Child Tax Credit child limit does not apply to this child " + "or qualifying young person because they were born before 6 April 2017." + ) definition_period = YEAR + reference = "https://www.legislation.gov.uk/ukpga/2002/21/section/9" def formula(person, period, parameters): - limit_year = parameters( - period - ).gov.dwp.tax_credits.child_tax_credit.limit.start_year - # Children must be born before April 2017. - # We use < 2017 as the closer approximation than <= 2017. - born_before_limit = person("birth_year", period) < limit_year + limit = parameters(period).gov.dwp.tax_credits.child_tax_credit.limit + # Tax Credits Act 2002 s.9(3A): the limit applies to a child or + # qualifying young person born on or after 6 April 2017. + born_before_limit = birth_day(person, period) < limit.born_before # Reform proposal age_exemption = ( diff --git a/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py b/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py index 6e7e36dfbd..2a5406d95e 100644 --- a/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py +++ b/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_instant from policyengine_uk.utils.state_pension_age import months_since_attaining @@ -9,15 +10,17 @@ class months_since_state_pension_age(Variable): documentation = ( "Months between the day this person attains State Pension age and the " "middle of the fiscal year (6 October); negative if they attain it " - "later. A whole age adds months_since_last_birthday; a fractional age " - "is the exact age and overrides those months." + "later. Exact age is measured from the same instant of birth as " + "state_pension_age: date_of_birth where given, otherwise a whole age " + "plus months_since_last_birthday, or a fractional age as the exact " + "age." ) definition_period = YEAR unit = "month" def formula(person, period, parameters): return months_since_attaining( - person("age", period), - person("months_since_last_birthday", period), + birth_instant(person, period), + period.start.year, person("state_pension_age", period), ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py index 8a6677bece..456d065394 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day class child_minimum_guarantee_addition(Variable): @@ -14,18 +15,21 @@ def formula(benunit, period, parameters): is_child_or_qualifying_young_person = person( "is_child_or_qualifying_young_person_for_pension_credit", period ) + # Sch IIA para 10: the eldest child or qualifying young person, by date + # of birth, gets the first child amount if born before 6 April 2017. + date_of_birth = birth_day(person, period) child_index = ( person.get_rank( person.benunit, - -person("age", period), + date_of_birth, condition=is_child_or_qualifying_young_person, ) + 1 ) - first_child_born_before_2017 = (child_index == 1) & ( - person("birth_year", period) < 2017 - ) gc = parameters(period).gov.dwp.pension_credit.guarantee_credit + first_child_born_before_cutoff = (child_index == 1) & ( + date_of_birth < gc.child.first.born_before + ) standard_disability_benefits = gc.child.disability.eligibility severe_disability_benefits = gc.child.disability.severe.eligibility is_disabled = add(person, period, standard_disability_benefits) > 0 @@ -33,7 +37,7 @@ def formula(benunit, period, parameters): is_standard_disabled = is_disabled & ~is_severely_disabled is_not_disabled = ~is_disabled child_addition = where( - first_child_born_before_2017, + first_child_born_before_cutoff, gc.child.first.addition, gc.child.addition, ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py b/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py index 8d077a2379..dee25d4d10 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_instant from policyengine_uk.utils.state_pension_age import months_since_attaining @@ -22,8 +23,8 @@ class has_attained_state_pension_credit_qualifying_age(Variable): def formula(person, period, parameters): months_since = months_since_attaining( - person("age", period), - person("months_since_last_birthday", period), + birth_instant(person, period), + period.start.year, person("state_pension_credit_qualifying_age", period), ) return months_since >= 0 diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py b/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py index 8b516b2b53..35cd614de0 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day class has_mixed_age_couple_pension_credit_saving(Variable): @@ -66,11 +67,11 @@ def reported(variables): receives_uc = reported(["universal_credit_reported"]) # Art. 4(1) needs the couple to have been a mixed-age couple on # 14 May 2019, so its older member had reached the qualifying age by - # then. + # then: born on or before 5 February 1954. older_member_qualified = benunit.any( claimant_or_partner & person("has_attained_state_pension_credit_qualifying_age", period) - & (person("birth_year", period) <= p.saving_latest_birth_year) + & (birth_day(person, period) <= p.saving_latest_birth_date) ) return ( benunit("is_mixed_age_couple", period) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py index e7c69f67ff..1d7fe51aed 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py @@ -1,7 +1,8 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.dates import ( MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR, - exact_age_in_months, + birth_instant, + grid_month, ) @@ -28,7 +29,9 @@ def formula(person, period, parameters): person("months_since_state_pension_age", period) > months_from_cutoff_to_mid_year ) - age_in_months = exact_age_in_months( - person("age", period), person("months_since_last_birthday", period) + # The exact age on 6 October, from the same instant of birth as State + # Pension age. + age_in_months = grid_month(period.start.year, 10) - birth_instant( + person, period ) return reached_before_cutoff & (age_in_months >= 12 * p.minimum_age) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py index a4590c7e8a..3a2129b962 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py @@ -1,8 +1,6 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.state_pension_age import ( - age_attaining_pensionable_age, - date_of_birth, -) +from policyengine_uk.utils.dates import birth_instant, grid_months_to_yyyymmdd +from policyengine_uk.utils.state_pension_age import age_attaining_pensionable_age class state_pension_credit_qualifying_age(Variable): @@ -17,7 +15,7 @@ class state_pension_credit_qualifying_age(Variable): "1953, whose pensionable age is 65. Universal Credit, Housing Benefit, " "Council Tax Reduction, Income Support and Winter Fuel Payment before " "September 2024 use this age as well as Pension Credit. The date of " - "birth comes from age and " + "birth is date_of_birth where given, and otherwise comes from age and " "months_since_last_birthday." ) definition_period = YEAR @@ -29,11 +27,8 @@ class state_pension_credit_qualifying_age(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.state_pension.age - birth, birth_date = date_of_birth( - person("age", period), - person("months_since_last_birthday", period), - period.start.year, - ) + birth = birth_instant(person, period) + birth_date = grid_months_to_yyyymmdd(birth) # State Pension Credit Act 2002 s.1(6): "(a) in the case of a woman, # pensionable age; or (b) in the case of a man, the age which is # pensionable age in the case of a woman born on the same day as the diff --git a/policyengine_uk/variables/gov/dwp/state_pension_age.py b/policyengine_uk/variables/gov/dwp/state_pension_age.py index 6681ef9b63..7701a40826 100644 --- a/policyengine_uk/variables/gov/dwp/state_pension_age.py +++ b/policyengine_uk/variables/gov/dwp/state_pension_age.py @@ -1,7 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_instant, grid_months_to_yyyymmdd from policyengine_uk.utils.state_pension_age import ( age_attaining_pensionable_age, - date_of_birth, ) @@ -13,7 +13,8 @@ class state_pension_age(Variable): "The age at which this person attains State Pension age (pensionable " "age), set by their date of birth. Where the statute sets the day on " "which it is attained, this is the person's age on that day. The date " - "of birth comes from age and months_since_last_birthday." + "of birth is date_of_birth where given, and otherwise comes from age and " + "months_since_last_birthday." ) definition_period = YEAR unit = "year" @@ -24,11 +25,13 @@ class state_pension_age(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.state_pension.age - birth, birth_date = date_of_birth( - person("age", period), - person("months_since_last_birthday", period), - period.start.year, - ) + # The instant of birth: date_of_birth where given, otherwise from age + # and months_since_last_birthday. months_since_state_pension_age + # measures from the same instant, so it is 6 October less the day of + # attainment. The day of birth is the one starting at or after it, so + # the person's legal age on 6 October is age. + birth = birth_instant(person, period) + birth_date = grid_months_to_yyyymmdd(birth) # Pensions Act 1995 Sch 4 para 1 rule (1): men born before 6 December # 1953 attain pensionable age at 65. Everyone else follows the # timetable in rules (2) to (10), which sets either an age or a day. diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_child_index.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_child_index.py index f1f69a3f72..56ffb7fbe4 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_child_index.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_child_index.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day class uc_child_index(Variable): @@ -9,6 +10,7 @@ class uc_child_index(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2013/376/regulation/24", "https://www.legislation.gov.uk/uksi/2013/376/regulation/24A", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/24B", "https://www.gov.uk/universal-credit/what-youll-get", ) @@ -16,10 +18,15 @@ def formula(person, period, parameters): is_uc_child = person( "is_child_or_qualifying_young_person_for_universal_credit", period ) & ~person("is_uc_claimant", period) + # Reg 24B(1) orders children and qualifying young persons by date of + # birth, taking the earliest first. Regs 24A and 24B were revoked from + # 6 April 2026; the eldest child is then still the one for whom the + # reg 43 saving is checked, as it holds if any child was born before + # the cutoff. child_ranking = ( person.get_rank( person.benunit, - -person("age", period), + birth_day(person, period), condition=is_uc_child, ) + 1 diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_is_child_born_before_child_limit.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_is_child_born_before_child_limit.py index 2220ee4586..4eca4b0040 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_is_child_born_before_child_limit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_is_child_born_before_child_limit.py @@ -1,17 +1,26 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day class uc_is_child_born_before_child_limit(Variable): value_type = bool entity = Person label = "Born before Universal Credit child limit" + documentation = ( + "Whether this child or qualifying young person was born before 6 April " + "2017, so is transitionally protected from the child limit, and their " + "responsible claimant keeps the higher child element for the first " + "child." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/24A/2025-04-06", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/43", + ) def formula(person, period, parameters): p = parameters(period).gov.dwp.universal_credit.elements.child.limit - start_year = p.start_year - birth_year = person("birth_year", period) - born_before_limit = birth_year < start_year + born_before_limit = birth_day(person, period) < p.born_before return ( person("is_child_or_qualifying_young_person_for_universal_credit", period) & born_before_limit diff --git a/policyengine_uk/variables/household/demographic/birth_year.py b/policyengine_uk/variables/household/demographic/birth_year.py index f555e29c1f..85cd88b27a 100644 --- a/policyengine_uk/variables/household/demographic/birth_year.py +++ b/policyengine_uk/variables/household/demographic/birth_year.py @@ -1,12 +1,15 @@ from policyengine_uk.model_api import * -import pandas as pd +from policyengine_uk.utils.dates import birth_day class birth_year(Variable): value_type = int entity = Person label = "The birth year of the person" + documentation = ( + "The calendar year of the person's date of birth (date_of_birth where given)." + ) definition_period = YEAR def formula(person, period, parameters): - return period.start.year - person("age", period) + return birth_day(person, period) // 10000 diff --git a/policyengine_uk/variables/household/demographic/date_of_birth.py b/policyengine_uk/variables/household/demographic/date_of_birth.py new file mode 100644 index 0000000000..025f3a934b --- /dev/null +++ b/policyengine_uk/variables/household/demographic/date_of_birth.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import ( + birth_instant_from_age, + grid_months_to_yyyymmdd, +) + + +class date_of_birth(Variable): + value_type = int + entity = Person + label = "date of birth" + documentation = ( + "The person's date of birth, written as a YYYYMMDD number, as date " + "parameters are. The person was born age years and " + "months_since_last_birthday months before the middle of the fiscal " + "year (6 October); the day of birth is the one starting at or after " + "that instant, so the person's legal age on 6 October is age (Family " + "Law Reform Act 1969 s.9(1)). Rules that turn on a date of birth, " + "such as State Pension age and the 6 April 2017 cutoffs in Universal " + "Credit, Child Tax Credit and Pension Credit, compare it with a date. " + "A person's age is held fixed across years, so their date of birth " + "moves with the period. It can be set directly for a year, with age " + "set to the person's age on 6 October of it (anything else raises an " + "error); like age, an input applies only to the year it is given for. " + "A situation that sets it for some people gives the others 0, which " + "the rules read as not given." + ) + definition_period = YEAR + unit = "date" + + def formula(person, period, parameters): + # On a grid whose months start on the 6th: age years and + # months_since_last_birthday months before 6 October. + birth = birth_instant_from_age( + period.start.year, + person("age", period), + person("months_since_last_birthday", period), + ) + return grid_months_to_yyyymmdd(birth)