ltc-code 0.1.95__tar.gz → 0.1.97__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.1.95 → ltc_code-0.1.97}/PKG-INFO +1 -1
- {ltc_code-0.1.95 → ltc_code-0.1.97}/pyproject.toml +1 -1
- ltc_code-0.1.97/src/ltc_code/june13.py +330 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/README.md +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/__init__.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/aspire.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/check_cmo_apps.do +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/christel_house.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/green_dot.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/helpers.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/june2.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/june5.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/june7.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/kipp_nj.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/kipp_tx.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/main.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/mappings.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/may27.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/polars_dates.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/rocketship.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/schema_mapping.py +0 -0
- {ltc_code-0.1.95 → ltc_code-0.1.97}/src/ltc_code/yes_prep.py +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from typing import Callable, Optional, Sequence, Union
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
Frame =
|
|
10
|
+
Rule = Union[pl.Expr, Callable[[Frame], pl.Expr]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def impute_offers(
|
|
14
|
+
frame: Union[pl.DataFrame, pl.LazyFrame],
|
|
15
|
+
offer_col: str,
|
|
16
|
+
lottery_number_col: Optional[str] = None,
|
|
17
|
+
waitlist_number_col: Optional[str] = None,
|
|
18
|
+
enrollment_col: Optional[str] = None,
|
|
19
|
+
lottery_imputation: Optional[Union[pl.Expr, Callable[[Frame], pl.Expr]]] = None,
|
|
20
|
+
waitlist_imputation: Optional[Union[pl.Expr, Callable[[Frame], pl.Expr]]] = None,
|
|
21
|
+
enrollment_imputation: Optional[Union[pl.Expr, Callable[[Frame], pl.Expr]]] = None,
|
|
22
|
+
sibling_priority_cols: Optional[Sequence[str]] = None,
|
|
23
|
+
within_cols: Optional[Sequence[str]] = None,
|
|
24
|
+
) -> Union[pl.DataFrame, pl.LazyFrame]:
|
|
25
|
+
"""
|
|
26
|
+
Impute offer indicators in a Polars DataFrame or LazyFrame.
|
|
27
|
+
|
|
28
|
+
The function treats null offers as 0. It sets ``offer_col`` to 1 for rows
|
|
29
|
+
that satisfy any supplied or default imputation rule, and creates:
|
|
30
|
+
|
|
31
|
+
- ``offer_imputed_ln``: newly imputed from lottery number information
|
|
32
|
+
- ``offer_imputed_wn``: newly imputed from waitlist number information
|
|
33
|
+
- ``offer_imputed_enr``: newly imputed from enrollment information
|
|
34
|
+
|
|
35
|
+
Flags are only set for rows whose original offer was 0 or null. Rows that
|
|
36
|
+
already had an observed offer are never flagged. Multiple flags may be 1
|
|
37
|
+
for the same row if multiple rules independently imply an offer.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
frame:
|
|
42
|
+
Polars DataFrame or LazyFrame.
|
|
43
|
+
offer_col:
|
|
44
|
+
Name of the observed offer column. Null is treated as 0.
|
|
45
|
+
lottery_number_col:
|
|
46
|
+
Optional lottery number column.
|
|
47
|
+
waitlist_number_col:
|
|
48
|
+
Optional waitlist number column.
|
|
49
|
+
enrollment_col:
|
|
50
|
+
Optional enrollment indicator column.
|
|
51
|
+
lottery_imputation, waitlist_imputation, enrollment_imputation:
|
|
52
|
+
Optional custom imputation rules. Each may be a Polars expression or a
|
|
53
|
+
callable that accepts ``frame`` and returns a Polars expression.
|
|
54
|
+
sibling_priority_cols:
|
|
55
|
+
Optional columns indicating sibling priority. If supplied, observed
|
|
56
|
+
sibling-priority offers are excluded when computing the default lottery
|
|
57
|
+
cutoff.
|
|
58
|
+
within_cols:
|
|
59
|
+
Optional columns defining the risk set within which lottery and waitlist
|
|
60
|
+
cutoffs are computed. For example:
|
|
61
|
+
|
|
62
|
+
["school_name", "school_year", "entry_grade_clean", "priority_group"]
|
|
63
|
+
|
|
64
|
+
If omitted, cutoffs are computed globally.
|
|
65
|
+
|
|
66
|
+
Default Rules
|
|
67
|
+
-------------
|
|
68
|
+
Lottery:
|
|
69
|
+
Within each ``within_cols`` group, find the highest lottery number among
|
|
70
|
+
observed offers, excluding sibling-priority offers if
|
|
71
|
+
``sibling_priority_cols`` is supplied. Impute offers for originally
|
|
72
|
+
non-offered rows with lottery numbers at or below that cutoff.
|
|
73
|
+
|
|
74
|
+
Enrollment:
|
|
75
|
+
Impute offers for originally non-offered enrolled rows.
|
|
76
|
+
|
|
77
|
+
Waitlist:
|
|
78
|
+
If enrollment information is supplied, first infer offers from
|
|
79
|
+
enrollment, then within each group use the highest waitlist number among
|
|
80
|
+
those enrollment-implied offers as the waitlist cutoff.
|
|
81
|
+
|
|
82
|
+
If enrollment information is not supplied, within each group use the
|
|
83
|
+
highest waitlist number among observed offers as the waitlist cutoff.
|
|
84
|
+
|
|
85
|
+
Then impute offers for originally non-offered rows with waitlist numbers
|
|
86
|
+
at or below that cutoff.
|
|
87
|
+
|
|
88
|
+
Example
|
|
89
|
+
-------
|
|
90
|
+
>>> result = impute_offers(
|
|
91
|
+
... df,
|
|
92
|
+
... offer_col="offer",
|
|
93
|
+
... lottery_number_col="lottery_number",
|
|
94
|
+
... waitlist_number_col="waitlist_number",
|
|
95
|
+
... enrollment_col="enroll_school_year",
|
|
96
|
+
... sibling_priority_cols=["sibling", "sibling_concur"],
|
|
97
|
+
... within_cols=["school_name", "school_year", "entry_grade_clean", "priority_group"],
|
|
98
|
+
... )
|
|
99
|
+
"""
|
|
100
|
+
if not isinstance(frame, (pl.DataFrame, pl.LazyFrame)):
|
|
101
|
+
raise TypeError("frame must be a Polars DataFrame or LazyFrame.")
|
|
102
|
+
|
|
103
|
+
within_cols = list(within_cols or [])
|
|
104
|
+
sibling_priority_cols = list(sibling_priority_cols or [])
|
|
105
|
+
|
|
106
|
+
original_offer = "__original_offer_for_imputation__"
|
|
107
|
+
|
|
108
|
+
def as_rule_expr(rule: Union[pl.Expr, Callable[[Frame], pl.Expr]]) -> pl.Expr:
|
|
109
|
+
expr = rule(frame) if callable(rule) else rule
|
|
110
|
+
if not isinstance(expr, pl.Expr):
|
|
111
|
+
raise TypeError("Imputation rules must be Polars expressions.")
|
|
112
|
+
return expr.fill_null(False)
|
|
113
|
+
|
|
114
|
+
def any_sibling_priority(cols: Sequence[str]) -> pl.Expr:
|
|
115
|
+
priority_expr = pl.lit(False)
|
|
116
|
+
for col in cols:
|
|
117
|
+
priority_expr = priority_expr | (pl.col(col).fill_null(0) == 1)
|
|
118
|
+
return priority_expr
|
|
119
|
+
|
|
120
|
+
def maybe_over(expr: pl.Expr) -> pl.Expr:
|
|
121
|
+
if within_cols:
|
|
122
|
+
return expr.over(within_cols)
|
|
123
|
+
return expr
|
|
124
|
+
|
|
125
|
+
work = frame.with_columns(
|
|
126
|
+
pl.col(offer_col).fill_null(0).cast(pl.Int8, strict=False).alias(original_offer)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
originally_no_offer = pl.col(original_offer) == 0
|
|
130
|
+
|
|
131
|
+
lottery_rule = None
|
|
132
|
+
if lottery_imputation is not None:
|
|
133
|
+
lottery_rule = as_rule_expr(lottery_imputation)
|
|
134
|
+
elif lottery_number_col is not None:
|
|
135
|
+
observed_regular_offer = (
|
|
136
|
+
(pl.col(original_offer) == 1)
|
|
137
|
+
& ~any_sibling_priority(sibling_priority_cols)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
lottery_cutoff = maybe_over(
|
|
141
|
+
pl.col(lottery_number_col)
|
|
142
|
+
.filter(observed_regular_offer)
|
|
143
|
+
.max()
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
lottery_rule = (
|
|
147
|
+
pl.col(lottery_number_col).is_not_null()
|
|
148
|
+
& (pl.col(lottery_number_col) <= lottery_cutoff)
|
|
149
|
+
).fill_null(False)
|
|
150
|
+
|
|
151
|
+
enrollment_rule = None
|
|
152
|
+
if enrollment_imputation is not None:
|
|
153
|
+
enrollment_rule = as_rule_expr(enrollment_imputation)
|
|
154
|
+
elif enrollment_col is not None:
|
|
155
|
+
enrollment_rule = (pl.col(enrollment_col).fill_null(0) == 1).fill_null(False)
|
|
156
|
+
|
|
157
|
+
waitlist_rule = None
|
|
158
|
+
if waitlist_imputation is not None:
|
|
159
|
+
waitlist_rule = as_rule_expr(waitlist_imputation)
|
|
160
|
+
elif waitlist_number_col is not None:
|
|
161
|
+
if enrollment_rule is not None:
|
|
162
|
+
waitlist_cutoff_source = originally_no_offer & enrollment_rule
|
|
163
|
+
else:
|
|
164
|
+
waitlist_cutoff_source = pl.col(original_offer) == 1
|
|
165
|
+
|
|
166
|
+
waitlist_cutoff = maybe_over(
|
|
167
|
+
pl.col(waitlist_number_col)
|
|
168
|
+
.filter(waitlist_cutoff_source)
|
|
169
|
+
.max()
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
waitlist_rule = (
|
|
173
|
+
pl.col(waitlist_number_col).is_not_null()
|
|
174
|
+
& (pl.col(waitlist_number_col) <= waitlist_cutoff)
|
|
175
|
+
).fill_null(False)
|
|
176
|
+
|
|
177
|
+
false_expr = pl.lit(False)
|
|
178
|
+
lottery_rule = lottery_rule if lottery_rule is not None else false_expr
|
|
179
|
+
waitlist_rule = waitlist_rule if waitlist_rule is not None else false_expr
|
|
180
|
+
enrollment_rule = enrollment_rule if enrollment_rule is not None else false_expr
|
|
181
|
+
|
|
182
|
+
ln_flag = originally_no_offer & lottery_rule
|
|
183
|
+
enr_flag = originally_no_offer & enrollment_rule
|
|
184
|
+
wn_flag = originally_no_offer & waitlist_rule
|
|
185
|
+
|
|
186
|
+
any_imputation = ln_flag | wn_flag | enr_flag
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
work.with_columns(
|
|
190
|
+
[
|
|
191
|
+
pl.when(ln_flag)
|
|
192
|
+
.then(1)
|
|
193
|
+
.otherwise(0)
|
|
194
|
+
.cast(pl.Int8)
|
|
195
|
+
.alias("offer_imputed_ln"),
|
|
196
|
+
pl.when(wn_flag)
|
|
197
|
+
.then(1)
|
|
198
|
+
.otherwise(0)
|
|
199
|
+
.cast(pl.Int8)
|
|
200
|
+
.alias("offer_imputed_wn"),
|
|
201
|
+
pl.when(enr_flag)
|
|
202
|
+
.then(1)
|
|
203
|
+
.otherwise(0)
|
|
204
|
+
.cast(pl.Int8)
|
|
205
|
+
.alias("offer_imputed_enr"),
|
|
206
|
+
pl.when((pl.col(original_offer) == 1) | any_imputation)
|
|
207
|
+
.then(1)
|
|
208
|
+
.otherwise(0)
|
|
209
|
+
.cast(pl.Int8)
|
|
210
|
+
.alias(offer_col),
|
|
211
|
+
]
|
|
212
|
+
)
|
|
213
|
+
.drop(original_offer)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# mappings.py
|
|
218
|
+
|
|
219
|
+
import polars as pl
|
|
220
|
+
|
|
221
|
+
FINAL_SCHEMA = {
|
|
222
|
+
"cmo_name": {"dtype": pl.String, "default": None},
|
|
223
|
+
"cmo_region": {"dtype": pl.String, "default": None},
|
|
224
|
+
"ren_num": {"dtype": pl.String, "default": None},
|
|
225
|
+
"sid_cepr": {"dtype": pl.String, "default": None},
|
|
226
|
+
|
|
227
|
+
"school_name": {"dtype": pl.String, "default": None},
|
|
228
|
+
"school_year": {"dtype": pl.Int64, "default": None},
|
|
229
|
+
"entry_grade_clean": {"dtype": pl.String, "default": None},
|
|
230
|
+
"priority_group": {"dtype": pl.Int64, "default": None},
|
|
231
|
+
"risk_id_name": {"dtype": pl.String, "default": None},
|
|
232
|
+
"risk_id": {"dtype": pl.Int64, "default": None},
|
|
233
|
+
|
|
234
|
+
"offer": {"dtype": pl.Int8, "default": 0},
|
|
235
|
+
"waitlist": {"dtype": pl.Int8, "default": 0},
|
|
236
|
+
"initial_offer": {"dtype": pl.Int8, "default": 0},
|
|
237
|
+
"waitlist_offer": {"dtype": pl.Int8, "default": 0},
|
|
238
|
+
"offer_accepted": {"dtype": pl.Int8, "default": None},
|
|
239
|
+
"accept_school": {"dtype": pl.String, "default": None},
|
|
240
|
+
"offer_imputed_ln": {"dtype": pl.Int8, "default": 0},
|
|
241
|
+
"offer_imputed_wn": {"dtype": pl.Int8, "default": 0},
|
|
242
|
+
"offer_imputed_enr": {"dtype": pl.Int8, "default": 0},
|
|
243
|
+
|
|
244
|
+
"priority_group_name": {"dtype": pl.String, "default": None},
|
|
245
|
+
"priority_group_clean": {"dtype": pl.String, "default": None},
|
|
246
|
+
"sibling": {"dtype": pl.Int8, "default": 0},
|
|
247
|
+
"sibling_concur": {"dtype": pl.Int8, "default": 0},
|
|
248
|
+
"staff": {"dtype": pl.Int8, "default": 0},
|
|
249
|
+
"zoned": {"dtype": pl.Int8, "default": 0},
|
|
250
|
+
"transfer": {"dtype": pl.Int8, "default": 0},
|
|
251
|
+
|
|
252
|
+
"application_cancel": {"dtype": pl.Int8, "default": 0},
|
|
253
|
+
"late_app": {"dtype": pl.Int8, "default": 0},
|
|
254
|
+
"former_dupe": {"dtype": pl.Int8, "default": 0},
|
|
255
|
+
|
|
256
|
+
"enroll": {"dtype": pl.Int8, "default": 0},
|
|
257
|
+
"enroll_school": {"dtype": pl.Int8, "default": 0},
|
|
258
|
+
"enroll_year": {"dtype": pl.Int8, "default": 0},
|
|
259
|
+
"enroll_school_year": {"dtype": pl.Int8, "default": 0},
|
|
260
|
+
"enroll_missing": {"dtype": pl.Int8, "default": 1},
|
|
261
|
+
|
|
262
|
+
"enrollment_years": {"dtype": pl.Int64, "default": None},
|
|
263
|
+
"year_enrolled": {"dtype": pl.Int64, "default": None},
|
|
264
|
+
"withdraw": {"dtype": pl.Int8, "default": 0},
|
|
265
|
+
"waitlist_number": {"dtype": pl.Int64, "default": None},
|
|
266
|
+
"lottery_number": {"dtype": pl.Int64, "default": None},
|
|
267
|
+
"offer_date": {"dtype": pl.Date, "default": None},
|
|
268
|
+
"application_date": {"dtype": pl.Date, "default": None},
|
|
269
|
+
"birth_cohort": {"dtype": pl.Int64, "default": None},
|
|
270
|
+
"age": {"dtype": pl.Int64, "default": None},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
from typing import Mapping, Union
|
|
276
|
+
|
|
277
|
+
import polars as pl
|
|
278
|
+
|
|
279
|
+
import mappings
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def apply_final_schema(
|
|
283
|
+
frame: Union[pl.DataFrame, pl.LazyFrame],
|
|
284
|
+
schema: Mapping[str, Mapping[str, object]] = mappings.FINAL_SCHEMA,
|
|
285
|
+
) -> Union[pl.DataFrame, pl.LazyFrame]:
|
|
286
|
+
"""
|
|
287
|
+
Restrict a Polars DataFrame/LazyFrame to the final LTC application schema.
|
|
288
|
+
|
|
289
|
+
Existing columns are kept, cast to their declared dtype, and ordered
|
|
290
|
+
according to ``schema``. Columns not listed in ``schema`` are dropped.
|
|
291
|
+
Missing schema columns are created from their declared defaults.
|
|
292
|
+
|
|
293
|
+
The schema should be a mapping like:
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
"offer": {"dtype": pl.Int8, "default": 0},
|
|
297
|
+
"school_name": {"dtype": pl.String, "default": None},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
Defaults may be either scalar values or Polars expressions. Expression
|
|
301
|
+
defaults are useful when a missing final column should be derived from
|
|
302
|
+
other columns.
|
|
303
|
+
"""
|
|
304
|
+
if not isinstance(frame, (pl.DataFrame, pl.LazyFrame)):
|
|
305
|
+
raise TypeError("frame must be a Polars DataFrame or LazyFrame.")
|
|
306
|
+
|
|
307
|
+
existing_columns = (
|
|
308
|
+
frame.collect_schema().names()
|
|
309
|
+
if isinstance(frame, pl.LazyFrame)
|
|
310
|
+
else frame.columns
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
final_columns = []
|
|
314
|
+
|
|
315
|
+
for column, spec in schema.items():
|
|
316
|
+
dtype = spec["dtype"]
|
|
317
|
+
default = spec.get("default")
|
|
318
|
+
|
|
319
|
+
if column in existing_columns:
|
|
320
|
+
expr = pl.col(column)
|
|
321
|
+
elif isinstance(default, pl.Expr):
|
|
322
|
+
expr = default
|
|
323
|
+
else:
|
|
324
|
+
expr = pl.lit(default)
|
|
325
|
+
|
|
326
|
+
final_columns.append(
|
|
327
|
+
expr.cast(dtype, strict=False).alias(column)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return frame.select(final_columns)
|
|
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
|