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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The Rhode Island CTC Calculator allows users to model how different Child Tax Cr
This tool exemplifies PolicyEngine's approach to policy analysis: providing transparent, accessible modeling that empowers policymakers, advocates, and the public to understand the trade-offs inherent in policy design. Use the calculator below:

<iframe src="https://policyengine.org/us/rhode-island-ctc-calculator" width="100%"
height="800" frameborder="0"></iframe>
height="800" frameborder="0"></iframe>

The Niskanen Center tested the calculator and used it to evaluate how different CTC parameters would affect Rhode Island households across the income distribution. They then shared the tool with Governor Dan McKee's office, which used the calculator to evaluate reform options and design the Child Tax Credit proposal included in his fiscal year 2027 budget.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Governor McKee's proposal would replace this exemption with a $325 fully refunda
- **Credit amount:** $325 per qualifying child
- **Age eligibility:** Children under 19 years old
- **Refundability:** Fully refundable, meaning families can receive the credit with no earnings
- **Phase-out:** Mirrors the personal exemption's phase-out structure. The credit amount is reduced by 20% for each $7,590 of AGI above $265,965 in 2027.
- **Exemption integration:** Eliminates the personal exemption for CTC-qualifying children; the exemption remains for dependents 19 and older.
- **Phase-out:** Mirrors the personal exemption's phase-out structure. The credit amount is reduced by 20% for each $7,590 of AGI above $265,965 in 2027.
- **Exemption integration:** Eliminates the personal exemption for CTC-qualifying children; the exemption remains for dependents 19 and older.

Every household with a child would receive the same credit amount of $325 before the phase-out, rather than receiving tax savings determined by their income.

Expand All @@ -36,15 +36,15 @@ The reform primarily benefits lower-income families with children, as they see l

## Statewide impacts

For tax year 2027, the proposal would reduce state revenues by $36.7 million, according to PolicyEngine's static modeling. The reform would also raise the net income of 29.2% of Rhode Island residents, with beneficiaries concentrated in lower income ranges (see Figure 2). We project that the proposal would reduce poverty and child poverty by 1.0 and 2.1%, respectively, as measured by the Supplemental Poverty Measure. Figure 3 displays the statewide impacts of Governor McKee's CTC proposal.
For tax year 2027, the proposal would reduce state revenues by $36.7 million, according to PolicyEngine's static modeling. The reform would also raise the net income of 29.2% of Rhode Island residents, with beneficiaries concentrated in lower income ranges (see Figure 2). We project that the proposal would reduce poverty and child poverty by 1.0 and 2.1%, respectively, as measured by the Supplemental Poverty Measure. Figure 3 displays the statewide impacts of Governor McKee's CTC proposal.

![Figure 2: Average household benefit by income range](/assets/posts/ri-governor-ctc-income-chart.png)

![Figure 3: Statewide impacts of Governor McKee's child tax credit proposal](/assets/posts/ri-governor-ctc-statewide-impacts.png)

## Conclusion

Governor McKee's proposed child tax credit would replace the personal exemption with a $325 fully refundable CTC for children under the age of 19. We project the reform would benefit 29.2% of Rhode Island residents, while lowering net income for no households.
Governor McKee's proposed child tax credit would replace the personal exemption with a $325 fully refundable CTC for children under the age of 19. We project the reform would benefit 29.2% of Rhode Island residents, while lowering net income for no households.

As policymakers evaluate reforms such as these, analytical tools like PolicyEngine offer critical insights into the impacts on diverse household compositions and the broader economy.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ TRIM3 does not document how it selects which non-reporters to assign UI or how i

## Summary

| Model | Method | Distribution | Open source |
| ------------------- | ------------------------------ | ----------------------- | -------------------------------------------------------------------- |
| **PolicyEngine** | QRF + reweight to SOI | Nonparametric (learned) | [Yes](https://github.com/PolicyEngine/policyengine-us-data) |
| **Fed/JCT** | Normal draw by percentile | Normal(μ, σ) | [Yes (Stata)](https://davidsplinter.com/CPS-UI-Imputation.txt) |
| **CBO** | Probit → assign group averages | Point estimates | [Partial](https://github.com/US-CBO/means_tested_transfer_imputations) |
| **TRIM3** | Adjust reported amounts | Deterministic | No |
| Model | Method | Distribution | Open source |
| ---------------- | ------------------------------ | ----------------------- | ---------------------------------------------------------------------- |
| **PolicyEngine** | QRF + reweight to SOI | Nonparametric (learned) | [Yes](https://github.com/PolicyEngine/policyengine-us-data) |
| **Fed/JCT** | Normal draw by percentile | Normal(μ, σ) | [Yes (Stata)](https://davidsplinter.com/CPS-UI-Imputation.txt) |
| **CBO** | Probit → assign group averages | Point estimates | [Partial](https://github.com/US-CBO/means_tested_transfer_imputations) |
| **TRIM3** | Adjust reported amounts | Deterministic | No |

## Why methodology matters

Expand Down
6 changes: 6 additions & 0 deletions app/src/hooks/useUserReports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@/types/ingredients/UserPopulation';
import { UserReport } from '@/types/ingredients/UserReport';
import { UserSimulation } from '@/types/ingredients/UserSimulation';
import { findPlaceFromRegionString, getPlaceDisplayName } from '@/utils/regionStrategies';
import { householdKeys, policyKeys, reportKeys, simulationKeys } from '../libs/queryKeys';
import { useGeographicAssociationsByUser } from './useUserGeographic';
import { useHouseholdAssociationsByUser } from './useUserHousehold';
Expand Down Expand Up @@ -470,6 +471,11 @@ export const useUserReportById = (userReportId: string, options?: { enabled?: bo
let name: string;
if (isNational) {
name = sim.countryId.toUpperCase();
} else if (sim.populationId.startsWith('place/')) {
// For US places (municipalities), use the place lookup function
// Import at top: import { findPlaceFromRegionString, getPlaceDisplayName } from '@/utils/regionStrategies';
const place = findPlaceFromRegionString(sim.populationId);
name = place ? getPlaceDisplayName(place.name) : sim.populationId;
} else {
// For subnational, extract the base geography ID and look up in metadata
// e.g., "us-fl" -> "fl", "uk-scotland" -> "scotland"
Expand Down
5 changes: 5 additions & 0 deletions app/src/hooks/utils/useFetchReportIngredients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from '@/types/ingredients/UserPopulation';
import { UserReport } from '@/types/ingredients/UserReport';
import { UserSimulation } from '@/types/ingredients/UserSimulation';
import { findPlaceFromRegionString, getPlaceDisplayName } from '@/utils/regionStrategies';
import { combineLoadingStates, extractUniqueIds, useParallelQueries } from './normalizedUtils';

// Type for geography options from redux store
Expand Down Expand Up @@ -60,6 +61,10 @@ export function buildGeographiesFromSimulations(
let name: string;
if (isNational) {
name = sim.countryId.toUpperCase();
} else if (sim.populationId.startsWith('place/')) {
// For US places (municipalities), use the place lookup function
const place = findPlaceFromRegionString(sim.populationId);
name = place ? getPlaceDisplayName(place.name) : sim.populationId;
} else {
// For subnational, extract the base geography ID and look up in metadata
const parts = sim.populationId.split('-');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Box, Radio } from '@mantine/core';
import { USScopeType } from '@/types/regionTypes';
import { RegionOption, US_REGION_TYPES } from '@/utils/regionStrategies';
import USDistrictSelector from './USDistrictSelector';
import USPlaceSelector from './USPlaceSelector';
import USStateSelector from './USStateSelector';

interface USGeographicOptionsProps {
Expand Down Expand Up @@ -75,6 +76,21 @@ export default function USGeographicOptions({
)}
</Box>

{/* Place (city) option */}
<Box>
<Radio
value={US_REGION_TYPES.PLACE}
label="All households in a city"
checked={scope === US_REGION_TYPES.PLACE}
onChange={() => handleScopeChange(US_REGION_TYPES.PLACE)}
/>
{scope === US_REGION_TYPES.PLACE && (
<Box ml={24} mt="xs">
<USPlaceSelector selectedPlace={selectedRegion} onPlaceChange={onRegionChange} />
</Box>
)}
</Box>

{/* Household option */}
<Radio
value="household"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useEffect, useMemo, useState } from 'react';
import { Group, Select, Text } from '@mantine/core';
import {
filterPlacesByState,
findPlaceFromRegionString,
getPlaceDisplayName,
getPlaceStateNames,
placeToRegionString,
} from '@/utils/regionStrategies';

interface USPlaceSelectorProps {
selectedPlace: string;
onPlaceChange: (place: string) => void;
}

export default function USPlaceSelector({ selectedPlace, onPlaceChange }: USPlaceSelectorProps) {
const [selectedStateName, setSelectedStateName] = useState<string>('');

// Get unique state names for the state dropdown
const stateNames = useMemo(() => getPlaceStateNames(), []);

// Initialize selectedStateName from selectedPlace if one is already selected
useEffect(() => {
if (selectedPlace) {
const place = findPlaceFromRegionString(selectedPlace);
if (place && place.stateName !== selectedStateName) {
setSelectedStateName(place.stateName);
}
}
}, [selectedPlace, selectedStateName]);

// Filter places based on selected state name
const filteredPlaces = useMemo(() => filterPlacesByState(selectedStateName), [selectedStateName]);

// Format places for the dropdown with clean display names, sorted alphabetically
const placeOptions = useMemo(
() =>
filteredPlaces
.map((p) => ({
value: placeToRegionString(p),
label: getPlaceDisplayName(p.name),
}))
.sort((a, b) => a.label.localeCompare(b.label)),
[filteredPlaces]
);

// Handle state change for place selection
const handleStateChange = (stateName: string | null) => {
setSelectedStateName(stateName || '');
// Auto-select the first place when a state is chosen
if (stateName) {
const statePlaces = filterPlacesByState(stateName);
if (statePlaces.length > 0) {
onPlaceChange(placeToRegionString(statePlaces[0]));
}
} else {
onPlaceChange('');
}
};

return (
<Group gap="sm" align="flex-start">
<div style={{ flex: 1 }}>
<Text size="sm" fw={500} mb={4}>
Select state
</Text>
<Select
placeholder="Choose a state"
data={stateNames}
value={selectedStateName}
onChange={handleStateChange}
searchable
/>
</div>
<div style={{ flex: 2 }}>
<Text size="sm" fw={500} mb={4}>
Select city
</Text>
{selectedStateName ? (
<Select
placeholder="Choose a city"
data={placeOptions}
value={selectedPlace}
onChange={(val) => {
// If user clicks the same option again (val is null), keep the current selection
if (val !== null) {
onPlaceChange(val);
}
}}
searchable
/>
) : (
<Select placeholder="--" data={[]} disabled />
)}
</div>
</Group>
);
}
20 changes: 17 additions & 3 deletions app/src/pathways/report/views/population/PopulationLabelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import PathwayView from '@/components/common/PathwayView';
import { useCurrentCountry } from '@/hooks/useCurrentCountry';
import { PathwayMode } from '@/types/pathwayModes/PathwayMode';
import { PopulationStateProps } from '@/types/pathwayState';
import { extractRegionDisplayValue } from '@/utils/regionStrategies';
import {
extractRegionDisplayValue,
findPlaceFromRegionString,
getPlaceDisplayName,
} from '@/utils/regionStrategies';

interface PopulationLabelViewProps {
population: PopulationStateProps;
Expand Down Expand Up @@ -50,8 +54,18 @@ export default function PopulationLabelView({
if (population.geography.scope === 'national') {
return 'National Households';
} else if (population.geography.geographyId) {
// Use display value (strip prefix for UK regions)
const displayValue = extractRegionDisplayValue(population.geography.geographyId);
const geographyId = population.geography.geographyId;

// Check if this is a US place (city)
if (geographyId.startsWith('place/')) {
const place = findPlaceFromRegionString(geographyId);
if (place) {
return `${getPlaceDisplayName(place.name)} Households`;
}
}

// Use display value (strip prefix for UK regions and other types)
const displayValue = extractRegionDisplayValue(geographyId);
return `${displayValue} Households`;
}
return 'Regional Households';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function PopulationScopeView({
const needsRegion = [
US_REGION_TYPES.STATE,
US_REGION_TYPES.CONGRESSIONAL_DISTRICT,
US_REGION_TYPES.PLACE,
UK_REGION_TYPES.COUNTRY,
UK_REGION_TYPES.CONSTITUENCY,
UK_REGION_TYPES.LOCAL_AUTHORITY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@ export const MOCK_REGIONS: MetadataRegionEntry[] = [
state_abbreviation: 'TX',
state_name: 'Texas',
},
{
name: 'NYC',
label: 'New York City',
type: US_REGION_TYPES.CITY,
},
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { vi } from 'vitest';
import { US_REGION_TYPES, USScopeType } from '@/types/regionTypes';

// Mock state options
export const MOCK_STATE_OPTIONS = [
{ value: 'state/ca', label: 'California', type: US_REGION_TYPES.STATE },
{ value: 'state/ny', label: 'New York', type: US_REGION_TYPES.STATE },
{ value: 'state/tx', label: 'Texas', type: US_REGION_TYPES.STATE },
];

// Mock district options
export const MOCK_DISTRICT_OPTIONS = [
{
value: 'congressional_district/CA-01',
label: "California's 1st congressional district",
type: US_REGION_TYPES.CONGRESSIONAL_DISTRICT,
stateAbbreviation: 'CA',
stateName: 'California',
},
{
value: 'congressional_district/CA-02',
label: "California's 2nd congressional district",
type: US_REGION_TYPES.CONGRESSIONAL_DISTRICT,
stateAbbreviation: 'CA',
stateName: 'California',
},
];

// Test scope values
export const TEST_SCOPE_VALUES = {
NATIONAL: US_REGION_TYPES.NATIONAL,
STATE: US_REGION_TYPES.STATE,
CONGRESSIONAL_DISTRICT: US_REGION_TYPES.CONGRESSIONAL_DISTRICT,
PLACE: US_REGION_TYPES.PLACE,
HOUSEHOLD: 'household' as const,
};

// Default props for testing
export const DEFAULT_PROPS = {
scope: US_REGION_TYPES.NATIONAL as USScopeType,
selectedRegion: '',
stateOptions: MOCK_STATE_OPTIONS,
districtOptions: MOCK_DISTRICT_OPTIONS,
onScopeChange: vi.fn(),
onRegionChange: vi.fn(),
};

// Factory function to create fresh props for each test
export function createMockProps(overrides: Partial<typeof DEFAULT_PROPS> = {}) {
return {
scope: US_REGION_TYPES.NATIONAL as USScopeType,
selectedRegion: '',
stateOptions: [...MOCK_STATE_OPTIONS],
districtOptions: [...MOCK_DISTRICT_OPTIONS],
onScopeChange: vi.fn(),
onRegionChange: vi.fn(),
...overrides,
};
}

// Radio button labels
export const RADIO_LABELS = {
NATIONAL: 'All households nationally',
STATE: 'All households in a state or federal district',
CONGRESSIONAL_DISTRICT: 'All households in a congressional district',
PLACE: 'All households in a city',
HOUSEHOLD: 'Custom household',
} as const;
Loading