Spaces:
Sleeping
Sleeping
Commit ·
b979635
1
Parent(s): 2d052cd
FIPS vs Contiguous County List logic added for FIPS only column
Browse files
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 = "
|
| 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 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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",
|