Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- bump: minor
changes:
added:
- Winner, loser, and no-change percentages for `congressional_district_impact`
in legacy 0.x economy comparisons
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,9 @@ class USCongressionalDistrictImpact(BaseModel):
district: str # e.g., "GA-05"
average_household_income_change: float
relative_household_income_change: float
winner_percentage: float
loser_percentage: float
no_change_percentage: float


class USCongressionalDistrictBreakdownWithValues(BaseModel):
Expand Down Expand Up @@ -906,9 +909,22 @@ def us_congressional_district_breakdown(
district_name = geoid_to_district_name(geoid)

# Extract household data for this district
weights = [baseline.household_weight[i] for i in indices]
baseline_incomes = [baseline.household_net_income[i] for i in indices]
reform_incomes = [reform.household_net_income[i] for i in indices]
weights = np.array([baseline.household_weight[i] for i in indices])
baseline_incomes = np.array(
[baseline.household_net_income[i] for i in indices]
)
reform_incomes = np.array(
[reform.household_net_income[i] for i in indices]
)
household_count_people = getattr(
baseline, "household_count_people", None
)
if household_count_people is None:
people_weights = weights
else:
people_weights = weights * np.array(
[household_count_people[i] for i in indices]
)

baseline_income = MicroSeries(baseline_incomes, weights=weights)
reform_income = MicroSeries(reform_incomes, weights=weights)
Expand All @@ -925,6 +941,30 @@ def us_congressional_district_breakdown(
relative_household_income_change = (
reform_income.sum() / baseline_income.sum() - 1
)
income_change = (reform_incomes - baseline_incomes) / np.maximum(
baseline_incomes, 1.0
)
total_people = float(np.sum(people_weights))

if total_people == 0:
winner_percentage = 0.0
loser_percentage = 0.0
no_change_percentage = 1.0
else:
winner_percentage = float(
np.sum(people_weights[income_change > 1e-3]) / total_people
)
loser_percentage = float(
np.sum(people_weights[income_change <= -1e-3]) / total_people
)
no_change_percentage = float(
np.sum(
people_weights[
(income_change > -1e-3) & (income_change <= 1e-3)
]
)
/ total_people
)

districts.append(
USCongressionalDistrictImpact(
Expand All @@ -935,6 +975,9 @@ def us_congressional_district_breakdown(
relative_household_income_change=float(
relative_household_income_change
),
winner_percentage=winner_percentage,
loser_percentage=loser_percentage,
no_change_percentage=no_change_percentage,
)
)

Expand Down
66 changes: 66 additions & 0 deletions tests/country/test_us_congressional_districts.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,13 @@ def test__given_income_increase__then_returns_positive_changes(self):
baseline = create_mock_single_economy(
household_net_income=[50000.0, 60000.0, 70000.0],
household_weight=[1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2],
congressional_district_geoid=[1305, 1305, 1305],
)
reform = create_mock_single_economy(
household_net_income=[51000.0, 61000.0, 71000.0],
household_weight=[1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2],
congressional_district_geoid=[1305, 1305, 1305],
)
country_id = "us"
Expand All @@ -211,11 +213,13 @@ def test__given_income_decrease__then_returns_negative_changes(self):
baseline = create_mock_single_economy(
household_net_income=[50000.0, 60000.0, 70000.0],
household_weight=[1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2],
congressional_district_geoid=[1305, 1305, 1305],
)
reform = create_mock_single_economy(
household_net_income=[49000.0, 59000.0, 69000.0],
household_weight=[1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2],
congressional_district_geoid=[1305, 1305, 1305],
)
country_id = "us"
Expand All @@ -239,11 +243,13 @@ def test__given_weighted_households__then_calculates_weighted_averages(
baseline = create_mock_single_economy(
household_net_income=[50000.0, 100000.0],
household_weight=[3000.0, 1000.0], # First household has 3x weight
household_count_people=[2, 2],
congressional_district_geoid=[1305, 1305],
)
reform = create_mock_single_economy(
household_net_income=[51000.0, 101000.0],
household_weight=[3000.0, 1000.0],
household_count_people=[2, 2],
congressional_district_geoid=[1305, 1305],
)
country_id = "us"
Expand Down Expand Up @@ -279,9 +285,69 @@ def test__given_district_impact__then_has_required_fields(
assert hasattr(district, "district")
assert hasattr(district, "average_household_income_change")
assert hasattr(district, "relative_household_income_change")
assert hasattr(district, "winner_percentage")
assert hasattr(district, "loser_percentage")
assert hasattr(district, "no_change_percentage")
assert isinstance(district.district, str)
assert isinstance(district.average_household_income_change, float)
assert isinstance(district.relative_household_income_change, float)
assert isinstance(district.winner_percentage, float)
assert isinstance(district.loser_percentage, float)
assert isinstance(district.no_change_percentage, float)

def test__given_mixed_outcomes__then_returns_people_weighted_percentages(
self,
):
# Given: one winner, one loser, one unchanged household with different
# people counts. Outcome shares should be people-weighted.
baseline = create_mock_single_economy(
household_net_income=[1000.0, 1000.0, 1000.0],
household_weight=[1.0, 1.0, 1.0],
household_count_people=[1, 2, 3],
congressional_district_geoid=[1305, 1305, 1305],
)
reform = create_mock_single_economy(
household_net_income=[1002.0, 999.0, 1000.0],
household_weight=[1.0, 1.0, 1.0],
household_count_people=[1, 2, 3],
congressional_district_geoid=[1305, 1305, 1305],
)

# When
result = us_congressional_district_breakdown(baseline, reform, "us")

# Then
district = result.districts[0]
assert district.winner_percentage == pytest.approx(1 / 6)
assert district.loser_percentage == pytest.approx(2 / 6)
assert district.no_change_percentage == pytest.approx(3 / 6)

def test__given_missing_household_count_people__then_falls_back_to_household_weights(
self,
):
# Given: people counts are unavailable, so outcome shares should fall
# back to household weights.
baseline = create_mock_single_economy(
household_net_income=[1000.0, 1000.0],
household_weight=[3.0, 1.0],
household_count_people=None,
congressional_district_geoid=[1305, 1305],
)
reform = create_mock_single_economy(
household_net_income=[1002.0, 998.0],
household_weight=[3.0, 1.0],
household_count_people=None,
congressional_district_geoid=[1305, 1305],
)

# When
result = us_congressional_district_breakdown(baseline, reform, "us")

# Then
district = result.districts[0]
assert district.winner_percentage == pytest.approx(0.75)
assert district.loser_percentage == pytest.approx(0.25)
assert district.no_change_percentage == pytest.approx(0.0)


class TestCongressionalDistrictGeoidExtraction:
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,15 @@ def mock_simulation_with_cliff_vars():
def create_mock_single_economy(
household_net_income: list[float],
household_weight: list[float],
household_count_people: list[int] | None = None,
congressional_district_geoid: list[int] | None = None,
):
"""Create a mock SingleEconomy with specified household data.

Args:
household_net_income: List of household net incomes
household_weight: List of household weights
household_count_people: List of people per household or None
congressional_district_geoid: List of district geoids (SSDD format) or None

Returns:
Expand All @@ -106,6 +108,7 @@ def create_mock_single_economy(
mock_economy = Mock()
mock_economy.household_net_income = household_net_income
mock_economy.household_weight = household_weight
mock_economy.household_count_people = household_count_people
mock_economy.congressional_district_geoid = congressional_district_geoid
return mock_economy

Expand All @@ -128,6 +131,7 @@ def mock_single_economy_with_ga_districts():
100000.0,
],
household_weight=[1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2, 2, 2, 2],
congressional_district_geoid=[1305, 1305, 1305, 1306, 1306, 1306],
)

Expand Down Expand Up @@ -163,6 +167,7 @@ def mock_single_economy_with_multi_state_districts():
1000.0,
1000.0,
],
household_count_people=[2, 2, 2, 2, 2, 2, 2, 2],
congressional_district_geoid=[
1305,
1305,
Expand All @@ -182,6 +187,7 @@ def mock_single_economy_without_districts():
return create_mock_single_economy(
household_net_income=[50000.0, 60000.0, 70000.0],
household_weight=[1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2],
congressional_district_geoid=[0, 0, 0],
)

Expand All @@ -192,5 +198,6 @@ def mock_single_economy_with_null_districts():
return create_mock_single_economy(
household_net_income=[50000.0, 60000.0, 70000.0],
household_weight=[1000.0, 1000.0, 1000.0],
household_count_people=[2, 2, 2],
congressional_district_geoid=None,
)
Loading