ltc-code 0.1.2__tar.gz → 0.1.3__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.2 → ltc_code-0.1.3}/PKG-INFO +1 -1
- {ltc_code-0.1.2 → ltc_code-0.1.3}/pyproject.toml +1 -1
- {ltc_code-0.1.2 → ltc_code-0.1.3}/src/ltc_code/may27.py +442 -0
- {ltc_code-0.1.2 → ltc_code-0.1.3}/README.md +0 -0
- {ltc_code-0.1.2 → ltc_code-0.1.3}/src/ltc_code/__init__.py +0 -0
- {ltc_code-0.1.2 → ltc_code-0.1.3}/src/ltc_code/polars_dates.py +0 -0
|
@@ -26,6 +26,7 @@ __all__ = [
|
|
|
26
26
|
"consolidate_columns",
|
|
27
27
|
"select_lottery_columns",
|
|
28
28
|
"select_enr_columns",
|
|
29
|
+
"lookup_sid_cepr",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
|
|
@@ -972,3 +973,444 @@ def select_enr_columns(frame: Frame) -> Frame:
|
|
|
972
973
|
"spec_ed_flag": "sped",
|
|
973
974
|
}
|
|
974
975
|
)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
###############################################################################
|
|
979
|
+
# SID/CEPR LOOKUP FROM ANNUAL CENSUS FILES
|
|
980
|
+
#
|
|
981
|
+
# Public entry point:
|
|
982
|
+
# matched = source.pipe(
|
|
983
|
+
# lookup_sid_cepr,
|
|
984
|
+
# cols=[
|
|
985
|
+
# "fname_clean", "lname_clean", "mname_clean",
|
|
986
|
+
# "suffix_clean", "dob_clean", "nickname_clean",
|
|
987
|
+
# ],
|
|
988
|
+
# cmo_name="Example CMO",
|
|
989
|
+
# )
|
|
990
|
+
#
|
|
991
|
+
# Census file locations, the CMO code mapping, and fixed annual-file edits are
|
|
992
|
+
# configured once below rather than provided with each dataset call.
|
|
993
|
+
###############################################################################
|
|
994
|
+
|
|
995
|
+
def _sid_name_key_expressions(
|
|
996
|
+
fname: str,
|
|
997
|
+
lname: str,
|
|
998
|
+
mname: Optional[str] = None,
|
|
999
|
+
suffix: Optional[str] = None,
|
|
1000
|
+
nickname: Optional[str] = None,
|
|
1001
|
+
) -> List[Tuple[str, pl.Expr, pl.Expr]]:
|
|
1002
|
+
# Start with the plain first-name / last-name key.
|
|
1003
|
+
modes = [("fname, lname", pl.col(fname), pl.col(lname))]
|
|
1004
|
+
|
|
1005
|
+
# If a middle-name column exists, build keys where the middle name may have
|
|
1006
|
+
# been appended to first name, prepended to last name, or glued to first name.
|
|
1007
|
+
if mname:
|
|
1008
|
+
has_mname = pl.col(mname).fill_null("").cast(pl.String).str.strip_chars().ne("")
|
|
1009
|
+
modes.extend(
|
|
1010
|
+
[
|
|
1011
|
+
(
|
|
1012
|
+
"fname + mname, lname",
|
|
1013
|
+
pl.when(has_mname).then(pl.concat_str([fname, mname], separator=" ")),
|
|
1014
|
+
pl.col(lname),
|
|
1015
|
+
),
|
|
1016
|
+
(
|
|
1017
|
+
"fname, mname + lname",
|
|
1018
|
+
pl.col(fname),
|
|
1019
|
+
pl.when(has_mname).then(pl.concat_str([mname, lname], separator=" ")),
|
|
1020
|
+
),
|
|
1021
|
+
(
|
|
1022
|
+
"fname + mname without space, lname",
|
|
1023
|
+
pl.when(has_mname).then(pl.concat_str([fname, mname], separator="")),
|
|
1024
|
+
pl.col(lname),
|
|
1025
|
+
),
|
|
1026
|
+
]
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
# If a suffix column exists, build keys where suffix may sit with first or
|
|
1030
|
+
# last name, either separated by a space or attached directly.
|
|
1031
|
+
if suffix:
|
|
1032
|
+
has_suffix = pl.col(suffix).fill_null("").cast(pl.String).str.strip_chars().ne("")
|
|
1033
|
+
modes.extend(
|
|
1034
|
+
[
|
|
1035
|
+
(
|
|
1036
|
+
"fname + suffix, lname",
|
|
1037
|
+
pl.when(has_suffix).then(pl.concat_str([fname, suffix], separator=" ")),
|
|
1038
|
+
pl.col(lname),
|
|
1039
|
+
),
|
|
1040
|
+
(
|
|
1041
|
+
"fname, lname + suffix",
|
|
1042
|
+
pl.col(fname),
|
|
1043
|
+
pl.when(has_suffix).then(pl.concat_str([lname, suffix], separator=" ")),
|
|
1044
|
+
),
|
|
1045
|
+
(
|
|
1046
|
+
"fname + suffix without space, lname",
|
|
1047
|
+
pl.when(has_suffix).then(pl.concat_str([fname, suffix], separator="")),
|
|
1048
|
+
pl.col(lname),
|
|
1049
|
+
),
|
|
1050
|
+
(
|
|
1051
|
+
"fname, lname + suffix without space",
|
|
1052
|
+
pl.col(fname),
|
|
1053
|
+
pl.when(has_suffix).then(pl.concat_str([lname, suffix], separator="")),
|
|
1054
|
+
),
|
|
1055
|
+
]
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# If a nickname column exists on the left data, allow nickname to replace
|
|
1059
|
+
# first name while keeping last name fixed.
|
|
1060
|
+
if nickname:
|
|
1061
|
+
has_nickname = (
|
|
1062
|
+
pl.col(nickname).fill_null("").cast(pl.String).str.strip_chars().ne("")
|
|
1063
|
+
)
|
|
1064
|
+
modes.append(
|
|
1065
|
+
(
|
|
1066
|
+
"nickname, lname",
|
|
1067
|
+
pl.when(has_nickname).then(pl.col(nickname)),
|
|
1068
|
+
pl.col(lname),
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
# Add broad text-position variants that can happen even without middle,
|
|
1073
|
+
# suffix, or nickname columns.
|
|
1074
|
+
modes.extend(
|
|
1075
|
+
[
|
|
1076
|
+
(
|
|
1077
|
+
"fname first word only, lname",
|
|
1078
|
+
pl.col(fname).cast(pl.String).str.split(" ").list.first(),
|
|
1079
|
+
pl.col(lname),
|
|
1080
|
+
),
|
|
1081
|
+
(
|
|
1082
|
+
"fname, lname after first word",
|
|
1083
|
+
pl.col(fname),
|
|
1084
|
+
pl.when(pl.col(lname).cast(pl.String).str.contains(r"\S+\s+\S+"))
|
|
1085
|
+
.then(pl.col(lname).cast(pl.String).str.replace(r"^\S+\s+", ""))
|
|
1086
|
+
.otherwise(pl.lit(None, dtype=pl.String)),
|
|
1087
|
+
),
|
|
1088
|
+
(
|
|
1089
|
+
"fname + lname in fname, lname",
|
|
1090
|
+
pl.concat_str([fname, lname], separator=" "),
|
|
1091
|
+
pl.col(lname),
|
|
1092
|
+
),
|
|
1093
|
+
]
|
|
1094
|
+
)
|
|
1095
|
+
# Return a list of labeled first-name and last-name expressions. The caller
|
|
1096
|
+
# uses the label in printed diagnostics.
|
|
1097
|
+
return modes
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def lookup_sid_cepr(
|
|
1101
|
+
frame: Frame,
|
|
1102
|
+
*,
|
|
1103
|
+
cols: Union[Sequence[str], Mapping[str, str]],
|
|
1104
|
+
cmo_name: str,
|
|
1105
|
+
) -> Frame:
|
|
1106
|
+
"""Add ``sid_cepr`` from annual census files using staged name/DOB keys.
|
|
1107
|
+
|
|
1108
|
+
``cols`` can be a mapping with at least ``fname``, ``lname``, and ``dob``;
|
|
1109
|
+
optional keys are ``mname``, ``suffix``, and ``nickname``. For a short list,
|
|
1110
|
+
pass ``[fname, lname, dob]``. For the old six-column order, pass
|
|
1111
|
+
``[fname, lname, mname, suffix, dob, nickname]``.
|
|
1112
|
+
"""
|
|
1113
|
+
# Get the census folder/path template from the project paths module.
|
|
1114
|
+
from paths import CENSUS_STUDENTS
|
|
1115
|
+
|
|
1116
|
+
# Get the CMO code-to-name map. The fallback spelling handles the typo you
|
|
1117
|
+
# mentioned in case the module is actually named mapppings.py.
|
|
1118
|
+
try:
|
|
1119
|
+
import mappings
|
|
1120
|
+
except ImportError:
|
|
1121
|
+
import mapppings as mappings
|
|
1122
|
+
|
|
1123
|
+
# This helper supports both eager and lazy Polars frames.
|
|
1124
|
+
if not isinstance(frame, (pl.DataFrame, pl.LazyFrame)):
|
|
1125
|
+
raise TypeError("lookup_sid_cepr expects a polars.DataFrame or polars.LazyFrame.")
|
|
1126
|
+
|
|
1127
|
+
# Normalize cols into a role dictionary, so later code can refer to
|
|
1128
|
+
# "fname", "lname", "dob", etc. regardless of how the caller supplied it.
|
|
1129
|
+
if isinstance(cols, Mapping):
|
|
1130
|
+
left_columns = dict(cols)
|
|
1131
|
+
else:
|
|
1132
|
+
# Minimal list syntax: first name, last name, DOB.
|
|
1133
|
+
if len(cols) == 3:
|
|
1134
|
+
left_columns = {"fname": cols[0], "lname": cols[1], "dob": cols[2]}
|
|
1135
|
+
# Full list syntax: first, last, middle, suffix, DOB, nickname.
|
|
1136
|
+
elif len(cols) == 6:
|
|
1137
|
+
left_columns = {
|
|
1138
|
+
"fname": cols[0],
|
|
1139
|
+
"lname": cols[1],
|
|
1140
|
+
"mname": cols[2],
|
|
1141
|
+
"suffix": cols[3],
|
|
1142
|
+
"dob": cols[4],
|
|
1143
|
+
"nickname": cols[5],
|
|
1144
|
+
}
|
|
1145
|
+
else:
|
|
1146
|
+
raise ValueError(
|
|
1147
|
+
"cols must be either [fname, lname, dob], "
|
|
1148
|
+
"[fname, lname, mname, suffix, dob, nickname], or a mapping."
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
# First name, last name, and DOB are required for any lookup.
|
|
1152
|
+
missing_roles = [role for role in ("fname", "lname", "dob") if role not in left_columns]
|
|
1153
|
+
if missing_roles:
|
|
1154
|
+
raise ValueError("cols is missing required roles: %s" % ", ".join(missing_roles))
|
|
1155
|
+
|
|
1156
|
+
# Preserve the input frame type at the end.
|
|
1157
|
+
is_lazy = isinstance(frame, pl.LazyFrame)
|
|
1158
|
+
|
|
1159
|
+
# Get the source column names without collecting the whole frame if it is lazy.
|
|
1160
|
+
input_columns = frame.collect_schema().names() if is_lazy else frame.columns
|
|
1161
|
+
|
|
1162
|
+
# Make sure every supplied left-side column actually exists.
|
|
1163
|
+
supplied_columns = [column for column in left_columns.values() if column]
|
|
1164
|
+
missing = [column for column in supplied_columns if column not in input_columns]
|
|
1165
|
+
if missing:
|
|
1166
|
+
raise ValueError("Lookup source is missing columns: %s" % ", ".join(missing))
|
|
1167
|
+
|
|
1168
|
+
# Find the CMO mapping dictionary from mappings.py. The first block checks
|
|
1169
|
+
# common names; the fallback finds any dict whose values include cmo_name.
|
|
1170
|
+
cmo_codes = None
|
|
1171
|
+
for mapping_name in (
|
|
1172
|
+
"CMO_CODE_TO_NAME",
|
|
1173
|
+
"CMO_CODE_TO_CMO_NAME",
|
|
1174
|
+
"CMO_CODES",
|
|
1175
|
+
"CMO_CODE_MAP",
|
|
1176
|
+
"cmo_code_to_name",
|
|
1177
|
+
"cmo_code_to_cmo_name",
|
|
1178
|
+
"cmo_codes",
|
|
1179
|
+
):
|
|
1180
|
+
cmo_codes = getattr(mappings, mapping_name, None)
|
|
1181
|
+
if cmo_codes is not None:
|
|
1182
|
+
break
|
|
1183
|
+
if cmo_codes is None:
|
|
1184
|
+
for value in vars(mappings).values():
|
|
1185
|
+
if isinstance(value, dict) and cmo_name in set(value.values()):
|
|
1186
|
+
cmo_codes = value
|
|
1187
|
+
break
|
|
1188
|
+
if cmo_codes is None:
|
|
1189
|
+
raise ValueError("Could not find a CMO code-to-name mapping in mappings.py.")
|
|
1190
|
+
|
|
1191
|
+
# Scan every annual census CSV lazily, standardize the small set of columns
|
|
1192
|
+
# needed for matching, map CMO code to CMO name, filter to the requested CMO,
|
|
1193
|
+
# and store each lazy frame for appending.
|
|
1194
|
+
annual_frames = []
|
|
1195
|
+
for year in range(1994, 2023):
|
|
1196
|
+
# CENSUS_STUDENTS can either be a folder or a path template with {year}.
|
|
1197
|
+
census_folder = str(CENSUS_STUDENTS)
|
|
1198
|
+
if "{year}" in census_folder:
|
|
1199
|
+
path = census_folder.format(year=year)
|
|
1200
|
+
else:
|
|
1201
|
+
path = "%s/census_students_%s.csv" % (census_folder.rstrip("/"), year)
|
|
1202
|
+
|
|
1203
|
+
# Read all columns as strings so IDs, codes, and DOB text are not
|
|
1204
|
+
# guessed into inconsistent types across years.
|
|
1205
|
+
annual = pl.scan_csv(path, infer_schema=False)
|
|
1206
|
+
|
|
1207
|
+
# Some years may not have every optional column; add null versions so
|
|
1208
|
+
# all years can be appended with the same schema.
|
|
1209
|
+
annual_columns = annual.collect_schema().names()
|
|
1210
|
+
optional = []
|
|
1211
|
+
if "mname_clean" not in annual_columns:
|
|
1212
|
+
optional.append(pl.lit(None, dtype=pl.String).alias("mname_clean"))
|
|
1213
|
+
if "suffix_clean" not in annual_columns:
|
|
1214
|
+
optional.append(pl.lit(None, dtype=pl.String).alias("suffix_clean"))
|
|
1215
|
+
if "dob_imp" not in annual_columns:
|
|
1216
|
+
optional.append(pl.lit(None, dtype=pl.String).alias("dob_imp"))
|
|
1217
|
+
|
|
1218
|
+
# Apply year-specific cleanup here if needed before appending.
|
|
1219
|
+
annual_frames.append(
|
|
1220
|
+
annual.with_columns(optional)
|
|
1221
|
+
# Convert CMO code to CMO name.
|
|
1222
|
+
.with_columns(
|
|
1223
|
+
pl.col("cmo_code")
|
|
1224
|
+
.cast(pl.String)
|
|
1225
|
+
.replace_strict(
|
|
1226
|
+
{str(code): name for code, name in cmo_codes.items()},
|
|
1227
|
+
default=None,
|
|
1228
|
+
return_dtype=pl.String,
|
|
1229
|
+
)
|
|
1230
|
+
.alias("cmo_name")
|
|
1231
|
+
)
|
|
1232
|
+
# Keep only the CMO requested by the caller.
|
|
1233
|
+
.filter(pl.col("cmo_name") == cmo_name)
|
|
1234
|
+
# Keep only right-side columns needed for matching and the SID.
|
|
1235
|
+
.select(
|
|
1236
|
+
[
|
|
1237
|
+
"sid_cepr",
|
|
1238
|
+
"fname_clean",
|
|
1239
|
+
"mname_clean",
|
|
1240
|
+
"lname_clean",
|
|
1241
|
+
"suffix_clean",
|
|
1242
|
+
"dob_clean",
|
|
1243
|
+
"dob_imp",
|
|
1244
|
+
]
|
|
1245
|
+
)
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
# Append all annual census lazy frames into one lazy right-side frame.
|
|
1249
|
+
census = pl.concat(annual_frames, how="vertical_relaxed")
|
|
1250
|
+
|
|
1251
|
+
# Do the matching lazily even when the caller gave an eager DataFrame.
|
|
1252
|
+
current = frame if is_lazy else frame.lazy()
|
|
1253
|
+
|
|
1254
|
+
# If the left data already has sid_cepr, preserve it as the initial matched
|
|
1255
|
+
# value; otherwise start every row unmatched.
|
|
1256
|
+
if "sid_cepr" in input_columns:
|
|
1257
|
+
current = current.with_columns(pl.col("sid_cepr").alias("_sid_matched"))
|
|
1258
|
+
else:
|
|
1259
|
+
current = current.with_columns(pl.lit(None, dtype=pl.String).alias("_sid_matched"))
|
|
1260
|
+
|
|
1261
|
+
# Count total left-side rows once for diagnostics.
|
|
1262
|
+
total_rows = current.select(pl.len().alias("rows")).collect().item()
|
|
1263
|
+
|
|
1264
|
+
# Count how many already had sid_cepr before any lookup pass.
|
|
1265
|
+
previous_matched = (
|
|
1266
|
+
current.select(pl.col("_sid_matched").is_not_null().sum()).collect().item()
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
# Build possible left-side name keys from the caller's available columns.
|
|
1270
|
+
left_name_modes = _sid_name_key_expressions(
|
|
1271
|
+
left_columns["fname"],
|
|
1272
|
+
left_columns["lname"],
|
|
1273
|
+
left_columns.get("mname"),
|
|
1274
|
+
left_columns.get("suffix"),
|
|
1275
|
+
left_columns.get("nickname"),
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
# Build possible right-side name keys from the census cleaned columns.
|
|
1279
|
+
right_name_modes = _sid_name_key_expressions(
|
|
1280
|
+
"fname_clean", "lname_clean", "mname_clean", "suffix_clean"
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
# First try exact DOB. Then, for remaining unmatched rows, use imputed DOB
|
|
1284
|
+
# as-is and shifted by one or two years in either direction.
|
|
1285
|
+
dob_passes = [("dob_clean", 0, "dob_clean")]
|
|
1286
|
+
dob_passes.extend(
|
|
1287
|
+
[
|
|
1288
|
+
("dob_imp", 0, "dob_imp"),
|
|
1289
|
+
("dob_imp", -1, "dob_imp - 1 year"),
|
|
1290
|
+
("dob_imp", 1, "dob_imp + 1 year"),
|
|
1291
|
+
("dob_imp", -2, "dob_imp - 2 years"),
|
|
1292
|
+
("dob_imp", 2, "dob_imp + 2 years"),
|
|
1293
|
+
]
|
|
1294
|
+
)
|
|
1295
|
+
print("SID/CEPR lookup diagnostics for CMO %r (%s rows)" % (cmo_name, total_rows))
|
|
1296
|
+
|
|
1297
|
+
# Try every ordered DOB pass, then name-key pass, printing coverage each time.
|
|
1298
|
+
for dob_column, day_offset, dob_description in dob_passes:
|
|
1299
|
+
for left_label, left_fname, left_lname in left_name_modes:
|
|
1300
|
+
for right_label, right_fname, right_lname in right_name_modes:
|
|
1301
|
+
# Avoid very loose fuzzy matching: mutate only one side at a
|
|
1302
|
+
# time, except the exact-vs-exact pass.
|
|
1303
|
+
if left_label != "fname, lname" and right_label != "fname, lname":
|
|
1304
|
+
continue
|
|
1305
|
+
|
|
1306
|
+
# Build the right-side DOB key, optionally shifted by calendar years.
|
|
1307
|
+
right_dob = pl.col(dob_column).cast(pl.Date, strict=False)
|
|
1308
|
+
if day_offset:
|
|
1309
|
+
right_dob = right_dob.dt.offset_by("%+dy" % day_offset)
|
|
1310
|
+
|
|
1311
|
+
# Collapse the census/right side to one row per lookup key.
|
|
1312
|
+
# A key is usable only when it points to exactly one sid_cepr.
|
|
1313
|
+
candidates = (
|
|
1314
|
+
census.select(
|
|
1315
|
+
[
|
|
1316
|
+
right_fname.alias("_fname_key"),
|
|
1317
|
+
right_lname.alias("_lname_key"),
|
|
1318
|
+
right_dob.alias("_dob_key"),
|
|
1319
|
+
"sid_cepr",
|
|
1320
|
+
]
|
|
1321
|
+
)
|
|
1322
|
+
.drop_nulls(["_fname_key", "_lname_key", "_dob_key", "sid_cepr"])
|
|
1323
|
+
.group_by(["_fname_key", "_lname_key", "_dob_key"])
|
|
1324
|
+
.agg(pl.col("sid_cepr").unique().alias("_sids"))
|
|
1325
|
+
.with_columns(pl.col("_sids").list.len().alias("_sid_count"))
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
# Count ambiguous right-side keys for diagnostics; these are
|
|
1329
|
+
# deliberately omitted rather than guessed.
|
|
1330
|
+
ambiguous = (
|
|
1331
|
+
candidates.filter(pl.col("_sid_count") > 1)
|
|
1332
|
+
.select(pl.len())
|
|
1333
|
+
.collect()
|
|
1334
|
+
.item()
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
# Keep only unambiguous right-side keys and extract their SID.
|
|
1338
|
+
lookup = candidates.filter(pl.col("_sid_count") == 1).select(
|
|
1339
|
+
[
|
|
1340
|
+
"_fname_key",
|
|
1341
|
+
"_lname_key",
|
|
1342
|
+
"_dob_key",
|
|
1343
|
+
pl.col("_sids").list.first().alias("_sid_candidate"),
|
|
1344
|
+
]
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
# Join the current left frame to this lookup key. Existing
|
|
1348
|
+
# matches win; this pass only fills still-null _sid_matched.
|
|
1349
|
+
current = (
|
|
1350
|
+
current.with_columns(
|
|
1351
|
+
[
|
|
1352
|
+
left_fname.alias("_fname_key"),
|
|
1353
|
+
left_lname.alias("_lname_key"),
|
|
1354
|
+
pl.col(left_columns["dob"])
|
|
1355
|
+
.cast(pl.Date, strict=False)
|
|
1356
|
+
.alias("_dob_key"),
|
|
1357
|
+
]
|
|
1358
|
+
)
|
|
1359
|
+
.join(
|
|
1360
|
+
lookup,
|
|
1361
|
+
on=["_fname_key", "_lname_key", "_dob_key"],
|
|
1362
|
+
how="left",
|
|
1363
|
+
validate="m:1",
|
|
1364
|
+
)
|
|
1365
|
+
.with_columns(
|
|
1366
|
+
pl.coalesce(["_sid_matched", "_sid_candidate"]).alias(
|
|
1367
|
+
"_sid_matched"
|
|
1368
|
+
)
|
|
1369
|
+
)
|
|
1370
|
+
.drop(["_fname_key", "_lname_key", "_dob_key", "_sid_candidate"])
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
# Count cumulative matches after this pass.
|
|
1374
|
+
matched = (
|
|
1375
|
+
current.select(pl.col("_sid_matched").is_not_null().sum())
|
|
1376
|
+
.collect()
|
|
1377
|
+
.item()
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1380
|
+
# Print the cumulative match count and how much this pass added.
|
|
1381
|
+
lookup_key = "%s -> %s using %s" % (
|
|
1382
|
+
left_label,
|
|
1383
|
+
right_label,
|
|
1384
|
+
dob_description,
|
|
1385
|
+
)
|
|
1386
|
+
ambiguity = (
|
|
1387
|
+
"; %s ambiguous right keys omitted" % ambiguous if ambiguous else ""
|
|
1388
|
+
)
|
|
1389
|
+
print(
|
|
1390
|
+
" %s: %s/%s matched (+%s)%s"
|
|
1391
|
+
% (
|
|
1392
|
+
lookup_key,
|
|
1393
|
+
matched,
|
|
1394
|
+
total_rows,
|
|
1395
|
+
matched - previous_matched,
|
|
1396
|
+
ambiguity,
|
|
1397
|
+
)
|
|
1398
|
+
)
|
|
1399
|
+
previous_matched = matched
|
|
1400
|
+
|
|
1401
|
+
# Final unmatched count after all lookup passes.
|
|
1402
|
+
unmatched_rows = total_rows - previous_matched
|
|
1403
|
+
print(" final unmatched sid_cepr rows: %s/%s" % (unmatched_rows, total_rows))
|
|
1404
|
+
|
|
1405
|
+
# Return exactly the input columns plus sid_cepr if it was not already there.
|
|
1406
|
+
output_columns = list(input_columns)
|
|
1407
|
+
if "sid_cepr" not in output_columns:
|
|
1408
|
+
output_columns.append("sid_cepr")
|
|
1409
|
+
|
|
1410
|
+
# Replace/add sid_cepr from the internal matched SID column.
|
|
1411
|
+
result = current.with_columns(pl.col("_sid_matched").alias("sid_cepr")).select(output_columns)
|
|
1412
|
+
|
|
1413
|
+
# Restore eager output when the input was eager.
|
|
1414
|
+
if not is_lazy:
|
|
1415
|
+
result = result.collect()
|
|
1416
|
+
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|