ltc-code 0.1.89__tar.gz → 0.1.91__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.89
3
+ Version: 0.1.91
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.89"
3
+ version = "0.1.91"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -11,20 +11,8 @@ def resolve_rank_choice_duplicate_applications(
11
11
  ) -> Union[pl.DataFrame, pl.LazyFrame]:
12
12
  """Resolve duplicate rank-choice application rows.
13
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.
14
+ If df is a LazyFrame, this helper collects it to do pairwise set/subset
15
+ comparisons, then returns a LazyFrame built from the result.
28
16
  """
29
17
  if not isinstance(df, (pl.DataFrame, pl.LazyFrame)):
30
18
  raise TypeError("df must be a polars DataFrame or LazyFrame.")
@@ -41,10 +29,6 @@ def resolve_rank_choice_duplicate_applications(
41
29
 
42
30
  requested = base_keys + school_choice_cols + ever_cols + info_cols
43
31
  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
32
 
49
33
  missing = [column for column in requested if column not in columns]
50
34
  if missing:
@@ -87,12 +71,8 @@ def resolve_rank_choice_duplicate_applications(
87
71
 
88
72
  def _outcome_signature(row: Mapping[str, Any], schools: Iterable[str]) -> Tuple[Any, ...]:
89
73
  overlap = sorted(set(schools))
90
- mapped_outcomes = [
91
- column for column in ever_cols if column in outcome_school_cols
92
- ]
74
+ mapped_outcomes = [column for column in ever_cols if column in outcome_school_cols]
93
75
 
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
76
  if mapped_outcomes:
97
77
  values = []
98
78
  for school in overlap:
@@ -108,9 +88,6 @@ def resolve_rank_choice_duplicate_applications(
108
88
  )
109
89
  return tuple(values)
110
90
 
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
91
  return tuple((column, int(_is_one(row[column]))) for column in ever_cols)
115
92
 
116
93
  def _same_overlap_outcomes(
@@ -126,18 +103,32 @@ def resolve_rank_choice_duplicate_applications(
126
103
  def _component_name(base_values: Tuple[Any, ...], school_set: Tuple[str, ...]) -> str:
127
104
  return "%r|schools=%r" % (base_values, school_set)
128
105
 
106
+ def _find(parent: Dict[int, int], row_id: int) -> int:
107
+ while parent[row_id] != row_id:
108
+ parent[row_id] = parent[parent[row_id]]
109
+ row_id = parent[row_id]
110
+ return row_id
111
+
112
+ def _union(parent: Dict[int, int], left_id: int, right_id: int) -> None:
113
+ left_root = _find(parent, left_id)
114
+ right_root = _find(parent, right_id)
115
+ if left_root == right_root:
116
+ return
117
+ parent[max(left_root, right_root)] = min(left_root, right_root)
118
+
129
119
  work = frame.with_row_index("_rc_row_id")
130
120
  rows = work.to_dicts()
131
121
 
132
122
  metadata = {}
123
+ parent = {}
133
124
  for row in rows:
134
125
  ordered_schools, school_set = _school_values(row)
135
126
  row_id = row["_rc_row_id"]
127
+ parent[row_id] = row_id
136
128
  metadata[row_id] = {
137
129
  "base": tuple(row[column] for column in base_keys),
138
130
  "ordered": ordered_schools,
139
131
  "set": school_set,
140
- "component": "%r|row=%s" % (tuple(row[column] for column in base_keys), row_id),
141
132
  "case": 0,
142
133
  "school_set_readable": " | ".join(school_set) or None,
143
134
  "school_ordered_readable": " | ".join(ordered_schools) or None,
@@ -152,32 +143,23 @@ def resolve_rank_choice_duplicate_applications(
152
143
  for row in group_rows:
153
144
  rows_by_set.setdefault(metadata[row["_rc_row_id"]]["set"], []).append(row)
154
145
 
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
146
  for school_set, set_rows in rows_by_set.items():
158
147
  if len(set_rows) <= 1:
159
148
  continue
160
- signatures = {
161
- _outcome_signature(row, school_set)
162
- for row in set_rows
163
- }
149
+ signatures = {_outcome_signature(row, school_set) for row in set_rows}
164
150
  case = 1 if len(signatures) == 1 else 2
165
- component = _component_name(base_values, school_set)
151
+ anchor_id = set_rows[0]["_rc_row_id"]
166
152
  for row in set_rows:
167
153
  row_id = row["_rc_row_id"]
168
- metadata[row_id]["component"] = component
154
+ _union(parent, anchor_id, row_id)
169
155
  metadata[row_id]["case"] = case
170
156
 
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
157
  for row in group_rows:
175
158
  row_id = row["_rc_row_id"]
176
159
  school_set = set(metadata[row_id]["set"])
177
160
  if not school_set:
178
161
  continue
179
162
 
180
- candidates = []
181
163
  for other in group_rows:
182
164
  other_id = other["_rc_row_id"]
183
165
  if row_id == other_id:
@@ -187,36 +169,49 @@ def resolve_rank_choice_duplicate_applications(
187
169
  continue
188
170
  if not _same_overlap_outcomes(row, other, school_set):
189
171
  continue
190
- candidates.append(other)
191
-
192
- if not candidates:
193
- continue
172
+ _union(parent, row_id, other_id)
173
+ if metadata[row_id]["case"] == 0:
174
+ metadata[row_id]["case"] = 3
175
+ if metadata[other_id]["case"] == 0:
176
+ metadata[other_id]["case"] = 3
194
177
 
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
178
+ component_rows = {}
179
+ for row in rows:
180
+ row_id = row["_rc_row_id"]
181
+ component_rows.setdefault(_find(parent, row_id), []).append(row_id)
182
+
183
+ component_labels = {}
184
+ for component_row_ids in component_rows.values():
185
+ representative_id = max(
186
+ component_row_ids,
187
+ key=lambda row_id: (
188
+ len(metadata[row_id]["set"]),
189
+ _info_count(rows[row_id]),
190
+ -row_id,
191
+ ),
192
+ )
193
+ component_school_set = metadata[representative_id]["set"]
194
+ component_base = metadata[representative_id]["base"]
195
+ if len(component_row_ids) == 1:
196
+ component = "%r|row=%s" % (component_base, representative_id)
197
+ else:
198
+ component = _component_name(component_base, component_school_set)
199
+
200
+ component_case = max(metadata[row_id]["case"] for row_id in component_row_ids)
201
+ for row_id in component_row_ids:
202
+ component_labels[row_id] = component
203
+ metadata[row_id]["case"] = component_case
210
204
 
211
205
  assignment_rows = []
212
206
  for row_id, values in metadata.items():
213
207
  assignment_rows.append(
214
208
  {
215
209
  "_rc_row_id": row_id,
216
- "_rc_component": values["component"],
210
+ "_rc_component": component_labels[row_id],
217
211
  "rank_choice_duplicate_case": values["case"],
218
212
  "rank_choice_school_set": values["school_set_readable"],
219
213
  "rank_choice_school_ordered": values["school_ordered_readable"],
214
+ "_rc_school_count": len(values["set"]),
220
215
  }
221
216
  )
222
217
 
@@ -234,12 +229,11 @@ def resolve_rank_choice_duplicate_applications(
234
229
  ]
235
230
  )
236
231
  )
232
+
237
233
  work = (
238
234
  work
239
235
  .join(component_sizes, on="_rc_component", how="left")
240
- .with_columns(
241
- pl.col("_rc_component_case").alias("rank_choice_duplicate_case")
242
- )
236
+ .with_columns(pl.col("_rc_component_case").alias("rank_choice_duplicate_case"))
243
237
  )
244
238
 
245
239
  outcome_rank = pl.lit(len(ever_cols))
@@ -255,6 +249,7 @@ def resolve_rank_choice_duplicate_applications(
255
249
  if late_col in columns
256
250
  else pl.lit(0)
257
251
  )
252
+
258
253
  cancel_rank = (
259
254
  pl.when(pl.col(cancel_col) == 1)
260
255
  .then(2)
@@ -264,6 +259,7 @@ def resolve_rank_choice_duplicate_applications(
264
259
  if cancel_col in columns
265
260
  else pl.lit(0)
266
261
  )
262
+
267
263
  info_count = (
268
264
  pl.sum_horizontal([pl.col(c).is_not_null().cast(pl.Int8) for c in info_cols])
269
265
  if info_cols
@@ -271,14 +267,17 @@ def resolve_rank_choice_duplicate_applications(
271
267
  )
272
268
 
273
269
  existing_date_tiebreakers = [column for column in date_tiebreakers if column in columns]
270
+
274
271
  sort_cols = [
275
272
  "_rc_component",
276
273
  "_late_rank",
277
274
  "_cancel_rank",
278
275
  "_outcome_rank",
276
+ "_rc_school_count",
279
277
  "_info_count",
280
278
  ] + existing_date_tiebreakers
281
- descending = [False, False, False, False, True] + [
279
+
280
+ descending = [False, False, False, False, True, True] + [
282
281
  False for _ in existing_date_tiebreakers
283
282
  ]
284
283
 
@@ -290,6 +289,7 @@ def resolve_rank_choice_duplicate_applications(
290
289
  )
291
290
 
292
291
  untouched = ranked.filter(pl.col("_rc_component_n") == 1)
292
+
293
293
  collapsed = (
294
294
  ranked
295
295
  .filter(pl.col("_rc_component_n") > 1)
@@ -311,16 +311,6 @@ def resolve_rank_choice_duplicate_applications(
311
311
  .agg([pl.col(c).max().alias("_%s_collapsed" % c) for c in ever_cols])
312
312
  )
313
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
314
  collapsed_flag_cols = ["_%s_collapsed" % column for column in ever_cols]
325
315
 
326
316
  collapsed_resolved = (
@@ -328,9 +318,7 @@ def resolve_rank_choice_duplicate_applications(
328
318
  .join(collapsed_flags, on="_rc_component", how="left")
329
319
  .with_columns(
330
320
  [
331
- pl.coalesce(pl.col("_%s_collapsed" % column), pl.col(column)).alias(
332
- column
333
- )
321
+ pl.coalesce(pl.col("_%s_collapsed" % column), pl.col(column)).alias(column)
334
322
  for column in ever_cols
335
323
  ]
336
324
  + [pl.lit(1, dtype=pl.Int8).alias("former_dupe")]
@@ -339,6 +327,7 @@ def resolve_rank_choice_duplicate_applications(
339
327
  )
340
328
 
341
329
  untouched = untouched.with_columns(pl.lit(0, dtype=pl.Int8).alias("former_dupe"))
330
+
342
331
  output_columns = [
343
332
  column for column in columns
344
333
  if column not in {"former_dupe", "current_dupe"}
@@ -362,6 +351,7 @@ def resolve_rank_choice_duplicate_applications(
362
351
  .group_by(base_keys)
363
352
  .agg((pl.len() > 1).cast(pl.Int8).alias("current_dupe"))
364
353
  )
354
+
365
355
  result = result.join(current_dupe_flags, on=base_keys, how="left")
366
356
 
367
357
  final_columns = output_columns + ["current_dupe"]
@@ -371,6 +361,7 @@ def resolve_rank_choice_duplicate_applications(
371
361
  (pl.col("rank_choice_duplicate_case").is_in([1, 2, 3]))
372
362
  & (pl.col("former_dupe") != 1)
373
363
  ).height
364
+
374
365
  assert unresolved_collapsible == 0, (
375
366
  "Found %s rows marked as collapsible cases but not collapsed."
376
367
  % unresolved_collapsible
@@ -380,6 +371,7 @@ def resolve_rank_choice_duplicate_applications(
380
371
  (pl.len().over(base_keys) > 1)
381
372
  & (pl.col("current_dupe") != 1)
382
373
  ).height
374
+
383
375
  assert current_dupe_errors == 0, (
384
376
  "Found %s rows remaining duplicated at base_keys without current_dupe=1."
385
377
  % current_dupe_errors
@@ -388,6 +380,8 @@ def resolve_rank_choice_duplicate_applications(
388
380
  return result.lazy() if is_lazy else result
389
381
 
390
382
 
383
+
384
+
391
385
  # Example call without school specific outcomes
392
386
  final = df.pipe(
393
387
  resolve_rank_choice_duplicate_applications,
File without changes