ltc-code 0.1.87__tar.gz → 0.1.89__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.87 → ltc_code-0.1.89}/PKG-INFO +1 -1
- {ltc_code-0.1.87 → ltc_code-0.1.89}/pyproject.toml +1 -1
- ltc_code-0.1.89/src/ltc_code/june5.py +205 -0
- ltc_code-0.1.89/src/ltc_code/june7.py +412 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/README.md +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/__init__.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/green_dot.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/june2.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/kipp_nj.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/kipp_tx.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/may27.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/polars_dates.py +0 -0
- {ltc_code-0.1.87 → ltc_code-0.1.89}/src/ltc_code/schema_mapping.py +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
DEDUP_KEYS = [
|
|
2
|
+
"sid_cepr",
|
|
3
|
+
"school_year",
|
|
4
|
+
"entry_grade_clean",
|
|
5
|
+
"school_name",
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
EVER_COLS = [
|
|
9
|
+
"offer",
|
|
10
|
+
"waitlist",
|
|
11
|
+
"withdraw",
|
|
12
|
+
"offer_accepted",
|
|
13
|
+
"enroll",
|
|
14
|
+
"sibling",
|
|
15
|
+
"staff",
|
|
16
|
+
"zoned",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
INFO_COLS = [
|
|
20
|
+
"lottery_number",
|
|
21
|
+
"waitlist_number",
|
|
22
|
+
"priority_group",
|
|
23
|
+
"priority_group_name",
|
|
24
|
+
"school_accept",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
from typing import List, Union
|
|
31
|
+
|
|
32
|
+
import polars as pl
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_duplicate_applications(
|
|
36
|
+
df: Union[pl.DataFrame, pl.LazyFrame],
|
|
37
|
+
dedup_keys: List[str],
|
|
38
|
+
ever_cols: List[str],
|
|
39
|
+
info_cols: List[str],
|
|
40
|
+
) -> Union[pl.DataFrame, pl.LazyFrame]:
|
|
41
|
+
columns = df.collect_schema().names() if isinstance(df, pl.LazyFrame) else df.columns
|
|
42
|
+
|
|
43
|
+
missing = [
|
|
44
|
+
c for c in dedup_keys + ever_cols + info_cols
|
|
45
|
+
if c not in columns
|
|
46
|
+
]
|
|
47
|
+
if missing:
|
|
48
|
+
raise ValueError("Missing specified columns: %s" % missing)
|
|
49
|
+
|
|
50
|
+
has_late_app = "late_app" in columns
|
|
51
|
+
has_application_cancel = "application_cancel" in columns
|
|
52
|
+
|
|
53
|
+
# These are optional tie-breakers only. They are not required and they do
|
|
54
|
+
# not affect the collapsed event flags.
|
|
55
|
+
date_tiebreakers = [
|
|
56
|
+
c for c in ["application_date", "offer_date"]
|
|
57
|
+
if c in columns
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
late_rank = (
|
|
61
|
+
pl.when(pl.col("late_app") == 1)
|
|
62
|
+
.then(2)
|
|
63
|
+
.when(pl.col("late_app").is_null())
|
|
64
|
+
.then(1)
|
|
65
|
+
.otherwise(0)
|
|
66
|
+
if has_late_app
|
|
67
|
+
else pl.lit(0)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
cancel_rank = (
|
|
71
|
+
pl.when(pl.col("application_cancel") == 1)
|
|
72
|
+
.then(2)
|
|
73
|
+
.when(pl.col("application_cancel").is_null())
|
|
74
|
+
.then(1)
|
|
75
|
+
.otherwise(0)
|
|
76
|
+
if has_application_cancel
|
|
77
|
+
else pl.lit(0)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Outcome ranking follows the order of ever_cols:
|
|
81
|
+
# earlier columns are treated as stronger representative-row evidence.
|
|
82
|
+
outcome_rank = pl.lit(len(ever_cols))
|
|
83
|
+
for i, col in reversed(list(enumerate(ever_cols))):
|
|
84
|
+
outcome_rank = (
|
|
85
|
+
pl.when(pl.col(col) == 1)
|
|
86
|
+
.then(i)
|
|
87
|
+
.otherwise(outcome_rank)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
sort_cols = (
|
|
91
|
+
dedup_keys
|
|
92
|
+
+ ["_late_rank", "_cancel_rank", "_outcome_rank", "_info_count"]
|
|
93
|
+
+ date_tiebreakers
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
descending = (
|
|
97
|
+
[False] * len(dedup_keys)
|
|
98
|
+
+ [
|
|
99
|
+
False, # prefer not late, if late_app exists
|
|
100
|
+
False, # prefer not cancelled, if application_cancel exists
|
|
101
|
+
False, # prefer earlier/stronger event columns from ever_cols
|
|
102
|
+
True, # prefer rows with more non-null info columns
|
|
103
|
+
]
|
|
104
|
+
+ [False] * len(date_tiebreakers)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
ranked = (
|
|
108
|
+
df
|
|
109
|
+
.with_columns(pl.len().over(dedup_keys).alias("_dup_n"))
|
|
110
|
+
.with_columns(
|
|
111
|
+
_late_rank=late_rank,
|
|
112
|
+
_cancel_rank=cancel_rank,
|
|
113
|
+
_outcome_rank=outcome_rank,
|
|
114
|
+
_info_count=pl.sum_horizontal(
|
|
115
|
+
[pl.col(c).is_not_null().cast(pl.Int8) for c in info_cols]
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
temp_cols = [
|
|
121
|
+
"_dup_n",
|
|
122
|
+
"_late_rank",
|
|
123
|
+
"_cancel_rank",
|
|
124
|
+
"_outcome_rank",
|
|
125
|
+
"_info_count",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
non_dupes = (
|
|
129
|
+
ranked
|
|
130
|
+
.filter(pl.col("_dup_n") == 1)
|
|
131
|
+
.drop(temp_cols, strict=False)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
dupes = (
|
|
135
|
+
ranked
|
|
136
|
+
.filter(pl.col("_dup_n") > 1)
|
|
137
|
+
.sort(sort_cols, descending=descending, nulls_last=True)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
rep_rows = dupes.group_by(dedup_keys).first()
|
|
141
|
+
|
|
142
|
+
valid_filter = pl.lit(True)
|
|
143
|
+
|
|
144
|
+
if has_late_app:
|
|
145
|
+
valid_filter = valid_filter & (pl.col("late_app") != 1).fill_null(True)
|
|
146
|
+
|
|
147
|
+
if has_application_cancel:
|
|
148
|
+
valid_filter = (
|
|
149
|
+
valid_filter
|
|
150
|
+
& (pl.col("application_cancel") != 1).fill_null(True)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
valid_dupes = dupes.filter(valid_filter)
|
|
154
|
+
|
|
155
|
+
collapsed_flags = (
|
|
156
|
+
valid_dupes
|
|
157
|
+
.group_by(dedup_keys)
|
|
158
|
+
.agg([pl.col(c).max().alias("_%s_collapsed" % c) for c in ever_cols])
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
dupes_resolved = (
|
|
162
|
+
rep_rows
|
|
163
|
+
.join(collapsed_flags, on=dedup_keys, how="left")
|
|
164
|
+
.with_columns(
|
|
165
|
+
[
|
|
166
|
+
pl.coalesce(pl.col("_%s_collapsed" % c), pl.col(c)).alias(c)
|
|
167
|
+
for c in ever_cols
|
|
168
|
+
] + [
|
|
169
|
+
pl.lit(1).alias("former_dupe")
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
.drop(
|
|
173
|
+
temp_cols + ["_%s_collapsed" % c for c in ever_cols],
|
|
174
|
+
strict=False,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
final = pl.concat(
|
|
179
|
+
[
|
|
180
|
+
non_dupes.select(columns + ["former_dupe"]),
|
|
181
|
+
dupes_resolved.select(columns + ["former_dupe"])
|
|
182
|
+
],
|
|
183
|
+
how="vertical_relaxed",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if isinstance(final, pl.DataFrame):
|
|
187
|
+
duplicate_count = (
|
|
188
|
+
final.select(dedup_keys).height
|
|
189
|
+
- final.select(dedup_keys).unique().height
|
|
190
|
+
)
|
|
191
|
+
assert duplicate_count == 0, (
|
|
192
|
+
"Found %s unresolved duplicate rows." % duplicate_count
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
n_rows = (
|
|
196
|
+
final.height
|
|
197
|
+
if isinstance(final, pl.DataFrame)
|
|
198
|
+
else final.select(pl.len()).collect().item()
|
|
199
|
+
)
|
|
200
|
+
print(f"Number of observations after de-duplicating: {n_rows}")
|
|
201
|
+
|
|
202
|
+
return final
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
def resolve_rank_choice_duplicate_applications(
|
|
2
|
+
df: Union[pl.DataFrame, pl.LazyFrame],
|
|
3
|
+
base_keys: Sequence[str],
|
|
4
|
+
school_choice_cols: Sequence[str],
|
|
5
|
+
ever_cols: Sequence[str],
|
|
6
|
+
info_cols: Sequence[str],
|
|
7
|
+
outcome_school_cols: Optional[Mapping[str, str]] = None,
|
|
8
|
+
late_col: str = "late_app",
|
|
9
|
+
cancel_col: str = "application_cancel",
|
|
10
|
+
date_tiebreakers: Sequence[str] = ("application_date", "offer_date"),
|
|
11
|
+
) -> Union[pl.DataFrame, pl.LazyFrame]:
|
|
12
|
+
"""Resolve duplicate rank-choice application rows.
|
|
13
|
+
|
|
14
|
+
Rank-choice files can put multiple school choices on one row. This helper
|
|
15
|
+
compares the set of school choices within each applicant context
|
|
16
|
+
(``base_keys``), collapses rows that look like the same underlying
|
|
17
|
+
application, and leaves distinct applications separate.
|
|
18
|
+
|
|
19
|
+
Rows are collapsed when they have the exact same school set, or when one
|
|
20
|
+
row's school set is a strict subset of another row's set and the outcomes
|
|
21
|
+
for overlapping schools agree. Collapsed rows keep one representative row
|
|
22
|
+
for record-specific fields and take maxes across ``ever_cols`` among valid
|
|
23
|
+
rows. Rows left separate are flagged as current duplicates if more than
|
|
24
|
+
one application remains for the same ``base_keys``.
|
|
25
|
+
|
|
26
|
+
If ``df`` is a LazyFrame, this helper collects it to perform pairwise
|
|
27
|
+
set/subset comparisons, then returns a LazyFrame built from the result.
|
|
28
|
+
"""
|
|
29
|
+
if not isinstance(df, (pl.DataFrame, pl.LazyFrame)):
|
|
30
|
+
raise TypeError("df must be a polars DataFrame or LazyFrame.")
|
|
31
|
+
|
|
32
|
+
is_lazy = isinstance(df, pl.LazyFrame)
|
|
33
|
+
frame = df.collect() if is_lazy else df
|
|
34
|
+
columns = list(frame.columns)
|
|
35
|
+
|
|
36
|
+
base_keys = list(base_keys)
|
|
37
|
+
school_choice_cols = list(school_choice_cols)
|
|
38
|
+
ever_cols = list(ever_cols)
|
|
39
|
+
info_cols = list(info_cols)
|
|
40
|
+
outcome_school_cols = dict(outcome_school_cols or {})
|
|
41
|
+
|
|
42
|
+
requested = base_keys + school_choice_cols + ever_cols + info_cols
|
|
43
|
+
requested.extend(outcome_school_cols.values())
|
|
44
|
+
if late_col in columns:
|
|
45
|
+
requested.append(late_col)
|
|
46
|
+
if cancel_col in columns:
|
|
47
|
+
requested.append(cancel_col)
|
|
48
|
+
|
|
49
|
+
missing = [column for column in requested if column not in columns]
|
|
50
|
+
if missing:
|
|
51
|
+
raise ValueError("Missing specified columns: %s" % sorted(set(missing)))
|
|
52
|
+
if not base_keys:
|
|
53
|
+
raise ValueError("base_keys must contain at least one column.")
|
|
54
|
+
if not school_choice_cols:
|
|
55
|
+
raise ValueError("school_choice_cols must contain at least one column.")
|
|
56
|
+
if not ever_cols:
|
|
57
|
+
raise ValueError("ever_cols must contain at least one column.")
|
|
58
|
+
|
|
59
|
+
invalid_outcome_keys = [column for column in outcome_school_cols if column not in ever_cols]
|
|
60
|
+
if invalid_outcome_keys:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"outcome_school_cols keys must also appear in ever_cols: %s"
|
|
63
|
+
% sorted(invalid_outcome_keys)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _norm_school(value: Any) -> Optional[str]:
|
|
67
|
+
if value is None:
|
|
68
|
+
return None
|
|
69
|
+
text = str(value).strip()
|
|
70
|
+
return text or None
|
|
71
|
+
|
|
72
|
+
def _is_one(value: Any) -> bool:
|
|
73
|
+
if value is None:
|
|
74
|
+
return False
|
|
75
|
+
return value == 1 or value is True
|
|
76
|
+
|
|
77
|
+
def _school_values(row: Mapping[str, Any]) -> Tuple[List[str], Tuple[str, ...]]:
|
|
78
|
+
ordered = []
|
|
79
|
+
seen = set()
|
|
80
|
+
for column in school_choice_cols:
|
|
81
|
+
school = _norm_school(row[column])
|
|
82
|
+
if school is None or school in seen:
|
|
83
|
+
continue
|
|
84
|
+
ordered.append(school)
|
|
85
|
+
seen.add(school)
|
|
86
|
+
return ordered, tuple(sorted(seen))
|
|
87
|
+
|
|
88
|
+
def _outcome_signature(row: Mapping[str, Any], schools: Iterable[str]) -> Tuple[Any, ...]:
|
|
89
|
+
overlap = sorted(set(schools))
|
|
90
|
+
mapped_outcomes = [
|
|
91
|
+
column for column in ever_cols if column in outcome_school_cols
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Best case: an outcome flag has a paired school column, e.g.
|
|
95
|
+
# offer + offer_school. Then compare outcomes only for overlap schools.
|
|
96
|
+
if mapped_outcomes:
|
|
97
|
+
values = []
|
|
98
|
+
for school in overlap:
|
|
99
|
+
for outcome_col in mapped_outcomes:
|
|
100
|
+
school_col = outcome_school_cols[outcome_col]
|
|
101
|
+
outcome_school = _norm_school(row[school_col])
|
|
102
|
+
values.append(
|
|
103
|
+
(
|
|
104
|
+
outcome_col,
|
|
105
|
+
school,
|
|
106
|
+
int(outcome_school == school and _is_one(row[outcome_col])),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
return tuple(values)
|
|
110
|
+
|
|
111
|
+
# Fallback: without school-specific outcome columns, the helper can only
|
|
112
|
+
# compare row-level outcome flags. This is enough for exact school-set
|
|
113
|
+
# duplicates, but it is a weaker assumption for subset decisions.
|
|
114
|
+
return tuple((column, int(_is_one(row[column]))) for column in ever_cols)
|
|
115
|
+
|
|
116
|
+
def _same_overlap_outcomes(
|
|
117
|
+
left: Mapping[str, Any],
|
|
118
|
+
right: Mapping[str, Any],
|
|
119
|
+
overlap: Iterable[str],
|
|
120
|
+
) -> bool:
|
|
121
|
+
return _outcome_signature(left, overlap) == _outcome_signature(right, overlap)
|
|
122
|
+
|
|
123
|
+
def _info_count(row: Mapping[str, Any]) -> int:
|
|
124
|
+
return sum(1 for column in info_cols if row[column] is not None)
|
|
125
|
+
|
|
126
|
+
def _component_name(base_values: Tuple[Any, ...], school_set: Tuple[str, ...]) -> str:
|
|
127
|
+
return "%r|schools=%r" % (base_values, school_set)
|
|
128
|
+
|
|
129
|
+
work = frame.with_row_index("_rc_row_id")
|
|
130
|
+
rows = work.to_dicts()
|
|
131
|
+
|
|
132
|
+
metadata = {}
|
|
133
|
+
for row in rows:
|
|
134
|
+
ordered_schools, school_set = _school_values(row)
|
|
135
|
+
row_id = row["_rc_row_id"]
|
|
136
|
+
metadata[row_id] = {
|
|
137
|
+
"base": tuple(row[column] for column in base_keys),
|
|
138
|
+
"ordered": ordered_schools,
|
|
139
|
+
"set": school_set,
|
|
140
|
+
"component": "%r|row=%s" % (tuple(row[column] for column in base_keys), row_id),
|
|
141
|
+
"case": 0,
|
|
142
|
+
"school_set_readable": " | ".join(school_set) or None,
|
|
143
|
+
"school_ordered_readable": " | ".join(ordered_schools) or None,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
rows_by_base = {}
|
|
147
|
+
for row in rows:
|
|
148
|
+
rows_by_base.setdefault(metadata[row["_rc_row_id"]]["base"], []).append(row)
|
|
149
|
+
|
|
150
|
+
for base_values, group_rows in rows_by_base.items():
|
|
151
|
+
rows_by_set = {}
|
|
152
|
+
for row in group_rows:
|
|
153
|
+
rows_by_set.setdefault(metadata[row["_rc_row_id"]]["set"], []).append(row)
|
|
154
|
+
|
|
155
|
+
# Cases 1-2: exact same application school set. These collapse even
|
|
156
|
+
# when row-level outcomes differ, because max(ever_cols) keeps evidence.
|
|
157
|
+
for school_set, set_rows in rows_by_set.items():
|
|
158
|
+
if len(set_rows) <= 1:
|
|
159
|
+
continue
|
|
160
|
+
signatures = {
|
|
161
|
+
_outcome_signature(row, school_set)
|
|
162
|
+
for row in set_rows
|
|
163
|
+
}
|
|
164
|
+
case = 1 if len(signatures) == 1 else 2
|
|
165
|
+
component = _component_name(base_values, school_set)
|
|
166
|
+
for row in set_rows:
|
|
167
|
+
row_id = row["_rc_row_id"]
|
|
168
|
+
metadata[row_id]["component"] = component
|
|
169
|
+
metadata[row_id]["case"] = case
|
|
170
|
+
|
|
171
|
+
# Case 3: a strict subset row can collapse into a richer superset row
|
|
172
|
+
# when outcomes agree for the overlapping schools. If more than one
|
|
173
|
+
# superset qualifies, choose the largest/richest one deterministically.
|
|
174
|
+
for row in group_rows:
|
|
175
|
+
row_id = row["_rc_row_id"]
|
|
176
|
+
school_set = set(metadata[row_id]["set"])
|
|
177
|
+
if not school_set:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
candidates = []
|
|
181
|
+
for other in group_rows:
|
|
182
|
+
other_id = other["_rc_row_id"]
|
|
183
|
+
if row_id == other_id:
|
|
184
|
+
continue
|
|
185
|
+
other_set = set(metadata[other_id]["set"])
|
|
186
|
+
if not school_set < other_set:
|
|
187
|
+
continue
|
|
188
|
+
if not _same_overlap_outcomes(row, other, school_set):
|
|
189
|
+
continue
|
|
190
|
+
candidates.append(other)
|
|
191
|
+
|
|
192
|
+
if not candidates:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
candidates.sort(
|
|
196
|
+
key=lambda candidate: (
|
|
197
|
+
len(metadata[candidate["_rc_row_id"]]["set"]),
|
|
198
|
+
_info_count(candidate),
|
|
199
|
+
-candidate["_rc_row_id"],
|
|
200
|
+
),
|
|
201
|
+
reverse=True,
|
|
202
|
+
)
|
|
203
|
+
target_set = metadata[candidates[0]["_rc_row_id"]]["set"]
|
|
204
|
+
component = _component_name(base_values, target_set)
|
|
205
|
+
metadata[row_id]["component"] = component
|
|
206
|
+
metadata[row_id]["case"] = 3
|
|
207
|
+
for target_row in rows_by_set[target_set]:
|
|
208
|
+
target_id = target_row["_rc_row_id"]
|
|
209
|
+
metadata[target_id]["component"] = component
|
|
210
|
+
|
|
211
|
+
assignment_rows = []
|
|
212
|
+
for row_id, values in metadata.items():
|
|
213
|
+
assignment_rows.append(
|
|
214
|
+
{
|
|
215
|
+
"_rc_row_id": row_id,
|
|
216
|
+
"_rc_component": values["component"],
|
|
217
|
+
"rank_choice_duplicate_case": values["case"],
|
|
218
|
+
"rank_choice_school_set": values["school_set_readable"],
|
|
219
|
+
"rank_choice_school_ordered": values["school_ordered_readable"],
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assignments = pl.DataFrame(assignment_rows)
|
|
224
|
+
work = work.join(assignments, on="_rc_row_id", how="left")
|
|
225
|
+
|
|
226
|
+
component_sizes = (
|
|
227
|
+
work.group_by("_rc_component")
|
|
228
|
+
.agg(
|
|
229
|
+
[
|
|
230
|
+
pl.len().alias("_rc_component_n"),
|
|
231
|
+
pl.col("rank_choice_duplicate_case")
|
|
232
|
+
.max()
|
|
233
|
+
.alias("_rc_component_case"),
|
|
234
|
+
]
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
work = (
|
|
238
|
+
work
|
|
239
|
+
.join(component_sizes, on="_rc_component", how="left")
|
|
240
|
+
.with_columns(
|
|
241
|
+
pl.col("_rc_component_case").alias("rank_choice_duplicate_case")
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
outcome_rank = pl.lit(len(ever_cols))
|
|
246
|
+
for index, column in reversed(list(enumerate(ever_cols))):
|
|
247
|
+
outcome_rank = pl.when(pl.col(column) == 1).then(index).otherwise(outcome_rank)
|
|
248
|
+
|
|
249
|
+
late_rank = (
|
|
250
|
+
pl.when(pl.col(late_col) == 1)
|
|
251
|
+
.then(2)
|
|
252
|
+
.when(pl.col(late_col).is_null())
|
|
253
|
+
.then(1)
|
|
254
|
+
.otherwise(0)
|
|
255
|
+
if late_col in columns
|
|
256
|
+
else pl.lit(0)
|
|
257
|
+
)
|
|
258
|
+
cancel_rank = (
|
|
259
|
+
pl.when(pl.col(cancel_col) == 1)
|
|
260
|
+
.then(2)
|
|
261
|
+
.when(pl.col(cancel_col).is_null())
|
|
262
|
+
.then(1)
|
|
263
|
+
.otherwise(0)
|
|
264
|
+
if cancel_col in columns
|
|
265
|
+
else pl.lit(0)
|
|
266
|
+
)
|
|
267
|
+
info_count = (
|
|
268
|
+
pl.sum_horizontal([pl.col(c).is_not_null().cast(pl.Int8) for c in info_cols])
|
|
269
|
+
if info_cols
|
|
270
|
+
else pl.lit(0)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
existing_date_tiebreakers = [column for column in date_tiebreakers if column in columns]
|
|
274
|
+
sort_cols = [
|
|
275
|
+
"_rc_component",
|
|
276
|
+
"_late_rank",
|
|
277
|
+
"_cancel_rank",
|
|
278
|
+
"_outcome_rank",
|
|
279
|
+
"_info_count",
|
|
280
|
+
] + existing_date_tiebreakers
|
|
281
|
+
descending = [False, False, False, False, True] + [
|
|
282
|
+
False for _ in existing_date_tiebreakers
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
ranked = work.with_columns(
|
|
286
|
+
_late_rank=late_rank,
|
|
287
|
+
_cancel_rank=cancel_rank,
|
|
288
|
+
_outcome_rank=outcome_rank,
|
|
289
|
+
_info_count=info_count,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
untouched = ranked.filter(pl.col("_rc_component_n") == 1)
|
|
293
|
+
collapsed = (
|
|
294
|
+
ranked
|
|
295
|
+
.filter(pl.col("_rc_component_n") > 1)
|
|
296
|
+
.sort(sort_cols, descending=descending, nulls_last=True)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
rep_rows = collapsed.group_by("_rc_component").first()
|
|
300
|
+
|
|
301
|
+
valid_filter = pl.lit(True)
|
|
302
|
+
if late_col in columns:
|
|
303
|
+
valid_filter = valid_filter & (pl.col(late_col) != 1).fill_null(True)
|
|
304
|
+
if cancel_col in columns:
|
|
305
|
+
valid_filter = valid_filter & (pl.col(cancel_col) != 1).fill_null(True)
|
|
306
|
+
|
|
307
|
+
collapsed_flags = (
|
|
308
|
+
collapsed
|
|
309
|
+
.filter(valid_filter)
|
|
310
|
+
.group_by("_rc_component")
|
|
311
|
+
.agg([pl.col(c).max().alias("_%s_collapsed" % c) for c in ever_cols])
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
temp_cols = [
|
|
315
|
+
"_rc_row_id",
|
|
316
|
+
"_rc_component",
|
|
317
|
+
"_rc_component_n",
|
|
318
|
+
"_rc_component_case",
|
|
319
|
+
"_late_rank",
|
|
320
|
+
"_cancel_rank",
|
|
321
|
+
"_outcome_rank",
|
|
322
|
+
"_info_count",
|
|
323
|
+
]
|
|
324
|
+
collapsed_flag_cols = ["_%s_collapsed" % column for column in ever_cols]
|
|
325
|
+
|
|
326
|
+
collapsed_resolved = (
|
|
327
|
+
rep_rows
|
|
328
|
+
.join(collapsed_flags, on="_rc_component", how="left")
|
|
329
|
+
.with_columns(
|
|
330
|
+
[
|
|
331
|
+
pl.coalesce(pl.col("_%s_collapsed" % column), pl.col(column)).alias(
|
|
332
|
+
column
|
|
333
|
+
)
|
|
334
|
+
for column in ever_cols
|
|
335
|
+
]
|
|
336
|
+
+ [pl.lit(1, dtype=pl.Int8).alias("former_dupe")]
|
|
337
|
+
)
|
|
338
|
+
.drop(collapsed_flag_cols, strict=False)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
untouched = untouched.with_columns(pl.lit(0, dtype=pl.Int8).alias("former_dupe"))
|
|
342
|
+
output_columns = [
|
|
343
|
+
column for column in columns
|
|
344
|
+
if column not in {"former_dupe", "current_dupe"}
|
|
345
|
+
] + [
|
|
346
|
+
"rank_choice_school_set",
|
|
347
|
+
"rank_choice_school_ordered",
|
|
348
|
+
"rank_choice_duplicate_case",
|
|
349
|
+
"former_dupe",
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
result = pl.concat(
|
|
353
|
+
[
|
|
354
|
+
untouched.select(output_columns),
|
|
355
|
+
collapsed_resolved.select(output_columns),
|
|
356
|
+
],
|
|
357
|
+
how="vertical_relaxed",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
current_dupe_flags = (
|
|
361
|
+
result
|
|
362
|
+
.group_by(base_keys)
|
|
363
|
+
.agg((pl.len() > 1).cast(pl.Int8).alias("current_dupe"))
|
|
364
|
+
)
|
|
365
|
+
result = result.join(current_dupe_flags, on=base_keys, how="left")
|
|
366
|
+
|
|
367
|
+
final_columns = output_columns + ["current_dupe"]
|
|
368
|
+
result = result.select(final_columns)
|
|
369
|
+
|
|
370
|
+
unresolved_collapsible = result.filter(
|
|
371
|
+
(pl.col("rank_choice_duplicate_case").is_in([1, 2, 3]))
|
|
372
|
+
& (pl.col("former_dupe") != 1)
|
|
373
|
+
).height
|
|
374
|
+
assert unresolved_collapsible == 0, (
|
|
375
|
+
"Found %s rows marked as collapsible cases but not collapsed."
|
|
376
|
+
% unresolved_collapsible
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
current_dupe_errors = result.filter(
|
|
380
|
+
(pl.len().over(base_keys) > 1)
|
|
381
|
+
& (pl.col("current_dupe") != 1)
|
|
382
|
+
).height
|
|
383
|
+
assert current_dupe_errors == 0, (
|
|
384
|
+
"Found %s rows remaining duplicated at base_keys without current_dupe=1."
|
|
385
|
+
% current_dupe_errors
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return result.lazy() if is_lazy else result
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# Example call without school specific outcomes
|
|
392
|
+
final = df.pipe(
|
|
393
|
+
resolve_rank_choice_duplicate_applications,
|
|
394
|
+
base_keys=["sid_cepr", "school_year", "entry_grade_clean"],
|
|
395
|
+
school_choice_cols=["first_choice", "second_choice", "third_choice"],
|
|
396
|
+
ever_cols=["offer", "waitlist", "withdraw", "offer_accepted", "enroll"],
|
|
397
|
+
info_cols=["lottery_number", "waitlist_number", "priority_group"],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Example call with school specific outcomes
|
|
401
|
+
final = df.pipe(
|
|
402
|
+
resolve_rank_choice_duplicate_applications,
|
|
403
|
+
base_keys=["sid_cepr", "school_year", "entry_grade_clean"],
|
|
404
|
+
school_choice_cols=["first_choice", "second_choice", "third_choice"],
|
|
405
|
+
ever_cols=["offer", "waitlist", "withdraw", "offer_accepted", "enroll"],
|
|
406
|
+
info_cols=["lottery_number", "waitlist_number", "priority_group"],
|
|
407
|
+
outcome_school_cols={
|
|
408
|
+
"offer": "offer_school",
|
|
409
|
+
"waitlist": "waitlist_school",
|
|
410
|
+
"withdraw": "withdraw_school",
|
|
411
|
+
},
|
|
412
|
+
)
|
|
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
|