Orthogonal boolean column alongside category, computed in classifier.py's new is_medical_center(). Always flags MED CTR-tagged titles and the existing HS CLIN PROF/-HCOMP patterns regardless of campus; additionally flags generic clinical-workforce titles (physicians, nurses, dentists, pharmacists, therapists, techs, etc.) only at the five campuses that operate a UC-owned academic medical center (UCSF, UCLA, UCD, UCI, UCSD), since those titles are indistinguishable from Student Health Services staff elsewhere. SHS-tagged titles are always excluded. Threaded through as a --group-by dimension for --aggregate-output and as a column on --top-earners-output rows, alongside the per-employee CSV.
444 lines
17 KiB
Python
444 lines
17 KiB
Python
"""
|
|
classifier.py
|
|
==============
|
|
UC employee title classification engine: sorts every employee into one of
|
|
8 analytical categories based on job title (and, for non-academic staff,
|
|
gross pay). Independent of any particular data source — feed it a raw
|
|
title string and (optionally) a gross pay figure.
|
|
|
|
CATEGORIES (8 total)
|
|
---------------------
|
|
Categories are organised around the UC Academic Senate membership rules
|
|
(Regents Standing Order 105.1), as listed at senate.ucla.edu/senate-membership
|
|
|
|
SENATE_FACULTY
|
|
All titles that carry Academic Senate membership (excluding administrative
|
|
officers such as Chancellor, Dean etc. which are kept in SENIOR_MANAGEMENT):
|
|
- Professor series (all ranks: Full/Assoc/Asst, all pay plans: AY/FY/HCOMP/
|
|
B/E/E/SFT-VM/Law/1-9 etc.)
|
|
- Professor of Teaching / SOE / PSOE (Senate members)
|
|
- Professor of Clinical X (all ranks) -- Senate per SO 105.1
|
|
- Professor-in-Residence (all ranks) -- Senate per SO 105.1
|
|
- Acting Professor and Acting Professor of Teaching (all ranks) -- Senate
|
|
- Instructor and Instructor-in-Residence -- Senate
|
|
- Emeritus/Recall faculty (retired from Senate title)
|
|
- AES Agronomists and Astronomers (tenure series)
|
|
|
|
NON_SENATE_ACADEMIC
|
|
Academic titles that do NOT carry Senate membership:
|
|
- Health Sciences Clinical Professor series (HS CLIN PROF) -- explicitly
|
|
listed as non-Senate; kept visible because their HCOMP supplements make
|
|
total pay structurally different from regular faculty
|
|
- Adjunct Professor series
|
|
- Visiting Professor series (including Vis Asst/Assoc Prof)
|
|
- Assistant Acting Professor (different from Acting Assistant Professor,
|
|
which IS Senate)
|
|
- Clinical Associate series
|
|
- Non-Senate Instructional / Lecturers (all variants including LECT-AY,
|
|
LECT-FY, LECT-AY-1/9, Summer Session, Continuing etc.)
|
|
- Professional Research series
|
|
- Project Scientist series
|
|
- Specialist / Cooperative Extension series
|
|
- Visiting Professional Research / Visiting Scholars
|
|
- Volunteer Clinical Professor
|
|
- Librarians, Academic Coordinators, Academic Administrators
|
|
- Postdoctoral Scholars (non-student, non-Senate)
|
|
|
|
SENIOR_MANAGEMENT -- kept as-is (anchored patterns to avoid false positives)
|
|
ATHLETICS -- kept as-is (separated to avoid inflating SMG totals)
|
|
STUDENT_TRAINEE -- TAs, GSRs, Readers, Residents, Interns, Student workers
|
|
SENIOR_PROF_STAFF -- non-academic staff, gross pay >= $100k
|
|
PROF_STAFF -- non-academic staff, $40k <= gross pay < $100k
|
|
SUPPORT_STAFF -- non-academic staff, gross pay < $40k
|
|
|
|
Pay thresholds for SENIOR_PROF_STAFF vs PROF_STAFF vs SUPPORT_STAFF are
|
|
applied to gross_pay and can be overridden per-call via classify_title()'s
|
|
senior_threshold / support_threshold arguments.
|
|
"""
|
|
|
|
import re
|
|
|
|
CATEGORIES = [
|
|
"SENATE_FACULTY",
|
|
"NON_SENATE_ACADEMIC",
|
|
"SENIOR_MANAGEMENT",
|
|
"ATHLETICS",
|
|
"STUDENT_TRAINEE",
|
|
"SENIOR_PROF_STAFF",
|
|
"PROF_STAFF",
|
|
"SUPPORT_STAFF",
|
|
]
|
|
|
|
CATEGORY_LABELS = {
|
|
"SENATE_FACULTY": "Senate Faculty",
|
|
"NON_SENATE_ACADEMIC": "Non-Senate Academic",
|
|
"SENIOR_MANAGEMENT": "Senior Management (SMG)",
|
|
"ATHLETICS": "Athletics",
|
|
"STUDENT_TRAINEE": "Student / Trainee",
|
|
"SENIOR_PROF_STAFF": "Senior Prof/Staff (>=$100k)",
|
|
"PROF_STAFF": "Professional Staff ($40k-$100k)",
|
|
"SUPPORT_STAFF": "Support Staff (<$40k)",
|
|
}
|
|
|
|
# Pay thresholds (adjustable per-call)
|
|
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]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pattern blocks — tested in the order listed in classify_title()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# 1. STUDENT / TRAINEE
|
|
# Tested first because some student titles contain "PROF" or "INSTR".
|
|
# Medical residents are trainees, not staff.
|
|
_STUDENT_RX = _rx(
|
|
r"\bGSR\b",
|
|
r"\bGRAD(?:UATE)? STUDENT RES",
|
|
r"\bTEACH(?:ING)? ASST\b",
|
|
r"[-\s]GSHIP\b",
|
|
r"\bASSOC IN ", # Associate-in-[dept]-GSHIP student series
|
|
r"\bREMEDIAL TUTOR\b",
|
|
r"\bTUT\b", # Tutor (GSHIP and non-GSHIP)
|
|
r"\bREADER\b", # Reader / Special Reader
|
|
r"\bPOSTDOC",
|
|
r"\bPOST[ -]?DOCTORAL",
|
|
r"\bINTERN(?!AL)", # Intern but not Internal
|
|
r"\bRESID(?:ENT)?\s+PHYS", # Resident Physician (medical trainees)
|
|
r"\bPOST D\.?D\.?S\b",
|
|
r"\bCLIN(?:ICAL)? TRAINEE\b",
|
|
r"\bVET(?:ERINARY)?.*(?:INTERN|RESIDENT)\b",
|
|
r"^STDT\b", # Student worker prefix
|
|
r"^STUDENT\b", # Student 1/2/3/4, Student Researcher etc.
|
|
r"\bSTUDENT RESEARCHER\b",
|
|
r"\bTEACHING FELLOW\b",
|
|
)
|
|
|
|
# 2. ATHLETICS
|
|
# Tested before SMG so coaches don't match DEAN/DIR patterns.
|
|
_ATHLETICS_RX = _rx(
|
|
r"\bHEAD COACH\b",
|
|
r"\bATH(?:LETIC)? COACH\b",
|
|
r"\bINTERCOL ATH\b",
|
|
r"\bATH DIR\b",
|
|
r"\bATHLETICS DIR",
|
|
r"\bATH ASSOC DIR\b",
|
|
r"\bASC HEAD COACH\b", # Associate Head Coach (GCC abbreviation)
|
|
r"^COACH (?:ASSISTANT|SPEC)\b", # Assistant/Specialist Coach series (2024)
|
|
r"^RECR COACH\b", # Recreation Coach series
|
|
r"^ATH (?:MANAGER|TRAINER|SUPERVISOR|PROFL)\b", # Athletics staff series
|
|
r"^SPORTS MEDICAL ATH\b", # Sports Medical Athletic Trainer
|
|
r"\bATH PROFL\b", # Ath Profl series
|
|
)
|
|
|
|
|
|
# 3. SENIOR MANAGEMENT
|
|
# Anchored/specific patterns to avoid false positives like
|
|
# "Assistant TO Dean" or "Director of Nursing".
|
|
_SMG_RX = _rx(
|
|
r"^CHANCELLOR\b",
|
|
r"^CHANC\b",
|
|
r"\bVICE CHANC\b",
|
|
r"\bVICE[ -]CHANC\b",
|
|
r"\bASSOC VICE CHANC\b",
|
|
r"\bASST VICE CHANC\b",
|
|
r"^PROVOST\b",
|
|
r"\bVICE PROVOST\b",
|
|
r"\bASSOC VICE PROVOST\b",
|
|
r"\bASST VICE PROVOST\b",
|
|
r"^EXEC VICE AND PROVOST\b",
|
|
r"\bPROVOST\b", # catches Executive Vice and Provost, Senior VP variants
|
|
r"\bVICE PRES\b",
|
|
r"^PRESIDENT\b",
|
|
r"^DEAN\b", # Anchored: DEAN at start, not "Assistant to Dean"
|
|
r"^DEAN AST\b", # Assistant Dean (2015 GCC form)
|
|
r"^DEAN ASC\b", # Associate Dean (2015 GCC form)
|
|
r"^ACT/INTERIM DEAN\b",
|
|
r"^DEAN UNIV EXT\b",
|
|
r"\bDIR EXEC\b",
|
|
r"\bEXEC DIR\b",
|
|
r"\bEXECUTIVE DIRECTOR\b",
|
|
r"\bLAB(?:ORATORY)? DIRECTOR\b",
|
|
)
|
|
|
|
# 4. SENATE FACULTY
|
|
# All titles conferring UC Academic Senate membership under SO 105.1,
|
|
# excluding administrative officers (Chancellor, Dean etc.) which are in SMG.
|
|
#
|
|
# Key inclusions confirmed from senate.ucla.edu/senate-membership:
|
|
# Professor (all ranks + pay plans)
|
|
# Professor of Teaching (SOE/PSOE) -- Senate
|
|
# Professor of Clinical X (all ranks) -- Senate (NOT the HS series)
|
|
# Professor-in-Residence (all ranks) -- Senate
|
|
# Acting Professor / Acting Professor of Teaching (all ranks) -- Senate
|
|
# Instructor / Instructor-in-Residence -- Senate
|
|
# Emeritus (retired from Senate title) / Recall Faculty
|
|
|
|
_SENATE_RX = _rx(
|
|
# --- Core professorial series (all ranks, all pay plans) ---
|
|
# Anchored at ^ to avoid matching "ADJ PROF", "VIS PROF", "HS CLIN PROF" etc.
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF-(?:AY|FY|HCOMP|SFT-VM)\b",
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF-AY-", # B/E/E, 1/9, 1/10, LAW variants
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF-FY-",
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF-SFT-VM\b",
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF-HCOMP\b", # bare HCOMP (GCC full-prof titles)
|
|
|
|
# --- Professor of Teaching / SOE / PSOE (Senate) ---
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF OF TEACH\b",
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF OF TEACH-",
|
|
r"PROF(?:\s+OF)?\s+TEACH.*EMERITUS",
|
|
|
|
# --- Professor of Clinical X (all ranks) -- Senate per SO 105.1 ---
|
|
# NB: "HS CLIN PROF" (Health Sciences series) is NON-Senate and is
|
|
# caught in NON_SENATE_ACADEMIC. This pattern matches the non-HS variant.
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF OF CLIN\b",
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF OF CLIN-",
|
|
|
|
# --- Professor-in-Residence (all ranks) -- Senate ---
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF IN RES\b",
|
|
r"^(?:ASST\s+|ASSOC\s+)?PROF IN RES-",
|
|
|
|
# --- Acting Professor and Acting Professor of Teaching -- Senate ---
|
|
# "Acting Professor" IS Senate; "Assistant Acting Professor" is NOT.
|
|
# Pattern: ACT [ASSOC|ASST] PROF (without "ASST ACT" order which = non-Senate)
|
|
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+ASST\s+PROF\b",
|
|
r"^ACT\s+ASST\s+PROF-",
|
|
r"^ACT\s+(?:ASSOC\s+|ASST\s+)?PROF OF TEACH\b",
|
|
r"^ACT\s+(?:ASSOC\s+|ASST\s+)?PROF OF TEACH-",
|
|
r"^ACT\s+(?:ASSOC\s+|ASST\s+)?PROF OF CLIN\b",
|
|
r"^ACT\s+(?:ASSOC\s+|ASST\s+)?PROF OF CLIN-",
|
|
r"^ACT\s+(?:ASSOC\s+|ASST\s+)?PROF IN RES\b",
|
|
r"^ACT\s+PROF-AY-LAW\b", # Acting Prof Law series
|
|
|
|
# --- Instructor / Instructor-in-Residence -- Senate ---
|
|
r"^INSTR\b",
|
|
r"^INSTR-",
|
|
r"^INSTRUCTOR\b",
|
|
r"^(?:ASST\s+|ASSOC\s+)?INSTR IN RES\b",
|
|
|
|
# --- Regents' Professor (endowed Senate title) ---
|
|
r"^REGENTS'? PROF\b",
|
|
|
|
# --- Emeritus / Recall (retired from Senate title) ---
|
|
r"\bPROF(?:ESSOR)?[-\s]EMERITUS\b",
|
|
r"\bPROF(?:ESSOR)?\s+EMERITUS\b",
|
|
r"^RECALL FACULTY\b",
|
|
|
|
# --- Specialty tenure series (Senate) ---
|
|
r"^(?:ASSOC\s+|ASST\s+)?AGRON AES(?:-AY|-FY|-B/E/E|-SFT-VM)?$",
|
|
r"^(?:ASSOC\s+|ASST\s+)?ASTRONOMER(?:-AY|-FY)?$",
|
|
)
|
|
|
|
# 5. NON-SENATE ACADEMIC
|
|
# Academic titles WITHOUT Senate membership per SO 105.1 and senate.ucla.edu.
|
|
_NON_SENATE_RX = _rx(
|
|
# --- Health Sciences Clinical Professor series (explicitly non-Senate) ---
|
|
r"^HS\s+(?:ASST\s+|ASSOC\s+)?CLIN PROF\b",
|
|
r"^HS\s+(?:ASST\s+|ASSOC\s+)?CLIN PROF-",
|
|
r"^HS\s+CLIN INSTR\b", # HS Clinical Instructor
|
|
r"^HS\s+CLIN INSTR-",
|
|
|
|
# --- Adjunct Professor series ---
|
|
r"\bADJ(?:UNCT)?\s+PROF\b",
|
|
r"\bADJ\s+INSTR\b",
|
|
|
|
# --- Visiting Professor series (non-Senate) ---
|
|
# "VIS PROF", "VIS ASST PROF", "VIS ASSOC PROF"
|
|
r"\bVIS\s+(?:ASST\s+|ASSOC\s+)?PROF\b",
|
|
r"\bVIS\s+(?:ASST\s+|ASSOC\s+)?PROF-",
|
|
|
|
# --- Assistant Acting Professor (non-Senate; reversed word order matters) ---
|
|
r"^ASST\s+ACT\s+PROF\b",
|
|
r"^ASST\s+ACT\s+PROF-",
|
|
|
|
# --- Clinical Associate series ---
|
|
r"^(?:ASST\s+|ASSOC\s+)?CLIN PROF\b", # non-HS clinical (no HS prefix)
|
|
r"^(?:ASST\s+|ASSOC\s+)?CLIN PROF-",
|
|
r"\bCLIN PROF-VOL\b", # Volunteer Clinical Professor
|
|
|
|
# --- Non-Senate Instructional / Lecturers (all variants) ---
|
|
r"\bLECTURER\b",
|
|
r"\bLECT\b", # LECT-AY, LECT-FY, LECT-AY-1/9 etc.
|
|
r"^LECT-",
|
|
r"\bSR\s+LECT\b",
|
|
r"\bSENIOR\s+LECT\b",
|
|
r"\bLECT\s+IN\b", # Lect In Summer Session
|
|
r"\bCONT(?:INUING)?\s+EDUCATOR\b",
|
|
r"\bSUPV\s+OF\s+TEACH\s+ED\b",
|
|
|
|
# --- Professional Research series ---
|
|
r"\bPROF(?:ESSIONAL)?\s+RES(?:EARCHER)?\b",
|
|
r"\bSR\s+RES\b",
|
|
r"\bASSOC\s+RES\b",
|
|
r"^RES-(?:FY|AY)\b",
|
|
r"^ASST\s+RES-",
|
|
|
|
# --- Project Scientist series ---
|
|
r"\bPROJ(?:ECT)?\s+SCI(?:ENT)?",
|
|
r"\bVIS\s+PROJ\s+SCI", # Visiting Project Scientist
|
|
|
|
# --- Specialist / Cooperative Extension series ---
|
|
# Anchored to avoid "PHYSICIAN SPECIALIST", "IT SPECIALIST" staff titles
|
|
r"^(?:SR\s+|ASSOC\s+|ASST\s+)?SPECIALIST(?:\s+AES|\s+COOP|\s+NEX|\s*$)",
|
|
r"\bCOOP\s+EXT\s+(?:SPEC|ADVISOR|ADV)\b",
|
|
|
|
# --- Recall/Recall-type lecturer ---
|
|
r"\bLECT-RECALL\b",
|
|
r"\bPROF-RECALL\b", # Recalled ladder faculty (post-emeritus)
|
|
|
|
# --- Librarians (non-Senate academic series) ---
|
|
r"\bLIBRARIAN\b",
|
|
|
|
# --- Academic Coordinators / Administrators ---
|
|
r"\bACAD(?:EMIC)?\s+COORD\b",
|
|
r"\bACAD(?:EMIC)?\s+ADMIN\b",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Normalisation
|
|
# GCC (State Controller) uses title case with full words:
|
|
# "Assistant Prof-Ay", "Associate Prof-Ay", "Visiting Prof", "Acting Prof-Ay"
|
|
# UCOP uses abbreviated upper case: "ASST PROF-AY", "ASSOC PROF-AY"
|
|
# Normalise to upper case + UCOP abbreviations so all patterns work on both.
|
|
# ---------------------------------------------------------------------------
|
|
_GCC_EXPANSIONS = [
|
|
(re.compile(r"\bASSISTANT\b"), "ASST"),
|
|
(re.compile(r"\bASSOCIATE\b"), "ASSOC"),
|
|
(re.compile(r"\bVISITING\b"), "VIS"),
|
|
(re.compile(r"\bACTING\b"), "ACT"),
|
|
# GCC 2024 uses "Professor-Ay" instead of "Prof-Ay".
|
|
# Also catches the UCSB corruption "Professoressor" (doubled suffix).
|
|
(re.compile(r"\bPROFESSOR(?:ESSOR)?\b"), "PROF"),
|
|
]
|
|
|
|
|
|
def normalise(t: str) -> str:
|
|
"""Upper-case, collapse whitespace, expand GCC full-word rank abbreviations."""
|
|
t = re.sub(r"\s+", " ", t.upper().strip())
|
|
for pattern, replacement in _GCC_EXPANSIONS:
|
|
t = pattern.sub(replacement, t)
|
|
return t
|
|
|
|
|
|
def classify_title(raw_title: str, gross_pay: float = 0.0,
|
|
senior_threshold: float = SENIOR_STAFF_THRESHOLD,
|
|
support_threshold: float = SUPPORT_STAFF_THRESHOLD) -> str:
|
|
t = normalise(raw_title)
|
|
|
|
if any(rx.search(t) for rx in _STUDENT_RX):
|
|
return "STUDENT_TRAINEE"
|
|
|
|
if any(rx.search(t) for rx in _ATHLETICS_RX):
|
|
return "ATHLETICS"
|
|
|
|
if any(rx.search(t) for rx in _SMG_RX):
|
|
return "SENIOR_MANAGEMENT"
|
|
|
|
# Senate faculty tested before non-Senate so that e.g. PROF OF CLIN
|
|
# (Senate) doesn't fall through to the CLIN PROF (non-Senate) pattern.
|
|
if any(rx.search(t) for rx in _SENATE_RX):
|
|
return "SENATE_FACULTY"
|
|
|
|
if any(rx.search(t) for rx in _NON_SENATE_RX):
|
|
return "NON_SENATE_ACADEMIC"
|
|
|
|
# Staff: split by pay level
|
|
if gross_pay >= senior_threshold:
|
|
return "SENIOR_PROF_STAFF"
|
|
elif gross_pay >= support_threshold:
|
|
return "PROF_STAFF"
|
|
else:
|
|
return "SUPPORT_STAFF"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MEDICAL CENTER FLAG
|
|
# ---------------------------------------------------------------------------
|
|
# Orthogonal to classify_title()'s category: marks employees working at a UC
|
|
# academic medical center (clinical/HCOMP faculty, physicians, nurses, and
|
|
# the broader allied-health/hospital-operations workforce), regardless of
|
|
# which of the 8 categories they land in above.
|
|
#
|
|
# Built from actual UC-wages title text, not guessed. Two signals:
|
|
#
|
|
# 1. ALWAYS (any campus): titles that are unambiguous wherever they appear.
|
|
# - "MED CTR" literal substring -- only ever seen on hospital-building
|
|
# facilities/exec titles (MED CTR COOK, CEO MED CTR, etc.), never on
|
|
# clinical professional titles, never at non-medical-center campuses.
|
|
# - HS CLIN PROF / HS CLIN INSTR series (Health Sciences clinical
|
|
# faculty -- same titles already routed to NON_SENATE_ACADEMIC above).
|
|
# - Bare "-HCOMP" ladder titles (Health Sciences Compensation Plan --
|
|
# same titles already routed to SENATE_FACULTY above).
|
|
#
|
|
# 2. ONLY at the 5 campuses that operate a UC-owned academic medical center
|
|
# (UCSF, UCLA, UCD, UCI, UCSD -- UC Riverside has a medical school but no
|
|
# UC-owned teaching hospital of its own): generic clinical-workforce
|
|
# titles (physicians, nurses, dentists, pharmacists, therapists, techs,
|
|
# etc.). These same titles show up identically at every campus (student
|
|
# health services exists everywhere), so outside these 5 campuses they
|
|
# can't be told apart from student health staff by title text alone.
|
|
#
|
|
# EXCLUDED everywhere: any title containing "SHS" (Student Health Services)
|
|
# -- present even at UCSF (e.g. "HC ADM SHS MGR 1"), a distinct campus-wide
|
|
# operation from the hospital system.
|
|
MEDICAL_CENTER_CAMPUSES = frozenset({"UCSF", "UCLA", "UCD", "UCI", "UCSD"})
|
|
|
|
_MEDICAL_ALWAYS_RX = _rx(
|
|
r"MED CTR",
|
|
r"^HS\s+(?:ASST\s+|ASSOC\s+)?CLIN (?:PROF|INSTR)\b",
|
|
r"-HCOMP\b",
|
|
)
|
|
|
|
_MEDICAL_CLINICAL_STAFF_RX = _rx(
|
|
r"\bPHYSICIAN\b", r"\bPHYSCN\b", r"\bNURSE\b", r"\bANESTHET",
|
|
r"\bDENTIST\b", r"\bPHARM", r"\bTHERAPIST\b", r"\bRESP THER",
|
|
r"\bPERFUSION", r"\bMIDWIFE\b", r"\bAUDIOLOG", r"\bOPTOMETR",
|
|
r"\bDIETIT", r"\bCLIN SOCIAL", r"\bRADIOLOG", r"\bCLIN LAB",
|
|
r"\bPHLEBOTOM", r"\bMED TCHN\b", r"\bNUC MED TCHNO\b", r"\bDIALYSIS\b",
|
|
r"\bULTRASOUND\b", r"\bECHOCARD", r"\bEEG\b", r"\bCATH LAB\b",
|
|
r"\bPSYCHIAT",
|
|
)
|
|
|
|
_MEDICAL_EXCLUDE_RX = _rx(r"\bSHS\b")
|
|
|
|
|
|
def is_medical_center(raw_title: str, campus: str) -> bool:
|
|
t = normalise(raw_title)
|
|
|
|
if any(rx.search(t) for rx in _MEDICAL_EXCLUDE_RX):
|
|
return False
|
|
|
|
if any(rx.search(t) for rx in _MEDICAL_ALWAYS_RX):
|
|
return True
|
|
|
|
if campus in MEDICAL_CENTER_CAMPUSES:
|
|
return any(rx.search(t) for rx in _MEDICAL_CLINICAL_STAFF_RX)
|
|
|
|
return False
|