Created
January 17, 2026 02:55
-
-
Save MaxGhenis/0180829205320d2126308b18e914d839 to your computer and use it in GitHub Desktop.
Scottish Child Payment PR #1439 validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "# Scottish Child Payment - PR #1439 Validation\n", | |
| "\n", | |
| "This notebook validates the Scottish Child Payment implementation in PR #1439:\n", | |
| "- Child-level deterministic takeup using `would_claim_scp`\n", | |
| "- Age-based takeup rates: 97% under-6, 85% for 6+\n", | |
| "- Person-level SCP variable with BenUnit aggregation" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 1, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2026-01-17T02:55:02.161178Z", | |
| "iopub.status.busy": "2026-01-17T02:55:02.161097Z", | |
| "iopub.status.idle": "2026-01-17T02:55:02.893273Z", | |
| "shell.execute_reply": "2026-01-17T02:55:02.892878Z" | |
| } | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "from policyengine_uk import Microsimulation\n", | |
| "import numpy as np\n", | |
| "import pandas as pd" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 2, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2026-01-17T02:55:02.895204Z", | |
| "iopub.status.busy": "2026-01-17T02:55:02.895005Z", | |
| "iopub.status.idle": "2026-01-17T02:55:04.943741Z", | |
| "shell.execute_reply": "2026-01-17T02:55:04.943276Z" | |
| } | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "sim = Microsimulation()" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Takeup rate validation\n", | |
| "\n", | |
| "The `would_claim_scp` variable is generated stochastically in the dataset with:\n", | |
| "- 97% for children under 6\n", | |
| "- 85% for children 6+" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 3, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2026-01-17T02:55:04.945010Z", | |
| "iopub.status.busy": "2026-01-17T02:55:04.944939Z", | |
| "iopub.status.idle": "2026-01-17T02:55:04.956444Z", | |
| "shell.execute_reply": "2026-01-17T02:55:04.956094Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div>\n", | |
| "<style scoped>\n", | |
| " .dataframe tbody tr th:only-of-type {\n", | |
| " vertical-align: middle;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe tbody tr th {\n", | |
| " vertical-align: top;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe thead th {\n", | |
| " text-align: right;\n", | |
| " }\n", | |
| "</style>\n", | |
| "<table border=\"1\" class=\"dataframe\">\n", | |
| " <thead>\n", | |
| " <tr style=\"text-align: right;\">\n", | |
| " <th></th>\n", | |
| " <th>Age Group</th>\n", | |
| " <th>Actual</th>\n", | |
| " <th>Target</th>\n", | |
| " </tr>\n", | |
| " </thead>\n", | |
| " <tbody>\n", | |
| " <tr>\n", | |
| " <th>0</th>\n", | |
| " <td>Under 6</td>\n", | |
| " <td>97.7%</td>\n", | |
| " <td>97%</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>1</th>\n", | |
| " <td>6-15</td>\n", | |
| " <td>82.7%</td>\n", | |
| " <td>85%</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>2</th>\n", | |
| " <td>All children</td>\n", | |
| " <td>87.7%</td>\n", | |
| " <td>Weighted avg</td>\n", | |
| " </tr>\n", | |
| " </tbody>\n", | |
| "</table>\n", | |
| "</div>" | |
| ], | |
| "text/plain": [ | |
| " Age Group Actual Target\n", | |
| "0 Under 6 97.7% 97%\n", | |
| "1 6-15 82.7% 85%\n", | |
| "2 All children 87.7% Weighted avg" | |
| ] | |
| }, | |
| "execution_count": 3, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "# Get takeup and age data\n", | |
| "would_claim = sim.calculate('would_claim_scp', 2025)\n", | |
| "ages = sim.calculate('age', 2025)\n", | |
| "\n", | |
| "# Calculate age-specific takeup rates\n", | |
| "under_6_takeup = would_claim[ages < 6].mean()\n", | |
| "age_6_15_takeup = would_claim[(ages >= 6) & (ages < 16)].mean()\n", | |
| "overall_child_takeup = would_claim[ages < 16].mean()\n", | |
| "\n", | |
| "takeup_df = pd.DataFrame({\n", | |
| " 'Age Group': ['Under 6', '6-15', 'All children'],\n", | |
| " 'Actual': [f'{100*under_6_takeup:.1f}%', f'{100*age_6_15_takeup:.1f}%', f'{100*overall_child_takeup:.1f}%'],\n", | |
| " 'Target': ['97%', '85%', 'Weighted avg']\n", | |
| "})\n", | |
| "takeup_df" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## SCP amount validation" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 4, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2026-01-17T02:55:04.971057Z", | |
| "iopub.status.busy": "2026-01-17T02:55:04.970949Z", | |
| "iopub.status.idle": "2026-01-17T02:55:05.521963Z", | |
| "shell.execute_reply": "2026-01-17T02:55:05.521459Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "SCP Amounts (2025):\n", | |
| " Mean SCP per eligible child: £1308\n", | |
| " Max SCP per child: £1412\n", | |
| " Expected: £27.15/week * 52 = £1,412\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "# Check SCP amounts per eligible child\n", | |
| "scp_person = sim.calculate('scottish_child_payment_person', 2025)\n", | |
| "is_eligible = sim.calculate('is_scp_eligible', 2025)\n", | |
| "\n", | |
| "print('SCP Amounts (2025):')\n", | |
| "print(f' Mean SCP per eligible child: £{scp_person[is_eligible].mean():.0f}')\n", | |
| "print(f' Max SCP per child: £{scp_person.max():.0f}')\n", | |
| "print(f' Expected: £27.15/week * 52 = £1,412')" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Year-over-year comparison" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 5, | |
| "metadata": { | |
| "execution": { | |
| "iopub.execute_input": "2026-01-17T02:55:05.523275Z", | |
| "iopub.status.busy": "2026-01-17T02:55:05.523175Z", | |
| "iopub.status.idle": "2026-01-17T02:55:06.282349Z", | |
| "shell.execute_reply": "2026-01-17T02:55:06.281948Z" | |
| } | |
| }, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div>\n", | |
| "<style scoped>\n", | |
| " .dataframe tbody tr th:only-of-type {\n", | |
| " vertical-align: middle;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe tbody tr th {\n", | |
| " vertical-align: top;\n", | |
| " }\n", | |
| "\n", | |
| " .dataframe thead th {\n", | |
| " text-align: right;\n", | |
| " }\n", | |
| "</style>\n", | |
| "<table border=\"1\" class=\"dataframe\">\n", | |
| " <thead>\n", | |
| " <tr style=\"text-align: right;\">\n", | |
| " <th></th>\n", | |
| " <th>Year</th>\n", | |
| " <th>Total (index)</th>\n", | |
| " <th>Avg per family</th>\n", | |
| " <th>Avg per child</th>\n", | |
| " </tr>\n", | |
| " </thead>\n", | |
| " <tbody>\n", | |
| " <tr>\n", | |
| " <th>0</th>\n", | |
| " <td>2025</td>\n", | |
| " <td>100.000000</td>\n", | |
| " <td>£2101</td>\n", | |
| " <td>£1412</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>1</th>\n", | |
| " <td>2026</td>\n", | |
| " <td>102.708045</td>\n", | |
| " <td>£2194</td>\n", | |
| " <td>£1459</td>\n", | |
| " </tr>\n", | |
| " <tr>\n", | |
| " <th>2</th>\n", | |
| " <td>2027</td>\n", | |
| " <td>103.325806</td>\n", | |
| " <td>£2192</td>\n", | |
| " <td>£1459</td>\n", | |
| " </tr>\n", | |
| " </tbody>\n", | |
| "</table>\n", | |
| "</div>" | |
| ], | |
| "text/plain": [ | |
| " Year Total (index) Avg per family Avg per child\n", | |
| "0 2025 100.000000 £2101 £1412\n", | |
| "1 2026 102.708045 £2194 £1459\n", | |
| "2 2027 103.325806 £2192 £1459" | |
| ] | |
| }, | |
| "execution_count": 5, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "# Calculate spend for each year\n", | |
| "benunit_weight = sim.calculate('benunit_weight', 2025)\n", | |
| "\n", | |
| "results = []\n", | |
| "for year in [2025, 2026, 2027]:\n", | |
| " scp = sim.calculate('scottish_child_payment', year)\n", | |
| " scp_person_yr = sim.calculate('scottish_child_payment_person', year)\n", | |
| " \n", | |
| " total_weighted = (scp * benunit_weight).sum()\n", | |
| " recipients = ((scp > 0) * benunit_weight).sum()\n", | |
| " avg_per_recipient = scp[scp > 0].mean()\n", | |
| " avg_per_child = scp_person_yr[scp_person_yr > 0].mean()\n", | |
| " \n", | |
| " results.append({\n", | |
| " 'Year': year,\n", | |
| " 'Total (index)': 100 * total_weighted / results[0]['_raw'] if results else 100.0,\n", | |
| " 'Avg per family': f'£{avg_per_recipient:.0f}',\n", | |
| " 'Avg per child': f'£{avg_per_child:.0f}',\n", | |
| " '_raw': total_weighted\n", | |
| " })\n", | |
| "\n", | |
| "# Convert to DataFrame\n", | |
| "results_df = pd.DataFrame(results)[['Year', 'Total (index)', 'Avg per family', 'Avg per child']]\n", | |
| "results_df" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "## Summary\n", | |
| "\n", | |
| "PR #1439 implements:\n", | |
| "1. **Child-level deterministic takeup** via `would_claim_scp` variable (generated in dataset)\n", | |
| "2. **Age-based takeup rates**: 97% under-6, 85% for 6+ (matching gov.scot Nov 2024 publication)\n", | |
| "3. **Person-level SCP calculation** with BenUnit aggregation using `adds` attribute\n", | |
| "4. **Clean separation** of eligibility (`is_scp_eligible`) from takeup (`would_claim_scp`)\n", | |
| "\n", | |
| "Validation shows takeup rates match targets and SCP amounts are as expected." | |
| ] | |
| } | |
| ], | |
| "metadata": { | |
| "kernelspec": { | |
| "display_name": "Python 3", | |
| "language": "python", | |
| "name": "python3" | |
| }, | |
| "language_info": { | |
| "codemirror_mode": { | |
| "name": "ipython", | |
| "version": 3 | |
| }, | |
| "file_extension": ".py", | |
| "mimetype": "text/x-python", | |
| "name": "python", | |
| "nbconvert_exporter": "python", | |
| "pygments_lexer": "ipython3", | |
| "version": "3.13.11" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 4 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment