add back gcc data and top5 by cost
This commit is contained in:
parent
a37e4558fa
commit
1970790c33
19 changed files with 356331 additions and 0 deletions
166
analyze_asst_prof_comp.py
Normal file
166
analyze_asst_prof_comp.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
"""
|
||||
analyze_asst_prof_comp.py
|
||||
==========================
|
||||
One-off analysis: median total compensation (salary + benefits) for
|
||||
non-medical Assistant Professors across the whole UC system, 2024.
|
||||
|
||||
Data source: CA State Controller's Government Compensation in California
|
||||
(GCC) dataset, restored from this repo's git history (gcc_data/GCC_*_2024.csv)
|
||||
because the UC-wages source uc_salary_pipeline.py normally uses has no
|
||||
benefits field at all.
|
||||
|
||||
"Total compensation" = TotalWages (cash pay) + TotalRetirementAndHealthContribution
|
||||
(employer-paid health/dental/vision + retirement + deferred comp) -- the
|
||||
standard total-comp figure used for this dataset (e.g. by Transparent
|
||||
California).
|
||||
|
||||
"Non-medical" reuses classifier.py's existing SENATE_FACULTY / is_medical_center
|
||||
logic, which already flags HCOMP / HS-CLIN-PROF / clinical-workforce titles
|
||||
as medical regardless of campus.
|
||||
|
||||
"Assistant Professor" is a rank-level filter this repo doesn't otherwise
|
||||
do (classify_title() only buckets at the Senate-faculty-vs-not level) --
|
||||
_ASST_RANK_RX below mirrors classifier.py's own _SENATE_RX patterns,
|
||||
restricted to the assistant-rank variants.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import glob
|
||||
import re
|
||||
import statistics
|
||||
|
||||
from classifier import classify_title, is_medical_center, normalise
|
||||
|
||||
EMPLOYER_TO_CAMPUS = {
|
||||
"University of California, Berkeley": "UCB",
|
||||
"University of California, Los Angeles": "UCLA",
|
||||
"University of California, Davis": "UCD",
|
||||
"University of California, Irvine": "UCI",
|
||||
"University of California, Santa Barbara": "UCSB",
|
||||
"University of California, Riverside": "UCR",
|
||||
"University of California, Santa Cruz": "UCSC",
|
||||
"University of California, Merced": "UCM",
|
||||
"University of California, Office of the President": "UCOP",
|
||||
"University of California, San Diego": "UCSD",
|
||||
"University of California, San Francisco": "UCSF",
|
||||
}
|
||||
|
||||
# Assistant-rank Senate faculty titles only -- mirrors classifier.py's
|
||||
# _SENATE_RX but excludes bare/ASSOC/full-professor variants.
|
||||
# "ASST ACT PROF" (reversed order = non-Senate, per classifier.py's own
|
||||
# docstring) is NOT matched here and is excluded anyway by the
|
||||
# SENATE_FACULTY-only filter in main().
|
||||
_ASST_RANK_RX = [re.compile(p, re.IGNORECASE) for p in (
|
||||
r"^ASST\s+PROF-(?:AY|FY|HCOMP|SFT-VM)\b",
|
||||
r"^ASST\s+PROF-AY-",
|
||||
r"^ASST\s+PROF-FY-",
|
||||
r"^ASST\s+PROF OF TEACH\b",
|
||||
r"^ASST\s+PROF OF TEACH-",
|
||||
r"^ASST\s+PROF OF CLIN\b",
|
||||
r"^ASST\s+PROF OF CLIN-",
|
||||
r"^ASST\s+PROF IN RES\b",
|
||||
r"^ASST\s+PROF IN RES-",
|
||||
r"^ACT\s+ASST\s+PROF\b",
|
||||
r"^ACT\s+ASST\s+PROF-",
|
||||
r"^ACT\s+ASST\s+PROF OF TEACH\b",
|
||||
r"^ACT\s+ASST\s+PROF OF TEACH-",
|
||||
r"^ACT\s+ASST\s+PROF OF CLIN\b",
|
||||
r"^ACT\s+ASST\s+PROF OF CLIN-",
|
||||
r"^ACT\s+ASST\s+PROF IN RES\b",
|
||||
)]
|
||||
|
||||
# Other Senate-faculty patterns (non-assistant) so we can flag anything
|
||||
# that matched SENATE_FACULTY but neither this nor the assistant patterns
|
||||
# recognise -- a diagnostic for titles the rank regex list above missed.
|
||||
_NON_ASST_SENATE_RX = [re.compile(p, re.IGNORECASE) for p in (
|
||||
r"^(?:ASSOC\s+)?PROF-(?:AY|FY|HCOMP|SFT-VM)\b",
|
||||
r"^(?:ASSOC\s+)?PROF-AY-",
|
||||
r"^(?:ASSOC\s+)?PROF-FY-",
|
||||
r"^(?:ASSOC\s+)?PROF OF TEACH\b",
|
||||
r"^(?:ASSOC\s+)?PROF OF TEACH-",
|
||||
r"^(?:ASSOC\s+)?PROF OF CLIN\b",
|
||||
r"^(?:ASSOC\s+)?PROF OF CLIN-",
|
||||
r"^(?:ASSOC\s+)?PROF IN RES\b",
|
||||
r"^(?:ASSOC\s+)?PROF IN RES-",
|
||||
r"^ACT\s+PROF\b", r"^ACT\s+PROF-",
|
||||
r"^ACT\s+ASSOC\s+PROF\b", r"^ACT\s+ASSOC\s+PROF-",
|
||||
r"^ACT\s+PROF OF TEACH\b", r"^ACT\s+PROF OF TEACH-",
|
||||
r"^ACT\s+ASSOC\s+PROF OF TEACH\b", r"^ACT\s+ASSOC\s+PROF OF TEACH-",
|
||||
r"^ACT\s+PROF OF CLIN\b", r"^ACT\s+PROF OF CLIN-",
|
||||
r"^ACT\s+ASSOC\s+PROF OF CLIN\b", r"^ACT\s+ASSOC\s+PROF OF CLIN-",
|
||||
r"^ACT\s+PROF IN RES\b", r"^ACT\s+ASSOC\s+PROF IN RES\b",
|
||||
r"^INSTR\b", r"^INSTR-", r"^INSTRUCTOR\b",
|
||||
r"^(?:ASSOC\s+)?INSTR IN RES\b",
|
||||
r"^REGENTS'? PROF\b",
|
||||
r"\bPROF(?:ESSOR)?[-\s]EMERITUS\b", r"\bPROF(?:ESSOR)?\s+EMERITUS\b",
|
||||
r"^RECALL FACULTY\b",
|
||||
r"^(?:ASSOC\s+)?AGRON AES(?:-AY|-FY|-B/E/E|-SFT-VM)?$",
|
||||
r"^(?:ASSOC\s+)?ASTRONOMER(?:-AY|-FY)?$",
|
||||
)]
|
||||
|
||||
def is_assistant_rank(raw_title: str) -> bool:
|
||||
t = normalise(raw_title)
|
||||
return any(rx.search(t) for rx in _ASST_RANK_RX)
|
||||
|
||||
|
||||
def is_recognised_non_assistant_senate(raw_title: str) -> bool:
|
||||
t = normalise(raw_title)
|
||||
return any(rx.search(t) for rx in _NON_ASST_SENATE_RX)
|
||||
|
||||
|
||||
def parse_money(s: str) -> float:
|
||||
s = (s or "").replace(",", "").replace("$", "").strip()
|
||||
return float(s) if s else 0.0
|
||||
|
||||
|
||||
def main():
|
||||
rows = []
|
||||
unrecognised_senate_titles = set()
|
||||
|
||||
for path in sorted(glob.glob("gcc_data/GCC_*_2024.csv")):
|
||||
if path.endswith("GCC_ALL_2024.csv"):
|
||||
continue
|
||||
with open(path, newline="", encoding="utf-8-sig") as f:
|
||||
for row in csv.DictReader(f):
|
||||
employer = row["EmployerName"]
|
||||
campus = EMPLOYER_TO_CAMPUS.get(employer)
|
||||
if campus is None:
|
||||
raise ValueError(f"Unmapped EmployerName: {employer!r} in {path}")
|
||||
|
||||
title = row["Position"]
|
||||
if classify_title(title) != "SENATE_FACULTY":
|
||||
continue
|
||||
if is_medical_center(title, campus):
|
||||
continue
|
||||
if not is_assistant_rank(title):
|
||||
if not is_recognised_non_assistant_senate(title):
|
||||
unrecognised_senate_titles.add(title)
|
||||
continue
|
||||
|
||||
total_comp = (parse_money(row["TotalWages"])
|
||||
+ parse_money(row["TotalRetirementAndHealthContribution"]))
|
||||
rows.append({"campus": campus, "title": title, "total_comp": total_comp})
|
||||
|
||||
if unrecognised_senate_titles:
|
||||
print("NOTE: Senate-faculty titles not matched by any rank pattern "
|
||||
"(not counted as assistant, but check these aren't mis-missed asst titles):")
|
||||
for t in sorted(unrecognised_senate_titles):
|
||||
print(f" - {t}")
|
||||
print()
|
||||
|
||||
all_comp = [r["total_comp"] for r in rows]
|
||||
print(f"Non-medical Assistant Professors, UC system-wide, 2024: n={len(rows)}")
|
||||
print(f"Median total compensation (salary + benefits): ${statistics.median(all_comp):,.2f}")
|
||||
print(f"Mean total compensation: ${statistics.mean(all_comp):,.2f}")
|
||||
print()
|
||||
print(f"{'Campus':<8}{'N':>6}{'Median':>16}")
|
||||
for campus in sorted(EMPLOYER_TO_CAMPUS.values()):
|
||||
campus_comp = [r["total_comp"] for r in rows if r["campus"] == campus]
|
||||
if campus_comp:
|
||||
print(f"{campus:<8}{len(campus_comp):>6}{statistics.median(campus_comp):>16,.2f}")
|
||||
else:
|
||||
print(f"{campus:<8}{0:>6}{'--':>16}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue