In [1]:
import pandas as pd
import numpy as np

import re
import sqlite3
from sqlalchemy import create_engine
In [3]:
# Years we want to process
years = [2018, 2021, 2022, 2023]
# Make a list of our numeric columns
num_cols = ['regular_pay', 'overtime_pay', 'other_pay', 'total_pay', 'benefits', 'total_pay_and_benefits']
In [4]:
# There are far too many job titles, so we'll add a column called job_class
# It will be Administrator, Teacher, or Support Staff
# I put the list of more than 60,000 job titles into Gemini and asked it to help me sort through them. 
# It came up with this function.
# I then (as we'll see below) vectorized the function and ran it across the job titles to sort into Teacher, Administrator,
# Support Staff, or Uncategorized.
def categorize_job_v4(title):
    # If title is null, make it uncategorized.
    if pd.isna(title):
        return "Uncategorized"

    # make it lowercase for regex
    t = str(title).lower()

    # if the lowercase title is in this list, it's uncategorized.
    if t in ['#ref!', '(blank)', 'nan', 'none', '#n/a', '***not found***'] or len(t.strip()) < 2:
        return "Uncategorized"

    # List of administrator words
    admin_kw = ['principal', 'superintendent', 'director', 'admin', 'manager', 
                'supervisor', 'coordinator', 'chief', 'dean', 'president', 
                'executive', 'board', 'chancellor', 'overseer', 'supt', 'vp']

    # List of teacher words
    teacher_kw = ['teacher', 'instructor', 'substitute', 'faculty', 'professor', 
                  'lecturer', 'educator', 'coach', 'certificated', 'teach', 
                  'tchr', 'rs/sdc', 'mild/mod', 'mild/moderate', 'mild moderate',
                  'mod/severe', 'mod severe', 'mod/sev', 'mod sev', 'rsp', 'sdc', 'special ed', 'special education', 'sped']

    # 1. Administrator Check
    if any(re.search(r'\b' + k + r'\b', t) for k in admin_kw) or 'administrator' in t:
        return "Administrator"
        
    # Check if it's an aide explicitly to prevent them from becoming teachers just because they have 'sped'
    is_aide = any(re.search(r'\b' + x + r'\b', t) for x in ['aide', 'assistant', 'para', 'paraeducator', 'para-educator', 'clerk', 'secretary', 'technician'])
    has_teacher = any(x in t for x in ['teacher', 'tchr', 'instructor'])
    
    if is_aide and not has_teacher:
        return "Support Staff"

    # 2. Teacher Check (Includes the user's new keywords and special education)
    if any(k in t for k in teacher_kw):
        return "Teacher"
        
    return "Support Staff"
