make staff-tier category labels reflect the actual --senior-threshold/--support-threshold values
This commit is contained in:
parent
a621b94512
commit
efce6f3dec
2 changed files with 31 additions and 12 deletions
|
|
@ -85,6 +85,24 @@ SENIOR_STAFF_THRESHOLD = 100_000
|
||||||
SUPPORT_STAFF_THRESHOLD = 40_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):
|
def _rx(*patterns):
|
||||||
return [re.compile(p, re.IGNORECASE) for p in patterns]
|
return [re.compile(p, re.IGNORECASE) for p in patterns]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ def _group_sort_key(key: tuple) -> tuple:
|
||||||
|
|
||||||
|
|
||||||
def process_file(path: Path, campus: str, year: int, writer: csv.writer,
|
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 = (),
|
agg: dict | None = None, group_dims: tuple = (),
|
||||||
top_earners: dict | None = None, top_n: int = 10,
|
top_earners: dict | None = None, top_n: int = 10,
|
||||||
tie_breaker: itertools.count | None = None,
|
tie_breaker: itertools.count | None = None,
|
||||||
|
|
@ -157,7 +157,7 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer,
|
||||||
)
|
)
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
year, campus, parsed["title"], category,
|
year, campus, parsed["title"], category,
|
||||||
classifier.CATEGORY_LABELS[category],
|
labels[category],
|
||||||
round(parsed["gross_pay"], 2), round(parsed["regular_pay"], 2),
|
round(parsed["gross_pay"], 2), round(parsed["regular_pay"], 2),
|
||||||
round(parsed["overtime_pay"], 2), round(parsed["other_pay"], 2),
|
round(parsed["overtime_pay"], 2), round(parsed["other_pay"], 2),
|
||||||
parsed["first_name"], parsed["last_name"],
|
parsed["first_name"], parsed["last_name"],
|
||||||
|
|
@ -180,7 +180,7 @@ def process_file(path: Path, campus: str, year: int, writer: csv.writer,
|
||||||
return count
|
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,
|
"""Write one row per (group_dims..., category) with employee count,
|
||||||
total/mean/median gross pay."""
|
total/mean/median gross pay."""
|
||||||
header = list(group_dims) + [
|
header = list(group_dims) + [
|
||||||
|
|
@ -196,14 +196,14 @@ def write_aggregate_csv(agg: dict, group_dims: tuple, outpath: Path) -> None:
|
||||||
values = agg[key]
|
values = agg[key]
|
||||||
total = sum(values)
|
total = sum(values)
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
*prefix, category, classifier.CATEGORY_LABELS[category],
|
*prefix, category, labels[category],
|
||||||
len(values), round(total, 2), round(total / len(values), 2),
|
len(values), round(total, 2), round(total / len(values), 2),
|
||||||
round(statistics.median(values), 2),
|
round(statistics.median(values), 2),
|
||||||
])
|
])
|
||||||
log.info("Aggregate CSV -> %s", outpath)
|
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
|
"""Write the top-N earners per (group_dims..., category) key, ranked
|
||||||
highest gross_pay first within each group."""
|
highest gross_pay first within each group."""
|
||||||
header = ["category", "category_label", "rank", "year", "campus", "title",
|
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):
|
for rank, (gross_pay, _, row) in enumerate(ranked, start=1):
|
||||||
year, campus, title, gross, regular, overtime, other, first, last = row
|
year, campus, title, gross, regular, overtime, other, first, last = row
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
category, classifier.CATEGORY_LABELS[category], rank,
|
category, labels[category], rank,
|
||||||
year, campus, title, round(gross, 2), round(regular, 2),
|
year, campus, title, round(gross, 2), round(regular, 2),
|
||||||
round(overtime, 2), round(other, 2), first, last,
|
round(overtime, 2), round(other, 2), first, last,
|
||||||
])
|
])
|
||||||
|
|
@ -232,7 +232,7 @@ def _money(v: float) -> str:
|
||||||
return f"${v:,.0f}"
|
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,
|
"""Print one formatted category-by-category summary table per year,
|
||||||
keyed off (year, category) -> [gross_pay, ...] entries in console_stats."""
|
keyed off (year, category) -> [gross_pay, ...] entries in console_stats."""
|
||||||
label_w, col_w = 34, 16
|
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
|
median = statistics.median(values) if values else 0.0
|
||||||
total_count += count
|
total_count += count
|
||||||
total_pay += total
|
total_pay += total
|
||||||
label = classifier.CATEGORY_LABELS[category]
|
label = labels[category]
|
||||||
print(f"{label:<{label_w}}{count:>{col_w},}"
|
print(f"{label:<{label_w}}{count:>{col_w},}"
|
||||||
f"{_money(total):>{col_w}}{_money(median):>{col_w}}")
|
f"{_money(total):>{col_w}}{_money(median):>{col_w}}")
|
||||||
|
|
||||||
|
|
@ -331,6 +331,7 @@ def main():
|
||||||
else:
|
else:
|
||||||
log.info("--skip-download set, classifying cached data only.")
|
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)
|
group_dims = tuple(dim for dim in ("year", "campus") if dim in args.group_by)
|
||||||
agg = {} if args.aggregate_output else None
|
agg = {} if args.aggregate_output else None
|
||||||
top_earners = {} if args.top_earners_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)
|
log.info("No data for %s %s, skipping.", campus, year)
|
||||||
continue
|
continue
|
||||||
count = process_file(path, campus, year, writer,
|
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,
|
agg=agg, group_dims=group_dims,
|
||||||
top_earners=top_earners, top_n=args.top_n,
|
top_earners=top_earners, top_n=args.top_n,
|
||||||
tie_breaker=tie_breaker,
|
tie_breaker=tie_breaker,
|
||||||
|
|
@ -365,13 +366,13 @@ def main():
|
||||||
log.info("Total rows written: %d -> %s", total_rows, output_path)
|
log.info("Total rows written: %d -> %s", total_rows, output_path)
|
||||||
|
|
||||||
if agg is not None:
|
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:
|
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:
|
if console_stats is not None:
|
||||||
print_console_summary(console_stats)
|
print_console_summary(console_stats, labels)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue