From 564835eb11dcc5c057414fbf5db3bb2f9ea39811 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 15 Jan 2026 16:45:43 -0500 Subject: [PATCH] Add Reform.from_dict() documentation Fixes #420 Add comprehensive documentation for the Reform.from_dict() method: - Method signature and parameters - Dictionary format with parameter paths and period-value mappings - Period format explanation (YYYY-MM-DD.YYYY-MM-DD) - Examples: basic reforms, multiple parameters, bracket syntax - Shorthand for permanent changes - Named reforms for API integration - Integration with Microsimulation class Co-Authored-By: Claude Opus 4.5 --- changelog_entry.yaml | 4 + docs/usage/reforms.ipynb | 444 ++++++++++++++++++++++++++++----------- 2 files changed, 329 insertions(+), 119 deletions(-) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb..19d4a0b81 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: patch + changes: + added: + - Documentation for Reform.from_dict() method with examples covering basic reforms, multiple parameters, bracket syntax, period formats, and Microsimulation integration. diff --git a/docs/usage/reforms.ipynb b/docs/usage/reforms.ipynb index 7827ced97..49f228bdf 100644 --- a/docs/usage/reforms.ipynb +++ b/docs/usage/reforms.ipynb @@ -1,124 +1,330 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Reforms\n", - "\n", - "To define a reform, simply define a class inheriting from `Reform` with an `apply(self)` function. Inside it, `self` is the tax-benefit system attached to the simulation with loaded data `self.simulation: Simulation`. From this, you can run any kind of modification on the `Simulation` instance that you like- modify parameters, variable logic or even adjust simulation data." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from policyengine_core.country_template import Microsimulation\n", - "from policyengine_core.model_api import *\n", - "\n", - "baseline = Microsimulation()\n", - "\n", - "\n", - "class reform(Reform):\n", - " def apply(self):\n", - " simulation = self.simulation\n", - "\n", - " # Modify parameters\n", - "\n", - " simulation.tax_benefit_system.parameters.taxes.housing_tax.rate.update(\n", - " 20\n", - " )\n", - "\n", - " # Modify simulation data\n", - "\n", - " salary = simulation.calculate(\"salary\", \"2022-01\")\n", - "\n", - " new_salary = salary * 1.1\n", - "\n", - " simulation.set_input(\"salary\", \"2022-01\", new_salary)\n", - "\n", - "\n", - "reformed = Microsimulation(reform=reform)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "( value weight\n", - " 0 110.0 1000000.0\n", - " 1 0.0 1000000.0\n", - " 2 220.0 1200000.0,\n", - " value weight\n", - " 0 100.0 1000000.0\n", - " 1 0.0 1000000.0\n", - " 2 200.0 1200000.0)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Reforms\n", + "\n", + "Reforms allow you to modify a tax-benefit system's parameters or variable logic to simulate policy changes. PolicyEngine Core provides two main approaches to creating reforms:\n", + "\n", + "1. **`Reform.from_dict()`** - Create reforms programmatically from a dictionary (recommended for most use cases)\n", + "2. **Reform subclass** - Create a custom class for complex reforms requiring variable logic changes\n", + "\n", + "## Creating reforms with `Reform.from_dict()`\n", + "\n", + "The `Reform.from_dict()` method is the simplest way to create parameter-based reforms. It takes a dictionary mapping parameter paths to their new values." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Method signature\n", + "\n", + "```python\n", + "Reform.from_dict(\n", + " parameter_values: dict, # Parameter path -> period -> value mappings\n", + " country_id: str = None, # Optional: country code for API integration\n", + " name: str = None, # Optional: human-readable name for the reform\n", + ") -> Reform\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dictionary format\n", + "\n", + "The `parameter_values` dictionary uses parameter paths as keys and period-value mappings as values:\n", + "\n", + "```python\n", + "{\n", + " 'path.to.parameter': {\n", + " 'YYYY-MM-DD.YYYY-MM-DD': value, # Period format: start.end\n", + " },\n", + " 'path.to.another.parameter': {\n", + " '2024-01-01.2100-12-31': 1000, # Example: effective 2024 onwards\n", + " },\n", + "}\n", + "```\n", + "\n", + "**Period formats:**\n", + "- `'YYYY-MM-DD.YYYY-MM-DD'` - Date range (start date, end date separated by `.`)\n", + "- `'year:YYYY:N'` - N years starting from YYYY (e.g., `'year:2024:10'` for 2024-2033)\n", + "- Simple value (no dict) - Applies for 100 years from 2000 (convenience shorthand)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Basic example\n", + "\n", + "Let's create a simple reform that increases the basic income amount in the country template." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from policyengine_core.country_template import Microsimulation\n", + "from policyengine_core.reforms import Reform\n", + "\n", + "# Create a reform that increases basic income from 600 to 1000\n", + "basic_income_reform = Reform.from_dict(\n", + " {\n", + " 'benefits.basic_income': {\n", + " '2020-01-01.2100-12-31': 1000,\n", + " }\n", + " }\n", + ")\n", + "\n", + "# Run simulations\n", + "baseline = Microsimulation()\n", + "reformed = Microsimulation(reform=basic_income_reform)\n", + "\n", + "# Compare results\n", + "print(\"Baseline basic income:\")\n", + "print(baseline.calculate(\"basic_income\", \"2022-01\"))\n", + "print(\"\\nReformed basic income:\")\n", + "print(reformed.calculate(\"basic_income\", \"2022-01\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Multiple parameters\n", + "\n", + "You can modify multiple parameters in a single reform." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reform modifying multiple parameters\n", + "multi_param_reform = Reform.from_dict(\n", + " {\n", + " 'benefits.basic_income': {\n", + " '2020-01-01.2100-12-31': 800,\n", + " },\n", + " 'taxes.income_tax_rate': {\n", + " '2020-01-01.2100-12-31': 0.20, # Increase from 0.15 to 0.20\n", + " },\n", + " }\n", + ")\n", + "\n", + "reformed_multi = Microsimulation(reform=multi_param_reform)\n", + "\n", + "print(\"Reformed basic income:\", reformed_multi.calculate(\"basic_income\", \"2022-01\"))\n", + "print(\"Reformed income tax:\", reformed_multi.calculate(\"income_tax\", \"2022-01\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bracket parameters\n", + "\n", + "For parameters with brackets (like tax scales), use array index notation: `parameter.brackets[index].rate` or `parameter.brackets[index].threshold`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reform modifying a bracket parameter (social security contribution scale)\n", + "bracket_reform = Reform.from_dict(\n", + " {\n", + " # Modify the first bracket's rate\n", + " 'taxes.social_security_contribution.brackets[0].rate': {\n", + " '2020-01-01.2100-12-31': 0.05,\n", + " },\n", + " # Modify the second bracket's threshold\n", + " 'taxes.social_security_contribution.brackets[1].threshold': {\n", + " '2020-01-01.2100-12-31': 15000,\n", + " },\n", + " }\n", + ")\n", + "\n", + "reformed_bracket = Microsimulation(reform=bracket_reform)\n", + "print(\"Reformed social security contribution:\", reformed_bracket.calculate(\"social_security_contribution\", \"2022-01\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Shorthand for permanent changes\n", + "\n", + "If you want a parameter change to apply indefinitely, you can omit the period dictionary and just provide the value directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Shorthand: value applies from year 2000 for 100 years\n", + "simple_reform = Reform.from_dict(\n", + " {\n", + " 'benefits.basic_income': 1200, # No period dict needed\n", + " 'taxes.income_tax_rate': 0.18,\n", + " }\n", + ")\n", + "\n", + "reformed_simple = Microsimulation(reform=simple_reform)\n", + "print(\"Basic income:\", reformed_simple.calculate(\"basic_income\", \"2022-01\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Named reforms for API integration\n", + "\n", + "When working with the PolicyEngine API, you can provide a `country_id` and `name` for better integration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Named reform with country ID (useful for API integration)\n", + "named_reform = Reform.from_dict(\n", + " {\n", + " 'benefits.basic_income': {\n", + " '2024-01-01.2100-12-31': 1500,\n", + " },\n", + " },\n", + " country_id='country_template',\n", + " name='Increased Basic Income Reform',\n", + ")\n", + "\n", + "# The reform class now has these attributes set\n", + "print(f\"Reform name: {named_reform.name}\")\n", + "print(f\"Country ID: {named_reform.country_id}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating reforms with a Reform subclass\n", + "\n", + "For more complex reforms that need to modify variable logic or simulation data, define a class inheriting from `Reform` with an `apply(self)` method. Inside it, `self` is the tax-benefit system attached to the simulation with loaded data via `self.simulation: Simulation`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from policyengine_core.model_api import *\n", + "\n", + "baseline = Microsimulation()\n", + "\n", + "\n", + "class custom_reform(Reform):\n", + " def apply(self):\n", + " simulation = self.simulation\n", + "\n", + " # Modify parameters\n", + " simulation.tax_benefit_system.parameters.taxes.housing_tax.rate.update(\n", + " 20\n", + " )\n", + "\n", + " # Modify simulation data\n", + " salary = simulation.calculate(\"salary\", \"2022-01\")\n", + " new_salary = salary * 1.1\n", + " simulation.set_input(\"salary\", \"2022-01\", new_salary)\n", + "\n", + "\n", + "reformed = Microsimulation(reform=custom_reform)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "reformed.calculate(\"salary\", \"2022-01\"), baseline.calculate(\n", + " \"salary\", \"2022-01\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "reformed.calculate(\"housing_tax\", 2022), baseline.calculate(\n", + " \"housing_tax\", 2022\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparing baseline and reformed simulations\n", + "\n", + "A common workflow is to compare results between baseline and reformed scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create reform\n", + "reform = Reform.from_dict(\n", + " {'benefits.basic_income': {'2020-01-01.2100-12-31': 1000}}\n", + ")\n", + "\n", + "# Run both simulations\n", + "baseline = Microsimulation()\n", + "reformed = Microsimulation(reform=reform)\n", + "\n", + "# Calculate and compare\n", + "baseline_income = baseline.calculate(\"basic_income\", \"2022-01\")\n", + "reformed_income = reformed.calculate(\"basic_income\", \"2022-01\")\n", + "\n", + "print(\"Baseline basic income:\")\n", + "print(baseline_income)\n", + "print(\"\\nReformed basic income:\")\n", + "print(reformed_income)\n", + "print(f\"\\nDifference: {reformed_income['value'].sum() - baseline_income['value'].sum():.2f}\")" + ] } - ], - "source": [ - "reformed.calculate(\"salary\", \"2022-01\"), baseline.calculate(\n", - " \"salary\", \"2022-01\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "( value weight\n", - " 0 200.0 1000000.0\n", - " 1 200.0 1200000.0,\n", - " value weight\n", - " 0 200.0 1000000.0\n", - " 1 200.0 1200000.0)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" } - ], - "source": [ - "reformed.calculate(\"housing_tax\", 2022), baseline.calculate(\n", - " \"housing_tax\", 2022\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 4 }