ltc-code 0.1.87__tar.gz → 0.1.88__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.87
3
+ Version: 0.1.88
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.87"
3
+ version = "0.1.88"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -0,0 +1,187 @@
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
+ )
170
+ .drop(
171
+ temp_cols + ["_%s_collapsed" % c for c in ever_cols],
172
+ strict=False,
173
+ )
174
+ )
175
+
176
+ final = pl.concat([non_dupes, dupes_resolved], how="vertical_relaxed")
177
+
178
+ if isinstance(final, pl.DataFrame):
179
+ duplicate_count = (
180
+ final.select(dedup_keys).height
181
+ - final.select(dedup_keys).unique().height
182
+ )
183
+ assert duplicate_count == 0, (
184
+ "Found %s unresolved duplicate rows." % duplicate_count
185
+ )
186
+
187
+ return final
File without changes