diff --git a/.github/workflows/any_changes.yaml b/.github/workflows/any_changes.yaml index 91d39f36..2f12633c 100644 --- a/.github/workflows/any_changes.yaml +++ b/.github/workflows/any_changes.yaml @@ -8,6 +8,9 @@ on: jobs: docs: + permissions: + contents: "read" + id-token: "write" name: Test documentation builds runs-on: ubuntu-latest steps: @@ -20,14 +23,16 @@ jobs: uses: actions/setup-python@v2 with: python-version: '3.11' + - uses: "google-github-actions/auth@v2" + with: + workload_identity_provider: "projects/322898545428/locations/global/workloadIdentityPools/policyengine-research-id-pool/providers/prod-github-provider" + service_account: "policyengine-research@policyengine-research.iam.gserviceaccount.com" - name: Install package run: uv pip install .[dev] --system - name: Test documentation builds run: make documentation - env: - HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - name: Check documentation build run: | diff --git a/.github/workflows/code_changes.yaml b/.github/workflows/code_changes.yaml index 7c7714ee..3bb144cd 100644 --- a/.github/workflows/code_changes.yaml +++ b/.github/workflows/code_changes.yaml @@ -21,6 +21,9 @@ jobs: args: ". -l 79 --check" Test: runs-on: ubuntu-latest + permissions: + contents: "read" + id-token: "write" steps: - name: Checkout repo uses: actions/checkout@v2 @@ -31,11 +34,13 @@ jobs: uses: actions/setup-python@v2 with: python-version: '3.11' + - uses: "google-github-actions/auth@v2" + with: + workload_identity_provider: "projects/322898545428/locations/global/workloadIdentityPools/policyengine-research-id-pool/providers/prod-github-provider" + service_account: "policyengine-research@policyengine-research.iam.gserviceaccount.com" - name: Install package run: uv pip install .[dev] --system - name: Run tests - run: make test - env: - HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} \ No newline at end of file + run: make test \ No newline at end of file diff --git a/.github/workflows/publish_documentation.yaml b/.github/workflows/publish_documentation.yaml index 3196d82c..10143cb0 100644 --- a/.github/workflows/publish_documentation.yaml +++ b/.github/workflows/publish_documentation.yaml @@ -7,6 +7,9 @@ on: jobs: Publish: + permissions: + contents: "read" + id-token: "write" runs-on: ubuntu-latest steps: - name: Checkout repo @@ -15,6 +18,10 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.12 + - uses: "google-github-actions/auth@v2" + with: + workload_identity_provider: "projects/322898545428/locations/global/workloadIdentityPools/policyengine-research-id-pool/providers/prod-github-provider" + service_account: "policyengine-research@policyengine-research.iam.gserviceaccount.com" - name: Publish a git tag run: ".github/publish-git-tag.sh || true" - name: Install package @@ -22,8 +29,6 @@ jobs: - name: Build documentation run: make documentation - env: - HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - name: Deploy documentation uses: JamesIves/github-pages-deploy-action@releases/v3 diff --git a/.github/workflows/publish_package.yaml b/.github/workflows/publish_package.yaml index 9a7fcc1e..167f0685 100644 --- a/.github/workflows/publish_package.yaml +++ b/.github/workflows/publish_package.yaml @@ -36,8 +36,6 @@ jobs: - name: Test documentation builds run: make documentation - env: - HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - name: Deploy documentation uses: JamesIves/github-pages-deploy-action@releases/v3 diff --git a/.gitignore b/.gitignore index 42030f64..a210954a 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,6 @@ cython_debug/ *.ipynb !docs/**/*.ipynb + +**/*.h5 +**/*.csv diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29b..c500fbe0 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - Error handling for data and package version mismatches. diff --git a/docs/concepts/simulation.ipynb b/docs/concepts/simulation.ipynb index 2aedd524..82635f8b 100644 --- a/docs/concepts/simulation.ipynb +++ b/docs/concepts/simulation.ipynb @@ -22,13 +22,39 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/nikhilwoodruff/policyengine/policyengine.py/.venv/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n", + "INFO:root:Using Google Cloud Storage for download.\n", + "WARNING:root:No metadata found for blob, so it has no version attached.\n", + "WARNING:root:No version specified for policyengine-uk-data-private, enhanced_frs_2022_23.h5. Using latest version: None\n", + "INFO:policyengine.utils.data.caching_google_storage_client:Syncing policyengine-uk-data-private, enhanced_frs_2022_23.h5, None to cache\n", + "INFO:policyengine.utils.data.simplified_google_storage_client:Downloading policyengine-uk-data-private, enhanced_frs_2022_23.h5\n", + "INFO:policyengine.utils.data.caching_google_storage_client:Copying downloaded data for policyengine-uk-data-private, enhanced_frs_2022_23.h5 to enhanced_frs_2022_23.h5\n", + "INFO:root:Using Google Cloud Storage for download.\n", + "WARNING:root:No metadata found for blob, so it has no version attached.\n", + "WARNING:root:No version specified for policyengine-uk-data-private, parliamentary_constituency_weights.h5. Using latest version: None\n", + "INFO:policyengine.utils.data.caching_google_storage_client:Syncing policyengine-uk-data-private, parliamentary_constituency_weights.h5, None to cache\n", + "INFO:policyengine.utils.data.simplified_google_storage_client:Downloading policyengine-uk-data-private, parliamentary_constituency_weights.h5\n", + "INFO:policyengine.utils.data.caching_google_storage_client:Copying downloaded data for policyengine-uk-data-private, parliamentary_constituency_weights.h5 to parliamentary_constituency_weights.h5\n", + "INFO:root:Using Google Cloud Storage for download.\n", + "WARNING:root:No metadata found for blob, so it has no version attached.\n", + "WARNING:root:No version specified for policyengine-uk-data-private, constituencies_2024.csv. Using latest version: None\n", + "INFO:policyengine.utils.data.caching_google_storage_client:Syncing policyengine-uk-data-private, constituencies_2024.csv, None to cache\n", + "INFO:policyengine.utils.data.simplified_google_storage_client:Downloading policyengine-uk-data-private, constituencies_2024.csv\n", + "INFO:policyengine.utils.data.caching_google_storage_client:Copying downloaded data for policyengine-uk-data-private, constituencies_2024.csv to constituencies_2024.csv\n" + ] + }, { "data": { "text/plain": [ - "EconomyComparison(fiscal=FiscalComparison(baseline=FiscalSummary(tax_revenue=658911285719.5891, federal_tax=658911285719.5891, state_tax=0.0, government_spending=349760026840.3932, tax_benefit_programs={'income_tax': 333376287037.05945, 'national_insurance': 52985626776.773834, 'ni_employer': 126330649370.35953, 'vat': 211671832822.39133, 'council_tax': 49007055050.00724, 'fuel_duty': 26506672341.204205, 'tax_credits': -34929879.49872104, 'universal_credit': -73459549194.97665, 'child_benefit': -14311471487.935827, 'state_pension': -132795868621.44594, 'pension_credit': -6252358021.417119}, household_net_income=1566030461192.7288), reform=FiscalSummary(tax_revenue=658911285719.5891, federal_tax=658911285719.5891, state_tax=0.0, government_spending=349760026840.3932, tax_benefit_programs={'income_tax': 333376287037.05945, 'national_insurance': 52985626776.773834, 'ni_employer': 126330649370.35953, 'vat': 211671832822.39133, 'council_tax': 49007055050.00724, 'fuel_duty': 26506672341.204205, 'tax_credits': -34929879.49872104, 'universal_credit': -73459549194.97665, 'child_benefit': -14311471487.935827, 'state_pension': -132795868621.44594, 'pension_credit': -6252358021.417119}, household_net_income=1566030461192.7288), change=FiscalSummary(tax_revenue=0.0, federal_tax=0.0, state_tax=0.0, government_spending=0.0, tax_benefit_programs={'income_tax': 0.0, 'national_insurance': 0.0, 'ni_employer': 0.0, 'vat': 0.0, 'council_tax': 0.0, 'fuel_duty': 0.0, 'tax_credits': 0.0, 'universal_credit': 0.0, 'child_benefit': 0.0, 'state_pension': 0.0, 'pension_credit': 0.0}, household_net_income=0.0)), inequality=InequalitySummary(gini=0.0, top_10_share=0.0, top_1_share=0.0))" + "EconomyComparison(country_package_version='2.24.1', budget=BudgetaryImpact(budgetary_impact=0.0, tax_revenue_impact=0.0, state_tax_revenue_impact=0.0, benefit_spending_impact=0.0, households=34067959.16713403, baseline_net_income=1594652644809.1484), detailed_budget={'income_tax': ProgramSpecificImpact(baseline=333485558893.2562, reform=333485558893.2562, difference=0.0), 'national_insurance': ProgramSpecificImpact(baseline=58106797201.72149, reform=58106797201.72149, difference=0.0), 'vat': ProgramSpecificImpact(baseline=217779970340.45303, reform=217779970340.45303, difference=0.0), 'council_tax': ProgramSpecificImpact(baseline=55919103106.82793, reform=55919103106.82793, difference=0.0), 'fuel_duty': ProgramSpecificImpact(baseline=29274240650.308544, reform=29274240650.308544, difference=0.0), 'tax_credits': ProgramSpecificImpact(baseline=-218647286.01696286, reform=-218647286.01696286, difference=0.0), 'universal_credit': ProgramSpecificImpact(baseline=-75937544684.01125, reform=-75937544684.01125, difference=0.0), 'child_benefit': ProgramSpecificImpact(baseline=-15904774022.820269, reform=-15904774022.820269, difference=0.0), 'state_pension': ProgramSpecificImpact(baseline=-136493500361.4934, reform=-136493500361.4934, difference=0.0), 'pension_credit': ProgramSpecificImpact(baseline=-6883460152.410097, reform=-6883460152.410097, difference=0.0), 'ni_employer': ProgramSpecificImpact(baseline=132081317376.75456, reform=132081317376.75456, difference=0.0)}, decile=DecileImpact(relative={1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}, average={1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}), inequality=InequalityImpact(gini=BaselineReformValues(baseline=0.35548246480717477, reform=0.35548246480717477), top_10_pct_share=BaselineReformValues(baseline=0.2761383438603205, reform=0.2761383438603205), top_1_pct_share=BaselineReformValues(baseline=0.07844282257306669, reform=0.07844282257306669)), poverty=PovertyImpact(poverty=AgeGroupBaselineReformValues(child=BaselineReformValues(baseline=0.1806112019067216, reform=0.1806112019067216), adult=BaselineReformValues(baseline=0.12147020231142763, reform=0.12147020231142763), senior=BaselineReformValues(baseline=0.08693354854833081, reform=0.08693354854833081), all=BaselineReformValues(baseline=0.12722600403258946, reform=0.12722600403258946)), deep_poverty=AgeGroupBaselineReformValues(child=BaselineReformValues(baseline=0.05188484608564893, reform=0.05188484608564893), adult=BaselineReformValues(baseline=0.03456228764618939, reform=0.03456228764618939), senior=BaselineReformValues(baseline=0.004885010754390452, reform=0.004885010754390452), all=BaselineReformValues(baseline=0.03250612463984824, reform=0.03250612463984824))), poverty_by_gender=PovertyGenderBreakdown(poverty=GenderBaselineReformValues(male=BaselineReformValues(baseline=0.12531502377752102, reform=0.12531502377752102), female=BaselineReformValues(baseline=0.1290056618478023, reform=0.1290056618478023)), deep_poverty=GenderBaselineReformValues(male=BaselineReformValues(baseline=0.03235609600210336, reform=0.03235609600210336), female=BaselineReformValues(baseline=0.03264584331928527, reform=0.03264584331928527))), poverty_by_race=None, intra_decile=IntraDecileImpact(deciles={'Lose more than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'Lose less than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'No change': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], 'Gain less than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'Gain more than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}, all={'Lose more than 5%': 0.0, 'Lose less than 5%': 0.0, 'No change': 1.0, 'Gain less than 5%': 0.0, 'Gain more than 5%': 0.0}), wealth_decile=WealthDecileImpactWithValues(relative={1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}, average={1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}), intra_wealth_decile=IntraWealthDecileImpactWithValues(deciles={'Lose more than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'Lose less than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'No change': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], 'Gain less than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'Gain more than 5%': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}, all={'Lose more than 5%': 0.0, 'Lose less than 5%': 0.0, 'No change': 1.0, 'Gain less than 5%': 0.0, 'Gain more than 5%': 0.0}), labor_supply_response=LaborSupplyResponse(substitution_lsr=0.0, income_lsr=0.0, relative_lsr={'income': 0.0, 'substitution': 0.0}, total_change=0.0, revenue_change=0.0, decile={'average': {'income': {-1: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}, 'substitution': {-1: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}}, 'relative': {'income': {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}, 'substitution': {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0}}}, hours=HoursResponse(baseline=0.0, reform=0.0, change=0.0, income_effect=0.0, substitution_effect=0.0)), constituency_impact=UKConstituencyBreakdownWithValues(by_constituency={'Aldershot': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-40), 'Aldridge-Brownhills': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-30), 'Altrincham and Sale West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-25), 'Amber Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-27), 'Arundel and South Downs': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-44), 'Ashfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-27), 'Ashford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=72, y=-42), 'Ashton-under-Lyne': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-23), 'Aylesbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-35), 'Banbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-33), 'Barking': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-38), 'Barnsley North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-23), 'Barnsley South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-23), 'Barrow and Furness': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-16), 'Basildon and Billericay': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-34), 'Basingstoke': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-39), 'Bassetlaw': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-26), 'Bath': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-40), 'Battersea': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-41), 'Beaconsfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-37), 'Beckenham and Penge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-43), 'Bedford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-32), 'Bermondsey and Old Southwark': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-40), 'Bethnal Green and Stepney': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-39), 'Beverley and Holderness': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-22), 'Bexhill and Battle': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=70, y=-44), 'Bexleyheath and Crayford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-39), 'Bicester and Woodstock': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-34), 'Birkenhead': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-27), 'Birmingham Edgbaston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-33), 'Birmingham Erdington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-31), 'Birmingham Hall Green and Moseley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-32), 'Birmingham Hodge Hill and Solihull North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-31), 'Birmingham Ladywood': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-32), 'Birmingham Northfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-34), 'Birmingham Perry Barr': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-31), 'Birmingham Selly Oak': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-33), 'Birmingham Yardley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-32), 'Bishop Auckland': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-14), 'Blackburn': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-19), 'Blackley and Middleton South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-23), 'Blackpool North and Fleetwood': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-18), 'Blackpool South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-18), 'Blaydon and Consett': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-14), 'Blyth and Ashington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-12), 'Bognor Regis and Littlehampton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-44), 'Bolsover': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-26), 'Bolton North East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-21), 'Bolton South and Walkden': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-22), 'Bolton West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-21), 'Bootle': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-22), 'Boston and Skegness': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-26), 'Bournemouth East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-43), 'Bournemouth West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-42), 'Bracknell': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-39), 'Bradford East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-20), 'Bradford South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-21), 'Bradford West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-20), 'Braintree': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-31), 'Brent East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-38), 'Brent West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-38), 'Brentford and Isleworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-40), 'Brentwood and Ongar': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-33), 'Bridgwater': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-41), 'Bridlington and The Wolds': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-20), 'Brigg and Immingham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-24), 'Brighton Kemptown and Peacehaven': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-45), 'Brighton Pavilion': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-44), 'Bristol Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-38), 'Bristol East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-38), 'Bristol North East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-37), 'Bristol North West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-38), 'Bristol South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-39), 'Broadland and Fakenham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-27), 'Bromley and Biggin Hill': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-42), 'Bromsgrove': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-33), 'Broxbourne': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-35), 'Broxtowe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-27), 'Buckingham and Bletchley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-34), 'Burnley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-19), 'Burton and Uttoxeter': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-28), 'Bury North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-21), 'Bury South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-22), 'Bury St Edmunds and Stowmarket': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-31), 'Calder Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-20), 'Camborne and Redruth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=43, y=-45), 'Cambridge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-30), 'Cannock Chase': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-29), 'Canterbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=71, y=-41), 'Carlisle': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-14), 'Carshalton and Wallington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-43), 'Castle Point': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-36), 'Central Devon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-42), 'Central Suffolk and North Ipswich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-29), 'Chatham and Aylesford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-40), 'Cheadle': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-26), 'Chelmsford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-33), 'Chelsea and Fulham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-40), 'Cheltenham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-36), 'Chesham and Amersham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-36), 'Chester North and Neston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-28), 'Chester South and Eddisbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-27), 'Chesterfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-26), 'Chichester': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-44), 'Chingford and Woodford Green': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-35), 'Chippenham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-39), 'Chipping Barnet': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-36), 'Chorley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-20), 'Christchurch': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-42), 'Cities of London and Westminster': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-40), 'City of Durham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-16), 'Clacton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-32), 'Clapham and Brixton Hill': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-42), 'Colchester': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-32), 'Colne Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-23), 'Congleton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-27), 'Corby and East Northamptonshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-30), 'Coventry East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-33), 'Coventry North West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-33), 'Coventry South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-34), 'Cramlington and Killingworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-12), 'Crawley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-44), 'Crewe and Nantwich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-27), 'Croydon East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-42), 'Croydon South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-43), 'Croydon West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-43), 'Dagenham and Rainham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-37), 'Darlington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-17), 'Dartford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-40), 'Daventry': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-32), 'Derby North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-28), 'Derby South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-28), 'Derbyshire Dales': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-26), 'Dewsbury and Batley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-22), 'Didcot and Wantage': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-38), 'Doncaster Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-23), 'Doncaster East and the Isle of Axholme': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-23), 'Doncaster North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-22), 'Dorking and Horley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-43), 'Dover and Deal': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=72, y=-41), 'Droitwich and Evesham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-36), 'Dudley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-31), 'Dulwich and West Norwood': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-42), 'Dunstable and Leighton Buzzard': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-33), 'Ealing Central and Acton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-39), 'Ealing North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-38), 'Ealing Southall': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-39), 'Earley and Woodley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-36), 'Easington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-16), 'East Grinstead and Uckfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-43), 'East Ham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-38), 'East Hampshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-41), 'East Surrey': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-43), 'East Thanet': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=71, y=-39), 'East Wiltshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-41), 'East Worthing and Shoreham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-44), 'Eastbourne': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-45), 'Eastleigh': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-41), 'Edmonton and Winchmore Hill': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-36), 'Ellesmere Port and Bromborough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-27), 'Eltham and Chislehurst': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-41), 'Ely and East Cambridgeshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-30), 'Enfield North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-35), 'Epping Forest': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-35), 'Epsom and Ewell': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-43), 'Erewash': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-28), 'Erith and Thamesmead': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-40), 'Esher and Walton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-42), 'Exeter': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-42), 'Exmouth and Exeter East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-43), 'Fareham and Waterlooville': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-43), 'Farnham and Bordon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-42), 'Faversham and Mid Kent': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=71, y=-40), 'Feltham and Heston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-40), 'Filton and Bradley Stoke': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-37), 'Finchley and Golders Green': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-37), 'Folkestone and Hythe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=71, y=-42), 'Forest of Dean': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-35), 'Frome and East Somerset': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-41), 'Fylde': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-19), 'Gainsborough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-25), 'Gateshead Central and Whickham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-15), 'Gedling': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-28), 'Gillingham and Rainham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=70, y=-40), 'Glastonbury and Somerton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-41), 'Gloucester': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-35), 'Godalming and Ash': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-42), 'Goole and Pocklington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-21), 'Gorton and Denton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-24), 'Gosport': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-43), 'Grantham and Bourne': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-28), 'Gravesham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-39), 'Great Grimsby and Cleethorpes': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-24), 'Great Yarmouth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-27), 'Greenwich and Woolwich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-40), 'Guildford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-41), 'Hackney North and Stoke Newington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-38), 'Hackney South and Shoreditch': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-39), 'Halesowen': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-33), 'Halifax': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-21), 'Hamble Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-43), 'Hammersmith and Chiswick': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-39), 'Hampstead and Highgate': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-38), 'Harborough, Oadby and Wigston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-31), 'Harlow': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-32), 'Harpenden and Berkhamsted': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-34), 'Harrogate and Knaresborough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-18), 'Harrow East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-37), 'Harrow West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-37), 'Hartlepool': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-16), 'Harwich and North Essex': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-31), 'Hastings and Rye': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=70, y=-43), 'Havant': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-44), 'Hayes and Harlington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-38), 'Hazel Grove': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-25), 'Hemel Hempstead': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-34), 'Hendon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-36), 'Henley and Thame': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-35), 'Hereford and South Herefordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-34), 'Herne Bay and Sandwich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=72, y=-40), 'Hertford and Stortford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-32), 'Hertsmere': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-34), 'Hexham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-13), 'Heywood and Middleton North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-20), 'High Peak': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-25), 'Hinckley and Bosworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-30), 'Hitchin': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-32), 'Holborn and St Pancras': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-39), 'Honiton and Sidmouth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-43), 'Hornchurch and Upminster': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-37), 'Hornsey and Friern Barnet': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-36), 'Horsham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-44), 'Houghton and Sunderland South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-15), 'Hove and Portslade': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-44), 'Huddersfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-22), 'Huntingdon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-31), 'Hyndburn': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-19), 'Ilford North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-36), 'Ilford South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-37), 'Ipswich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-30), 'Isle of Wight East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-45), 'Isle of Wight West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-45), 'Islington North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-38), 'Islington South and Finsbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-39), 'Jarrow and Gateshead East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-14), 'Keighley and Ilkley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-19), 'Kenilworth and Southam': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-34), 'Kensington and Bayswater': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-39), 'Kettering': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-30), 'Kingston and Surbiton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-42), 'Kingston upon Hull East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-22), 'Kingston upon Hull North and Cottingham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-21), 'Kingston upon Hull West and Haltemprice': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-22), 'Kingswinford and South Staffordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-30), 'Knowsley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-23), 'Lancaster and Wyre': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-18), 'Leeds Central and Headingley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-20), 'Leeds East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-20), 'Leeds North East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-19), 'Leeds North West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-19), 'Leeds South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-21), 'Leeds South West and Morley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-21), 'Leeds West and Pudsey': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-20), 'Leicester East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-30), 'Leicester South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-31), 'Leicester West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-31), 'Leigh and Atherton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-25), 'Lewes': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-45), 'Lewisham East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-42), 'Lewisham North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-40), 'Lewisham West and East Dulwich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-41), 'Leyton and Wanstead': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-37), 'Lichfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-29), 'Lincoln': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-25), 'Liverpool Garston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-25), 'Liverpool Riverside': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-24), 'Liverpool Walton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-23), 'Liverpool Wavertree': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-25), 'Liverpool West Derby': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-24), 'Loughborough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-30), 'Louth and Horncastle': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-25), 'Lowestoft': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-28), 'Luton North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-33), 'Luton South and South Bedfordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-34), 'Macclesfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-26), 'Maidenhead': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-36), 'Maidstone and Malling': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-41), 'Makerfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-22), 'Maldon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-33), 'Manchester Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-24), 'Manchester Rusholme': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-25), 'Manchester Withington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-26), 'Mansfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-27), 'Melksham and Devizes': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-40), 'Melton and Syston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-29), 'Meriden and Solihull East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-33), 'Mid Bedfordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-32), 'Mid Buckinghamshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-35), 'Mid Cheshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-27), 'Mid Derbyshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-27), 'Mid Dorset and North Poole': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-43), 'Mid Leicestershire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-31), 'Mid Norfolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-28), 'Mid Sussex': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-43), 'Middlesbrough and Thornaby East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-17), 'Middlesbrough South and East Cleveland': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-17), 'Milton Keynes Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-34), 'Milton Keynes North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-33), 'Mitcham and Morden': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-43), 'Morecambe and Lunesdale': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-17), 'New Forest East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-43), 'New Forest West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-43), 'Newark': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-26), 'Newbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-37), 'Newcastle upon Tyne Central and West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-13), 'Newcastle upon Tyne East and Wallsend': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-14), 'Newcastle upon Tyne North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-13), 'Newcastle-under-Lyme': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-28), 'Newton Abbot': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-43), 'Newton Aycliffe and Spennymoor': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-16), 'Normanton and Hemsworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-23), 'North Bedfordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-31), 'North Cornwall': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-43), 'North Cotswolds': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-37), 'North Devon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-41), 'North Dorset': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-42), 'North Durham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-15), 'North East Cambridgeshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-29), 'North East Derbyshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-26), 'North East Hampshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-38), 'North East Hertfordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-32), 'North East Somerset and Hanham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-39), 'North Herefordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-34), 'North Norfolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-27), 'North Northumberland': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-12), 'North Shropshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-29), 'North Somerset': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-39), 'North Warwickshire and Bedworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-32), 'North West Cambridgeshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-30), 'North West Essex': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-31), 'North West Hampshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-39), 'North West Leicestershire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-29), 'North West Norfolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-28), 'Northampton North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-32), 'Northampton South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-33), 'Norwich North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-28), 'Norwich South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-29), 'Nottingham East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-29), 'Nottingham North and Kimberley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-28), 'Nottingham South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-29), 'Nuneaton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-31), 'Old Bexley and Sidcup': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-41), 'Oldham East and Saddleworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-22), 'Oldham West, Chadderton and Royton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-22), 'Orpington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-43), 'Ossett and Denby Dale': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-22), 'Oxford East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-34), 'Oxford West and Abingdon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-35), 'Peckham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-41), 'Pendle and Clitheroe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-18), 'Penistone and Stocksbridge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-23), 'Penrith and Solway': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-15), 'Peterborough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-29), 'Plymouth Moor View': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-43), 'Plymouth Sutton and Devonport': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-44), 'Pontefract, Castleford and Knottingley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-22), 'Poole': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-43), 'Poplar and Limehouse': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-39), 'Portsmouth North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-43), 'Portsmouth South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-44), 'Preston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-19), 'Putney': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-41), \"Queen's Park and Maida Vale\": UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-40), 'Rawmarsh and Conisbrough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-24), 'Rayleigh and Wickford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-34), 'Reading Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-37), 'Reading West and Mid Berkshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-36), 'Redcar': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-17), 'Redditch': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-35), 'Reigate': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-44), 'Ribble Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-18), 'Richmond and Northallerton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-18), 'Richmond Park': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-41), 'Rochdale': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-21), 'Rochester and Strood': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-39), 'Romford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-36), 'Romsey and Southampton North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-40), 'Rossendale and Darwen': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-20), 'Rother Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-25), 'Rotherham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-24), 'Rugby': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-32), 'Ruislip, Northwood and Pinner': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-36), 'Runcorn and Helsby': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-28), 'Runnymede and Weybridge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-41), 'Rushcliffe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-28), 'Rutland and Stamford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-29), 'Salford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-24), 'Salisbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-41), 'Scarborough and Whitby': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-19), 'Scunthorpe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-24), 'Sefton Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-20), 'Selby': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-21), 'Sevenoaks': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-42), 'Sheffield Brightside and Hillsborough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-24), 'Sheffield Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-25), 'Sheffield Hallam': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-24), 'Sheffield Heeley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-25), 'Sheffield South East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-25), 'Sherwood Forest': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-27), 'Shipley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-19), 'Shrewsbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-30), 'Sittingbourne and Sheppey': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=70, y=-39), 'Skipton and Ripon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-18), 'Sleaford and North Hykeham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-26), 'Slough': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-37), 'Smethwick': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-32), 'Solihull West and Shirley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-34), 'South Basildon and East Thurrock': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-36), 'South Cambridgeshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-31), 'South Cotswolds': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-38), 'South Derbyshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-29), 'South Devon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-45), 'South Dorset': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-44), 'South East Cornwall': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-44), 'South Holland and The Deepings': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-27), 'South Leicestershire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-32), 'South Norfolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-29), 'South Northamptonshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-33), 'South Ribble': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-20), 'South Shields': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-14), 'South Shropshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-31), 'South Suffolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-30), 'South West Devon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-45), 'South West Hertfordshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-35), 'South West Norfolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-29), 'South West Wiltshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-41), 'Southampton Itchen': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-42), 'Southampton Test': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-42), 'Southend East and Rochford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-34), 'Southend West and Leigh': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-35), 'Southgate and Wood Green': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-35), 'Southport': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-19), 'Spelthorne': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-40), 'Spen Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-21), 'St Albans': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-34), 'St Austell and Newquay': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-44), 'St Helens North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-21), 'St Helens South and Whiston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-22), 'St Ives': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=43, y=-46), 'St Neots and Mid Cambridgeshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-31), 'Stafford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-28), 'Staffordshire Moorlands': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-27), 'Stalybridge and Hyde': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-24), 'Stevenage': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-33), 'Stockport': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-25), 'Stockton North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-16), 'Stockton West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-17), 'Stoke-on-Trent Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-28), 'Stoke-on-Trent North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-27), 'Stoke-on-Trent South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-29), 'Stone, Great Wyrley and Penkridge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-28), 'Stourbridge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-32), 'Stratford and Bow': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-38), 'Stratford-on-Avon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-35), 'Streatham and Croydon North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-42), 'Stretford and Urmston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-24), 'Stroud': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-37), 'Suffolk Coastal': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-29), 'Sunderland Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-15), 'Surrey Heath': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-39), 'Sussex Weald': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=70, y=-42), 'Sutton and Cheam': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-42), 'Sutton Coldfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-31), 'Swindon North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-39), 'Swindon South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-40), 'Tamworth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-30), 'Tatton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-26), 'Taunton and Wellington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-42), 'Telford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-29), 'Tewkesbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-36), 'The Wrekin': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-29), 'Thirsk and Malton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-18), 'Thornbury and Yate': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-36), 'Thurrock': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-36), 'Tipton and Wednesbury': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-31), 'Tiverton and Minehead': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-41), 'Tonbridge': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-41), 'Tooting': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-42), 'Torbay': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-44), 'Torridge and Tavistock': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-42), 'Tottenham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-37), 'Truro and Falmouth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-45), 'Tunbridge Wells': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=69, y=-42), 'Twickenham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-41), 'Tynemouth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-13), 'Uxbridge and South Ruislip': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-37), 'Vauxhall and Camberwell Green': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-41), 'Wakefield and Rothwell': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=59, y=-22), 'Wallasey': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-27), 'Walsall and Bloxwich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-30), 'Walthamstow': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-37), 'Warrington North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-23), 'Warrington South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-24), 'Warwick and Leamington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-35), 'Washington and Gateshead South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-15), 'Watford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-35), 'Waveney Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-28), 'Weald of Kent': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=70, y=-41), 'Wellingborough and Rushden': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=63, y=-30), 'Wells and Mendip Hills': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-40), 'Welwyn Hatfield': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=65, y=-33), 'West Bromwich': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-32), 'West Dorset': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-44), 'West Ham and Beckton': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=66, y=-38), 'West Lancashire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-21), 'West Suffolk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=67, y=-30), 'West Worcestershire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-35), 'Westmorland and Lonsdale': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-15), 'Weston-super-Mare': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-40), 'Wetherby and Easingwold': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=62, y=-20), 'Whitehaven and Workington': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-16), 'Widnes and Halewood': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-26), 'Wigan': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-20), 'Wimbledon': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-41), 'Winchester': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-40), 'Windsor': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-38), 'Wirral West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-28), 'Witham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=68, y=-33), 'Witney': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=56, y=-35), 'Woking': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=57, y=-40), 'Wokingham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=55, y=-38), 'Wolverhampton North East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-29), 'Wolverhampton South East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-30), 'Wolverhampton West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-30), 'Worcester': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-34), 'Worsley and Eccles': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-23), 'Worthing West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=64, y=-44), 'Wycombe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=58, y=-36), 'Wyre Forest': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-33), 'Wythenshawe and Sale East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-26), 'Yeovil': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-42), 'York Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=60, y=-19), 'York Outer': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=61, y=-18), 'Belfast East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-17), 'Belfast North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-16), 'Belfast South and Mid Down': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-18), 'Belfast West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-17), 'East Antrim': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-15), 'East Londonderry': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=43, y=-15), 'Fermanagh and South Tyrone': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=42, y=-17), 'Foyle': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=42, y=-15), 'Lagan Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-18), 'Mid Ulster': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=43, y=-16), 'Newry and Armagh': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-19), 'North Antrim': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-15), 'North Down': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-16), 'South Antrim': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-16), 'South Down': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-18), 'Strangford': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-17), 'Upper Bann': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=43, y=-18), 'West Tyrone': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=42, y=-16), 'East Renfrewshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-11), 'Na h-Eileanan an Iar': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-2), 'Midlothian': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-11), 'North Ayrshire and Arran': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-10), 'Orkney and Shetland': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=0), 'Aberdeen North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-3), 'Aberdeen South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-4), 'Aberdeenshire North and Moray East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-3), 'Airdrie and Shotts': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-11), 'Alloa and Grangemouth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-7), 'Angus and Perthshire Glens': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-5), 'Arbroath and Broughty Ferry': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-5), 'Argyll, Bute and South Lochaber': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-5), 'Bathgate and Linlithgow': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-9), 'Caithness, Sutherland and Easter Ross': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-2), 'Coatbridge and Bellshill': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-12), 'Cowdenbeath and Kirkcaldy': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-7), 'Cumbernauld and Kirkintilloch': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-8), 'Dumfries and Galloway': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-13), 'Dumfriesshire, Clydesdale and Tweeddale': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-13), 'Dundee Central': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-6), 'Dunfermline and Dollar': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-7), 'East Kilbride and Strathaven': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-13), 'Edinburgh East and Musselburgh': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=54, y=-10), 'Edinburgh North and Leith': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-9), 'Edinburgh South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-10), 'Edinburgh South West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-10), 'Edinburgh West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-9), 'Falkirk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-8), 'Glasgow East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-10), 'Glasgow North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-9), 'Glasgow North East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-9), 'Glasgow South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-11), 'Glasgow South West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-10), 'Glasgow West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-8), 'Glenrothes and Mid Fife': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-6), 'Gordon and Buchan': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-4), 'Hamilton and Clyde Valley': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-12), 'Inverclyde and Renfrewshire West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-8), 'Inverness, Skye and West Ross-shire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-3), 'Livingston': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-11), 'Lothian East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-11), 'Mid Dunbartonshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-7), 'Moray West, Nairn and Strathspey': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-4), 'Motherwell, Wishaw and Carluke': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=52, y=-12), 'North East Fife': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-6), 'Paisley and Renfrewshire North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-9), 'Paisley and Renfrewshire South': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-10), 'Perth and Kinross-shire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-5), 'Rutherglen': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-12), 'Stirling and Strathallan': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-6), 'West Dunbartonshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-7), 'Ayr, Carrick and Cumnock': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-13), 'Berwickshire, Roxburgh and Selkirk': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=53, y=-12), 'Central Ayrshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-12), 'Kilmarnock and Loudoun': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-13), 'West Aberdeenshire and Kincardine': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=51, y=-4), 'Aberafan Maesteg': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-36), 'Alyn and Deeside': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-29), 'Bangor Aberconwy': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-31), 'Blaenau Gwent and Rhymney': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-33), 'Brecon, Radnor and Cwm Tawe': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-32), 'Bridgend': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-37), 'Caerfyrddin': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-32), 'Caerphilly': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-35), 'Cardiff East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-37), 'Cardiff North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-36), 'Cardiff South and Penarth': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-38), 'Cardiff West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-37), 'Ceredigion Preseli': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-34), 'Clwyd East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-30), 'Clwyd North': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-30), 'Dwyfor Meirionnydd': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-31), 'Gower': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-37), 'Llanelli': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-36), 'Merthyr Tydfil and Aberdare': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-34), 'Mid and South Pembrokeshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=44, y=-36), 'Monmouthshire': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-36), 'Montgomeryshire and Glyndwr': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-31), 'Neath and Swansea East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-35), 'Newport East': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-37), 'Newport West and Islwyn': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=49, y=-36), 'Pontypridd': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=48, y=-35), 'Rhondda and Ogmore': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-36), 'Swansea West': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=45, y=-37), 'Torfaen': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-34), 'Vale of Glamorgan': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=47, y=-38), 'Wrexham': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=50, y=-30), 'Ynys Môn': UKConstituencyBreakdownByConstituency(average_household_income_change=0.0, relative_household_income_change=0.0, x=46, y=-29)}, outcomes_by_region={'uk': {'Gain more than 5%': 0, 'Gain less than 5%': 0, 'No change': 650, 'Lose less than 5%': 0, 'Lose more than 5%': 0}, 'england': {'Gain more than 5%': 0, 'Gain less than 5%': 0, 'No change': 543, 'Lose less than 5%': 0, 'Lose more than 5%': 0}, 'scotland': {'Gain more than 5%': 0, 'Gain less than 5%': 0, 'No change': 57, 'Lose less than 5%': 0, 'Lose more than 5%': 0}, 'wales': {'Gain more than 5%': 0, 'Gain less than 5%': 0, 'No change': 32, 'Lose less than 5%': 0, 'Lose more than 5%': 0}, 'northern_ireland': {'Gain more than 5%': 0, 'Gain less than 5%': 0, 'No change': 18, 'Lose less than 5%': 0, 'Lose more than 5%': 0}}), cliff_impact=None)" ] }, "execution_count": 1, @@ -79,7 +105,7 @@ "\n", "If you set `scope` to `macro`, you should provide either:\n", "\n", - "* A Hugging Face `.h5` dataset address in this format: `\"hf://policyengine/policyengine-us-data/cps_2023.h5\"` (`hf://owner/dataset-name/path.h5`).\n", + "* A Google Cloud `.h5` dataset address in this format: `\"gcs://policyengine-us-data/cps_2023.h5\"` (`gcs://bucket/path.h5`).\n", "* An instance of `policyengine_core.data.Dataset` (advanced).\n", "\n", "See `policyengine.constants` for the available datasets.\n", @@ -118,7 +144,7 @@ ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -132,7 +158,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/policyengine/constants.py b/policyengine/constants.py index 43351271..de9b6799 100644 --- a/policyengine/constants.py +++ b/policyengine/constants.py @@ -2,40 +2,26 @@ from policyengine_core.data import Dataset from policyengine.utils.data_download import download +from typing import Tuple, Optional -# Datasets -ENHANCED_FRS = "hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5" -FRS = "hf://policyengine/policyengine-uk-data/frs_2022_23.h5" -ENHANCED_CPS = "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5" -CPS = "hf://policyengine/policyengine-us-data/cps_2023.h5" -POOLED_CPS = "hf://policyengine/policyengine-us-data/pooled_3_year_cps_2023.h5" +EFRS_2022 = "gcs://policyengine-uk-data-private/enhanced_frs_2022_23.h5" +FRS_2022 = "gcs://policyengine-uk-data-private/frs_2022_23.h5" +CPS_2023_POOLED = "gcs://policyengine-us-data/pooled_3_year_cps_2023.h5" +CPS_2023 = "gcs://policyengine-us-data/cps_2023.h5" +ECPS_2024 = "gcs://policyengine-us-data/ecps_2024.h5" -def get_default_dataset(country: str, region: str): +def get_default_dataset( + country: str, region: str, version: Optional[str] = None +) -> str: if country == "uk": - data_file = download( - filepath="enhanced_frs_2022_23.h5", - huggingface_repo="policyengine-uk-data", - gcs_bucket="policyengine-uk-data-private", - ) - time_period = None + return EFRS_2022 elif country == "us": if region is not None and region != "us": - data_file = download( - filepath="pooled_3_year_cps_2023.h5", - huggingface_repo="policyengine-us-data", - gcs_bucket="policyengine-us-data", - ) - time_period = 2023 + return CPS_2023_POOLED else: - data_file = download( - filepath="cps_2023.h5", - huggingface_repo="policyengine-us-data", - gcs_bucket="policyengine-us-data", - ) - time_period = 2023 + return CPS_2023 - return Dataset.from_file( - file_path=data_file, - time_period=time_period, + raise ValueError( + f"Unable to select a default dataset for country {country} and region {region}." ) diff --git a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py index 636717c5..bc53044c 100644 --- a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py +++ b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py @@ -10,8 +10,7 @@ from policyengine.outputs.macro.single.calculate_single_economy import ( SingleEconomy, ) -from policyengine.utils.packages import get_country_package_version -from typing import List, Dict +from typing import List, Dict, Optional class BudgetaryImpact(BaseModel): @@ -711,7 +710,6 @@ def uk_constituency_breakdown( reform_hnet = reform.household_net_income constituency_weights_path = download( - huggingface_repo="policyengine-uk-data", gcs_bucket="policyengine-uk-data-private", filepath="parliamentary_constituency_weights.h5", ) @@ -721,7 +719,6 @@ def uk_constituency_breakdown( ] # {2025: array(650, 100180) where cell i, j is the weight of household record i in constituency j} constituency_names_path = download( - huggingface_repo="policyengine-uk-data", gcs_bucket="policyengine-uk-data-private", filepath="constituencies_2024.csv", ) @@ -786,7 +783,10 @@ class CliffImpact(BaseModel): class EconomyComparison(BaseModel): - country_package_version: str + model_version: Optional[str] = ( + None # Optional while some datasets have no tagged version. + ) + data_version: Optional[str] = None budget: BudgetaryImpact detailed_budget: DetailedBudgetaryImpact decile: DecileImpact @@ -849,7 +849,8 @@ def calculate_economy_comparison( cliff_impact = None return EconomyComparison( - country_package_version=get_country_package_version(country_id), + model_version=simulation.model_version, + data_version=simulation.data_version, budget=budgetary_impact_data, detailed_budget=detailed_budgetary_impact_data, decile=decile_impact_data, diff --git a/policyengine/simulation.py b/policyengine/simulation.py index aa5858a9..93fb5a94 100644 --- a/policyengine/simulation.py +++ b/policyengine/simulation.py @@ -18,10 +18,11 @@ Simulation as UKSimulation, Microsimulation as UKMicrosimulation, ) +from importlib import metadata import h5py from pathlib import Path import pandas as pd -from typing import Type +from typing import Type, Optional from functools import wraps, partial from typing import Dict, Any, Callable import importlib @@ -34,8 +35,8 @@ ) # Needs stricter typing. Any==policyengine_core.data.Dataset, but pydantic refuses for some reason. TimePeriodType = int ReformType = ParametricReform | Type[StructuralReform] | None -RegionType = str | None -SubsampleType = int | None +RegionType = Optional[str] +SubsampleType = Optional[int] class SimulationOptions(BaseModel): @@ -54,14 +55,22 @@ class SimulationOptions(BaseModel): None, description="How many, if a subsample, households to randomly simulate.", ) - title: str | None = Field( + title: Optional[str] = Field( "[Analysis title]", description="The title of the analysis (for charts). If not provided, a default title will be generated.", ) - include_cliffs: bool | None = Field( + include_cliffs: Optional[bool] = Field( False, description="Whether to include tax-benefit cliffs in the simulation analyses. If True, cliffs will be included.", ) + model_version: Optional[str] = Field( + None, + description="The version of the country model used in the simulation. If not provided, the current package version will be used. If provided, this package will throw an error if the package version does not match. Use this as an extra safety check.", + ) + data_version: Optional[str] = Field( + None, + description="The version of the data used in the simulation. If not provided, the current data version will be used. If provided, this package will throw an error if the data version does not match. Use this as an extra safety check.", + ) class Simulation: @@ -73,12 +82,16 @@ class Simulation: """The baseline tax-benefit simulation.""" reform_simulation: CountrySimulation | None = None """The reform tax-benefit simulation.""" + data_version: Optional[str] = None + """The version of the data used in the simulation.""" + model_version: Optional[str] = None def __init__(self, **options: SimulationOptions): self.options = SimulationOptions(**options) - + self.check_model_version() self._set_data() self._initialise_simulations() + self.check_data_version() self._add_output_functions() def _add_output_functions(self): @@ -119,29 +132,23 @@ def _set_data(self): region=self.options.region, ) - elif isinstance(self.options.data, str): + if isinstance(self.options.data, str): filename = self.options.data - if "://" in self.options.data: - bucket = None - hf_repo = None - hf_org = None - if "gs://" in self.options.data: - bucket, filename = self.options.data.split("://")[ - -1 - ].split("/") - hf_org = "policyengine" - elif "hf://" in self.options.data: - hf_org, hf_repo, filename = self.options.data.split("://")[ - -1 - ].split("/", 2) + if self.options.data[:6] == "gcs://": + bucket, filename = self.options.data.split("://")[-1].split( + "/" + ) + version = self.options.data_version file_path = download( filepath=filename, - huggingface_org=hf_org, - huggingface_repo=hf_repo, gcs_bucket=bucket, + version=version, ) filename = str(Path(file_path)) + else: + # If it's a local file, we can't infer the version. + version = None if "cps_2023" in filename: time_period = 2023 else: @@ -260,7 +267,6 @@ def _apply_region_to_simulation( elif "constituency/" in region: constituency = region.split("/")[1] constituency_names_file_path = download( - huggingface_repo="policyengine-uk-data", gcs_bucket="policyengine-uk-data-private", filepath="constituencies_2024.csv", ) @@ -281,7 +287,6 @@ def _apply_region_to_simulation( f"Constituency {constituency} not found. See {constituency_names_file_path} for the list of available constituencies." ) weights_file_path = download( - huggingface_repo="policyengine-uk-data", gcs_bucket="policyengine-uk-data-private", filepath="parliamentary_constituency_weights.h5", ) @@ -297,7 +302,6 @@ def _apply_region_to_simulation( elif "local_authority/" in region: la = region.split("/")[1] la_names_file_path = download( - huggingface_repo="policyengine-uk-data", gcs_bucket="policyengine-uk-data-private", filepath="local_authorities_2021.csv", ) @@ -312,7 +316,6 @@ def _apply_region_to_simulation( f"Local authority {la} not found. See {la_names_file_path} for the list of available local authorities." ) weights_file_path = download( - huggingface_repo="policyengine-uk-data", gcs_bucket="policyengine-uk-data-private", filepath="local_authority_weights.h5", ) @@ -327,3 +330,32 @@ def _apply_region_to_simulation( ) return simulation + + def check_model_version(self) -> None: + """ + Check the package versions of the simulation against the current package versions. + """ + if self.options.model_version is not None: + target_version = self.options.model_version + package = f"policyengine-{self.options.country}" + try: + installed_version = metadata.version(package) + self.model_version = installed_version + except metadata.PackageNotFoundError: + raise ValueError( + f"Package {package} not found. Try running `pip install {package}`." + ) + if installed_version != target_version: + raise ValueError( + f"Package {package} version {installed_version} does not match expected version {target_version}. Try running `pip install {package}=={target_version}`." + ) + + def check_data_version(self) -> None: + """ + Check the data versions of the simulation against the current data versions. + """ + if self.options.data_version is not None: + if self.data_version != self.options.data_version: + raise ValueError( + f"Data version {self.data_version} does not match expected version {self.options.data_version}." + ) diff --git a/policyengine/utils/data/caching_google_storage_client.py b/policyengine/utils/data/caching_google_storage_client.py index f00d8c08..10de203a 100644 --- a/policyengine/utils/data/caching_google_storage_client.py +++ b/policyengine/utils/data/caching_google_storage_client.py @@ -4,6 +4,7 @@ from policyengine_core.data.dataset import atomic_write import logging from .simplified_google_storage_client import SimplifiedGoogleStorageClient +from typing import Optional logger = logging.getLogger(__name__) @@ -18,17 +19,31 @@ def __init__(self): self.client = SimplifiedGoogleStorageClient() self.cache = diskcache.Cache() - def _data_key(self, bucket: str, key: str) -> str: - return f"{bucket}.{key}.data" + def _data_key( + self, bucket: str, key: str, version: Optional[str] = None + ) -> str: + return f"{bucket}.{key}.{version}.data" # To absolutely 100% avoid any possible issue with file corruption or thread contention # always replace the current target file with whatever we have cached as an atomic write. - def download(self, bucket: str, key: str, target: Path): + def download( + self, + bucket: str, + key: str, + target: Path, + version: Optional[str] = None, + ): """ Atomically write the latest version of the cloud storage blob to the target path. """ - self.sync(bucket, key) - data = self.cache.get(self._data_key(bucket, key)) + if version is None: + # If no version is specified, get the latest version from the cache + version = self.client._get_latest_version(bucket, key) + logging.warning( + f"No version specified for {bucket}, {key}. Using latest version: {version}" + ) + self.sync(bucket, key, version) + data = self.cache.get(self._data_key(bucket, key, version)) if type(data) is bytes: logger.info( f"Copying downloaded data for {bucket}, {key} to {target}" @@ -39,15 +54,17 @@ def download(self, bucket: str, key: str, target: Path): # If the crc has changed from what we downloaded last time download it again. # then update the CRC to whatever we actually downloaded. - def sync(self, bucket: str, key: str) -> None: + def sync( + self, bucket: str, key: str, version: Optional[str] = None + ) -> None: """ Cache the resource if the CRC has changed. """ - logger.info(f"Syncing {bucket}, {key} to cache") - datakey = f"{bucket}.{key}.data" - crckey = f"{bucket}.{key}.crc" + logger.info(f"Syncing {bucket}, {key}, {version} to cache") + datakey = f"{bucket}.{key}.{version}.data" + crckey = f"{bucket}.{key}.{version}.crc" - crc = self.client.crc32c(bucket, key) + crc = self.client.crc32c(bucket, key, version=version) if crc is None: raise Exception(f"Unable to find {key} in bucket {bucket}") @@ -59,8 +76,10 @@ def sync(self, bucket: str, key: str) -> None: ) return - [content, downloaded_crc] = self.client.download(bucket, key) - logger.info( + [content, downloaded_crc] = self.client.download( + bucket, key, version=version + ) + logger.debug( f"Downloaded new version of {bucket}, {key} with crc {downloaded_crc}" ) diff --git a/policyengine/utils/data/simplified_google_storage_client.py b/policyengine/utils/data/simplified_google_storage_client.py index b7c2e895..f15f337b 100644 --- a/policyengine/utils/data/simplified_google_storage_client.py +++ b/policyengine/utils/data/simplified_google_storage_client.py @@ -1,7 +1,8 @@ import asyncio from policyengine_core.data.dataset import atomic_write import logging -from google.cloud.storage import Client +from google.cloud.storage import Client, Blob +from typing import Iterable, Optional logger = logging.getLogger(__name__) @@ -17,25 +18,67 @@ class SimplifiedGoogleStorageClient: def __init__(self): self.client = Client() - def crc32c(self, bucket: str, key: str) -> str | None: + def get_versioned_blob( + self, bucket: str, key: str, version: Optional[str] = None + ) -> Blob: + """ + Get a versioned blob from the specified bucket and key. + If version is None, returns the latest version of the blob. + """ + bucket = self.client.bucket(bucket) + if version is None: + return bucket.blob(key) + else: + versions: Iterable[Blob] = bucket.list_blobs( + prefix=key, versions=True + ) + for v in versions: + if v.metadata is None: + continue # Skip blobs without metadata + if v.metadata.get("version") == version: + return v + raise ValueError( + f"Could not find version {version} of blob {key} in bucket {bucket.name}" + ) + + def crc32c( + self, bucket_name: str, key: str, version: Optional[str] = None + ) -> Optional[str]: """ get the current CRC of the specified blob. None if it doesn't exist. """ - logger.debug(f"Getting crc for {bucket}, {key}") - blob = self.client.bucket(bucket).blob(key) + logger.debug(f"Getting crc for {bucket_name}, {key}") + blob = self.get_versioned_blob(bucket_name, key, version) + blob.reload() logger.debug(f"Crc is {blob.crc32c}") return blob.crc32c - def download(self, bucket: str, key: str) -> tuple[bytes, str]: + def download( + self, bucket: str, key: str, version: Optional[str] = None + ) -> tuple[bytes, str]: """ get the blob content and associated CRC from google storage. """ - logger.debug(f"Downloading {bucket}, {key}") - blob = self.client.bucket(bucket).blob(key) + logger.info(f"Downloading {bucket}, {key}") + blob = self.get_versioned_blob(bucket, key, version) result = blob.download_as_bytes() # According to documentation blob.crc32c is updated as a side effect of # downloading the content. As a result this should now be the crc of the downloaded # content (i.e. there is not a race condition where it's getting the CRC from the cloud) return (result, blob.crc32c) + + def _get_latest_version(self, bucket: str, key: str) -> Optional[str]: + """ + Get the latest version of a blob in the specified bucket and key. + If no version is specified, return None. + """ + blob = self.client.get_bucket(bucket).get_blob(key) + if blob.metadata is None: + logging.warning( + "No metadata found for blob, so it has no version attached." + ) + return None + else: + return blob.metadata.get("version") diff --git a/policyengine/utils/data_download.py b/policyengine/utils/data_download.py index 5b0f776a..fd16adcf 100644 --- a/policyengine/utils/data_download.py +++ b/policyengine/utils/data_download.py @@ -1,54 +1,22 @@ from pathlib import Path import logging import os -from policyengine.utils.huggingface import download_from_hf from policyengine.utils.google_cloud_bucket import download_file_from_gcs from pydantic import BaseModel - - -class DataFile(BaseModel): - filepath: str - huggingface_org: str - huggingface_repo: str | None = None - gcs_bucket: str | None = None +import json +from typing import Tuple, Optional def download( filepath: str, - huggingface_repo: str = None, - gcs_bucket: str = None, - huggingface_org: str = "policyengine", -): - data_file = DataFile( - filepath=filepath, - huggingface_org=huggingface_org, - huggingface_repo=huggingface_repo, - gcs_bucket=gcs_bucket, - ) - - logging.info = print - # NOTE: tests will break on build if you don't default to huggingface. - if data_file.huggingface_repo is not None: - logging.info("Using Hugging Face for download.") - try: - return download_from_hf( - repo=data_file.huggingface_org - + "/" - + data_file.huggingface_repo, - repo_filename=data_file.filepath, - ) - except: - logging.info("Failed to download from Hugging Face.") - - if data_file.gcs_bucket is not None: - logging.info("Using Google Cloud Storage for download.") - download_file_from_gcs( - bucket_name=data_file.gcs_bucket, - file_name=filepath, - destination_path=filepath, - ) - return filepath - - raise ValueError( - "No valid download method specified. Please provide either a Hugging Face repo or a Google Cloud Storage bucket." + gcs_bucket: str, + version: Optional[str] = None, +) -> str: + logging.info("Using Google Cloud Storage for download.") + download_file_from_gcs( + bucket_name=gcs_bucket, + file_name=filepath, + destination_path=filepath, + version=version, ) + return filepath diff --git a/policyengine/utils/google_cloud_bucket.py b/policyengine/utils/google_cloud_bucket.py index f080c21b..3516b231 100644 --- a/policyengine/utils/google_cloud_bucket.py +++ b/policyengine/utils/google_cloud_bucket.py @@ -1,6 +1,8 @@ from .data.caching_google_storage_client import CachingGoogleStorageClient import asyncio from pathlib import Path +from google.cloud.storage import Blob +from typing import Iterable _caching_client: CachingGoogleStorageClient | None = None @@ -19,7 +21,10 @@ def _clear_client(): def download_file_from_gcs( - bucket_name: str, file_name: str, destination_path: str + bucket_name: str, + file_name: str, + destination_path: str, + version: str = None, ) -> None: """ Download a file from Google Cloud Storage to a local path. @@ -32,4 +37,7 @@ def download_file_from_gcs( Returns: None """ - _get_client().download(bucket_name, file_name, Path(destination_path)) + + return _get_client().download( + bucket_name, file_name, Path(destination_path), version=version + ) diff --git a/policyengine/utils/huggingface.py b/policyengine/utils/huggingface.py deleted file mode 100644 index 8277c7a4..00000000 --- a/policyengine/utils/huggingface.py +++ /dev/null @@ -1,49 +0,0 @@ -from huggingface_hub import hf_hub_download -import os -from getpass import getpass -import time - - -def download_from_hf( - repo: str, - repo_filename: str, - local_folder: str | None = None, - version: str | None = None, -): - token = os.environ.get("HUGGING_FACE_TOKEN") - if token is None: - token = getpass( - "Enter your Hugging Face token (or set HUGGING_FACE_TOKEN environment variable): " - ) - # Optionally store in env for subsequent calls in same session - os.environ["HUGGING_FACE_TOKEN"] = token - try: - result = hf_hub_download( - repo_id=repo, - repo_type="model", - filename=repo_filename, - local_dir=local_folder, - revision=version, - token=token, - ) - except: - # In the case of a 429 Too Many Requests error, retry up to 5 times, waiting 30 seconds - # between attempts - for i in range(5): - try: - result = hf_hub_download( - repo_id=repo, - repo_type="model", - filename=repo_filename, - local_dir=local_folder, - revision=version, - token=token, - ) - break - except Exception as e: - if i == 4: - raise e - print(f"Error downloading {repo_filename} from {repo}: {e}") - print("Retrying in 30 seconds...") - time.sleep(30) - return result diff --git a/tests/country/test_uk.py b/tests/country/test_uk.py index 6f083aaa..c28f1e59 100644 --- a/tests/country/test_uk.py +++ b/tests/country/test_uk.py @@ -1,3 +1,6 @@ +import pytest + + def test_uk_macro_single(): from policyengine import Simulation @@ -21,3 +24,31 @@ def test_uk_macro_comparison(): ) sim.calculate_economy_comparison() + + +def test_uk_macro_bad_package_versions_fail(): + from policyengine import Simulation + + with pytest.raises(ValueError): + Simulation( + scope="macro", + country="uk", + reform={ + "gov.hmrc.income_tax.allowances.personal_allowance.amount": 15_000, + }, + model_version="a", + ) + + +def test_uk_macro_bad_data_version_fails(): + from policyengine import Simulation + + with pytest.raises(ValueError): + Simulation( + scope="macro", + country="uk", + reform={ + "gov.hmrc.income_tax.allowances.personal_allowance.amount": 15_000, + }, + data_version="a", + ) diff --git a/tests/utils/data/conftest.py b/tests/utils/data/conftest.py index 7a692420..19cbcd9a 100644 --- a/tests/utils/data/conftest.py +++ b/tests/utils/data/conftest.py @@ -1,6 +1,8 @@ import pytest from unittest.mock import patch +VALID_VERSION = "1.2.3" + class MockedStorageSupport: def __init__(self, mock_simple_storage_client): @@ -12,6 +14,9 @@ def given_stored_data(self, data: str, crc: str): data.encode(), crc, ) + self.mock_simple_storage_client._get_latest_version.return_value = ( + VALID_VERSION + ) def given_crc_changes_on_download( self, data: str, initial_crc: str, download_crc: str @@ -21,6 +26,9 @@ def given_crc_changes_on_download( data.encode(), download_crc, ) + self.mock_simple_storage_client._get_latest_version.return_value = ( + VALID_VERSION + ) @pytest.fixture() diff --git a/tests/utils/data/test_google_cloud_bucket.py b/tests/utils/data/test_google_cloud_bucket.py index c141c0f9..e1e2abe9 100644 --- a/tests/utils/data/test_google_cloud_bucket.py +++ b/tests/utils/data/test_google_cloud_bucket.py @@ -19,10 +19,13 @@ def setUp(self): def test_download_uses_storage_client(self, client_class): client_instance = client_class.return_value download_file_from_gcs( - "TEST_BUCKET", "TEST/FILE/NAME.TXT", "TARGET/PATH" + "TEST_BUCKET", "TEST/FILE/NAME.TXT", "TARGET/PATH", version=None ) client_instance.download.assert_called_with( - "TEST_BUCKET", "TEST/FILE/NAME.TXT", Path("TARGET/PATH") + "TEST_BUCKET", + "TEST/FILE/NAME.TXT", + Path("TARGET/PATH"), + version=None, ) @patch( @@ -31,9 +34,9 @@ def test_download_uses_storage_client(self, client_class): ) def test_download_only_creates_client_once(self, client_class): download_file_from_gcs( - "TEST_BUCKET", "TEST/FILE/NAME.TXT", "TARGET/PATH" + "TEST_BUCKET", "TEST/FILE/NAME.TXT", "TARGET/PATH", version=None ) download_file_from_gcs( - "TEST_BUCKET", "TEST/FILE/NAME.TXT", "ANOTHER/PATH" + "TEST_BUCKET", "TEST/FILE/NAME.TXT", "ANOTHER/PATH", version=None ) client_class.assert_called_once() diff --git a/tests/utils/data/test_simplified_google_storage_client.py b/tests/utils/data/test_simplified_google_storage_client.py index fc692f02..b61fbf5f 100644 --- a/tests/utils/data/test_simplified_google_storage_client.py +++ b/tests/utils/data/test_simplified_google_storage_client.py @@ -1,7 +1,9 @@ -from unittest.mock import patch +from unittest.mock import patch, call import pytest from policyengine.utils.data import SimplifiedGoogleStorageClient +VALID_VERSION = "1.2.3" + class TestSimplifiedGoogleStorageClient: @patch( @@ -40,3 +42,66 @@ def test_download__downloads_content(self, mock_client_class): mock_instance.bucket.assert_called_with("bucket") bucket.blob.assert_called_with("blob.txt") + + @patch( + "policyengine.utils.data.simplified_google_storage_client.Client", + autospec=True, + ) + def test_get_latest_version__returns_version_from_metadata( + self, mock_client_class + ): + mock_instance = mock_client_class.return_value + bucket = mock_instance.get_bucket.return_value + blob = bucket.get_blob.return_value + + # Test case where metadata exists with version + blob.metadata = {"version": VALID_VERSION} + + client = SimplifiedGoogleStorageClient() + result = client._get_latest_version("test_bucket", "test_key") + + assert result == VALID_VERSION + mock_instance.get_bucket.assert_called_with("test_bucket") + bucket.get_blob.assert_called_with("test_key") + + @patch( + "policyengine.utils.data.simplified_google_storage_client.Client", + autospec=True, + ) + def test_get_latest_version__returns_none_when_no_metadata( + self, mock_client_class + ): + mock_instance = mock_client_class.return_value + bucket = mock_instance.get_bucket.return_value + blob = bucket.get_blob.return_value + + # Test case where metadata is None + blob.metadata = None + + client = SimplifiedGoogleStorageClient() + result = client._get_latest_version("test_bucket", "test_key") + + assert result is None + mock_instance.get_bucket.assert_called_with("test_bucket") + bucket.get_blob.assert_called_with("test_key") + + @patch( + "policyengine.utils.data.simplified_google_storage_client.Client", + autospec=True, + ) + def test_get_latest_version__returns_none_when_no_version_in_metadata( + self, mock_client_class + ): + mock_instance = mock_client_class.return_value + bucket = mock_instance.get_bucket.return_value + blob = bucket.get_blob.return_value + + # Test case where metadata exists but no version field + blob.metadata = {"other_field": "value"} + + client = SimplifiedGoogleStorageClient() + result = client._get_latest_version("test_bucket", "test_key") + + assert result is None + mock_instance.get_bucket.assert_called_with("test_bucket") + bucket.get_blob.assert_called_with("test_key")