| """ |
| join_reports.py |
| βββββββββββββββ |
| Left-join parsed_reports_with_agency (left) onto raw_reports_table (right) |
| on: left["Unnamed: 0"] == right["raw_text"] |
| |
| All rows from the parsed file are kept; matching columns from raw_reports_table |
| (source_file, chunk_count, pages, parse_errors, assessment) are appended. |
| The redundant raw_text column from the right side is dropped after the join. |
| |
| Usage |
| ----- |
| python join_reports.py |
| python join_reports.py --left parsed_reports_with_agency.xlsx \ |
| --right raw_reports_table.csv \ |
| --out joined_reports.xlsx |
| """ |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
|
|
| import pandas as pd |
|
|
|
|
| def read_any(path: Path) -> pd.DataFrame: |
| """Read CSV or Excel, sniffing format from magic bytes.""" |
| with open(path, "rb") as fh: |
| magic = fh.read(4) |
| is_excel = magic[:4] in (b"\xd0\xcf\x11\xe0", b"PK\x03\x04") |
| if is_excel or path.suffix.lower() in (".xlsx", ".xls", ".xlsm"): |
| return pd.read_excel(path, dtype=str) |
| for sep in (",", ";", "\t", "|"): |
| try: |
| df = pd.read_csv(path, sep=sep, dtype=str, encoding="utf-8-sig") |
| if len(df.columns) > 1: |
| return df |
| except Exception: |
| pass |
| return pd.read_csv(path, dtype=str, encoding="utf-8-sig") |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser(description="Left-join parsed reports with raw reports table") |
| ap.add_argument("--left", default="parsed_reports_with_agency.xlsx") |
| ap.add_argument("--right", default="raw_reports_table.csv") |
| ap.add_argument("--out", default="joined_reports.xlsx") |
| args = ap.parse_args() |
|
|
| left_path = Path(args.left) |
| right_path = Path(args.right) |
| out_path = Path(args.out) |
|
|
| for p in (left_path, right_path): |
| if not p.exists(): |
| sys.exit(f"ERROR: file not found: {p}") |
|
|
| print(f"Reading left : {left_path}") |
| df_left = read_any(left_path) |
| print(f" {len(df_left):,} rows Γ {len(df_left.columns)} cols") |
| print(f" Columns: {df_left.columns.tolist()}\n") |
|
|
| print(f"Reading right : {right_path}") |
| df_right = read_any(right_path) |
| print(f" {len(df_right):,} rows Γ {len(df_right.columns)} cols") |
| print(f" Columns: {df_right.columns.tolist()}\n") |
|
|
| |
| for col, df, label in [ |
| ("Unnamed: 0", df_left, "left (parsed_reports_with_agency)"), |
| ("raw_text", df_right, "right (raw_reports_table)"), |
| ]: |
| if col not in df.columns: |
| sys.exit( |
| f"ERROR: column '{col}' not found in {label}.\n" |
| f" Available: {df.columns.tolist()}" |
| ) |
|
|
| |
| df_left["Unnamed: 0"] = df_left["Unnamed: 0"].str.strip() |
| df_right["raw_text"] = df_right["raw_text"].str.strip() |
|
|
| |
| merged = df_left.merge( |
| df_right, |
| left_on="Unnamed: 0", |
| right_on="raw_text", |
| how="left", |
| suffixes=("", "_right"), |
| ) |
|
|
| |
| if "raw_text" in merged.columns: |
| merged = merged.drop(columns=["raw_text"]) |
|
|
| |
| unnamed_right = [c for c in merged.columns if c.startswith("Unnamed:") and c != "Unnamed: 0"] |
| if unnamed_right: |
| merged = merged.drop(columns=unnamed_right) |
|
|
| |
| merged = merged.rename(columns={"Unnamed: 0": "raw_text"}) |
|
|
| |
| matched = merged["source_file"].notna().sum() if "source_file" in merged.columns else "?" |
| unmatched = len(merged) - (matched if isinstance(matched, int) else 0) |
| print(f"Join complete: {len(merged):,} rows total | matched: {matched} | unmatched: {unmatched}") |
|
|
| if unmatched and isinstance(unmatched, int) and unmatched > 0: |
| miss = merged[merged["source_file"].isna()]["raw_text"].str[:80] |
| print(f"\nFirst 5 unmatched raw_text previews:") |
| for s in miss.head(5): |
| print(f" {s!r}") |
|
|
| |
| if out_path.suffix.lower() in (".xlsx", ".xls", ".xlsm"): |
| merged.to_excel(out_path, index=False) |
| else: |
| merged.to_csv(out_path, index=False) |
| print(f"\nSaved β {out_path.resolve()}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|