ltc-code 0.1.83__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.83
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.83"
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
@@ -281,3 +281,170 @@ enrollment = enrollment.select(
281
281
 
282
282
 
283
283
 
284
+ submission = submission.select(
285
+ # --------------------------------------------------
286
+ # Metadata
287
+ # --------------------------------------------------
288
+ pl.col("school_year"),
289
+ pl.col("cmo_name"),
290
+ pl.col("data_type"),
291
+ pl.col("ren_num"),
292
+
293
+ # --------------------------------------------------
294
+ # Student IDs / application IDs
295
+ # --------------------------------------------------
296
+ pl.col("Student ID"),
297
+ pl.col("stu_ID").alias("student_id"),
298
+ pl.col("SubmissionRecordID").alias("application_id"),
299
+ pl.col("Snapcode"),
300
+ pl.col("FamilyMemberID"),
301
+ pl.col("FamilyID").alias("family_id"),
302
+ pl.col("ActionSessionID"),
303
+
304
+ # --------------------------------------------------
305
+ # Student name
306
+ # --------------------------------------------------
307
+ # 2009 uses FirstName/LastName; later years use First Name/Last Name
308
+ pl.col("First Name").alias("fname"),
309
+ pl.col("Last Name").alias("lname"),
310
+ pl.col("FirstName"),
311
+ pl.col("LastName"),
312
+
313
+ pl.col("stu_MiddleName").alias("mname"),
314
+
315
+ # --------------------------------------------------
316
+ # DOB
317
+ # --------------------------------------------------
318
+ # Duplicate DOB fields; keep raw for mismatch checks
319
+ pl.col("DateOfBirth"),
320
+ pl.col("stu_DoB"),
321
+ pl.col("stu_DoBCheck"),
322
+
323
+ # --------------------------------------------------
324
+ # Grade
325
+ # --------------------------------------------------
326
+ # Grade only exists in 2009/2010
327
+ pl.col("Grade"),
328
+
329
+ # Enroll Grade exists from 2011 onward
330
+ pl.col("Enroll Grade").alias("enrollment_grade"),
331
+
332
+ # --------------------------------------------------
333
+ # School / campus
334
+ # --------------------------------------------------
335
+ # School only exists in 2009/2010
336
+ pl.col("School").alias("school_name"),
337
+
338
+ # Campus exists from 2011 onward; check mismatch with School/stu_Campus
339
+ pl.col("Campus"),
340
+ pl.col("stu_Campus"),
341
+
342
+ # --------------------------------------------------
343
+ # Application / submission status
344
+ # --------------------------------------------------
345
+ pl.col("Status").alias("submission_status"),
346
+ pl.col("stu_Status"),
347
+ pl.col("SWMMatchStatus"),
348
+
349
+ # Enrollment status useful 2016-2019
350
+ pl.col("EnrollStatus").alias("enroll_status"),
351
+
352
+ # --------------------------------------------------
353
+ # Dates / timestamps
354
+ # --------------------------------------------------
355
+ pl.col("Submitted").alias("application_date"),
356
+
357
+ # Signature / possible offer accepted date
358
+ pl.col("p_Date1"),
359
+
360
+ pl.col("ApprovalDate"),
361
+ pl.col("DeliveryHistory"),
362
+ pl.col("LetterHistory"),
363
+
364
+ # --------------------------------------------------
365
+ # Prior school / prior setting
366
+ # --------------------------------------------------
367
+ # Earlier prior-school fields
368
+ pl.col("stu_PrevSchool").alias("prior_school"),
369
+ pl.col("stu_PrevSchoolType").alias("prior_school_type"),
370
+
371
+ # Later prior-school fields; keep raw because meaning may differ
372
+ pl.col("stu_PrevSchoolName"),
373
+ pl.col("stu_Previous_Campus"),
374
+ pl.col("stu_PreSchool"),
375
+ pl.col("stu_PreSchoolCamden"),
376
+ pl.col("stu_PrevSchoolCharter"),
377
+ pl.col("stu_EntrySchoolUS"),
378
+
379
+ # --------------------------------------------------
380
+ # Address / geography
381
+ # --------------------------------------------------
382
+ pl.col("p1_Address").alias("address1"),
383
+ pl.col("p1_Address2").alias("address2"),
384
+ pl.col("p1_City").alias("city"),
385
+ pl.col("p1_Zip").alias("zip"),
386
+
387
+ # Newark residency / geography priority-ish field
388
+ pl.col("p1_NewarkRes"),
389
+
390
+ # Birth city fields; possible geography/debugging fields
391
+ pl.col("stu_BirthCity"),
392
+ pl.col("stu_BirthCityNJ"),
393
+ pl.col("stu_BirthCityNotNJ"),
394
+
395
+ # --------------------------------------------------
396
+ # Language
397
+ # --------------------------------------------------
398
+ # typo field and later corrected field
399
+ pl.col("stu_Langage"),
400
+ pl.col("stu_Language").alias("preferred_language"),
401
+
402
+ # --------------------------------------------------
403
+ # SPED / IEP fields
404
+ # --------------------------------------------------
405
+ pl.col("stu_NeedsIEP"),
406
+ pl.col("stu_IEP").alias("sped"),
407
+ pl.col("stu_IEPRequsted"),
408
+ pl.col("stu_IEPReceived"),
409
+
410
+ # --------------------------------------------------
411
+ # Parent / guardian / consent
412
+ # --------------------------------------------------
413
+ pl.col("p_CarRidesWithStaff"),
414
+ pl.col("stu_OptInAgreement"),
415
+
416
+ # --------------------------------------------------
417
+ # Sibling variables
418
+ # --------------------------------------------------
419
+ pl.col("sib1_FirstName").alias("sib1_fname"),
420
+ pl.col("sib1_LastName").alias("sib1_lname"),
421
+ pl.col("sib1_Grade").alias("sib1_grade"),
422
+ pl.col("sib1_School").alias("sib1_school"),
423
+
424
+ # Sibling priority / sibling enrolled-type fields
425
+ pl.col("sib1_KIPPEnroll"),
426
+ pl.col("sib1_PreSchool"),
427
+ pl.col("stu_NumberOfSibs"),
428
+ pl.col("stu_SiblingKIPPNJ").alias("sib_enroll_cmo"),
429
+
430
+ # --------------------------------------------------
431
+ # Registration / enrollment process fields
432
+ # --------------------------------------------------
433
+ pl.col("stu_RegEnrollReceived"),
434
+
435
+ # --------------------------------------------------
436
+ # Call / follow-up fields
437
+ # --------------------------------------------------
438
+ pl.col("Call_Date"),
439
+ pl.col("Call_Follow-up"),
440
+ pl.col("Call_HomeVisitStatus"),
441
+ pl.col("Call_Outcome"),
442
+ pl.col("Call_OutcomeDecl"),
443
+
444
+ # --------------------------------------------------
445
+ # Notes / admin fields
446
+ # --------------------------------------------------
447
+ pl.col("Tags"),
448
+ pl.col("Notes"),
449
+ pl.col("RosterNotes"),
450
+ )
File without changes