saulsolis09 commited on
Commit
b979635
·
1 Parent(s): 2d052cd

FIPS vs Contiguous County List logic added for FIPS only column

Browse files
Files changed (1) hide show
  1. app.py +65 -12
app.py CHANGED
@@ -56,7 +56,7 @@ FILTER_DOMINANT_COUNTY = False # process ALL counties (multi-county / full-stat
56
  # "csv" -> Master_Dataset.zip, every result bundled as a CSV (faster: no Excel
57
  # writing -- use this for very large / state-sized runs)
58
  # The individual per-category files are CSV in both modes.
59
- MASTER_OUTPUT = "csv"
60
 
61
  # How records are processed:
62
  # "stream" -> ONE COUNTY AT A TIME. The input is split into per-county temp
@@ -118,7 +118,7 @@ _SAFETY_CACHE: dict | None = None
118
 
119
 
120
  def build_whitelist_from_path(path):
121
- """Parse a safety-list file (xlsx or csv) into {pairs, cities, rows}."""
122
  if str(path).lower().endswith(".csv"):
123
  safety_df = pd.read_csv(path)
124
  else:
@@ -126,17 +126,31 @@ def build_whitelist_from_path(path):
126
  acceptable_cols = [c for c in safety_df.columns if str(c).startswith("Acceptable FIPS")]
127
 
128
  valid_pairs, valid_cities = set(), set()
 
 
 
 
 
129
  for _, r in safety_df.iterrows():
130
  c = _norm_city(r["City Name"])
131
- if c is None:
132
- continue
133
- valid_cities.add(c)
134
- for col in acceptable_cols:
135
- f = _norm_fips(r[col])
136
- if f:
137
- valid_pairs.add(c + "||" + f)
138
-
139
- return {"pairs": valid_pairs, "cities": valid_cities, "rows": len(safety_df)}
 
 
 
 
 
 
 
 
 
140
 
141
 
142
  def load_whitelist():
@@ -175,7 +189,7 @@ def _norm_fips(x):
175
  # values shaped like "05045-FAULKNER" (FIPS digits, a dash, a county name).
176
  # Header names are only used as a last-resort fallback.
177
  FIPS_COUNTY_PATTERN = r"^\s*\d{1,5}\s*-\s*[A-Za-z]"
178
- FALLBACK_FIPS_HEADERS = ["** REC FIPS-COUNTY NAME", "** REC FIPS-COUNTY", "County", "DocumentRecordingCountyFIPS"]
179
 
180
 
181
  def detect_fips_county_column(df, sample_size=1000, min_score=0.5):
@@ -209,6 +223,41 @@ def detect_fips_county_column(df, sample_size=1000, min_score=0.5):
209
  return None, "not found"
210
 
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  # =====================================================================
213
  # SELF-TEST (startup health check — same logic as the notebook's cell)
214
  # =====================================================================
@@ -616,6 +665,9 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
616
  "\n\nCOLUMN ERROR: could not find a FIPS-county column. Expected "
617
  "values like '05045-FAULKNER' in one of the columns."), []
618
  _step(log, f"FIPS-county column: '{fips_col}' ({fips_reason})")
 
 
 
619
 
620
  # ---- FIPS validation (uses the reference list) -------------------
621
  _step(log, "Validating FIPS/city pairs...")
