ltc-code 0.2.19__tar.gz → 0.2.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ltc_code-0.2.19 → ltc_code-0.2.21}/PKG-INFO +1 -1
- {ltc_code-0.2.19 → ltc_code-0.2.21}/pyproject.toml +2 -1
- ltc_code-0.2.21/src/ltc_code/.DS_Store +0 -0
- ltc_code-0.2.21/src/ltc_code/make_summary_stats_table.py +235 -0
- ltc_code-0.2.21/src/ltc_code/nsc/.DS_Store +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/build_nsc_outcomes.py +313 -17
- ltc_code-0.2.21/src/ltc_code/nsc/raw/.DS_Store +0 -0
- ltc_code-0.2.21/src/ltc_code/signal_var_calcs.py +41 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/README.md +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/aspire.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/check_cmo_apps.do +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/christel_house.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/democracy_prep.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/green_dot.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/helpers.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/ilt.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/kipp_nj.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/main.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/mappings.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/rocketship.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/yes_prep.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/aspire.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/christel_house.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/democracy_prep.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/green_dot.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/ilt.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/kipp.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/kipp_nj.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/rocketship.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/yes_prep.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/BALANCE.do +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/FS.do +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/ITT.do +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/TOT.do +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/apps_helpers.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/harmony.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260706_ceprscripts/main.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260712_uncommon_scripts/main.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260712_uncommon_scripts/mappings.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260712_uncommon_scripts/uncommon.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/__init__.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/aspire.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/check_cmo_apps.do +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/christel_house.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/green_dot.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/helpers.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/june13.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/june2.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/june30.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/june5.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/june7.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/kipp_nj.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/kipp_tx.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/main.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/mappings.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/may27.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/__init__.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/input/.gitkeep +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/naics.csv +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/naics.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/naics_raw.xlsx +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/output/.gitkeep +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/CREDENTIAL_LEVEL_LOOKUP_TABLE.xlsx +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/IPEDS_IC_2013.csv +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/IPEDS_IC_manual.xlsx +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/chetty/mrc_table11.dta +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/chetty/mrc_table2.dta +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/college_crosswalk.xls +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/directory.dta +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/raw/ipeds_data.dta +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/nsc/run_nsc_outcomes.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/polars_dates.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/rocketship.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/schema_mapping.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/school_name_xwalk/__init__.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/school_name_xwalk/all_schools_with_ccd.csv +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/school_name_xwalk/merge_school_ccd.py +0 -0
- {ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/yes_prep.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ltc-code"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.21"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
|
|
13
13
|
[project.scripts]
|
|
14
14
|
ltc-code = "ltc_code:main"
|
|
15
|
+
ltc-summary-stats = "ltc_code.make_summary_stats_table:main"
|
|
15
16
|
|
|
16
17
|
[build-system]
|
|
17
18
|
requires = ["uv_build>=0.11.16,<0.12.0"]
|
|
Binary file
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Create a paper-ready two-panel summary-statistics table from a CSV."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
|
|
11
|
+
import polars as pl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
REQUIRED_COLUMNS = ["sample", "Variable", "Mean", "P50", "SD"]
|
|
15
|
+
COUNT_COLUMNS = ["N", "N_in_group"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def clean_latex(value: object) -> str:
|
|
19
|
+
text = "" if value is None else str(value)
|
|
20
|
+
for old, new in {
|
|
21
|
+
"\\": r"\textbackslash{}",
|
|
22
|
+
"&": r"\&",
|
|
23
|
+
"%": r"\%",
|
|
24
|
+
"$": r"\$",
|
|
25
|
+
"#": r"\#",
|
|
26
|
+
"_": r"\_",
|
|
27
|
+
"{": r"\{",
|
|
28
|
+
"}": r"\}",
|
|
29
|
+
"~": r"\textasciitilde{}",
|
|
30
|
+
"^": r"\textasciicircum{}",
|
|
31
|
+
}.items():
|
|
32
|
+
text = text.replace(old, new)
|
|
33
|
+
return text
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_cell(column: str, value: object) -> str:
|
|
37
|
+
if value is None:
|
|
38
|
+
return ""
|
|
39
|
+
if column in COUNT_COLUMNS:
|
|
40
|
+
try:
|
|
41
|
+
return f"{float(value):,.0f}"
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return clean_latex(value)
|
|
44
|
+
try:
|
|
45
|
+
return f"{float(value):,.2f}"
|
|
46
|
+
except (TypeError, ValueError):
|
|
47
|
+
return clean_latex(value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def standardize_columns(stats: pl.DataFrame) -> pl.DataFrame:
|
|
51
|
+
rename = {}
|
|
52
|
+
aliases = {
|
|
53
|
+
"sample": "sample",
|
|
54
|
+
"samples": "sample",
|
|
55
|
+
"variable": "Variable",
|
|
56
|
+
"var": "Variable",
|
|
57
|
+
"name": "Variable",
|
|
58
|
+
"mean": "Mean",
|
|
59
|
+
"avg": "Mean",
|
|
60
|
+
"average": "Mean",
|
|
61
|
+
"p50": "P50",
|
|
62
|
+
"median": "P50",
|
|
63
|
+
"med": "P50",
|
|
64
|
+
"sd": "SD",
|
|
65
|
+
"std": "SD",
|
|
66
|
+
"stddev": "SD",
|
|
67
|
+
"standard deviation": "SD",
|
|
68
|
+
"n": "N",
|
|
69
|
+
"obs": "N",
|
|
70
|
+
"observations": "N",
|
|
71
|
+
"n_in_group": "N_in_group",
|
|
72
|
+
"n group": "N_in_group",
|
|
73
|
+
"group n": "N_in_group",
|
|
74
|
+
"groups": "N_in_group",
|
|
75
|
+
"unique count": "N_in_group",
|
|
76
|
+
"unique counts": "N_in_group",
|
|
77
|
+
}
|
|
78
|
+
for column in stats.columns:
|
|
79
|
+
target = aliases.get(column.strip().lower())
|
|
80
|
+
if target is not None:
|
|
81
|
+
rename[column] = target
|
|
82
|
+
return stats.rename(rename)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def panel_for(sample: str) -> str:
|
|
86
|
+
if "Age 20+" in sample:
|
|
87
|
+
return "A"
|
|
88
|
+
if "Age 25+" in sample:
|
|
89
|
+
return "B"
|
|
90
|
+
return "Other"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def sample_label(sample: str) -> str:
|
|
94
|
+
return sample.replace(" Age 20+", "").replace(" Age 25+", "")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def make_tex(
|
|
98
|
+
stats: pl.DataFrame,
|
|
99
|
+
*,
|
|
100
|
+
caption: str = "Summary Statistics by Sample",
|
|
101
|
+
label: str = "tab:summary_stats_by_sample",
|
|
102
|
+
note: str = "Notes: Means, medians, and standard deviations are reported by analysis sample.",
|
|
103
|
+
) -> str:
|
|
104
|
+
numeric_columns = [
|
|
105
|
+
column for column in ["Mean", "P50", "SD", "N", "N_in_group"] if column in stats.columns
|
|
106
|
+
]
|
|
107
|
+
header_labels = {"Mean": "Mean", "P50": "Median", "SD": "SD", "N": "N", "N_in_group": "Group N"}
|
|
108
|
+
panel_width = len(numeric_columns)
|
|
109
|
+
num_columns = 1 + 2 * panel_width
|
|
110
|
+
headers = ["Variable"] + [header_labels[column] for column in numeric_columns] * 2
|
|
111
|
+
column_spec = (
|
|
112
|
+
r"@{}p{1.35in}"
|
|
113
|
+
+ "r" * panel_width
|
|
114
|
+
+ r"@{\hspace{0.22in}}"
|
|
115
|
+
+ "r" * panel_width
|
|
116
|
+
+ "@{}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
lines = [
|
|
120
|
+
r"\documentclass[11pt]{article}",
|
|
121
|
+
r"\usepackage[margin=0.25in]{geometry}",
|
|
122
|
+
r"\usepackage{booktabs}",
|
|
123
|
+
r"\usepackage{caption}",
|
|
124
|
+
r"\usepackage{longtable}",
|
|
125
|
+
r"\usepackage{pdflscape}",
|
|
126
|
+
r"\usepackage[table]{xcolor}",
|
|
127
|
+
r"\begin{document}",
|
|
128
|
+
r"\begin{landscape}",
|
|
129
|
+
r"\scriptsize",
|
|
130
|
+
r"\setlength{\tabcolsep}{3.5pt}",
|
|
131
|
+
r"\renewcommand{\arraystretch}{1.08}",
|
|
132
|
+
rf"\begin{{longtable}}{{{column_spec}}}",
|
|
133
|
+
rf"\caption{{{clean_latex(caption)}}}\label{{{label}}}\\",
|
|
134
|
+
r"\toprule",
|
|
135
|
+
rf" & \multicolumn{{{panel_width}}}{{c}}{{\textbf{{Panel A: Age 20+}}}} & \multicolumn{{{panel_width}}}{{c}}{{\textbf{{Panel B: Age 25+}}}} \\",
|
|
136
|
+
rf"\cmidrule(lr){{2-{1 + panel_width}}}\cmidrule(l){{{2 + panel_width}-{num_columns}}}",
|
|
137
|
+
" & ".join(clean_latex(column) for column in headers) + r" \\",
|
|
138
|
+
r"\midrule",
|
|
139
|
+
r"\endfirsthead",
|
|
140
|
+
rf"\caption*{{{clean_latex(caption)} (continued)}}\\",
|
|
141
|
+
r"\toprule",
|
|
142
|
+
rf" & \multicolumn{{{panel_width}}}{{c}}{{\textbf{{Panel A: Age 20+}}}} & \multicolumn{{{panel_width}}}{{c}}{{\textbf{{Panel B: Age 25+}}}} \\",
|
|
143
|
+
rf"\cmidrule(lr){{2-{1 + panel_width}}}\cmidrule(l){{{2 + panel_width}-{num_columns}}}",
|
|
144
|
+
" & ".join(clean_latex(column) for column in headers) + r" \\",
|
|
145
|
+
r"\midrule",
|
|
146
|
+
r"\endhead",
|
|
147
|
+
r"\midrule",
|
|
148
|
+
rf"\multicolumn{{{num_columns}}}{{r}}{{Continued on next page}} \\",
|
|
149
|
+
r"\endfoot",
|
|
150
|
+
r"\bottomrule",
|
|
151
|
+
r"\endlastfoot",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
groups: list[str] = []
|
|
155
|
+
variables: dict[str, list[str]] = {}
|
|
156
|
+
values: dict[tuple[str, str, str], dict[str, object]] = {}
|
|
157
|
+
for row in stats.iter_rows(named=True):
|
|
158
|
+
group = sample_label(str(row["sample"]))
|
|
159
|
+
panel = panel_for(str(row["sample"]))
|
|
160
|
+
variable = str(row["Variable"])
|
|
161
|
+
if group not in groups:
|
|
162
|
+
groups.append(group)
|
|
163
|
+
variables.setdefault(group, [])
|
|
164
|
+
if variable not in variables[group]:
|
|
165
|
+
variables[group].append(variable)
|
|
166
|
+
values[(panel, group, variable)] = row
|
|
167
|
+
|
|
168
|
+
for group_index, group in enumerate(groups):
|
|
169
|
+
if group_index:
|
|
170
|
+
lines.append(r"\addlinespace[0.25em]")
|
|
171
|
+
lines.append(
|
|
172
|
+
rf"\rowcolor{{black!8}}\multicolumn{{{num_columns}}}{{@{{}}l@{{}}}}{{\textbf{{{clean_latex(group)}}}}} \\"
|
|
173
|
+
)
|
|
174
|
+
for variable in variables[group]:
|
|
175
|
+
cells = [clean_latex(variable)]
|
|
176
|
+
for panel in ["A", "B"]:
|
|
177
|
+
row = values.get((panel, group, variable))
|
|
178
|
+
cells.extend(format_cell(column, row[column]) if row is not None else "" for column in numeric_columns)
|
|
179
|
+
lines.append(" & ".join(cells) + r" \\")
|
|
180
|
+
|
|
181
|
+
lines.extend([
|
|
182
|
+
r"\end{longtable}",
|
|
183
|
+
rf"\noindent\footnotesize {clean_latex(note)}",
|
|
184
|
+
r"\end{landscape}",
|
|
185
|
+
r"\end{document}",
|
|
186
|
+
])
|
|
187
|
+
return "\n".join(lines) + "\n"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_summary_stats_table(
|
|
191
|
+
input_csv: Union[str, Path],
|
|
192
|
+
tex_out: Union[str, Path],
|
|
193
|
+
pdf_out: Optional[Union[str, Path]] = None,
|
|
194
|
+
*,
|
|
195
|
+
caption: str = "Summary Statistics by Sample",
|
|
196
|
+
label: str = "tab:summary_stats_by_sample",
|
|
197
|
+
note: str = "Notes: Means, medians, and standard deviations are reported by analysis sample.",
|
|
198
|
+
compile_pdf: bool = True,
|
|
199
|
+
) -> Optional[Path]:
|
|
200
|
+
stats = standardize_columns(pl.read_csv(input_csv))
|
|
201
|
+
missing = [column for column in REQUIRED_COLUMNS if column not in stats.columns]
|
|
202
|
+
if missing:
|
|
203
|
+
raise ValueError(f"Missing required columns: {', '.join(missing)}")
|
|
204
|
+
display_columns = REQUIRED_COLUMNS + [column for column in COUNT_COLUMNS if column in stats.columns]
|
|
205
|
+
stats = stats.select(display_columns)
|
|
206
|
+
|
|
207
|
+
tex_path = Path(tex_out)
|
|
208
|
+
tex_path.parent.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
tex_path.write_text(make_tex(stats, caption=caption, label=label, note=note))
|
|
210
|
+
|
|
211
|
+
if pdf_out is None or not compile_pdf:
|
|
212
|
+
return None
|
|
213
|
+
engine = shutil.which("tectonic") or shutil.which("pdflatex")
|
|
214
|
+
if engine is None:
|
|
215
|
+
return None
|
|
216
|
+
pdf_path = Path(pdf_out)
|
|
217
|
+
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
command = [engine, tex_path.name]
|
|
219
|
+
if Path(engine).name == "pdflatex":
|
|
220
|
+
command.insert(1, "-interaction=nonstopmode")
|
|
221
|
+
subprocess.run(command, cwd=tex_path.parent, check=True)
|
|
222
|
+
return pdf_path
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main() -> None:
|
|
226
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
227
|
+
parser.add_argument("input_csv", type=Path)
|
|
228
|
+
parser.add_argument("--tex-out", type=Path, required=True)
|
|
229
|
+
parser.add_argument("--pdf-out", type=Path)
|
|
230
|
+
args = parser.parse_args()
|
|
231
|
+
build_summary_stats_table(args.input_csv, args.tex_out, args.pdf_out)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
Binary file
|
|
@@ -235,11 +235,13 @@ chetty_college_outcomes = (
|
|
|
235
235
|
missing_string_as_null=True,
|
|
236
236
|
value_labels_as_strings=False,
|
|
237
237
|
)
|
|
238
|
-
.select("super_opeid", "tier", "tier_name", "k_mean")
|
|
238
|
+
.select("super_opeid", "tier", "tier_name", "iclevel", "count", "k_mean")
|
|
239
239
|
.collect()
|
|
240
240
|
.with_columns(
|
|
241
241
|
pl.col("super_opeid").cast(pl.Int64, strict=False),
|
|
242
242
|
pl.col("tier").cast(pl.Int8, strict=False),
|
|
243
|
+
pl.col("iclevel").cast(pl.Int8, strict=False),
|
|
244
|
+
pl.col("count").cast(pl.Float64, strict=False),
|
|
243
245
|
pl.col("k_mean").cast(pl.Float64, strict=False),
|
|
244
246
|
)
|
|
245
247
|
.drop_nulls("super_opeid")
|
|
@@ -253,6 +255,38 @@ k_mean_insuffdata = chetty_college_outcomes.filter(
|
|
|
253
255
|
pl.col("super_opeid") == -1
|
|
254
256
|
).item(0, "k_mean")
|
|
255
257
|
|
|
258
|
+
# Collapse MRC college-specific earnings to national student-weighted means.
|
|
259
|
+
# MRC count is the mean number of children per cohort represented by each row.
|
|
260
|
+
mrc_sector_means = (
|
|
261
|
+
chetty_college_outcomes
|
|
262
|
+
.filter(
|
|
263
|
+
(pl.col("super_opeid") > 0)
|
|
264
|
+
& pl.col("iclevel").is_in([1, 2, 3])
|
|
265
|
+
& (pl.col("count") > 0)
|
|
266
|
+
& pl.col("k_mean").is_not_null()
|
|
267
|
+
)
|
|
268
|
+
.with_columns(
|
|
269
|
+
pl.when(pl.col("iclevel") == 1)
|
|
270
|
+
.then(pl.lit("4yr"))
|
|
271
|
+
.otherwise(pl.lit("2yr_or_less"))
|
|
272
|
+
.alias("college_sector")
|
|
273
|
+
)
|
|
274
|
+
.group_by("college_sector")
|
|
275
|
+
.agg(
|
|
276
|
+
(
|
|
277
|
+
(pl.col("k_mean") * pl.col("count")).sum()
|
|
278
|
+
/ pl.col("count").sum()
|
|
279
|
+
).alias("k_mean_coarse")
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
k_mean_4yr_coarse = mrc_sector_means.filter(
|
|
284
|
+
pl.col("college_sector") == "4yr"
|
|
285
|
+
).item(0, "k_mean_coarse")
|
|
286
|
+
k_mean_2yr_coarse = mrc_sector_means.filter(
|
|
287
|
+
pl.col("college_sector") == "2yr_or_less"
|
|
288
|
+
).item(0, "k_mean_coarse")
|
|
289
|
+
|
|
256
290
|
chetty_ope_crosswalk = (
|
|
257
291
|
scan_readstat(
|
|
258
292
|
CHETTY_MRC_TABLE11,
|
|
@@ -275,24 +309,102 @@ chetty_by_opeid = (
|
|
|
275
309
|
.unique("opeid", keep="first")
|
|
276
310
|
)
|
|
277
311
|
|
|
278
|
-
# IPEDS
|
|
279
|
-
#
|
|
312
|
+
# The IPEDS 150%-of-normal-time rate is six-year completion at four-year
|
|
313
|
+
# institutions and three-year completion at two-year institutions.
|
|
280
314
|
ipeds_completion = (
|
|
281
315
|
scan_readstat(
|
|
282
316
|
IPEDS_DATA,
|
|
283
317
|
missing_string_as_null=True,
|
|
284
318
|
value_labels_as_strings=False,
|
|
285
319
|
)
|
|
286
|
-
.select(
|
|
320
|
+
.select(
|
|
321
|
+
"unitid",
|
|
322
|
+
"completion_rate_150pct_ip",
|
|
323
|
+
"cohort_adj_150pct_ip",
|
|
324
|
+
"completers_150pct_ip",
|
|
325
|
+
)
|
|
287
326
|
.collect()
|
|
288
327
|
.with_columns(
|
|
289
328
|
pl.col("unitid").cast(pl.Int64, strict=False),
|
|
290
329
|
pl.col("completion_rate_150pct_ip").cast(pl.Float64, strict=False),
|
|
330
|
+
pl.col("cohort_adj_150pct_ip").cast(pl.Float64, strict=False),
|
|
331
|
+
pl.col("completers_150pct_ip").cast(pl.Float64, strict=False),
|
|
291
332
|
)
|
|
292
333
|
.drop_nulls("unitid")
|
|
293
334
|
.unique("unitid", keep="first")
|
|
294
335
|
)
|
|
295
336
|
|
|
337
|
+
# Use the graduation cohort itself as the student weight. Summing completers and
|
|
338
|
+
# cohorts is equivalent to weighting each college's rate by its IPEDS denominator.
|
|
339
|
+
ipeds_sector_rates = (
|
|
340
|
+
ipeds_completion
|
|
341
|
+
.join(
|
|
342
|
+
ipeds_2013.select("unitid", "years_ipeds_2013"),
|
|
343
|
+
on="unitid",
|
|
344
|
+
how="left",
|
|
345
|
+
validate="1:1",
|
|
346
|
+
)
|
|
347
|
+
.join(
|
|
348
|
+
ipeds_manual.select("unitid", "years_manual"),
|
|
349
|
+
on="unitid",
|
|
350
|
+
how="left",
|
|
351
|
+
validate="1:1",
|
|
352
|
+
)
|
|
353
|
+
.join(
|
|
354
|
+
directory.select("unitid", "years_directory"),
|
|
355
|
+
on="unitid",
|
|
356
|
+
how="left",
|
|
357
|
+
validate="1:1",
|
|
358
|
+
)
|
|
359
|
+
.with_columns(
|
|
360
|
+
pl.coalesce(
|
|
361
|
+
[
|
|
362
|
+
pl.col("years_manual"),
|
|
363
|
+
pl.col("years_ipeds_2013"),
|
|
364
|
+
pl.col("years_directory"),
|
|
365
|
+
]
|
|
366
|
+
).alias("college_years")
|
|
367
|
+
)
|
|
368
|
+
.filter(
|
|
369
|
+
pl.col("college_years").is_in([1, 2, 4])
|
|
370
|
+
& (pl.col("cohort_adj_150pct_ip") > 0)
|
|
371
|
+
& pl.col("completers_150pct_ip").is_not_null()
|
|
372
|
+
)
|
|
373
|
+
.with_columns(
|
|
374
|
+
pl.when(pl.col("college_years") == 4)
|
|
375
|
+
.then(pl.lit("4yr"))
|
|
376
|
+
.otherwise(pl.lit("2yr_or_less"))
|
|
377
|
+
.alias("college_sector")
|
|
378
|
+
)
|
|
379
|
+
.group_by("college_sector")
|
|
380
|
+
.agg(
|
|
381
|
+
pl.col("completers_150pct_ip").sum().alias("completers"),
|
|
382
|
+
pl.col("cohort_adj_150pct_ip").sum().alias("cohort"),
|
|
383
|
+
)
|
|
384
|
+
.with_columns((pl.col("completers") / pl.col("cohort")).alias("cmp_rate_coarse"))
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
cmp_rate_4yr_coarse = ipeds_sector_rates.filter(
|
|
388
|
+
pl.col("college_sector") == "4yr"
|
|
389
|
+
).item(0, "cmp_rate_coarse")
|
|
390
|
+
cmp_rate_2yr_coarse = ipeds_sector_rates.filter(
|
|
391
|
+
pl.col("college_sector") == "2yr_or_less"
|
|
392
|
+
).item(0, "cmp_rate_coarse")
|
|
393
|
+
|
|
394
|
+
for label, value in {
|
|
395
|
+
"four-year k_mean": k_mean_4yr_coarse,
|
|
396
|
+
"two-year-or-less k_mean": k_mean_2yr_coarse,
|
|
397
|
+
}.items():
|
|
398
|
+
if value is None or value <= 0:
|
|
399
|
+
raise ValueError(f"Invalid national student-weighted {label}: {value}")
|
|
400
|
+
|
|
401
|
+
for label, value in {
|
|
402
|
+
"four-year completion rate": cmp_rate_4yr_coarse,
|
|
403
|
+
"two-year-or-less completion rate": cmp_rate_2yr_coarse,
|
|
404
|
+
}.items():
|
|
405
|
+
if value is None or not 0 <= value <= 1:
|
|
406
|
+
raise ValueError(f"Invalid national student-weighted {label}: {value}")
|
|
407
|
+
|
|
296
408
|
|
|
297
409
|
###########################################################
|
|
298
410
|
# Build college reference data
|
|
@@ -331,7 +443,7 @@ college_ref = (
|
|
|
331
443
|
)
|
|
332
444
|
.with_columns(
|
|
333
445
|
(pl.col("college_years") == 4).cast(pl.Int8).alias("college_4yr"),
|
|
334
|
-
|
|
446
|
+
pl.col("college_years").is_in([1, 2]).cast(pl.Int8).alias("college_2yr"),
|
|
335
447
|
pl.col("tier").is_in([1, 2]).cast(pl.Int8).alias("college_elite"),
|
|
336
448
|
)
|
|
337
449
|
.with_columns(
|
|
@@ -365,20 +477,27 @@ college_ref = (
|
|
|
365
477
|
# the year the student turns 18 and anchors Sarah's Y1/Y2/... outcome windows.
|
|
366
478
|
|
|
367
479
|
|
|
480
|
+
apps_scan = pl.scan_csv(APPS)
|
|
481
|
+
app_columns = apps_scan.collect_schema().names()
|
|
482
|
+
app_select = ["sid_cepr", "cohort_lottery"]
|
|
483
|
+
if "offer" in app_columns:
|
|
484
|
+
app_select.append("offer")
|
|
485
|
+
|
|
368
486
|
apps = (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
)
|
|
372
|
-
.select("sid_cepr", "cohort_lottery")
|
|
487
|
+
apps_scan
|
|
488
|
+
.select(app_select)
|
|
373
489
|
.drop_nulls(["sid_cepr", "cohort_lottery"])
|
|
374
490
|
.with_columns(
|
|
375
491
|
pl.col("sid_cepr").cast(pl.Int64),
|
|
376
492
|
pl.col("cohort_lottery").cast(pl.Int64),
|
|
493
|
+
pl.col("offer").cast(pl.Int8, strict=False)
|
|
494
|
+
if "offer" in app_columns
|
|
495
|
+
else pl.lit(None).cast(pl.Int8).alias("offer"),
|
|
377
496
|
)
|
|
378
497
|
.filter(
|
|
379
498
|
pl.col("cohort_lottery").is_between(1990, 2005)
|
|
380
499
|
)
|
|
381
|
-
.sort("cohort_lottery")
|
|
500
|
+
.sort(["cohort_lottery", "offer"], nulls_last=True)
|
|
382
501
|
.unique(["sid_cepr"])
|
|
383
502
|
.with_columns((pl.col("cohort_lottery") + 18).alias("cohort_18"))
|
|
384
503
|
.collect()
|
|
@@ -919,11 +1038,12 @@ for column in enrollment_outcomes.columns:
|
|
|
919
1038
|
.alias(column)
|
|
920
1039
|
)
|
|
921
1040
|
|
|
922
|
-
# First post-high-school institution is useful for auditing the match.
|
|
1041
|
+
# First post-high-school institution is useful for auditing the match. Sarah's
|
|
1042
|
+
# final Stata sort keeps the highest ID_FSC when enrollment start dates tie.
|
|
923
1043
|
first_institution = (
|
|
924
1044
|
enroll.with_columns(pl.date(pl.col("cohort_18"), 7, 1).alias("_hs_grad_start"))
|
|
925
1045
|
.filter(pl.col("term_start_date") >= pl.col("_hs_grad_start"))
|
|
926
|
-
.sort(["sid_cepr", "term_start_date", "ID_FSC"])
|
|
1046
|
+
.sort(["sid_cepr", "term_start_date", "ID_FSC"], descending=[False, False, True])
|
|
927
1047
|
.group_by("sid_cepr", maintain_order=True)
|
|
928
1048
|
.agg(
|
|
929
1049
|
pl.col("ID_FSC").first().alias("ID_FSC_firstinst"),
|
|
@@ -931,18 +1051,33 @@ first_institution = (
|
|
|
931
1051
|
.first()
|
|
932
1052
|
.alias("college_name_firstinst"),
|
|
933
1053
|
pl.col("college_years").first().alias("college_years_firstinst"),
|
|
934
|
-
pl.col("k_mean").first().alias("k_mean_firstinst"),
|
|
935
1054
|
pl.col("completion_rate_150pct_ip")
|
|
936
1055
|
.first()
|
|
937
1056
|
.alias("completion_rate_150pct_firstinst"),
|
|
938
1057
|
)
|
|
1058
|
+
.with_columns(
|
|
1059
|
+
pl.col("ID_FSC_firstinst")
|
|
1060
|
+
.str.slice(0, 6)
|
|
1061
|
+
.cast(pl.Int64, strict=False)
|
|
1062
|
+
.alias("opeid_firstinst")
|
|
1063
|
+
)
|
|
1064
|
+
.join(
|
|
1065
|
+
chetty_by_opeid.select("opeid", "k_mean", "tier"),
|
|
1066
|
+
left_on="opeid_firstinst",
|
|
1067
|
+
right_on="opeid",
|
|
1068
|
+
how="left",
|
|
1069
|
+
validate="m:1",
|
|
1070
|
+
)
|
|
1071
|
+
.rename({"k_mean": "k_mean_firstinst", "tier": "tier_firstinst"})
|
|
939
1072
|
)
|
|
940
1073
|
|
|
1074
|
+
# Carry Sarah's raw IPEDS completion-rate variable from the first institution.
|
|
1075
|
+
# Nonattenders remain missing here; Sarah handles the adjusted version later.
|
|
941
1076
|
first_institution = first_institution.with_columns(
|
|
942
1077
|
pl.col("k_mean_firstinst")
|
|
943
1078
|
.fill_null(k_mean_insuffdata)
|
|
944
1079
|
.alias("k_mean"),
|
|
945
|
-
pl.col("completion_rate_150pct_firstinst").alias("
|
|
1080
|
+
pl.col("completion_rate_150pct_firstinst").alias("completion_rate_150pct_ip"),
|
|
946
1081
|
)
|
|
947
1082
|
|
|
948
1083
|
enrollment_outcomes = enrollment_outcomes.join(
|
|
@@ -1347,9 +1482,48 @@ nsc_outcomes = (
|
|
|
1347
1482
|
.otherwise(pl.col("k_mean"))
|
|
1348
1483
|
.alias("k_mean"),
|
|
1349
1484
|
pl.when(pl.col("ID_FSC_firstinst").is_null())
|
|
1350
|
-
.then(
|
|
1351
|
-
.
|
|
1352
|
-
.
|
|
1485
|
+
.then(k_mean_neverattend)
|
|
1486
|
+
.when(pl.col("college_years_firstinst") == 4)
|
|
1487
|
+
.then(k_mean_4yr_coarse)
|
|
1488
|
+
.when(pl.col("college_years_firstinst").is_in([1, 2]))
|
|
1489
|
+
.then(k_mean_2yr_coarse)
|
|
1490
|
+
.otherwise(k_mean_insuffdata)
|
|
1491
|
+
.alias("k_mean_coarse"),
|
|
1492
|
+
)
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
# Sarah's later cleanup takes medians over the student-level analysis file by
|
|
1496
|
+
# tier. Apply the same residual fill only when an attendee is still missing.
|
|
1497
|
+
tier_medians = (
|
|
1498
|
+
nsc_outcomes.group_by("tier_firstinst")
|
|
1499
|
+
.agg(
|
|
1500
|
+
pl.col("k_mean").median().alias("k_mean_tier_median"),
|
|
1501
|
+
pl.col("completion_rate_150pct_ip")
|
|
1502
|
+
.median()
|
|
1503
|
+
.alias("completion_rate_150pct_tier_median"),
|
|
1504
|
+
)
|
|
1505
|
+
.drop_nulls("tier_firstinst")
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
nsc_outcomes = (
|
|
1509
|
+
nsc_outcomes.join(
|
|
1510
|
+
tier_medians,
|
|
1511
|
+
on="tier_firstinst",
|
|
1512
|
+
how="left",
|
|
1513
|
+
validate="m:1",
|
|
1514
|
+
)
|
|
1515
|
+
.with_columns(
|
|
1516
|
+
pl.when(pl.col("k_mean").is_null() & (pl.col("att_any_byY2") == 1))
|
|
1517
|
+
.then(pl.col("k_mean_tier_median"))
|
|
1518
|
+
.otherwise(pl.col("k_mean"))
|
|
1519
|
+
.alias("k_mean"),
|
|
1520
|
+
pl.when(
|
|
1521
|
+
pl.col("completion_rate_150pct_ip").is_null()
|
|
1522
|
+
& (pl.col("att_any_byY2") == 1)
|
|
1523
|
+
)
|
|
1524
|
+
.then(pl.col("completion_rate_150pct_tier_median"))
|
|
1525
|
+
.otherwise(pl.col("completion_rate_150pct_ip"))
|
|
1526
|
+
.alias("completion_rate_150pct_ip"),
|
|
1353
1527
|
)
|
|
1354
1528
|
)
|
|
1355
1529
|
|
|
@@ -1412,6 +1586,89 @@ for year in range(1, N_YEARS_OUT + 1):
|
|
|
1412
1586
|
)
|
|
1413
1587
|
|
|
1414
1588
|
|
|
1589
|
+
# Sarah removes the raw school-specific rate for students who were not observed
|
|
1590
|
+
# attending by Y2 before creating cmp_rate.
|
|
1591
|
+
nsc_outcomes = nsc_outcomes.with_columns(
|
|
1592
|
+
pl.when(pl.col("att_any_byY2") == 0)
|
|
1593
|
+
.then(None)
|
|
1594
|
+
.otherwise(pl.col("completion_rate_150pct_ip"))
|
|
1595
|
+
.alias("completion_rate_150pct_ip")
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
# Sarah creates cmp_rate as a copy of the restricted raw completion rate, then
|
|
1599
|
+
# fills missing cmp_rate values with the late-enrollee/control-group mean.
|
|
1600
|
+
nsc_outcomes = nsc_outcomes.with_columns(
|
|
1601
|
+
pl.col("completion_rate_150pct_ip").alias("cmp_rate"),
|
|
1602
|
+
pl.when(pl.col("att_any_byY2") == 0)
|
|
1603
|
+
.then(None)
|
|
1604
|
+
.when(pl.col("college_years_firstinst") == 4)
|
|
1605
|
+
.then(cmp_rate_4yr_coarse)
|
|
1606
|
+
.when(pl.col("college_years_firstinst").is_in([1, 2]))
|
|
1607
|
+
.then(cmp_rate_2yr_coarse)
|
|
1608
|
+
.otherwise(None)
|
|
1609
|
+
.alias("cmp_rate_coarse"),
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
late_enrollee_means = (
|
|
1613
|
+
nsc_outcomes
|
|
1614
|
+
.filter(
|
|
1615
|
+
(pl.col("att_4yr_inY2") == 0)
|
|
1616
|
+
& (pl.col("att_4yr_inY3") == 1)
|
|
1617
|
+
& (
|
|
1618
|
+
(pl.col("cmp_BA_byY6") == 1)
|
|
1619
|
+
| (pl.col("cmp_BA_byY7") == 1)
|
|
1620
|
+
| (pl.col("cmp_BA_byY8") == 1)
|
|
1621
|
+
)
|
|
1622
|
+
& (pl.col("offer") == 0)
|
|
1623
|
+
)
|
|
1624
|
+
.select(
|
|
1625
|
+
pl.col("cmp_rate").mean().alias("cmp_rate"),
|
|
1626
|
+
pl.col("cmp_rate_coarse").mean().alias("cmp_rate_coarse"),
|
|
1627
|
+
)
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1630
|
+
late_enrollee_mean = late_enrollee_means.item(0, "cmp_rate")
|
|
1631
|
+
late_enrollee_coarse_mean = late_enrollee_means.item(0, "cmp_rate_coarse")
|
|
1632
|
+
|
|
1633
|
+
if late_enrollee_mean is None or late_enrollee_coarse_mean is None:
|
|
1634
|
+
raise ValueError(
|
|
1635
|
+
"The late-enrollee control donor group has no usable completion-rate values."
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
nsc_outcomes = nsc_outcomes.with_columns(
|
|
1639
|
+
pl.when(pl.col("cmp_rate").is_null())
|
|
1640
|
+
.then(pl.lit(late_enrollee_mean, dtype=pl.Float64))
|
|
1641
|
+
.otherwise(pl.col("cmp_rate"))
|
|
1642
|
+
.alias("adj_cmp_rate"),
|
|
1643
|
+
pl.when(pl.col("cmp_rate_coarse").is_null())
|
|
1644
|
+
.then(pl.lit(late_enrollee_coarse_mean, dtype=pl.Float64))
|
|
1645
|
+
.otherwise(pl.col("cmp_rate_coarse"))
|
|
1646
|
+
.alias("adj_cmp_rate_coarse"),
|
|
1647
|
+
).with_columns(
|
|
1648
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1649
|
+
.then(None)
|
|
1650
|
+
.otherwise(pl.col("cmp_rate"))
|
|
1651
|
+
.alias("cmp_rate"),
|
|
1652
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1653
|
+
.then(None)
|
|
1654
|
+
.otherwise(pl.col("adj_cmp_rate"))
|
|
1655
|
+
.alias("adj_cmp_rate"),
|
|
1656
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1657
|
+
.then(None)
|
|
1658
|
+
.otherwise(pl.col("cmp_rate_coarse"))
|
|
1659
|
+
.alias("cmp_rate_coarse"),
|
|
1660
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1661
|
+
.then(None)
|
|
1662
|
+
.otherwise(pl.col("adj_cmp_rate_coarse"))
|
|
1663
|
+
.alias("adj_cmp_rate_coarse"),
|
|
1664
|
+
# Preserve the package's existing name as an alias for Sarah's adjusted rate.
|
|
1665
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1666
|
+
.then(None)
|
|
1667
|
+
.otherwise(pl.col("adj_cmp_rate"))
|
|
1668
|
+
.alias("predicted_completion"),
|
|
1669
|
+
)
|
|
1670
|
+
|
|
1671
|
+
|
|
1415
1672
|
###########################################################
|
|
1416
1673
|
# Write final outcomes
|
|
1417
1674
|
###########################################################
|
|
@@ -1443,9 +1700,16 @@ nsc_outcomes = nsc_outcomes.with_columns(
|
|
|
1443
1700
|
keep_columns = [
|
|
1444
1701
|
"sid_cepr",
|
|
1445
1702
|
"k_mean",
|
|
1703
|
+
"k_mean_coarse",
|
|
1704
|
+
"completion_rate_150pct_ip",
|
|
1705
|
+
"cmp_rate",
|
|
1706
|
+
"adj_cmp_rate",
|
|
1707
|
+
"cmp_rate_coarse",
|
|
1708
|
+
"adj_cmp_rate_coarse",
|
|
1446
1709
|
"predicted_completion",
|
|
1447
1710
|
"ID_FSC_firstinst",
|
|
1448
1711
|
"college_name_firstinst",
|
|
1712
|
+
"tier_firstinst",
|
|
1449
1713
|
"completion_rate_150pct_firstinst",
|
|
1450
1714
|
] + outcome_columns
|
|
1451
1715
|
|
|
@@ -1479,6 +1743,38 @@ print(
|
|
|
1479
1743
|
f"{first_college_coverage['has_completion_rate'].sum()}/"
|
|
1480
1744
|
f"{first_college_coverage.height} with IPEDS completion rate"
|
|
1481
1745
|
)
|
|
1746
|
+
print(
|
|
1747
|
+
"National student-weighted coarse values: "
|
|
1748
|
+
f"k_mean 4yr={k_mean_4yr_coarse:.2f}, "
|
|
1749
|
+
f"k_mean 2yr-or-less={k_mean_2yr_coarse:.2f}, "
|
|
1750
|
+
f"completion 4yr={cmp_rate_4yr_coarse:.4f}, "
|
|
1751
|
+
f"completion 2yr-or-less={cmp_rate_2yr_coarse:.4f}, "
|
|
1752
|
+
f"nonattender completion imputation={late_enrollee_mean:.4f}, "
|
|
1753
|
+
f"coarse nonattender imputation={late_enrollee_coarse_mean:.4f}"
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
observable_y2 = nsc_outcomes.filter(pl.col("att_4yr_byY2").is_not_null())
|
|
1757
|
+
coarse_audit = observable_y2.select(
|
|
1758
|
+
pl.len().alias("students"),
|
|
1759
|
+
pl.col("k_mean_coarse").null_count().alias("k_mean_coarse_missing"),
|
|
1760
|
+
pl.col("k_mean_coarse").min().alias("k_mean_coarse_min"),
|
|
1761
|
+
pl.col("k_mean_coarse").max().alias("k_mean_coarse_max"),
|
|
1762
|
+
pl.col("adj_cmp_rate_coarse")
|
|
1763
|
+
.null_count()
|
|
1764
|
+
.alias("adj_cmp_rate_coarse_missing"),
|
|
1765
|
+
pl.col("adj_cmp_rate_coarse").min().alias("adj_cmp_rate_coarse_min"),
|
|
1766
|
+
pl.col("adj_cmp_rate_coarse").max().alias("adj_cmp_rate_coarse_max"),
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
if coarse_audit.item(0, "k_mean_coarse_missing") > 0:
|
|
1770
|
+
raise ValueError("k_mean_coarse is unexpectedly missing for observable students.")
|
|
1771
|
+
if coarse_audit.item(0, "adj_cmp_rate_coarse_missing") > 0:
|
|
1772
|
+
raise ValueError(
|
|
1773
|
+
"adj_cmp_rate_coarse is unexpectedly missing for observable students."
|
|
1774
|
+
)
|
|
1775
|
+
|
|
1776
|
+
print("Coarse outcome audit:")
|
|
1777
|
+
print(coarse_audit)
|
|
1482
1778
|
print(
|
|
1483
1779
|
nsc_outcomes.select("sid_cepr", "cohort_lottery", "recovered_nsc_outcome").sort(
|
|
1484
1780
|
"sid_cepr"
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# --- Import necessary packages ---
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
SIGNAL_DEP_VARS = ["wages25", "college20"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def calculate_signal_variance(results: pl.DataFrame) -> pl.DataFrame:
|
|
9
|
+
"""Calculate signal variance and its implied 90-10 spread by outcome."""
|
|
10
|
+
return (
|
|
11
|
+
results
|
|
12
|
+
.filter(pl.col("depvar").is_in(SIGNAL_DEP_VARS))
|
|
13
|
+
.drop_nulls(["sample", "depvar", "coef", "se"])
|
|
14
|
+
.group_by("depvar")
|
|
15
|
+
.agg(
|
|
16
|
+
pl.col("sample").n_unique().alias("n_cmos"),
|
|
17
|
+
pl.col("coef").mean().alias("mean_te"),
|
|
18
|
+
pl.col("coef").var().alias("raw_variance"),
|
|
19
|
+
(pl.col("se") ** 2).mean().alias("noise_variance"),
|
|
20
|
+
)
|
|
21
|
+
.with_columns(
|
|
22
|
+
(
|
|
23
|
+
pl.col("raw_variance")
|
|
24
|
+
- pl.col("noise_variance")
|
|
25
|
+
).alias("signal_variance_raw")
|
|
26
|
+
)
|
|
27
|
+
# Retain the raw estimate, but use a nonnegative value for the spread.
|
|
28
|
+
.with_columns(
|
|
29
|
+
pl.col("signal_variance_raw")
|
|
30
|
+
.clip(lower_bound=0)
|
|
31
|
+
.alias("signal_variance")
|
|
32
|
+
)
|
|
33
|
+
.with_columns(
|
|
34
|
+
pl.col("signal_variance").sqrt().alias("signal_sd")
|
|
35
|
+
)
|
|
36
|
+
# Under normality, P90 - P10 equals 2.563103 standard deviations.
|
|
37
|
+
.with_columns(
|
|
38
|
+
(2.563103 * pl.col("signal_sd")).alias("spread_90_10")
|
|
39
|
+
)
|
|
40
|
+
.sort("depvar")
|
|
41
|
+
)
|
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/aspire.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/check_cmo_apps.do
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/christel_house.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/democracy_prep.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/green_dot.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/kipp_nj.py
RENAMED
|
File without changes
|
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/mappings.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/rocketship.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260614_new_build_scripts_update/yes_prep.py
RENAMED
|
File without changes
|
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/christel_house.py
RENAMED
|
File without changes
|
{ltc_code-0.2.19 → ltc_code-0.2.21}/src/ltc_code/20260630_census_disclosure/democracy_prep.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|