ltc-code 0.1.84__tar.gz → 0.1.85__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.84
3
+ Version: 0.1.85
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.84"
3
+ version = "0.1.85"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -0,0 +1,619 @@
1
+ def _parse_dob_expr(col: str) -> pl.Expr:
2
+ return pl.coalesce(
3
+ [
4
+ pl.col(col).cast(pl.String).str.strptime(pl.Date, "%m/%d/%Y", strict=False),
5
+ pl.col(col).cast(pl.String).str.strptime(pl.Date, "%m/%d/%y", strict=False),
6
+ pl.col(col).cast(pl.String).str.strptime(pl.Date, "%Y-%m-%d", strict=False),
7
+ ]
8
+ )
9
+
10
+
11
+ def _month_day_expr(col: str) -> pl.Expr:
12
+ return pl.col(col).dt.strftime("%m-%d")
13
+
14
+
15
+ def _first_word_expr(col: str) -> pl.Expr:
16
+ return pl.col(col).cast(pl.String).str.split(" ").list.first()
17
+
18
+
19
+ def _second_word_expr(col: str) -> pl.Expr:
20
+ return pl.col(col).cast(pl.String).str.split(" ").list.get(1, null_on_oob=True)
21
+
22
+
23
+ def _build_lookup(
24
+ census: pl.DataFrame,
25
+ *,
26
+ fname_expr: pl.Expr,
27
+ lname_expr: pl.Expr,
28
+ dob_col: str,
29
+ label: str,
30
+ ) -> pl.DataFrame:
31
+ lookup = (
32
+ census
33
+ .select(
34
+ fname_expr.alias("_fname_key"),
35
+ lname_expr.alias("_lname_key"),
36
+ pl.col(dob_col).alias("_dob_key"),
37
+ pl.col("sid_cepr"),
38
+ )
39
+ .drop_nulls(["_fname_key", "_lname_key", "_dob_key", "sid_cepr"])
40
+ .group_by(["_fname_key", "_lname_key", "_dob_key"])
41
+ .agg(pl.col("sid_cepr").unique().alias("_sids"))
42
+ .select(
43
+ "_fname_key",
44
+ "_lname_key",
45
+ "_dob_key",
46
+ pl.col("_sids").list.sort().list.first().alias("sid_cepr"),
47
+ )
48
+ )
49
+
50
+ print(f"built lookup: {label} ({len(lookup):,} usable keys)")
51
+ return lookup
52
+
53
+
54
+ def _add_name_lookups(
55
+ lookups: dict[str, pl.DataFrame],
56
+ census: pl.DataFrame,
57
+ *,
58
+ prefix: str,
59
+ dob_col: str,
60
+ ) -> None:
61
+ lookups[f"{prefix}exact"] = _build_lookup(
62
+ census,
63
+ fname_expr=pl.col("fname"),
64
+ lname_expr=pl.col("lname"),
65
+ dob_col=dob_col,
66
+ label=f"{prefix}exact",
67
+ )
68
+
69
+ lookups[f"{prefix}mname"] = _build_lookup(
70
+ census,
71
+ fname_expr=pl.concat_str([pl.col("fname"), pl.col("mname")], separator=" "),
72
+ lname_expr=pl.col("lname"),
73
+ dob_col=dob_col,
74
+ label=f"{prefix}right fname + mname",
75
+ )
76
+
77
+ lookups[f"{prefix}mname_lname"] = _build_lookup(
78
+ census,
79
+ fname_expr=pl.col("fname"),
80
+ lname_expr=pl.concat_str([pl.col("mname"), pl.col("lname")], separator=" "),
81
+ dob_col=dob_col,
82
+ label=f"{prefix}right mname + lname",
83
+ )
84
+
85
+ lookups[f"{prefix}mname_nospace"] = _build_lookup(
86
+ census,
87
+ fname_expr=pl.concat_str([pl.col("fname"), pl.col("mname")], separator=""),
88
+ lname_expr=pl.col("lname"),
89
+ dob_col=dob_col,
90
+ label=f"{prefix}right fname + mname no space",
91
+ )
92
+
93
+ lookups[f"{prefix}suffix"] = _build_lookup(
94
+ census,
95
+ fname_expr=pl.col("fname"),
96
+ lname_expr=pl.concat_str([pl.col("lname"), pl.col("suffix")], separator=" "),
97
+ dob_col=dob_col,
98
+ label=f"{prefix}right lname + suffix",
99
+ )
100
+
101
+ lookups[f"{prefix}suffix_fname"] = _build_lookup(
102
+ census,
103
+ fname_expr=pl.concat_str([pl.col("fname"), pl.col("suffix")], separator=" "),
104
+ lname_expr=pl.col("lname"),
105
+ dob_col=dob_col,
106
+ label=f"{prefix}right fname + suffix",
107
+ )
108
+
109
+ lookups[f"{prefix}suffix_fname_nospace"] = _build_lookup(
110
+ census,
111
+ fname_expr=pl.concat_str([pl.col("fname"), pl.col("suffix")], separator=""),
112
+ lname_expr=pl.col("lname"),
113
+ dob_col=dob_col,
114
+ label=f"{prefix}right fname + suffix no space",
115
+ )
116
+
117
+ lookups[f"{prefix}suffix_lname_nospace"] = _build_lookup(
118
+ census,
119
+ fname_expr=pl.col("fname"),
120
+ lname_expr=pl.concat_str([pl.col("lname"), pl.col("suffix")], separator=""),
121
+ dob_col=dob_col,
122
+ label=f"{prefix}right lname + suffix no space",
123
+ )
124
+
125
+ lookups[f"{prefix}fname_first_word"] = _build_lookup(
126
+ census,
127
+ fname_expr=_first_word_expr("fname"),
128
+ lname_expr=pl.col("lname"),
129
+ dob_col=dob_col,
130
+ label=f"{prefix}right fname first word",
131
+ )
132
+
133
+ lookups[f"{prefix}lname_first_word"] = _build_lookup(
134
+ census,
135
+ fname_expr=pl.col("fname"),
136
+ lname_expr=_first_word_expr("lname"),
137
+ dob_col=dob_col,
138
+ label=f"{prefix}right lname first word",
139
+ )
140
+
141
+ lookups[f"{prefix}lname_second_word"] = _build_lookup(
142
+ census,
143
+ fname_expr=pl.col("fname"),
144
+ lname_expr=_second_word_expr("lname"),
145
+ dob_col=dob_col,
146
+ label=f"{prefix}right lname second word",
147
+ )
148
+
149
+
150
+ def build_census_lookups(*, cmo_name: str) -> dict[str, pl.DataFrame]:
151
+ annual_frames = []
152
+
153
+ for year in range(1994, 2023):
154
+ path = CENSUS_STUDENTS / f"census_student_{year}.csv"
155
+
156
+ annual = (
157
+ pl.scan_csv(
158
+ path,
159
+ infer_schema=False,
160
+ null_values=[],
161
+ try_parse_dates=False,
162
+ ignore_errors=False,
163
+ )
164
+ .select(
165
+ "cmo_code",
166
+ "sid_cepr",
167
+ "fname_clean",
168
+ "lname_clean",
169
+ "mname_clean",
170
+ "suff_clean",
171
+ "birthdate_clean",
172
+ "birthdate_imp",
173
+ )
174
+ .rename(
175
+ {
176
+ "fname_clean": "fname",
177
+ "lname_clean": "lname",
178
+ "mname_clean": "mname",
179
+ "suff_clean": "suffix",
180
+ "birthdate_clean": "dob",
181
+ "birthdate_imp": "dob_imp",
182
+ }
183
+ )
184
+ .with_columns(
185
+ pl.col("cmo_code").replace(cmo_map).alias("cmo_name")
186
+ )
187
+ .filter(pl.col("cmo_name") == cmo_name)
188
+ .with_columns(
189
+ *clean_name("fname"),
190
+ *clean_name("lname"),
191
+ *clean_other_name("mname"),
192
+ *clean_other_name("suffix"),
193
+ )
194
+ .with_columns(*clean_dob(col="dob"))
195
+ .with_columns(*clean_dob(col="dob_imp"))
196
+ .with_columns(
197
+ _parse_dob_expr("dob_clean").alias("dob"),
198
+ _parse_dob_expr("dob_imp_clean").alias("dob_imp"),
199
+ )
200
+ .select(
201
+ "sid_cepr",
202
+ pl.col("fname_clean").alias("fname"),
203
+ pl.col("lname_clean").alias("lname"),
204
+ pl.col("mname_clean").alias("mname"),
205
+ pl.col("suffix_clean").alias("suffix"),
206
+ "dob",
207
+ "dob_imp",
208
+ )
209
+ )
210
+
211
+ annual_frames.append(annual)
212
+
213
+ census = (
214
+ pl.concat(annual_frames, how="vertical_relaxed")
215
+ .collect()
216
+ .with_columns(
217
+ _month_day_expr("dob").alias("dob_md"),
218
+ _month_day_expr("dob_imp").alias("dob_imp_md"),
219
+ )
220
+ )
221
+
222
+ print(f"census rows after CMO filter: {len(census):,}")
223
+
224
+ lookups = {}
225
+
226
+ _add_name_lookups(
227
+ lookups,
228
+ census,
229
+ prefix="",
230
+ dob_col="dob",
231
+ )
232
+
233
+ _add_name_lookups(
234
+ lookups,
235
+ census,
236
+ prefix="md_",
237
+ dob_col="dob_md",
238
+ )
239
+
240
+ _add_name_lookups(
241
+ lookups,
242
+ census,
243
+ prefix="mdi_",
244
+ dob_col="dob_imp_md",
245
+ )
246
+
247
+ lookups["dob_imp"] = _build_lookup(
248
+ census,
249
+ fname_expr=pl.col("fname"),
250
+ lname_expr=pl.col("lname"),
251
+ dob_col="dob_imp",
252
+ label="dob_imp",
253
+ )
254
+
255
+ for offset, key in [
256
+ ("-1y", "dob_imp_minus_1"),
257
+ ("1y", "dob_imp_plus_1"),
258
+ ("-2y", "dob_imp_minus_2"),
259
+ ("2y", "dob_imp_plus_2"),
260
+ ]:
261
+ lookups[key] = _build_lookup(
262
+ census.with_columns(pl.col("dob_imp").dt.offset_by(offset).alias(key)),
263
+ fname_expr=pl.col("fname"),
264
+ lname_expr=pl.col("lname"),
265
+ dob_col=key,
266
+ label=key,
267
+ )
268
+
269
+ return lookups
270
+
271
+
272
+ def _run_match_stage(
273
+ unmatched: pl.DataFrame,
274
+ *,
275
+ lookup: pl.DataFrame,
276
+ fname_expr: pl.Expr,
277
+ lname_expr: pl.Expr,
278
+ dob_expr: pl.Expr,
279
+ label: str,
280
+ ) -> tuple[pl.DataFrame, pl.DataFrame]:
281
+ before = len(unmatched)
282
+
283
+ stage = (
284
+ unmatched
285
+ .with_columns(
286
+ fname_expr.alias("_fname_key"),
287
+ lname_expr.alias("_lname_key"),
288
+ dob_expr.alias("_dob_key"),
289
+ )
290
+ .join(
291
+ lookup,
292
+ on=["_fname_key", "_lname_key", "_dob_key"],
293
+ how="left",
294
+ validate="m:1",
295
+ )
296
+ .drop(["_fname_key", "_lname_key", "_dob_key"])
297
+ )
298
+
299
+ matched = stage.filter(pl.col("sid_cepr").is_not_null())
300
+ unmatched = stage.filter(pl.col("sid_cepr").is_null()).drop("sid_cepr")
301
+
302
+ print(f"{label}: matched {len(matched):,}/{before:,}")
303
+ return matched, unmatched
304
+
305
+
306
+ def _run_right_side_name_stages(
307
+ unmatched: pl.DataFrame,
308
+ matched_frames: list[pl.DataFrame],
309
+ *,
310
+ cols: Mapping[str, str],
311
+ lookups: dict[str, pl.DataFrame],
312
+ prefix: str,
313
+ dob_expr: pl.Expr,
314
+ label_prefix: str,
315
+ ) -> pl.DataFrame:
316
+ for label, key in [
317
+ ("left exact -> right fname + mname", f"{prefix}mname"),
318
+ ("left exact -> right mname + lname", f"{prefix}mname_lname"),
319
+ ("left exact -> right fname + mname no space", f"{prefix}mname_nospace"),
320
+ ("left exact -> right lname + suffix", f"{prefix}suffix"),
321
+ ("left exact -> right fname + suffix", f"{prefix}suffix_fname"),
322
+ ("left exact -> right fname + suffix no space", f"{prefix}suffix_fname_nospace"),
323
+ ("left exact -> right lname + suffix no space", f"{prefix}suffix_lname_nospace"),
324
+ ("left exact -> right fname first word", f"{prefix}fname_first_word"),
325
+ ("left exact -> right lname first word", f"{prefix}lname_first_word"),
326
+ ("left exact -> right lname second word", f"{prefix}lname_second_word"),
327
+ ]:
328
+ matched, unmatched = _run_match_stage(
329
+ unmatched,
330
+ lookup=lookups[key],
331
+ fname_expr=pl.col(f"{cols['fname']}_clean"),
332
+ lname_expr=pl.col(f"{cols['lname']}_clean"),
333
+ dob_expr=dob_expr,
334
+ label=f"{label_prefix}{label}",
335
+ )
336
+ matched_frames.append(matched)
337
+
338
+ return unmatched
339
+
340
+
341
+ def _run_left_side_name_stages(
342
+ unmatched: pl.DataFrame,
343
+ matched_frames: list[pl.DataFrame],
344
+ *,
345
+ cols: Mapping[str, str],
346
+ lookups: dict[str, pl.DataFrame],
347
+ exact_key: str,
348
+ dob_expr: pl.Expr,
349
+ label_prefix: str,
350
+ ) -> pl.DataFrame:
351
+ for label, fname_expr, lname_expr in [
352
+ (
353
+ "left fname first word -> right exact",
354
+ _first_word_expr(f"{cols['fname']}_clean"),
355
+ pl.col(f"{cols['lname']}_clean"),
356
+ ),
357
+ (
358
+ "left lname first word -> right exact",
359
+ pl.col(f"{cols['fname']}_clean"),
360
+ _first_word_expr(f"{cols['lname']}_clean"),
361
+ ),
362
+ (
363
+ "left lname second word -> right exact",
364
+ pl.col(f"{cols['fname']}_clean"),
365
+ _second_word_expr(f"{cols['lname']}_clean"),
366
+ ),
367
+ ]:
368
+ matched, unmatched = _run_match_stage(
369
+ unmatched,
370
+ lookup=lookups[exact_key],
371
+ fname_expr=fname_expr,
372
+ lname_expr=lname_expr,
373
+ dob_expr=dob_expr,
374
+ label=f"{label_prefix}{label}",
375
+ )
376
+ matched_frames.append(matched)
377
+
378
+ if "mname" in cols:
379
+ unmatched = unmatched.with_columns(*clean_other_name(cols["mname"]))
380
+
381
+ for label, fname_expr, lname_expr in [
382
+ (
383
+ "left fname + mname -> right exact",
384
+ pl.concat_str(
385
+ [pl.col(f"{cols['fname']}_clean"), pl.col(f"{cols['mname']}_clean")],
386
+ separator=" ",
387
+ ),
388
+ pl.col(f"{cols['lname']}_clean"),
389
+ ),
390
+ (
391
+ "left mname + lname -> right exact",
392
+ pl.col(f"{cols['fname']}_clean"),
393
+ pl.concat_str(
394
+ [pl.col(f"{cols['mname']}_clean"), pl.col(f"{cols['lname']}_clean")],
395
+ separator=" ",
396
+ ),
397
+ ),
398
+ (
399
+ "left fname + mname no space -> right exact",
400
+ pl.concat_str(
401
+ [pl.col(f"{cols['fname']}_clean"), pl.col(f"{cols['mname']}_clean")],
402
+ separator="",
403
+ ),
404
+ pl.col(f"{cols['lname']}_clean"),
405
+ ),
406
+ ]:
407
+ matched, unmatched = _run_match_stage(
408
+ unmatched,
409
+ lookup=lookups[exact_key],
410
+ fname_expr=fname_expr,
411
+ lname_expr=lname_expr,
412
+ dob_expr=dob_expr,
413
+ label=f"{label_prefix}{label}",
414
+ )
415
+ matched_frames.append(matched)
416
+
417
+ if "suffix" in cols:
418
+ unmatched = unmatched.with_columns(*clean_other_name(cols["suffix"]))
419
+
420
+ for label, fname_expr, lname_expr in [
421
+ (
422
+ "left fname + suffix -> right exact",
423
+ pl.concat_str(
424
+ [pl.col(f"{cols['fname']}_clean"), pl.col(f"{cols['suffix']}_clean")],
425
+ separator=" ",
426
+ ),
427
+ pl.col(f"{cols['lname']}_clean"),
428
+ ),
429
+ (
430
+ "left fname + suffix no space -> right exact",
431
+ pl.concat_str(
432
+ [pl.col(f"{cols['fname']}_clean"), pl.col(f"{cols['suffix']}_clean")],
433
+ separator="",
434
+ ),
435
+ pl.col(f"{cols['lname']}_clean"),
436
+ ),
437
+ (
438
+ "left lname + suffix -> right exact",
439
+ pl.col(f"{cols['fname']}_clean"),
440
+ pl.concat_str(
441
+ [pl.col(f"{cols['lname']}_clean"), pl.col(f"{cols['suffix']}_clean")],
442
+ separator=" ",
443
+ ),
444
+ ),
445
+ (
446
+ "left lname + suffix no space -> right exact",
447
+ pl.col(f"{cols['fname']}_clean"),
448
+ pl.concat_str(
449
+ [pl.col(f"{cols['lname']}_clean"), pl.col(f"{cols['suffix']}_clean")],
450
+ separator="",
451
+ ),
452
+ ),
453
+ ]:
454
+ matched, unmatched = _run_match_stage(
455
+ unmatched,
456
+ lookup=lookups[exact_key],
457
+ fname_expr=fname_expr,
458
+ lname_expr=lname_expr,
459
+ dob_expr=dob_expr,
460
+ label=f"{label_prefix}{label}",
461
+ )
462
+ matched_frames.append(matched)
463
+
464
+ return unmatched
465
+
466
+
467
+ def lookup_sid_cepr(
468
+ frame,
469
+ *,
470
+ cols: Mapping[str, str],
471
+ lookups: dict[str, pl.DataFrame],
472
+ ):
473
+ is_lazy = isinstance(frame, pl.LazyFrame)
474
+ current = frame.collect() if is_lazy else frame
475
+
476
+ input_columns = current.columns
477
+
478
+ current = current.with_row_index("_row_id")
479
+
480
+ current = current.with_columns(
481
+ *clean_name(cols["fname"]),
482
+ *clean_name(cols["lname"]),
483
+ )
484
+
485
+ current = (
486
+ current
487
+ .with_columns(*clean_dob(col=cols["dob"]))
488
+ .with_columns(
489
+ _parse_dob_expr(f"{cols['dob']}_clean").alias(f"{cols['dob']}_clean")
490
+ )
491
+ .with_columns(
492
+ _month_day_expr(f"{cols['dob']}_clean").alias("_lhs_dob_md")
493
+ )
494
+ )
495
+
496
+ matched_frames = []
497
+ unmatched = current
498
+
499
+ matched, unmatched = _run_match_stage(
500
+ unmatched,
501
+ lookup=lookups["exact"],
502
+ fname_expr=pl.col(f"{cols['fname']}_clean"),
503
+ lname_expr=pl.col(f"{cols['lname']}_clean"),
504
+ dob_expr=pl.col(f"{cols['dob']}_clean"),
505
+ label="exact",
506
+ )
507
+ matched_frames.append(matched)
508
+
509
+ unmatched = _run_right_side_name_stages(
510
+ unmatched,
511
+ matched_frames,
512
+ cols=cols,
513
+ lookups=lookups,
514
+ prefix="",
515
+ dob_expr=pl.col(f"{cols['dob']}_clean"),
516
+ label_prefix="",
517
+ )
518
+
519
+ unmatched = _run_left_side_name_stages(
520
+ unmatched,
521
+ matched_frames,
522
+ cols=cols,
523
+ lookups=lookups,
524
+ exact_key="exact",
525
+ dob_expr=pl.col(f"{cols['dob']}_clean"),
526
+ label_prefix="",
527
+ )
528
+
529
+ for key in [
530
+ "dob_imp",
531
+ "dob_imp_minus_1",
532
+ "dob_imp_plus_1",
533
+ "dob_imp_minus_2",
534
+ "dob_imp_plus_2",
535
+ ]:
536
+ matched, unmatched = _run_match_stage(
537
+ unmatched,
538
+ lookup=lookups[key],
539
+ fname_expr=pl.col(f"{cols['fname']}_clean"),
540
+ lname_expr=pl.col(f"{cols['lname']}_clean"),
541
+ dob_expr=pl.col(f"{cols['dob']}_clean"),
542
+ label=key,
543
+ )
544
+ matched_frames.append(matched)
545
+
546
+ matched, unmatched = _run_match_stage(
547
+ unmatched,
548
+ lookup=lookups["md_exact"],
549
+ fname_expr=pl.col(f"{cols['fname']}_clean"),
550
+ lname_expr=pl.col(f"{cols['lname']}_clean"),
551
+ dob_expr=pl.col("_lhs_dob_md"),
552
+ label="month-day exact dob",
553
+ )
554
+ matched_frames.append(matched)
555
+
556
+ unmatched = _run_right_side_name_stages(
557
+ unmatched,
558
+ matched_frames,
559
+ cols=cols,
560
+ lookups=lookups,
561
+ prefix="md_",
562
+ dob_expr=pl.col("_lhs_dob_md"),
563
+ label_prefix="month-day dob: ",
564
+ )
565
+
566
+ unmatched = _run_left_side_name_stages(
567
+ unmatched,
568
+ matched_frames,
569
+ cols=cols,
570
+ lookups=lookups,
571
+ exact_key="md_exact",
572
+ dob_expr=pl.col("_lhs_dob_md"),
573
+ label_prefix="month-day dob: ",
574
+ )
575
+
576
+ matched, unmatched = _run_match_stage(
577
+ unmatched,
578
+ lookup=lookups["mdi_exact"],
579
+ fname_expr=pl.col(f"{cols['fname']}_clean"),
580
+ lname_expr=pl.col(f"{cols['lname']}_clean"),
581
+ dob_expr=pl.col("_lhs_dob_md"),
582
+ label="month-day dob_imp exact",
583
+ )
584
+ matched_frames.append(matched)
585
+
586
+ unmatched = _run_right_side_name_stages(
587
+ unmatched,
588
+ matched_frames,
589
+ cols=cols,
590
+ lookups=lookups,
591
+ prefix="mdi_",
592
+ dob_expr=pl.col("_lhs_dob_md"),
593
+ label_prefix="month-day dob_imp: ",
594
+ )
595
+
596
+ unmatched = _run_left_side_name_stages(
597
+ unmatched,
598
+ matched_frames,
599
+ cols=cols,
600
+ lookups=lookups,
601
+ exact_key="mdi_exact",
602
+ dob_expr=pl.col("_lhs_dob_md"),
603
+ label_prefix="month-day dob_imp: ",
604
+ )
605
+
606
+ result = (
607
+ pl.concat(matched_frames + [unmatched], how="diagonal_relaxed")
608
+ .sort("_row_id")
609
+ .drop(["_row_id", "_lhs_dob_md"])
610
+ )
611
+
612
+ if "sid_cepr" not in input_columns:
613
+ input_columns = input_columns + ["sid_cepr"]
614
+
615
+ result = result.select(input_columns)
616
+
617
+ print(f"final matched: {result['sid_cepr'].is_not_null().sum():,}/{len(result):,}")
618
+
619
+ return result
File without changes