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