From efce6f3decb8ee4a47bac1a189225866d39feede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Jovanovi=C4=87?= Date: Tue, 4 Aug 2026 22:32:06 +0000 Subject: [PATCH] make staff-tier category labels reflect the actual --senior-threshold/--support-threshold values --- classifier.py | 18 ++++++++++++++++++ uc_salary_pipeline.py | 25 +++++++++++++------------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/classifier.py b/classifier.py index 684fb99..4fe8190 100644 --- a/classifier.py +++ b/classifier.py @@ -85,6 +85,24 @@ SENIOR_STAFF_THRESHOLD = 100_000 SUPPORT_STAFF_THRESHOLD = 40_000 +def _fmt_threshold(v: float) -> str: + if v == int(v) and v % 1000 == 0: + return f"${int(v) // 1000}k" + return f"${v:,.0f}" + + +def category_labels(senior_threshold: float = SENIOR_STAFF_THRESHOLD, + support_threshold: float = SUPPORT_STAFF_THRESHOLD) -> dict: + """CATEGORY_LABELS with the staff-tier labels reflecting the actual + thresholds passed to classify_title(), instead of the defaults.""" + labels = dict(CATEGORY_LABELS) + labels["SENIOR_PROF_STAFF"] = f"Senior Prof/Staff (>={_fmt_threshold(senior_threshold)})" + labels["PROF_STAFF"] = (f"Professional Staff ({_fmt_threshold(support_threshold)}" + f"-{_fmt_threshold(senior_threshold)})") + labels["SUPPORT_STAFF"] = f"Support Staff (<{_fmt_threshold(support_threshold)})" + return labels + + def _rx(*patterns): return [re.compile(p, re.IGNORECASE) for p in patterns] diff --git a/uc_salary_pipeline.py b/uc_salary_pipeline.py index 5c14f53..95e7cbb 100644 --- a/uc_salary_pipeline.py +++ b/uc_salary_pipeline.py @@ -119,7 +119,7 @@ def _group_sort_key(key: tuple) -> tuple: def process_file(path: Path, campus: str, year: int, writer: csv.writer, - senior_threshold: float, support_threshold: float, + senior_threshold: float, support_threshold: float, labels: dict, agg: dict | None = None, group_dims: tuple = (), top_earners: dict | None = None, top_n: int = 10, tie_breaker: itertools.count | None = None, @@ -157,7 +157,7 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer, ) writer.writerow([ year, campus, parsed["title"], category, - classifier.CATEGORY_LABELS[category], + labels[category], round(parsed["gross_pay"], 2), round(parsed["regular_pay"], 2), round(parsed["overtime_pay"], 2), round(parsed["other_pay"], 2), parsed["first_name"], parsed["last_name"], @@ -180,7 +180,7 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer, return count -def write_aggregate_csv(agg: dict, group_dims: tuple, outpath: Path) -> None: +def write_aggregate_csv(agg: dict, group_dims: tuple, labels: dict, outpath: Path) -> None: """Write one row per (group_dims..., category) with employee count, total/mean/median gross pay.""" header = list(group_dims) + [ @@ -196,14 +196,14 @@ def write_aggregate_csv(agg: dict, group_dims: tuple, outpath: Path) -> None: values = agg[key] total = sum(values) writer.writerow([ - *prefix, category, classifier.CATEGORY_LABELS[category], + *prefix, category, labels[category], len(values), round(total, 2), round(total / len(values), 2), round(statistics.median(values), 2), ]) log.info("Aggregate CSV -> %s", outpath) -def write_top_earners_csv(top_earners: dict, outpath: Path) -> None: +def write_top_earners_csv(top_earners: dict, labels: dict, outpath: Path) -> None: """Write the top-N earners per (group_dims..., category) key, ranked highest gross_pay first within each group.""" header = ["category", "category_label", "rank", "year", "campus", "title", @@ -219,7 +219,7 @@ def write_top_earners_csv(top_earners: dict, outpath: Path) -> None: for rank, (gross_pay, _, row) in enumerate(ranked, start=1): year, campus, title, gross, regular, overtime, other, first, last = row writer.writerow([ - category, classifier.CATEGORY_LABELS[category], rank, + category, labels[category], rank, year, campus, title, round(gross, 2), round(regular, 2), round(overtime, 2), round(other, 2), first, last, ]) @@ -232,7 +232,7 @@ def _money(v: float) -> str: return f"${v:,.0f}" -def print_console_summary(console_stats: dict) -> None: +def print_console_summary(console_stats: dict, labels: 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 @@ -255,7 +255,7 @@ def print_console_summary(console_stats: dict) -> None: median = statistics.median(values) if values else 0.0 total_count += count total_pay += total - label = classifier.CATEGORY_LABELS[category] + label = labels[category] print(f"{label:<{label_w}}{count:>{col_w},}" f"{_money(total):>{col_w}}{_money(median):>{col_w}}") @@ -331,6 +331,7 @@ def main(): else: log.info("--skip-download set, classifying cached data only.") + labels = classifier.category_labels(args.senior_threshold, args.support_threshold) group_dims = tuple(dim for dim in ("year", "campus") if dim in args.group_by) agg = {} if args.aggregate_output else None top_earners = {} if args.top_earners_output else None @@ -350,7 +351,7 @@ def main(): log.info("No data for %s %s, skipping.", campus, year) continue count = process_file(path, campus, year, writer, - args.senior_threshold, args.support_threshold, + args.senior_threshold, args.support_threshold, labels, agg=agg, group_dims=group_dims, top_earners=top_earners, top_n=args.top_n, tie_breaker=tie_breaker, @@ -365,13 +366,13 @@ def main(): log.info("Total rows written: %d -> %s", total_rows, output_path) if agg is not None: - write_aggregate_csv(agg, group_dims, Path(args.aggregate_output)) + write_aggregate_csv(agg, group_dims, labels, Path(args.aggregate_output)) if top_earners is not None: - write_top_earners_csv(top_earners, Path(args.top_earners_output)) + write_top_earners_csv(top_earners, labels, Path(args.top_earners_output)) if console_stats is not None: - print_console_summary(console_stats) + print_console_summary(console_stats, labels) if __name__ == "__main__":