diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb..73f27e810 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,5 @@ +- bump: patch + changes: + fixed: + - Corrected the is_parent variable to properly identify parents. + - Fixed logic in childcare programs to ensure accurate calculations. \ No newline at end of file diff --git a/policyengine_uk/parameters/gov/dfe/weeks_per_year.yaml b/policyengine_uk/parameters/gov/dfe/weeks_per_year.yaml new file mode 100644 index 000000000..52d25a49e --- /dev/null +++ b/policyengine_uk/parameters/gov/dfe/weeks_per_year.yaml @@ -0,0 +1,14 @@ +description: The Department for Education provides targeted, extended, and universal childcare entitlement for these weeks per year. +metadata: + period: year + unit: week + label: childcare entitlement weeks per year + reference: + - title: The Local Authority (Duty to Secure Early Years Provision Free of Charge) Regulations 2014 - regulation 4 + href: https://www.legislation.gov.uk/uksi/2014/2147/regulation/4/made + - title: Childcare Choices document - 15 and 30 hours childcare support + href: https://www.childcarechoices.gov.uk/15-and-30-hours-childcare-support/working-families/eligibility + - title: Childcare (Early Years Provision Free of Charge) (Extended Entitlement) Regulations 2016 - Regulation 35(3) + href: https://www.legislation.gov.uk/uksi/2016/1257/part/4/made +values: + 2016-01-01: 38 \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml index 287af4c21..cdc9875c6 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml @@ -103,4 +103,37 @@ members: [child1] extended_childcare_entitlement_eligible: false output: - extended_childcare_entitlement: 0 # Not eligible \ No newline at end of file + extended_childcare_entitlement: 0 # Not eligible + +- name: Child using fewer hours than maximum entitlement + period: 2025 + absolute_error_margin: 1 + input: + people: + child1: + age: 3 + max_free_entitlement_hours_used: 20 + benunits: + benunit1: + members: [child1] + extended_childcare_entitlement_eligible: true + output: + extended_childcare_entitlement: 4611.457 # 20 hours * 38 weeks * £6.07 per hour (2025 rate) + +- name: Child using fewer hours than maximum entitlement - multiple children + period: 2025 + absolute_error_margin: 100 + input: + people: + child1: + age: 2 + max_free_entitlement_hours_used: 10 + child2: + age: 3 + max_free_entitlement_hours_used: 15 + benunits: + benunit1: + members: [child1, child2] + extended_childcare_entitlement_eligible: true + output: + extended_childcare_entitlement: 6705.43 # (10 hours * 38 weeks * £8.58 per hour) + (15 hours * 38 weeks * £6.07 per hour) for 2025 rates \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.yaml new file mode 100644 index 000000000..cdf663efd --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.yaml @@ -0,0 +1,80 @@ +- name: Child under 2 (ineligible) + period: 2024 + input: + people: + child: + age: 1 + benunits: + benunit: + members: [child] + targeted_childcare_entitlement_eligible: False + output: + targeted_childcare_entitlement: 0.0 + +- name: Child aged 2 (eligible) + period: 2024 + input: + people: + child: + age: 2 + benunits: + benunit: + members: [child] + targeted_childcare_entitlement_eligible: True + output: + targeted_childcare_entitlement: 4_719.60 # 15 hours * 38 weeks * £8.28 per hour + +- name: Child aged 3 (not eligible by age, but has targeting) + period: 2024 + input: + people: + child: + age: 3 + benunits: + benunit: + members: [child] + targeted_childcare_entitlement_eligible: True + output: + targeted_childcare_entitlement: 0.0 # Not eligible by age even if targeting criteria met + +- name: Child using fewer hours than maximum entitlement + period: 2024 + input: + people: + child: + age: 2 + max_free_entitlement_hours_used: 10 + benunits: + benunit: + members: [child] + targeted_childcare_entitlement_eligible: True + output: + targeted_childcare_entitlement: 3_146.4 # 10 hours * 38 weeks * £8.28 per hour + +- name: Child using exactly maximum entitlement + period: 2024 + input: + people: + child: + age: 2 + max_free_entitlement_hours_used: 15 + benunits: + benunit: + members: [child] + targeted_childcare_entitlement_eligible: True + output: + targeted_childcare_entitlement: 4_719.6 # 15 hours * 38 weeks * £8.28 per hour + +- name: Child using more than maximum entitlement (capped) + period: 2024 + input: + people: + child: + age: 2 + max_free_entitlement_hours_used: 20 + benunits: + benunit: + members: [child] + targeted_childcare_entitlement_eligible: True + output: + targeted_childcare_entitlement: 4_719.6 # Capped at 15 hours * 38 weeks * £8.28 per hour \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.yaml index 6dcc8c55e..9176525d1 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.yaml @@ -1,63 +1,123 @@ - name: Child under 3 isn't eligible (person-level) period: 2024 input: - age: 2 - universal_childcare_entitlement_eligible: False + people: + child: + age: 2 + universal_childcare_entitlement_eligible: False output: universal_childcare_entitlement: 0.0 - name: Child aged 3 is eligible (person-level) period: 2024 input: - age: 3 - universal_childcare_entitlement_eligible: True + people: + child: + age: 3 + universal_childcare_entitlement_eligible: True output: - universal_childcare_entitlement: 3351.60 + universal_childcare_entitlement: 3_351.60 - name: Child aged 4 is eligible (person-level) period: 2024 input: - age: 4 - universal_childcare_entitlement_eligible: True + people: + child: + age: 4 + universal_childcare_entitlement_eligible: True output: - universal_childcare_entitlement: 3351.60 + universal_childcare_entitlement: 3_351.60 - name: Parent (ineligible) period: 2024 input: - age: 35 - universal_childcare_entitlement_eligible: False + people: + parent: + age: 35 + universal_childcare_entitlement_eligible: False output: universal_childcare_entitlement: 0.0 - name: Child aged 2 (ineligible) period: 2024 input: - age: 2 - universal_childcare_entitlement_eligible: False + people: + child: + age: 2 + universal_childcare_entitlement_eligible: False output: universal_childcare_entitlement: 0.0 - name: Child aged 3 (eligible) period: 2024 input: - age: 3 - universal_childcare_entitlement_eligible: True + people: + child: + age: 3 + universal_childcare_entitlement_eligible: True output: - universal_childcare_entitlement: 3351.60 + universal_childcare_entitlement: 3_351.60 - name: Child aged 4 (eligible) period: 2024 input: - age: 4 - universal_childcare_entitlement_eligible: True + people: + child: + age: 4 + universal_childcare_entitlement_eligible: True output: - universal_childcare_entitlement: 3351.60 + universal_childcare_entitlement: 3_351.60 - name: Child aged 5 (ineligible) period: 2024 input: - age: 5 - universal_childcare_entitlement_eligible: False + people: + child: + age: 5 + universal_childcare_entitlement_eligible: False output: - universal_childcare_entitlement: 0.0 \ No newline at end of file + universal_childcare_entitlement: 0.0 + +- name: Child using fewer hours than maximum entitlement + period: 2024 + input: + people: + child: + age: 3 + universal_childcare_entitlement_eligible: True + max_free_entitlement_hours_used: 10 + output: + universal_childcare_entitlement: 2_234.4 # 10 hours * 38 weeks * £5.88 per hour + +- name: Child using half of maximum entitlement + period: 2024 + input: + people: + child: + age: 3 + universal_childcare_entitlement_eligible: True + max_free_entitlement_hours_used: 15 + output: + universal_childcare_entitlement: 3_351.6 # 15 hours * 38 weeks * £5.88 per hour + +- name: Child using exactly maximum entitlement + period: 2024 + input: + people: + child: + age: 4 + universal_childcare_entitlement_eligible: True + max_free_entitlement_hours_used: 15 + output: + universal_childcare_entitlement: 3_351.6 # 15 hours * 38 weeks * £5.88 per hour + +- name: Child using more than maximum entitlement (capped) + period: 2024 + input: + people: + child: + age: 3 + universal_childcare_entitlement_eligible: True + max_free_entitlement_hours_used: 20 + output: + universal_childcare_entitlement: 3_351.6 # Capped at 15 hours * 38 weeks * £5.88 per hour \ No newline at end of file diff --git a/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py b/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py index 4f75d59a8..8e5d70a2e 100644 --- a/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py +++ b/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py @@ -6,6 +6,7 @@ class care_to_learn_eligible(Variable): entity = Person label = "eligible for Care to Learn childcare support" definition_period = YEAR + defined_for = "would_claim_care_to_learn" def formula(person, period, parameters): # Link for instruction: https://www.gov.uk/care-to-learn/eligibility diff --git a/policyengine_uk/variables/gov/dfe/care_to_learn/would_claim_care_to_learn.py b/policyengine_uk/variables/gov/dfe/care_to_learn/would_claim_care_to_learn.py new file mode 100644 index 000000000..b6a7fad53 --- /dev/null +++ b/policyengine_uk/variables/gov/dfe/care_to_learn/would_claim_care_to_learn.py @@ -0,0 +1,12 @@ +from policyengine_uk.model_api import * + + +class would_claim_care_to_learn(Variable): + value_type = bool + entity = BenUnit + label = "would claim Care to Learn" + documentation = ( + "Whether this BenUnit would claim Care to Learn if eligible" + ) + definition_period = YEAR + default_value = True diff --git a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.py b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.py index b95066c39..cbc4fe0e2 100644 --- a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.py +++ b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.py @@ -19,13 +19,19 @@ def formula(benunit, period, parameters): age ) + # Get max hours used per child + max_hours_used = benunit.members( + "max_free_entitlement_hours_used", period + ) + + # Use the appropriate hours based on the condition + weekly_hours_to_use = min_(max_hours_used, weekly_hours_per_child) + # Compute weekly subsidy per child weekly_subsidy_per_child = ( - weekly_hours_per_child * p.childcare_funding_rate.calc(age) + weekly_hours_to_use * p.childcare_funding_rate.calc(age) ) # Compute total annual expenses - return ( - benunit.sum(weekly_subsidy_per_child) - * p.extended_childcare_entitlement.weeks_per_year - ) + weeks = p.weeks_per_year + return benunit.sum(weekly_subsidy_per_child) * weeks diff --git a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py index 1238d2c4b..c7bc26dce 100644 --- a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py +++ b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py @@ -6,6 +6,7 @@ class extended_childcare_entitlement_eligible(Variable): entity = BenUnit label = "eligibility for extended childcare entitlement" definition_period = YEAR + defined_for = "would_claim_extended_childcare" def formula(benunit, period, parameters): # Check if household is in England diff --git a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/would_claim_extended_childcare.py b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/would_claim_extended_childcare.py new file mode 100644 index 000000000..070c50542 --- /dev/null +++ b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/would_claim_extended_childcare.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class would_claim_extended_childcare(Variable): + value_type = bool + entity = BenUnit + label = "would claim extended childcare entitlement" + documentation = "Whether this family would claim extended childcare entitlement if eligible" + definition_period = YEAR + default_value = True diff --git a/policyengine_uk/variables/gov/dfe/max_free_entitlement_hours_used.py b/policyengine_uk/variables/gov/dfe/max_free_entitlement_hours_used.py new file mode 100644 index 000000000..3dc51f346 --- /dev/null +++ b/policyengine_uk/variables/gov/dfe/max_free_entitlement_hours_used.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class max_free_entitlement_hours_used(Variable): + value_type = float + entity = Person + label = "maximum hours of free childcare entitlement used" + documentation = "The maximum weekly hours of free childcare entitlement used by the person" + definition_period = YEAR + default_value = 30 diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.py index 0f20aafb7..c5e3e7ef4 100644 --- a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.py +++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement.py @@ -19,4 +19,9 @@ def formula(person, period, parameters): p.targeted_childcare_entitlement.hours_entitlement * eligible_by_age ) - return hours * p.childcare_funding_rate.calc(age) + max_hours_used = person("max_free_entitlement_hours_used", period) + weeks = p.weeks_per_year + total_hours_used = max_hours_used * weeks + + hours_to_use = min_(total_hours_used, hours) + return hours_to_use * p.childcare_funding_rate.calc(age) diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py index 4bee5e243..47be60f5e 100644 --- a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py +++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py @@ -6,6 +6,7 @@ class targeted_childcare_entitlement_eligible(Variable): entity = BenUnit label = "eligibility for targeted childcare entitlement" definition_period = YEAR + defined_for = "would_claim_targeted_childcare" def formula(benunit, period, parameters): @@ -16,6 +17,12 @@ def formula(benunit, period, parameters): # Get parameters p = parameters(period).gov.dfe.targeted_childcare_entitlement + # Check if household has extended childcare. If so, they are not eligible. Combining extended childcare and targeted childcare is not allowed. + # https://www.childcarechoices.gov.uk/combining-schemes + has_extended_childcare = benunit( + "extended_childcare_entitlement_eligible", period + ) + # Check if household receives any qualifying benefits has_qualifying_benefits = ( add(benunit, period, p.qualifying_benefits) > 0 @@ -24,4 +31,8 @@ def formula(benunit, period, parameters): # Check if household meets any additional qualifying criteria # from qualifying_criteria.yaml (UC/TC specific criteria) meets_any_criteria = add(benunit, period, p.qualifying_criteria) > 0 - return in_england & (has_qualifying_benefits | meets_any_criteria) + return ( + in_england + & (has_qualifying_benefits | meets_any_criteria) + & ~has_extended_childcare + ) diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/would_claim_targeted_childcare.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/would_claim_targeted_childcare.py new file mode 100644 index 000000000..bb755228f --- /dev/null +++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/would_claim_targeted_childcare.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class would_claim_targeted_childcare(Variable): + value_type = bool + entity = BenUnit + label = "would claim targeted childcare entitlement" + documentation = "Whether this family would claim targeted childcare entitlement if eligible" + definition_period = YEAR + default_value = True diff --git a/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.py b/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.py index 6a65101a9..1e6ebf520 100644 --- a/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.py +++ b/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement.py @@ -13,4 +13,9 @@ def formula(person, period, parameters): p = parameters(period).gov.dfe age = person("age", period) hours = p.universal_childcare_entitlement.hours - return hours * p.childcare_funding_rate.calc(age) + max_hours_used = person("max_free_entitlement_hours_used", period) + weeks = p.weeks_per_year + total_hours_used = max_hours_used * weeks + + hours_to_use = min_(total_hours_used, hours) + return hours_to_use * p.childcare_funding_rate.calc(age) diff --git a/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement_eligible.py b/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement_eligible.py index 811f34a75..9e6a9892f 100644 --- a/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement_eligible.py +++ b/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/universal_childcare_entitlement_eligible.py @@ -6,6 +6,7 @@ class universal_childcare_entitlement_eligible(Variable): entity = Person label = "eligible for universal childcare entitlement" definition_period = YEAR + defined_for = "would_claim_universal_childcare" def formula(person, period, parameters): country = person.household("country", period) @@ -18,6 +19,18 @@ def formula(person, period, parameters): p = parameters(period).gov.dfe.universal_childcare_entitlement meets_age_condition = (age >= p.min_age) & (age < p.max_age) not_compulsory_age = ~person("is_of_compulsory_school_age", period) + + # Check if person has extended childcare. If so, they are not eligible. Combining extended childcare and universal childcare is not allowed. + # https://www.childcarechoices.gov.uk/combining-schemes + has_extended_childcare = person.benunit( + "extended_childcare_entitlement_eligible", period + ) + # Section 7 of the Childcare Act 2006 # The regulation above limits free early years provision to children under compulsory school age. - return in_england & meets_age_condition & not_compulsory_age + return ( + in_england + & meets_age_condition + & not_compulsory_age + & ~has_extended_childcare + ) diff --git a/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/would_claim_universal_childcare.py b/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/would_claim_universal_childcare.py new file mode 100644 index 000000000..b6066b4bc --- /dev/null +++ b/policyengine_uk/variables/gov/dfe/universal_childcare_entitlement/would_claim_universal_childcare.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class would_claim_universal_childcare(Variable): + value_type = bool + entity = BenUnit + label = "would claim universal childcare entitlement" + documentation = "Whether this BenUnit would claim universal childcare entitlement if eligible" + definition_period = YEAR + default_value = True diff --git a/policyengine_uk/variables/gov/hmrc/tax_free_childcare/tax_free_childcare_eligibility.py b/policyengine_uk/variables/gov/hmrc/tax_free_childcare/tax_free_childcare_eligibility.py index d3ac579c3..92686b15b 100644 --- a/policyengine_uk/variables/gov/hmrc/tax_free_childcare/tax_free_childcare_eligibility.py +++ b/policyengine_uk/variables/gov/hmrc/tax_free_childcare/tax_free_childcare_eligibility.py @@ -6,6 +6,7 @@ class tax_free_childcare_eligible(Variable): entity = BenUnit label = "overall eligibility for tax-free childcare" definition_period = YEAR + defined_for = "would_claim_tfc" def formula(benunit, period, parameters): meets_age_condition = benunit.any( diff --git a/policyengine_uk/variables/gov/hmrc/tax_free_childcare/would_claim_tfc.py b/policyengine_uk/variables/gov/hmrc/tax_free_childcare/would_claim_tfc.py new file mode 100644 index 000000000..2a623372c --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/tax_free_childcare/would_claim_tfc.py @@ -0,0 +1,12 @@ +from policyengine_uk.model_api import * + + +class would_claim_tfc(Variable): + value_type = bool + entity = BenUnit + label = "would claim Tax-Free Childcare" + documentation = ( + "Whether this family would claim Tax-Free Childcare if eligible" + ) + definition_period = YEAR + default_value = True diff --git a/policyengine_uk/variables/household/demographic/is_parent.py b/policyengine_uk/variables/household/demographic/is_parent.py index 415bae7aa..540c8202d 100644 --- a/policyengine_uk/variables/household/demographic/is_parent.py +++ b/policyengine_uk/variables/household/demographic/is_parent.py @@ -14,22 +14,19 @@ def formula(person, period, parameters): # Find two oldest members benunit_ages = benunit.members("age", period) - first_highest = benunit.max(benunit_ages) - second_highest = benunit.max( - where(benunit_ages < first_highest, benunit_ages, -np.inf) - ) + adult_index = person("adult_index", period) # Get family types enum family_types = family_type.possible_values # For lone parents (FamilyType.LONE_PARENT), only the eldest is parent is_lone_parent = (family_type == family_types.LONE_PARENT) & ( - age == first_highest + adult_index == 1 ) # For couples with children (FamilyType.COUPLE_WITH_CHILDREN), two eldest are parents is_couple_parent = ( family_type == family_types.COUPLE_WITH_CHILDREN - ) & ((age == first_highest) | (age == second_highest)) + ) & ((adult_index == 1) | (adult_index == 2)) return is_lone_parent | is_couple_parent