@@ -994,6 +1046,7 @@ def process_county(cdf, wl, fips_col, cluster_offset,
994
  Cluster_IDs are offset by `cluster_offset` so, processing counties in sorted
995
  order, the numbering matches the whole-dataset ("memory") path."""
996
  ignore = []
 
997
  cdf, audit = fips_validate(cdf, blank_cities_not_in_list, ignore, wl=wl, fips_col=fips_col)
998
 
999
  for col in ["DDPPropertyCityFinal", "DDPPropertyZIPFinal",
 
56
  # "csv" -> Master_Dataset.zip, every result bundled as a CSV (faster: no Excel
57
  # writing -- use this for very large / state-sized runs)
58
  # The individual per-category files are CSV in both modes.
59
+ MASTER_OUTPUT = "excel"
60
 
61
  # How records are processed:
62
  # "stream" -> ONE COUNTY AT A TIME. The input is split into per-county temp
 
118
 
119
 
120
  def build_whitelist_from_path(path):
121
+ """Parse a safety-list file (xlsx or csv) into {pairs, cities, rows, fips_name}."""
122
  if str(path).lower().endswith(".csv"):
123
  safety_df = pd.read_csv(path)
124
  else:
 
126
  acceptable_cols = [c for c in safety_df.columns if str(c).startswith("Acceptable FIPS")]
127
 
128
  valid_pairs, valid_cities = set(), set()
129
+ # FIPS -> county name, from the positional pairing of "Acceptable counties"
130
+ # (comma-separated) against Acceptable FIPS 1..N. Used to synthesize the
131
+ # "<FIPS>-<NAME>" column when source data has only bare FIPS codes.
132
+ fips_name_votes = defaultdict(lambda: defaultdict(int))
133
+ has_names = "Acceptable counties" in safety_df.columns
134
  for _, r in safety_df.iterrows():
135
  c = _norm_city(r["City Name"])
136
+ if c is not None:
137
+ valid_cities.add(c)
138
+ for col in acceptable_cols:
139
+ f = _norm_fips(r[col])
140
+ if f:
141
+ valid_pairs.add(c + "||" + f)
142
+ if has_names:
143
+ names = [n.strip().upper() for n in str(r["Acceptable counties"]).split(",") if n.strip()]
144
+ codes = [_norm_fips(r[col]) for col in acceptable_cols]
145
+ for nm, code in zip(names, codes):
146
+ if code and nm:
147
+ fips_name_votes[code][nm] += 1
148
+
149
+ fips_name = {code: max(v.items(), key=lambda kv: kv[1])[0]
150
+ for code, v in fips_name_votes.items()}
151
+
152
+ return {"pairs": valid_pairs, "cities": valid_cities, "rows": len(safety_df),
153
+ "fips_name": fips_name}
154
 
155
 
156
  def load_whitelist():
 
189
  # values shaped like "05045-FAULKNER" (FIPS digits, a dash, a county name).
190
  # Header names are only used as a last-resort fallback.
191
  FIPS_COUNTY_PATTERN = r"^\s*\d{1,5}\s*-\s*[A-Za-z]"
192
+ FALLBACK_FIPS_HEADERS = ["** REC FIPS-COUNTY NAME", "** REC FIPS-COUNTY", "County"]
193
 
194
 
195
  def detect_fips_county_column(df, sample_size=1000, min_score=0.5):
 
223
  return None, "not found"
224
 
225
 
226
+ def _is_combined_fips(series, sample=1000):
227
+ """True if the column already holds '<FIPS>-<NAME>' values (vs bare codes)."""
228
+ s = series.dropna().astype(str).str.strip().head(sample)
229
+ if len(s) == 0:
230
+ return False
231
+ try:
232
+ return float(s.str.match(FIPS_COUNTY_PATTERN).mean()) >= 0.5
233
+ except Exception:
234
+ return False
235
+
236
+
237
+ def ensure_combined_county(df, fips_col, reason, wl, log=None):
238
+ """Guarantee a '<FIPS>-<NAME>' county column. If `fips_col` already holds
239
+ combined values (PBIX-processed data), keep it. If it holds BARE FIPS codes
240
+ (raw SQL, e.g. DocumentRecordingCountyFIPS = '48201'), synthesize a 'County'
241
+ column '48201-HARRIS' using the FIPS->name map from the safety list, so the
242
+ rest of the pipeline behaves exactly as it does for the combined-column states.
243
+ Returns (column_name_to_use, reason)."""
244
+ if fips_col is None:
245
+ return fips_col, reason
246
+ if _is_combined_fips(df[fips_col]):
247
+ return fips_col, reason
248
+ fips_name = wl.get("fips_name", {}) if isinstance(wl, dict) else {}
249
+ if not fips_name:
250
+ return fips_col, reason # no map -> leave bare (still groups/validates by FIPS)
251
+ codes = df[fips_col].map(_norm_fips) # 5-digit string or None
252
+ names = codes.map(lambda c: fips_name.get(c, "") if c else "")
253
+ df["County"] = [f"{c}-{n}" if c else "" for c, n in zip(codes.fillna(""), names)]
254
+ if log is not None:
255
+ unresolved = int(((names == "") & codes.notna()).sum())
256
+ log.append(f"Built county names from bare FIPS column '{fips_col}' via the "
257
+ f"safety list ({unresolved:,} rows had no name match).")
258
+ return "County", f"synthesized '<FIPS>-<NAME>' from bare FIPS column '{fips_col}'"
259
+
260
+
261
  # =====================================================================
262
  # SELF-TEST (startup health check — same logic as the notebook's cell)
263
  # =====================================================================
 
665
  "\n\nCOLUMN ERROR: could not find a FIPS-county column. Expected "
666
  "values like '05045-FAULKNER' in one of the columns."), []
667
  _step(log, f"FIPS-county column: '{fips_col}' ({fips_reason})")
668
+ fips_col, fips_reason = ensure_combined_county(df, fips_col, fips_reason, wl, log)
669
+ if "synthesized" in fips_reason:
670
+ _step(log, f"County column now '{fips_col}' ({fips_reason})")
671
 
672
  # ---- FIPS validation (uses the reference list) -------------------
673
  _step(log, "Validating FIPS/city pairs...")
 
1046
  Cluster_IDs are offset by `cluster_offset` so, processing counties in sorted
1047
  order, the numbering matches the whole-dataset ("memory") path."""
1048
  ignore = []
1049
+ fips_col, _r = ensure_combined_county(cdf, fips_col, "", wl, None)
1050
  cdf, audit = fips_validate(cdf, blank_cities_not_in_list, ignore, wl=wl, fips_col=fips_col)
1051
 
1052
  for col in ["DDPPropertyCityFinal", "DDPPropertyZIPFinal",