Compute President and top-10 compensation CAGRs over a shared ten-year window
The top-10 CAGR was hardcoded to the last five years while the President CAGR used the full range, so the two cards covered different periods. Also fix start_comp, which returned the end value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9jG4CUFcH6n4vFvgtwndY
This commit is contained in:
parent
2cd7aa1c9d
commit
5c82c22aa2
3 changed files with 40 additions and 34 deletions
|
|
@ -32,8 +32,8 @@ reports detailed compensation for officers, directors, trustees, and key
|
|||
employees of tax-exempt organizations. The University of Delaware (EIN
|
||||
516000297) and UD Research Foundation (EIN 516017306) are the filing entities.
|
||||
Titles are normalized to canonical roles (President, Provost, VP Finance, etc.)
|
||||
using pattern matching. CAGR is computed as compound annual growth rate from
|
||||
first to last available year.
|
||||
using pattern matching. President and top-10 CAGRs are compound annual growth
|
||||
rates over the most recent ten tax years of data.
|
||||
|
||||
**Administrative Cost Ratios** use IPEDS finance survey data. "Institutional
|
||||
support" is the IPEDS functional expense category that most closely
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import polars as pl
|
|||
from admin_analytics.config import UD_UNITID
|
||||
from admin_analytics.irs990.titles import normalize_title
|
||||
|
||||
# Compensation CAGRs cover the most recent ten tax years of Schedule J data
|
||||
CAGR_WINDOW_YEARS = 10
|
||||
|
||||
# Shared CTE for CPI adjustment
|
||||
_CPI_CTE = """
|
||||
WITH annual_cpi AS (
|
||||
|
|
@ -125,19 +128,18 @@ def query_aggregate_comp(conn: duckdb.DuckDBPyConnection) -> pl.DataFrame:
|
|||
""").pl()
|
||||
|
||||
|
||||
def query_aggregate_comp_cagr(conn: duckdb.DuckDBPyConnection) -> dict | None:
|
||||
"""CAGR of aggregate Schedule J compensation over the last 5 years of data."""
|
||||
df = query_aggregate_comp(conn)
|
||||
def _windowed_cagr(df: pl.DataFrame, value_col: str) -> dict | None:
|
||||
"""CAGR over the most recent CAGR_WINDOW_YEARS tax years of ``df``."""
|
||||
if df.height == 0:
|
||||
return None
|
||||
end_year = df["tax_year"][-1]
|
||||
df = df.filter(pl.col("tax_year") > end_year - CAGR_WINDOW_YEARS)
|
||||
if df.height < 2:
|
||||
return None
|
||||
|
||||
# Use last 5 years of available data
|
||||
df = df.tail(min(5, df.height))
|
||||
|
||||
start_year = df["tax_year"][0]
|
||||
end_year = df["tax_year"][-1]
|
||||
start_comp = float(df["total_comp"][0])
|
||||
end_comp = float(df["total_comp"][-1])
|
||||
start_comp = float(df[value_col][0])
|
||||
end_comp = float(df[value_col][-1])
|
||||
n_years = end_year - start_year
|
||||
|
||||
if n_years <= 0 or start_comp <= 0:
|
||||
|
|
@ -148,13 +150,18 @@ def query_aggregate_comp_cagr(conn: duckdb.DuckDBPyConnection) -> dict | None:
|
|||
"cagr_pct": round(cagr, 1),
|
||||
"start_year": start_year,
|
||||
"end_year": end_year,
|
||||
"start_comp": int(end_comp),
|
||||
"start_comp": int(start_comp),
|
||||
"end_comp": int(end_comp),
|
||||
}
|
||||
|
||||
|
||||
def query_aggregate_comp_cagr(conn: duckdb.DuckDBPyConnection) -> dict | None:
|
||||
"""CAGR of top-10 Schedule J compensation over the last ten tax years."""
|
||||
return _windowed_cagr(query_aggregate_comp(conn), "total_comp")
|
||||
|
||||
|
||||
def query_comp_cagr(conn: duckdb.DuckDBPyConnection) -> dict | None:
|
||||
"""Annualized growth rate (CAGR) of President compensation.
|
||||
"""Annualized growth rate (CAGR) of President compensation over the last ten tax years.
|
||||
|
||||
Tracks the President role specifically using title normalization.
|
||||
Returns dict with cagr_pct, start_year, end_year, start_comp, end_comp,
|
||||
|
|
@ -183,27 +190,7 @@ def query_comp_cagr(conn: duckdb.DuckDBPyConnection) -> dict | None:
|
|||
.sort("tax_year")
|
||||
)
|
||||
|
||||
if df.height < 2:
|
||||
return None
|
||||
|
||||
start_year = df["tax_year"][0]
|
||||
end_year = df["tax_year"][-1]
|
||||
start_comp = df["top_comp"][0]
|
||||
end_comp = df["top_comp"][-1]
|
||||
n_years = end_year - start_year
|
||||
|
||||
if n_years <= 0 or start_comp <= 0:
|
||||
return None
|
||||
|
||||
cagr = ((end_comp / start_comp) ** (1.0 / n_years) - 1) * 100
|
||||
|
||||
return {
|
||||
"cagr_pct": round(cagr, 1),
|
||||
"start_year": start_year,
|
||||
"end_year": end_year,
|
||||
"start_comp": start_comp,
|
||||
"end_comp": end_comp,
|
||||
}
|
||||
return _windowed_cagr(df, "top_comp")
|
||||
|
||||
|
||||
def query_top_earners(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from admin_analytics.dashboard.queries import (
|
|||
query_admin_per_student,
|
||||
query_admin_faculty_ratio,
|
||||
query_top_earners,
|
||||
query_comp_cagr,
|
||||
query_aggregate_comp_cagr,
|
||||
query_comp_by_role,
|
||||
query_comp_vs_cpi,
|
||||
query_staff_composition,
|
||||
|
|
@ -129,6 +131,23 @@ class TestTopEarners:
|
|||
assert df_empty.height == 0
|
||||
|
||||
|
||||
class TestCompCagr:
|
||||
def test_uses_last_ten_tax_years(self, db_conn):
|
||||
# 12 years of data; the window should start at 2015, not 2013
|
||||
for year in range(2013, 2025):
|
||||
comp = 1_000_000 if year < 2015 else 500_000 * 2 ** ((year - 2015) / 9)
|
||||
db_conn.execute(
|
||||
"INSERT INTO raw_990_schedule_j (tax_year, person_name, title, total_compensation) "
|
||||
"VALUES (?, 'P', 'PRESIDENT', ?)",
|
||||
[year, round(comp)],
|
||||
)
|
||||
for result in (query_comp_cagr(db_conn), query_aggregate_comp_cagr(db_conn)):
|
||||
assert (result["start_year"], result["end_year"]) == (2015, 2024)
|
||||
assert result["start_comp"] == 500_000
|
||||
assert result["end_comp"] == 1_000_000
|
||||
assert result["cagr_pct"] == 8.0
|
||||
|
||||
|
||||
class TestCompByRole:
|
||||
def test_groups_by_role(self, db_conn):
|
||||
_seed_990(db_conn)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue