@@ -111,7 +111,7 @@ def validate_fips_code(fips_code: str, expected_length: int, code_type: str = "F
111111 return fips_code
112112
113113
114- # Variable name mappings
114+ # Variable name mappings (single Census API code per friendly name)
115115VARIABLE_MAPPING = {
116116 'population' : 'B01003_001E' ,
117117 'total_population' : 'B01003_001E' ,
@@ -127,8 +127,6 @@ def validate_fips_code(fips_code: str, expected_length: int, code_type: str = "F
127127 'black_population' : 'B02001_003E' ,
128128 'asian_population' : 'B02001_005E' ,
129129 'hispanic_population' : 'B03002_012E' ,
130- 'poverty' : 'B17001_002E' ,
131- 'poverty_population' : 'B17001_002E' ,
132130 'bachelors_degree' : 'B15003_022E' ,
133131 'high_school' : 'B15003_017E' ,
134132 'households_with_vehicle' : 'B08201_001E' , # Total households (subtract no_vehicle)
@@ -137,15 +135,25 @@ def validate_fips_code(fips_code: str, expected_length: int, code_type: str = "F
137135 'median_rent' : 'B25064_001E' ,
138136}
139137
138+ # Variables computed as sums of multiple Census API codes.
139+ # The B17001 poverty table is NOT available at block group level;
140+ # the C17002 ratio-of-income-to-poverty table IS. Summing the
141+ # "under 0.50" and "0.50 to 0.99" buckets gives total population
142+ # below 100 % of the federal poverty level.
143+ COMPOUND_VARIABLES : dict [str , list [str ]] = {
144+ 'poverty' : ['C17002_002E' , 'C17002_003E' ],
145+ 'poverty_population' : ['C17002_002E' , 'C17002_003E' ],
146+ }
147+
140148
141- def normalize_variable_names (variables : list [str ]) -> list [str ]:
149+ def normalize_variable_names (variables : list [str ]) -> tuple [ list [str ], dict [ str , list [ str ]] ]:
142150 """
143151 Convert human-readable variable names to census codes.
144152
145153 Maps common demographic variable names to their corresponding
146154 Census Bureau API variable codes (e.g., 'population' to
147- 'B01003_001E'). If a variable is already a census code,
148- it is returned unchanged .
155+ 'B01003_001E'). Compound variables (like 'poverty') are expanded
156+ into their component codes .
149157
150158 Parameters
151159 ----------
@@ -156,39 +164,56 @@ def normalize_variable_names(variables: list[str]) -> list[str]:
156164
157165 Returns
158166 -------
159- list of str
160- List of census variable codes corresponding to the input variables.
161- Unknown variables are kept as-is with a warning logged.
167+ tuple of (list[str], dict[str, list[str]])
168+ A 2-tuple:
169+ - List of census variable codes to fetch from the API.
170+ - Dict mapping compound friendly names to their component codes.
171+ Empty if no compound variables were requested.
162172
163173 Examples
164174 --------
165175 >>> normalize_variable_names(['population', 'median_income'])
166- ['B01003_001E', 'B19013_001E']
176+ ( ['B01003_001E', 'B19013_001E'], {})
167177
168- >>> normalize_variable_names(['B01003_001E ', 'housing_units '])
169- ['B01003_001E', 'B25001_001E']
178+ >>> normalize_variable_names(['population ', 'poverty '])
179+ ( ['B01003_001E', 'C17002_002E', 'C17002_003E'], {'poverty': ['C17002_002E', 'C17002_003E']})
170180
171181 Notes
172182 -----
173183 Variable names are case-insensitive and spaces are converted to
174184 underscores during the mapping process.
175185 """
176186 normalized = []
187+ compounds : dict [str , list [str ]] = {}
177188
178189 for var in variables :
179190 # Check if already a census code (has underscore and starts with letter)
180191 if '_' in var and var [0 ].isalpha () and var [0 ].isupper ():
181192 normalized .append (var )
193+ continue
194+
195+ key = var .lower ().replace (' ' , '_' )
196+
197+ # Check compound variables first (e.g., poverty = sum of two codes)
198+ if key in COMPOUND_VARIABLES :
199+ components = COMPOUND_VARIABLES [key ]
200+ compounds [key ] = components
201+ normalized .extend (components )
202+ elif key in VARIABLE_MAPPING :
203+ normalized .append (VARIABLE_MAPPING [key ])
182204 else :
183- # Try to map from human-readable name
184- mapped = VARIABLE_MAPPING .get (var .lower ().replace (' ' , '_' ))
185- if mapped :
186- normalized .append (mapped )
187- else :
188- logger .warning (f"Unknown variable '{ var } ', keeping as-is" )
189- normalized .append (var )
205+ logger .warning (f"Unknown variable '{ var } ', keeping as-is" )
206+ normalized .append (var )
207+
208+ # Deduplicate while preserving order
209+ seen : set [str ] = set ()
210+ deduped = []
211+ for code in normalized :
212+ if code not in seen :
213+ seen .add (code )
214+ deduped .append (code )
190215
191- return normalized
216+ return deduped , compounds
192217
193218
194219def fetch_block_groups_for_area (geometry : Polygon ) -> list [dict [str , Any ]]:
@@ -666,7 +691,12 @@ def fetch_census_data(
666691 for j , header in enumerate (headers ):
667692 if header in variables :
668693 try :
669- geoid_data [header ] = float (row [j ]) if row [j ] else None
694+ val = float (row [j ]) if row [j ] else None
695+ # Census API uses large negative sentinels
696+ # for unavailable data (-666666666, etc.)
697+ if val is not None and val <= - 666666666 :
698+ val = None
699+ geoid_data [header ] = val
670700 except (ValueError , TypeError ):
671701 geoid_data [header ] = row [j ]
672702
0 commit comments