ltc-code 0.2.18__tar.gz → 0.2.20__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.18 → ltc_code-0.2.20}/PKG-INFO +1 -1
- {ltc_code-0.2.18 → ltc_code-0.2.20}/pyproject.toml +2 -1
- ltc_code-0.2.20/src/ltc_code/.DS_Store +0 -0
- ltc_code-0.2.20/src/ltc_code/make_summary_stats_table.py +235 -0
- ltc_code-0.2.20/src/ltc_code/nsc/.DS_Store +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/build_nsc_outcomes.py +125 -13
- ltc_code-0.2.20/src/ltc_code/nsc/raw/.DS_Store +0 -0
- ltc_code-0.2.20/src/ltc_code/school_name_xwalk/__init__.py +3 -0
- ltc_code-0.2.20/src/ltc_code/school_name_xwalk/all_schools_with_ccd.csv +1069 -0
- ltc_code-0.2.20/src/ltc_code/school_name_xwalk/merge_school_ccd.py +43 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/README.md +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/aspire.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/check_cmo_apps.do +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/christel_house.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/democracy_prep.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/green_dot.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/helpers.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/ilt.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/kipp_nj.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/main.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/mappings.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/rocketship.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260614_new_build_scripts_update/yes_prep.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/aspire.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/christel_house.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/democracy_prep.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/green_dot.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/ilt.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/kipp.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/kipp_nj.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/rocketship.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260630_census_disclosure/yes_prep.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/BALANCE.do +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/FS.do +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/ITT.do +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/TOT.do +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/apps_helpers.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/harmony.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260706_ceprscripts/main.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260712_uncommon_scripts/main.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260712_uncommon_scripts/mappings.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/20260712_uncommon_scripts/uncommon.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/__init__.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/aspire.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/check_cmo_apps.do +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/christel_house.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/green_dot.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/helpers.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/june13.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/june2.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/june30.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/june5.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/june7.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/kipp_nj.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/kipp_tx.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/main.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/mappings.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/may27.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/__init__.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/input/.gitkeep +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/naics.csv +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/naics.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/naics_raw.xlsx +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/output/.gitkeep +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/CREDENTIAL_LEVEL_LOOKUP_TABLE.xlsx +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/IPEDS_IC_2013.csv +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/IPEDS_IC_manual.xlsx +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/chetty/mrc_table11.dta +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/chetty/mrc_table2.dta +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/college_crosswalk.xls +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/directory.dta +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/raw/ipeds_data.dta +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/nsc/run_nsc_outcomes.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/polars_dates.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/rocketship.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/src/ltc_code/schema_mapping.py +0 -0
- {ltc_code-0.2.18 → ltc_code-0.2.20}/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.20"
|
|
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
|
|
@@ -247,7 +247,7 @@ chetty_college_outcomes = (
|
|
|
247
247
|
)
|
|
248
248
|
|
|
249
249
|
k_mean_neverattend = chetty_college_outcomes.filter(
|
|
250
|
-
pl.col("super_opeid") == -
|
|
250
|
+
pl.col("super_opeid") == -9
|
|
251
251
|
).item(0, "k_mean")
|
|
252
252
|
k_mean_insuffdata = chetty_college_outcomes.filter(
|
|
253
253
|
pl.col("super_opeid") == -1
|
|
@@ -365,20 +365,27 @@ college_ref = (
|
|
|
365
365
|
# the year the student turns 18 and anchors Sarah's Y1/Y2/... outcome windows.
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
apps_scan = pl.scan_csv(APPS)
|
|
369
|
+
app_columns = apps_scan.collect_schema().names()
|
|
370
|
+
app_select = ["sid_cepr", "cohort_lottery"]
|
|
371
|
+
if "offer" in app_columns:
|
|
372
|
+
app_select.append("offer")
|
|
373
|
+
|
|
368
374
|
apps = (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
)
|
|
372
|
-
.select("sid_cepr", "cohort_lottery")
|
|
375
|
+
apps_scan
|
|
376
|
+
.select(app_select)
|
|
373
377
|
.drop_nulls(["sid_cepr", "cohort_lottery"])
|
|
374
378
|
.with_columns(
|
|
375
379
|
pl.col("sid_cepr").cast(pl.Int64),
|
|
376
380
|
pl.col("cohort_lottery").cast(pl.Int64),
|
|
381
|
+
pl.col("offer").cast(pl.Int8, strict=False)
|
|
382
|
+
if "offer" in app_columns
|
|
383
|
+
else pl.lit(None).cast(pl.Int8).alias("offer"),
|
|
377
384
|
)
|
|
378
385
|
.filter(
|
|
379
386
|
pl.col("cohort_lottery").is_between(1990, 2005)
|
|
380
387
|
)
|
|
381
|
-
.sort("cohort_lottery")
|
|
388
|
+
.sort(["cohort_lottery", "offer"], nulls_last=True)
|
|
382
389
|
.unique(["sid_cepr"])
|
|
383
390
|
.with_columns((pl.col("cohort_lottery") + 18).alias("cohort_18"))
|
|
384
391
|
.collect()
|
|
@@ -919,11 +926,12 @@ for column in enrollment_outcomes.columns:
|
|
|
919
926
|
.alias(column)
|
|
920
927
|
)
|
|
921
928
|
|
|
922
|
-
# First post-high-school institution is useful for auditing the match.
|
|
929
|
+
# First post-high-school institution is useful for auditing the match. Sarah's
|
|
930
|
+
# final Stata sort keeps the highest ID_FSC when enrollment start dates tie.
|
|
923
931
|
first_institution = (
|
|
924
932
|
enroll.with_columns(pl.date(pl.col("cohort_18"), 7, 1).alias("_hs_grad_start"))
|
|
925
933
|
.filter(pl.col("term_start_date") >= pl.col("_hs_grad_start"))
|
|
926
|
-
.sort(["sid_cepr", "term_start_date", "ID_FSC"])
|
|
934
|
+
.sort(["sid_cepr", "term_start_date", "ID_FSC"], descending=[False, False, True])
|
|
927
935
|
.group_by("sid_cepr", maintain_order=True)
|
|
928
936
|
.agg(
|
|
929
937
|
pl.col("ID_FSC").first().alias("ID_FSC_firstinst"),
|
|
@@ -936,13 +944,29 @@ first_institution = (
|
|
|
936
944
|
.first()
|
|
937
945
|
.alias("completion_rate_150pct_firstinst"),
|
|
938
946
|
)
|
|
947
|
+
.with_columns(
|
|
948
|
+
pl.col("ID_FSC_firstinst")
|
|
949
|
+
.str.slice(0, 6)
|
|
950
|
+
.cast(pl.Int64, strict=False)
|
|
951
|
+
.alias("opeid_firstinst")
|
|
952
|
+
)
|
|
953
|
+
.join(
|
|
954
|
+
chetty_by_opeid.select("opeid", "k_mean", "tier"),
|
|
955
|
+
left_on="opeid_firstinst",
|
|
956
|
+
right_on="opeid",
|
|
957
|
+
how="left",
|
|
958
|
+
validate="m:1",
|
|
959
|
+
)
|
|
960
|
+
.rename({"k_mean": "k_mean_firstinst", "tier": "tier_firstinst"})
|
|
939
961
|
)
|
|
940
962
|
|
|
963
|
+
# Carry Sarah's raw IPEDS completion-rate variable from the first institution.
|
|
964
|
+
# Nonattenders remain missing here; Sarah handles the adjusted version later.
|
|
941
965
|
first_institution = first_institution.with_columns(
|
|
942
966
|
pl.col("k_mean_firstinst")
|
|
943
967
|
.fill_null(k_mean_insuffdata)
|
|
944
968
|
.alias("k_mean"),
|
|
945
|
-
pl.col("completion_rate_150pct_firstinst").alias("
|
|
969
|
+
pl.col("completion_rate_150pct_firstinst").alias("completion_rate_150pct_ip"),
|
|
946
970
|
)
|
|
947
971
|
|
|
948
972
|
enrollment_outcomes = enrollment_outcomes.join(
|
|
@@ -1346,10 +1370,41 @@ nsc_outcomes = (
|
|
|
1346
1370
|
.then(k_mean_neverattend)
|
|
1347
1371
|
.otherwise(pl.col("k_mean"))
|
|
1348
1372
|
.alias("k_mean"),
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1373
|
+
)
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
# Sarah's later cleanup takes medians over the student-level analysis file by
|
|
1377
|
+
# tier. Apply the same residual fill only when an attendee is still missing.
|
|
1378
|
+
tier_medians = (
|
|
1379
|
+
nsc_outcomes.group_by("tier_firstinst")
|
|
1380
|
+
.agg(
|
|
1381
|
+
pl.col("k_mean").median().alias("k_mean_tier_median"),
|
|
1382
|
+
pl.col("completion_rate_150pct_ip")
|
|
1383
|
+
.median()
|
|
1384
|
+
.alias("completion_rate_150pct_tier_median"),
|
|
1385
|
+
)
|
|
1386
|
+
.drop_nulls("tier_firstinst")
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
nsc_outcomes = (
|
|
1390
|
+
nsc_outcomes.join(
|
|
1391
|
+
tier_medians,
|
|
1392
|
+
on="tier_firstinst",
|
|
1393
|
+
how="left",
|
|
1394
|
+
validate="m:1",
|
|
1395
|
+
)
|
|
1396
|
+
.with_columns(
|
|
1397
|
+
pl.when(pl.col("k_mean").is_null() & (pl.col("att_any_byY2") == 1))
|
|
1398
|
+
.then(pl.col("k_mean_tier_median"))
|
|
1399
|
+
.otherwise(pl.col("k_mean"))
|
|
1400
|
+
.alias("k_mean"),
|
|
1401
|
+
pl.when(
|
|
1402
|
+
pl.col("completion_rate_150pct_ip").is_null()
|
|
1403
|
+
& (pl.col("att_any_byY2") == 1)
|
|
1404
|
+
)
|
|
1405
|
+
.then(pl.col("completion_rate_150pct_tier_median"))
|
|
1406
|
+
.otherwise(pl.col("completion_rate_150pct_ip"))
|
|
1407
|
+
.alias("completion_rate_150pct_ip"),
|
|
1353
1408
|
)
|
|
1354
1409
|
)
|
|
1355
1410
|
|
|
@@ -1412,6 +1467,59 @@ for year in range(1, N_YEARS_OUT + 1):
|
|
|
1412
1467
|
)
|
|
1413
1468
|
|
|
1414
1469
|
|
|
1470
|
+
# Sarah removes the raw school-specific rate for students who were not observed
|
|
1471
|
+
# attending by Y2 before creating cmp_rate.
|
|
1472
|
+
nsc_outcomes = nsc_outcomes.with_columns(
|
|
1473
|
+
pl.when(pl.col("att_any_byY2") == 0)
|
|
1474
|
+
.then(None)
|
|
1475
|
+
.otherwise(pl.col("completion_rate_150pct_ip"))
|
|
1476
|
+
.alias("completion_rate_150pct_ip")
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
# Sarah creates cmp_rate as a copy of the restricted raw completion rate, then
|
|
1480
|
+
# fills missing cmp_rate values with the late-enrollee/control-group mean.
|
|
1481
|
+
nsc_outcomes = nsc_outcomes.with_columns(
|
|
1482
|
+
pl.col("completion_rate_150pct_ip").alias("cmp_rate")
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
late_enrollee_mean = (
|
|
1486
|
+
nsc_outcomes
|
|
1487
|
+
.filter(
|
|
1488
|
+
(pl.col("att_4yr_inY2") == 0)
|
|
1489
|
+
& (pl.col("att_4yr_inY3") == 1)
|
|
1490
|
+
& (
|
|
1491
|
+
(pl.col("cmp_BA_byY6") == 1)
|
|
1492
|
+
| (pl.col("cmp_BA_byY7") == 1)
|
|
1493
|
+
| (pl.col("cmp_BA_byY8") == 1)
|
|
1494
|
+
)
|
|
1495
|
+
& (pl.col("offer") == 0)
|
|
1496
|
+
)
|
|
1497
|
+
.select(pl.col("cmp_rate").mean())
|
|
1498
|
+
.item()
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
nsc_outcomes = nsc_outcomes.with_columns(
|
|
1502
|
+
pl.when(pl.col("cmp_rate").is_null())
|
|
1503
|
+
.then(pl.lit(late_enrollee_mean, dtype=pl.Float64))
|
|
1504
|
+
.otherwise(pl.col("cmp_rate"))
|
|
1505
|
+
.alias("adj_cmp_rate")
|
|
1506
|
+
).with_columns(
|
|
1507
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1508
|
+
.then(None)
|
|
1509
|
+
.otherwise(pl.col("cmp_rate"))
|
|
1510
|
+
.alias("cmp_rate"),
|
|
1511
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1512
|
+
.then(None)
|
|
1513
|
+
.otherwise(pl.col("adj_cmp_rate"))
|
|
1514
|
+
.alias("adj_cmp_rate"),
|
|
1515
|
+
# Preserve the package's existing name as an alias for Sarah's adjusted rate.
|
|
1516
|
+
pl.when(pl.col("att_4yr_byY2").is_null())
|
|
1517
|
+
.then(None)
|
|
1518
|
+
.otherwise(pl.col("adj_cmp_rate"))
|
|
1519
|
+
.alias("predicted_completion"),
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
|
|
1415
1523
|
###########################################################
|
|
1416
1524
|
# Write final outcomes
|
|
1417
1525
|
###########################################################
|
|
@@ -1443,9 +1551,13 @@ nsc_outcomes = nsc_outcomes.with_columns(
|
|
|
1443
1551
|
keep_columns = [
|
|
1444
1552
|
"sid_cepr",
|
|
1445
1553
|
"k_mean",
|
|
1554
|
+
"completion_rate_150pct_ip",
|
|
1555
|
+
"cmp_rate",
|
|
1556
|
+
"adj_cmp_rate",
|
|
1446
1557
|
"predicted_completion",
|
|
1447
1558
|
"ID_FSC_firstinst",
|
|
1448
1559
|
"college_name_firstinst",
|
|
1560
|
+
"tier_firstinst",
|
|
1449
1561
|
"completion_rate_150pct_firstinst",
|
|
1450
1562
|
] + outcome_columns
|
|
1451
1563
|
|
|
Binary file
|