# Use numpy's vectorize to minimize resources needed to apply the operation across the column
vectorized_categorize = np.vectorize(categorize_job_v4)
In [5]:
for year in years:
    print(year)
    # Create connection to our SQLite db
    con = sqlite3.connect(f'Data/sqlite_dbs/{year}.db')
    # Use pandas to read all the data from the salaries table using the connection
    this_data = pd.read_sql('SELECT * FROM salaries', con)
    this_data
    # Close the connection
    con.close()
    # Loop through our numeric columns and make them numeric in pandas dtypes
    for col in num_cols:
        this_data[col] = pd.to_numeric(this_data[col], errors='coerce')
        
    # We'll cut the data off at 800,000 for the upper limit and 30,000 for the lower limit since we care more about data being
    # accurate than we do about it being statistically outliers or not.
    this_data = this_data[(this_data['total_pay_and_benefits'] <= 800_000) & (this_data['total_pay_and_benefits'] >= 30_000)]
    this_data['job_class'] = vectorized_categorize(this_data['job_title'])

    # Update None values to Alameda
    this_data.loc[this_data['county'] == 'None', 'county'] = 'Alameda'
    this_data = this_data.rename(columns={'entity': 'district'})

    # We'll make a dictionary of suffix as key and dataframe as value. Then we'll use them to write to different tables
    # in the PostgreSQL database
    df_dict = {}
    # Split the data into the various groups we want to aggregate.
    # Get rid of any uncategorized (they are few; 1 in 2021 for example)
    this_data = this_data[this_data['job_class'] != 'Uncategorized']
    # This is also our Teachers, Admins, and Support Staff group
    df_dict['tas'] = this_data.copy()
    # Teachers and administrators
    df_dict['ta'] = this_data[this_data['job_class'] != 'Support Staff']
    # Just Teachers
    df_dict['t'] = this_data[this_data['job_class'] == 'Teacher']
    # Just Administrators
    df_dict['a'] = this_data[this_data['job_class'] == 'Administrator']
    # Just support staff
    df_dict['s'] = this_data[this_data['job_class'] == 'Support Staff']
    # Create database connection engine:
    engine = create_engine('postgresql+psycopg2://USER:PASS@localhost:5432/caled')

    for pre, df in df_dict.items():
        print(f'Aggregating county for {pre}')
        this_county_grouped = df.groupby(['county'])[num_cols].agg(['min', 'max', 'mean', 'sum'])
        this_county_grouped.columns = ['_'.join(col) for col in this_county_grouped.columns]
        this_county_agg = this_county_grouped.reset_index()
        this_county_agg['year'] = year

        print(f'Aggregating district for {pre}')
        this_district_grouped = df.groupby(['county', 'district'])[num_cols].agg(['min', 'max', 'mean', 'sum'])
        this_district_grouped.columns = ['_'.join(col) for col in this_district_grouped.columns]
        this_district_agg = this_district_grouped.reset_index()
        this_district_agg['year'] = year

        print(f'Writing county for {pre}')
        this_county_agg.to_sql(f'CalEd_aggbycounty{pre}', con=engine, if_exists='append', index=False)
        print(f'Writing district for {pre}')
        this_district_agg.to_sql(f'CalEd_aggbydistrict{pre}', con=engine, if_exists='append', index=False)
2018
Aggregating county for tas
Aggregating district for {pre}
Writing county for tas
Writing district for tas
Aggregating county for ta
Aggregating district for {pre}
Writing county for ta
Writing district for ta
Aggregating county for t
Aggregating district for {pre}
Writing county for t
Writing district for t
Aggregating county for a
Aggregating district for {pre}
Writing county for a
Writing district for a
Aggregating county for s
Aggregating district for {pre}
Writing county for s
Writing district for s
2021
Aggregating county for tas
Aggregating district for {pre}
Writing county for tas
Writing district for tas
Aggregating county for ta
Aggregating district for {pre}
Writing county for ta
Writing district for ta
Aggregating county for t
Aggregating district for {pre}
Writing county for t
Writing district for t
Aggregating county for a
Aggregating district for {pre}
Writing county for a
Writing district for a
Aggregating county for s
Aggregating district for {pre}
Writing county for s
Writing district for s
2022
Aggregating county for tas
Aggregating district for {pre}
Writing county for tas
Writing district for tas
Aggregating county for ta
Aggregating district for {pre}
Writing county for ta
Writing district for ta
Aggregating county for t
Aggregating district for {pre}
Writing county for t
Writing district for t
Aggregating county for a
Aggregating district for {pre}
Writing county for a
Writing district for a
Aggregating county for s
Aggregating district for {pre}
Writing county for s
Writing district for s
2023
Aggregating county for tas
Aggregating district for {pre}
Writing county for tas
Writing district for tas
Aggregating county for ta
Aggregating district for {pre}
Writing county for ta
Writing district for ta
Aggregating county for t
Aggregating district for {pre}
Writing county for t
Writing district for t
Aggregating county for a
Aggregating district for {pre}
Writing county for a
Writing district for a
Aggregating county for s
Aggregating district for {pre}
Writing county for s
Writing district for s
In [ ]: