diff --git a/uc_salary_pipeline.py b/uc_salary_pipeline.py index 6863450..5c14f53 100644 --- a/uc_salary_pipeline.py +++ b/uc_salary_pipeline.py @@ -122,7 +122,8 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer, senior_threshold: float, support_threshold: float, agg: dict | None = None, group_dims: tuple = (), top_earners: dict | None = None, top_n: int = 10, - tie_breaker: itertools.count | None = None) -> int: + tie_breaker: itertools.count | None = None, + console_stats: dict | None = None) -> int: """Classify every record in one UC-wages JSON file, writing a CSV row per employee. Returns the number of rows written. @@ -131,6 +132,10 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer, If `top_earners` is given, also maintains a size-`top_n` min-heap of the highest gross_pay employees per (group_dims..., category) key. + + If `console_stats` is given, also accumulates each record's gross_pay + under its (year, category) for the end-of-run summary tables printed + to stdout (one table per year). """ with open(path, encoding="utf-8") as fh: data = json.load(fh) @@ -160,6 +165,8 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer, count += 1 if agg is not None: agg.setdefault(group_prefix + (category,), []).append(parsed["gross_pay"]) + if console_stats is not None: + console_stats.setdefault((year, category), []).append(parsed["gross_pay"]) if top_earners is not None: entry = (parsed["gross_pay"], next(tie_breaker), (year, campus, parsed["title"], parsed["gross_pay"], @@ -219,6 +226,44 @@ def write_top_earners_csv(top_earners: dict, outpath: Path) -> None: log.info("Top earners CSV -> %s", outpath) +def _money(v: float) -> str: + if abs(v) >= 1_000_000: + return f"${v / 1_000_000:,.2f}M" + return f"${v:,.0f}" + + +def print_console_summary(console_stats: dict) -> None: + """Print one formatted category-by-category summary table per year, + keyed off (year, category) -> [gross_pay, ...] entries in console_stats.""" + label_w, col_w = 34, 16 + header = (f"{'Category':<{label_w}}{'Employees':>{col_w}}" + f"{'Total Pay':>{col_w}}{'Median Pay':>{col_w}}") + rule = "=" * len(header) + + years = sorted({year for year, _ in console_stats}) + for year in years: + print(f"\n{rule}") + print(f"UC SALARY CLASSIFICATION SUMMARY -- {year}") + print(rule) + print(header) + print("-" * len(header)) + + total_count, total_pay = 0, 0.0 + for category in classifier.CATEGORIES: + values = console_stats.get((year, category), []) + count, total = len(values), sum(values) + median = statistics.median(values) if values else 0.0 + total_count += count + total_pay += total + label = classifier.CATEGORY_LABELS[category] + print(f"{label:<{label_w}}{count:>{col_w},}" + f"{_money(total):>{col_w}}{_money(median):>{col_w}}") + + print("-" * len(header)) + print(f"{'TOTAL':<{label_w}}{total_count:>{col_w},}{_money(total_pay):>{col_w}}") + print(rule) + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -266,6 +311,9 @@ def main(): parser.add_argument("--skip-download", action="store_true", help="Classify whatever is already cached in " "--data-dir without hitting the network.") + parser.add_argument("--quiet", action="store_true", + help="Suppress the formatted summary table printed " + "to stdout after processing.") parser.add_argument("--senior-threshold", type=float, default=classifier.SENIOR_STAFF_THRESHOLD, help=f"Gross pay threshold for SENIOR_PROF_STAFF " @@ -287,6 +335,7 @@ def main(): agg = {} if args.aggregate_output else None top_earners = {} if args.top_earners_output else None tie_breaker = itertools.count() if top_earners is not None else None + console_stats = {} if not args.quiet else None output_path = Path(args.output) total_rows = 0 @@ -304,7 +353,8 @@ def main(): args.senior_threshold, args.support_threshold, agg=agg, group_dims=group_dims, top_earners=top_earners, top_n=args.top_n, - tie_breaker=tie_breaker) + tie_breaker=tie_breaker, + console_stats=console_stats) total_rows += count log.info("%s %s: classified %d records", campus, year, count) @@ -320,6 +370,9 @@ def main(): if top_earners is not None: write_top_earners_csv(top_earners, Path(args.top_earners_output)) + if console_stats is not None: + print_console_summary(console_stats) + if __name__ == "__main__": main()