saulsolis09 commited on
Commit
bb973ac
·
1 Parent(s): 7806dff

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +51 -10
  2. requirements.txt +2 -0
app.py CHANGED
@@ -56,7 +56,30 @@ 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
 
62
  def get_safety_list_path() -> str:
@@ -141,7 +164,7 @@ def _norm_fips(x):
141
  # values shaped like "05045-FAULKNER" (FIPS digits, a dash, a county name).
142
  # Header names are only used as a last-resort fallback.
143
  FIPS_COUNTY_PATTERN = r"^\s*\d{1,5}\s*-\s*[A-Za-z]"
144
- FALLBACK_FIPS_HEADERS = ["** REC FIPS-COUNTY NAME", "** REC FIPS-COUNTY", "County", "DocumentRecordingCountyFIPS"]
145
 
146
 
147
  def detect_fips_county_column(df, sample_size=1000, min_score=0.5):
@@ -562,16 +585,18 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
562
  name = f.name
563
  try:
564
  if str(name).lower().endswith(".csv"):
565
- dfs.append(pd.read_csv(name))
566
  else:
567
  dfs.append(pd.read_excel(name))
568
- log.append(f"Loaded {Path(name).name}")
569
  except Exception as e:
570
- log.append(f"ERROR loading {Path(name).name}: {e}")
571
  if not dfs:
572
  return "\n".join(log) + "\nNo files could be read.", []
573
  df = pd.concat(dfs, ignore_index=True)
574
- log.append(f"Merged shape: {df.shape}")
 
 
575
 
576
  # ---- Locate the FIPS-county column by CONTENT --------------------
577
  fips_col, fips_reason = detect_fips_county_column(df)
@@ -579,13 +604,15 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
579
  return ("\n".join(log) +
580
  "\n\nCOLUMN ERROR: could not find a FIPS-county column. Expected "
581
  "values like '05045-FAULKNER' in one of the columns."), []
582
- log.append(f"FIPS-county column: '{fips_col}' ({fips_reason})")
583
 
584
  # ---- FIPS validation (uses the reference list) -------------------
 
585
  try:
586
  df, audit = fips_validate(df, blank_cities_not_in_list, log, wl=wl, fips_col=fips_col)
587
  except FileNotFoundError as e:
588
  return "\n".join(log) + f"\n\nREFERENCE FILE ERROR:\n{e}", []
 
589
 
590
  # ---- Verification files (audit + RawFile_FIPSValidated), CSV -----
591
  if len(audit) > 0:
@@ -596,6 +623,7 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
596
  rp = out_dir / "RawFile_FIPSValidated.csv"
597
  df.to_csv(rp, index=False)
598
  outputs.append(str(rp))
 
599
 
600
  # ---- UNDETERMINED/NULL -> NaN ------------------------------------
601
  for col in ["DDPPropertyCityFinal", "DDPPropertyZIPFinal",
@@ -623,12 +651,15 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
623
  zip_df = df.loc[df["DDPPropertyZIPFinal"].notna()].copy()
624
  census_tracts_df = df.loc[df["DDPRevisedCensusTract"].notna()].copy()
625
  original = df.loc[df["LegalSubDivision"].notna()].copy()
 
626
 
627
  # ---- Cluster ------------------------------------------------------
 
628
  cluster_df = build_clusters(city_df, zip_df, census_tracts_df, cluster_threshold)
629
- log.append(f"Clusters: {cluster_df['Cluster_ID'].nunique():,}")
630
 
631
  # ---- Replace-with (majority vote) --------------------------------
 
632
  zip_w = build_replace_with(
633
  build_cluster_wide_table(zip_df, cluster_df, "DDPPropertyZIPFinal", "ZIP_"),
634
  "ZIP_", replace_threshold).rename(columns={"Replace with": "Replace with Zip Codes"})
@@ -638,8 +669,10 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
638
  cen_w = build_replace_with(
639
  build_cluster_wide_table(census_tracts_df, cluster_df, "DDPRevisedCensusTract", "CensusTract_"),
640
  "CensusTract_", replace_threshold).rename(columns={"Replace with": "Replace with Census Tracts"})
 
641
 
642
  # ---- Map back onto original --------------------------------------
 
643
  # Same fix: assign each row its county's cluster, keyed on (County, name).
644
  lk = cluster_df[["County", "LegalSubDivision", "Cluster_ID"]].drop_duplicates(
645
  ["County", "LegalSubDivision"])
@@ -673,7 +706,10 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
673
  other = [c for c in existing if c not in lead]
674
  original_final = original_final[lead + other]
675
 
676
- original_final_output = original_final.copy() # full output snapshot
 
 
 
677
 
678
  # ---- Split into final cities / zips / census tracts --------------
679
  keep = ["RecordKey", "ComesFromMtg2", "IsFlagged",
@@ -735,12 +771,15 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
735
  p = out_dir / name
736
  d.to_csv(p, index=False)
737
  outputs.append(str(p))
 
738
 
739
  # ---- Full output CSV (chunked at 1,000,000 rows) -----------------
740
  # NB: no '%' in the filename -- '%' is the URL-encoding character and makes
741
  # the browser download fail with "unknown server error".
 
742
  csv_name = f"OutputFile_{replace_threshold}Threshold_{cluster_threshold}ClustersRapidFuzz.csv"
743
  outputs.extend(save_csv_in_parts(original_final_output, csv_name, out_dir, row_limit=1_000_000))
 
744
 
745
  # ---- Summary sheet: ONE ROW PER COUNTY ---------------------------
746
  # Totals (records / subdivisions) come from the FULL post-validation frame
@@ -807,13 +846,14 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
807
  "Total Blank Zips", "Total Zips Assignment", "Total Blank Census",
808
  "Total Census Assignment"]
809
  summary_df = pd.DataFrame(summary_rows, columns=summary_cols)
810
- log.append(f"Summary: {len(summary_df)} county row(s).")
811
 
812
  summary_path = out_dir / "Summary.csv"
813
  summary_df.to_csv(summary_path, index=False)
814
  outputs.append(str(summary_path))
815
 
816
  # ---- Master bundle: Excel workbook OR zip of CSVs (see MASTER_OUTPUT) -
 
817
  if MASTER_OUTPUT == "csv":
818
  # Bundle the already-written CSVs into one zip. No Excel writing -> fast.
819
  master_path = out_dir / "Master_Dataset.zip"
@@ -842,6 +882,7 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
842
 
843
  elapsed_min = (time.time() - t_start) / 60
844
  log.append(f"Done. {len(outputs)} files ready.")
 
845
  log.append(f"Total processing time: {elapsed_min:.1f} minutes.")
846
  gc.collect()
847
  return "\n".join(log), outputs
 
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
+
62
+ def _mem_mb():
63
+ """Current process memory (RSS) in MB. psutil if available, else peak RSS."""
64
+ try:
65
+ import psutil
66
+ return psutil.Process().memory_info().rss / (1024 * 1024)
67
+ except Exception:
68
+ try:
69
+ import resource
70
+ return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 # KB -> MB
71
+ except Exception:
72
+ return float("nan")
73
+
74
+
75
+ def _step(log, msg):
76
+ """Record a phase line with a timestamp + current RAM, and PRINT it live
77
+ (flushed) so it reaches the Space's container log immediately. If the process
78
+ is killed (e.g. out of memory), the last printed line pinpoints the phase."""
79
+ line = f"[{time.strftime('%H:%M:%S')}] {msg} | RAM {_mem_mb():.0f} MB"
80
+ print(line, flush=True)
81
+ log.append(line)
82
+ return line
83
 
84
 
85
  def get_safety_list_path() -> str:
 
164
  # values shaped like "05045-FAULKNER" (FIPS digits, a dash, a county name).
165
  # Header names are only used as a last-resort fallback.
166
  FIPS_COUNTY_PATTERN = r"^\s*\d{1,5}\s*-\s*[A-Za-z]"
167
+ FALLBACK_FIPS_HEADERS = ["** REC FIPS-COUNTY NAME", "** REC FIPS-COUNTY", "County"]
168
 
169
 
170
  def detect_fips_county_column(df, sample_size=1000, min_score=0.5):
 
585
  name = f.name
586
  try:
587
  if str(name).lower().endswith(".csv"):
588
+ dfs.append(pd.read_csv(name, low_memory=False))
589
  else:
590
  dfs.append(pd.read_excel(name))
591
+ _step(log, f"Loaded {Path(name).name} ({len(dfs[-1]):,} rows)")
592
  except Exception as e:
593
+ _step(log, f"ERROR loading {Path(name).name}: {e}")
594
  if not dfs:
595
  return "\n".join(log) + "\nNo files could be read.", []
596
  df = pd.concat(dfs, ignore_index=True)
597
+ del dfs
598
+ gc.collect()
599
+ _step(log, f"Merged shape: {df.shape}")
600
 
601
  # ---- Locate the FIPS-county column by CONTENT --------------------
602
  fips_col, fips_reason = detect_fips_county_column(df)
 
604
  return ("\n".join(log) +
605
  "\n\nCOLUMN ERROR: could not find a FIPS-county column. Expected "
606
  "values like '05045-FAULKNER' in one of the columns."), []
607
+ _step(log, f"FIPS-county column: '{fips_col}' ({fips_reason})")
608
 
609
  # ---- FIPS validation (uses the reference list) -------------------
610
+ _step(log, "Validating FIPS/city pairs...")
611
  try:
612
  df, audit = fips_validate(df, blank_cities_not_in_list, log, wl=wl, fips_col=fips_col)
613
  except FileNotFoundError as e:
614
  return "\n".join(log) + f"\n\nREFERENCE FILE ERROR:\n{e}", []
615
+ _step(log, "Validation done")
616
 
617
  # ---- Verification files (audit + RawFile_FIPSValidated), CSV -----
618
  if len(audit) > 0:
 
623
  rp = out_dir / "RawFile_FIPSValidated.csv"
624
  df.to_csv(rp, index=False)
625
  outputs.append(str(rp))
626
+ _step(log, "Wrote RawFile_FIPSValidated.csv")
627
 
628
  # ---- UNDETERMINED/NULL -> NaN ------------------------------------
629
  for col in ["DDPPropertyCityFinal", "DDPPropertyZIPFinal",
 
651
  zip_df = df.loc[df["DDPPropertyZIPFinal"].notna()].copy()
652
  census_tracts_df = df.loc[df["DDPRevisedCensusTract"].notna()].copy()
653
  original = df.loc[df["LegalSubDivision"].notna()].copy()
654
+ _step(log, f"Source frames built (subdivisions: {len(original):,})")
655
 
656
  # ---- Cluster ------------------------------------------------------
657
+ _step(log, "Clustering LegalSubDivision per county...")
658
  cluster_df = build_clusters(city_df, zip_df, census_tracts_df, cluster_threshold)
659
+ _step(log, f"Clusters: {cluster_df['Cluster_ID'].nunique():,}")
660
 
661
  # ---- Replace-with (majority vote) --------------------------------
662
+ _step(log, "Building majority-vote tables...")
663
  zip_w = build_replace_with(
664
  build_cluster_wide_table(zip_df, cluster_df, "DDPPropertyZIPFinal", "ZIP_"),
665
  "ZIP_", replace_threshold).rename(columns={"Replace with": "Replace with Zip Codes"})
 
669
  cen_w = build_replace_with(
670
  build_cluster_wide_table(census_tracts_df, cluster_df, "DDPRevisedCensusTract", "CensusTract_"),
671
  "CensusTract_", replace_threshold).rename(columns={"Replace with": "Replace with Census Tracts"})
672
+ _step(log, "Majority-vote tables done")
673
 
674
  # ---- Map back onto original --------------------------------------
675
+ _step(log, "Mapping clusters + fills back onto records...")
676
  # Same fix: assign each row its county's cluster, keyed on (County, name).
677
  lk = cluster_df[["County", "LegalSubDivision", "Cluster_ID"]].drop_duplicates(
678
  ["County", "LegalSubDivision"])
 
706
  other = [c for c in existing if c not in lead]
707
  original_final = original_final[lead + other]
708
 
709
+ # original_final is only READ from here on (never mutated), so we can alias
710
+ # instead of copying -- this avoids a second full copy of the 7M-row frame.
711
+ original_final_output = original_final
712
+ _step(log, "Full output frame ready")
713
 
714
  # ---- Split into final cities / zips / census tracts --------------
715
  keep = ["RecordKey", "ComesFromMtg2", "IsFlagged",
 
771
  p = out_dir / name
772
  d.to_csv(p, index=False)
773
  outputs.append(str(p))
774
+ _step(log, "Wrote final_cities / final_zips / final_census_tracts / cluster_df")
775
 
776
  # ---- Full output CSV (chunked at 1,000,000 rows) -----------------
777
  # NB: no '%' in the filename -- '%' is the URL-encoding character and makes
778
  # the browser download fail with "unknown server error".
779
+ _step(log, "Writing full output CSV...")
780
  csv_name = f"OutputFile_{replace_threshold}Threshold_{cluster_threshold}ClustersRapidFuzz.csv"
781
  outputs.extend(save_csv_in_parts(original_final_output, csv_name, out_dir, row_limit=1_000_000))
782
+ _step(log, "Full output CSV written")
783
 
784
  # ---- Summary sheet: ONE ROW PER COUNTY ---------------------------
785
  # Totals (records / subdivisions) come from the FULL post-validation frame
 
846
  "Total Blank Zips", "Total Zips Assignment", "Total Blank Census",
847
  "Total Census Assignment"]
848
  summary_df = pd.DataFrame(summary_rows, columns=summary_cols)
849
+ _step(log, f"Summary: {len(summary_df)} county row(s).")
850
 
851
  summary_path = out_dir / "Summary.csv"
852
  summary_df.to_csv(summary_path, index=False)
853
  outputs.append(str(summary_path))
854
 
855
  # ---- Master bundle: Excel workbook OR zip of CSVs (see MASTER_OUTPUT) -
856
+ _step(log, f"Building master ({MASTER_OUTPUT})...")
857
  if MASTER_OUTPUT == "csv":
858
  # Bundle the already-written CSVs into one zip. No Excel writing -> fast.
859
  master_path = out_dir / "Master_Dataset.zip"
 
882
 
883
  elapsed_min = (time.time() - t_start) / 60
884
  log.append(f"Done. {len(outputs)} files ready.")
885
+ _step(log, f"DONE — {len(outputs)} files ready in {elapsed_min:.1f} min")
886
  log.append(f"Total processing time: {elapsed_min:.1f} minutes.")
887
  gc.collect()
888
  return "\n".join(log), outputs
requirements.txt CHANGED
@@ -1,5 +1,7 @@
1
  pandas>=2.0
2
  numpy>=1.24
3
  openpyxl>=3.1
 
4
  rapidfuzz>=3.0
 
5
  huggingface_hub>=0.23
 
1
  pandas>=2.0
2
  numpy>=1.24
3
  openpyxl>=3.1
4
+ xlsxwriter>=3.1
5
  rapidfuzz>=3.0
6
+ psutil>=5.9
7
  huggingface_hub>=0.23