813 lines
27 KiB
Python
813 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
|
|
import narwhals as nw
|
|
from narwhals.typing import FrameT
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_encounter(core__encounter: FrameT) -> FrameT:
|
|
"""Build readmissions._int_encounter
|
|
|
|
Filters core.encounter to acute inpatient only, then selects and renames
|
|
columns for the readmissions pipeline.
|
|
|
|
Operations: filter, select
|
|
Dependencies: core.encounter
|
|
"""
|
|
return core__encounter.filter(nw.col("encounter_type") == "acute inpatient").select(
|
|
nw.col("encounter_id"),
|
|
nw.col("person_id"),
|
|
nw.col("encounter_start_date").alias("admit_date"),
|
|
nw.col("encounter_end_date").alias("discharge_date"),
|
|
nw.col("discharge_disposition_code"),
|
|
nw.col("facility_id"),
|
|
nw.col("drg_code_type"),
|
|
nw.col("drg_code"),
|
|
nw.col("paid_amount"),
|
|
nw.col("primary_diagnosis_code"),
|
|
nw.col("encounter_source_type"),
|
|
nw.col("data_source"),
|
|
)
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_encounter_data_quality(
|
|
readmissions___int_encounter_with_ccs: FrameT,
|
|
readmissions___int_encounter_overlap: FrameT,
|
|
terminology__discharge_disposition: FrameT,
|
|
terminology__ms_drg: FrameT,
|
|
terminology__apr_drg: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_encounter_data_quality
|
|
|
|
Joins encounter data with terminology lookups to compute data quality flags,
|
|
then derives a disqualified_encounter_flag from the union of all issues.
|
|
|
|
Operations: join, select (CASE WHEN flags)
|
|
Dependencies: readmissions._int_encounter_with_ccs,
|
|
readmissions._int_encounter_overlap,
|
|
terminology.discharge_disposition, terminology.ms_drg,
|
|
terminology.apr_drg
|
|
"""
|
|
# Find encounters that lost the overlap "best encounter" selection
|
|
# Overlap losers: encounters not selected as best
|
|
overlap_losers = (
|
|
readmissions___int_encounter_overlap.filter(nw.col("is_best_encounter") == 0)
|
|
.select("encounter_id")
|
|
.unique()
|
|
.with_columns(nw.lit(1).alias("_overlap"))
|
|
)
|
|
|
|
# Join encounter_with_ccs to terminology lookups
|
|
aa = readmissions___int_encounter_with_ccs
|
|
result = aa.join(
|
|
terminology__discharge_disposition,
|
|
on=["discharge_disposition_code"],
|
|
how="left",
|
|
suffix="_dd",
|
|
)
|
|
ms = terminology__ms_drg.with_columns(nw.lit(1).alias("_ms_valid"))
|
|
result = result.join(
|
|
ms,
|
|
left_on=["drg_code"],
|
|
right_on=["ms_drg_code"],
|
|
how="left",
|
|
suffix="_ms",
|
|
)
|
|
apr = terminology__apr_drg.with_columns(nw.lit(1).alias("_apr_valid"))
|
|
result = result.join(
|
|
apr,
|
|
left_on=["drg_code"],
|
|
right_on=["apr_drg_code"],
|
|
how="left",
|
|
suffix="_apr",
|
|
)
|
|
result = result.join(overlap_losers, on=["encounter_id"], how="left")
|
|
|
|
# Compute data quality flags
|
|
result = result.with_columns(
|
|
nw.when(nw.col("admit_date").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("missing_admit_date_flag"),
|
|
nw.when(nw.col("discharge_date").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("missing_discharge_date_flag"),
|
|
nw.when(nw.col("admit_date") > nw.col("discharge_date"))
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("admit_after_discharge_flag"),
|
|
nw.when(nw.col("discharge_disposition_code").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("missing_discharge_disposition_code_flag"),
|
|
nw.when(
|
|
~nw.col("discharge_disposition_code").is_null()
|
|
& nw.col("discharge_disposition_description").is_null()
|
|
)
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("invalid_discharge_disposition_code_flag"),
|
|
nw.when(nw.col("primary_diagnosis_code").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("missing_primary_diagnosis_flag"),
|
|
nw.when(nw.col("valid_primary_diagnosis_code_flag") == 0)
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("invalid_primary_diagnosis_code_flag"),
|
|
nw.when(nw.col("ccs_diagnosis_category").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("no_diagnosis_ccs_flag"),
|
|
nw.col("ccs_diagnosis_category").alias("diagnosis_ccs"),
|
|
nw.when(~nw.col("_overlap").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("overlaps_with_another_encounter_flag"),
|
|
nw.when(nw.col("drg_code").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("missing_drg_flag"),
|
|
nw.when(nw.col("_ms_valid").is_null() & nw.col("_apr_valid").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("invalid_drg_flag"),
|
|
)
|
|
|
|
# Derive disqualified_encounter_flag from any issue flag being 1
|
|
result = result.with_columns(
|
|
nw.when(
|
|
(nw.col("missing_admit_date_flag") == 1)
|
|
| (nw.col("missing_discharge_date_flag") == 1)
|
|
| (nw.col("admit_after_discharge_flag") == 1)
|
|
| (nw.col("missing_discharge_disposition_code_flag") == 1)
|
|
| (nw.col("invalid_discharge_disposition_code_flag") == 1)
|
|
| (nw.col("missing_primary_diagnosis_flag") == 1)
|
|
| (nw.col("invalid_primary_diagnosis_code_flag") == 1)
|
|
| (nw.col("no_diagnosis_ccs_flag") == 1)
|
|
| (nw.col("overlaps_with_another_encounter_flag") == 1)
|
|
| (nw.col("missing_drg_flag") == 1)
|
|
| (nw.col("invalid_drg_flag") == 1),
|
|
)
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("disqualified_encounter_flag")
|
|
)
|
|
|
|
return result.select(
|
|
"encounter_id",
|
|
"diagnosis_ccs",
|
|
"disqualified_encounter_flag",
|
|
"missing_admit_date_flag",
|
|
"missing_discharge_date_flag",
|
|
"admit_after_discharge_flag",
|
|
"missing_discharge_disposition_code_flag",
|
|
"invalid_discharge_disposition_code_flag",
|
|
"missing_primary_diagnosis_flag",
|
|
"invalid_primary_diagnosis_code_flag",
|
|
"no_diagnosis_ccs_flag",
|
|
"overlaps_with_another_encounter_flag",
|
|
"missing_drg_flag",
|
|
"invalid_drg_flag",
|
|
)
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_encounter_overlap(readmissions___int_encounter: FrameT) -> FrameT:
|
|
"""Build readmissions._int_encounter_overlap
|
|
|
|
Detects overlapping encounters per person, groups them, ranks by priority,
|
|
and selects the best encounter per overlap group.
|
|
|
|
Operations: select, window
|
|
Dependencies: readmissions._int_encounter
|
|
|
|
NOTE: The full overlap detection (self-join WHERE admit ranges intersect)
|
|
and window-based ranking require imperative frame operations that go beyond
|
|
simple narwhals expressions. This function computes the derived columns
|
|
that can be expressed declaratively; the overlap grouping and ranking
|
|
should be implemented as a multi-step pipeline by the caller.
|
|
"""
|
|
enc = readmissions___int_encounter
|
|
|
|
# Compute derived columns
|
|
days_diff = (
|
|
(
|
|
nw.col("discharge_date").cast(nw.Datetime)
|
|
- nw.col("admit_date").cast(nw.Datetime)
|
|
).dt.total_seconds()
|
|
/ 86400
|
|
).cast(nw.Int64)
|
|
enhanced = enc.with_columns(
|
|
nw.when(days_diff >= 1)
|
|
.then(days_diff)
|
|
.otherwise(1)
|
|
.alias("actual_length_of_stay"),
|
|
nw.when(
|
|
nw.col("encounter_source_type")
|
|
.fill_null("")
|
|
.str.to_uppercase()
|
|
.str.strip_chars()
|
|
== "CLAIM"
|
|
)
|
|
.then(1)
|
|
.otherwise(2)
|
|
.alias("source_type_priority"),
|
|
)
|
|
enhanced = enhanced.with_columns(
|
|
(
|
|
nw.when(
|
|
~nw.col("discharge_disposition_code").is_null()
|
|
& (nw.col("discharge_disposition_code").str.strip_chars() != "")
|
|
& (nw.col("discharge_disposition_code").str.strip_chars() != "00")
|
|
)
|
|
.then(1)
|
|
.otherwise(0)
|
|
+ nw.when(
|
|
~nw.col("drg_code_type").is_null()
|
|
& (nw.col("drg_code_type").str.strip_chars() != "")
|
|
)
|
|
.then(1)
|
|
.otherwise(0)
|
|
+ nw.when(
|
|
~nw.col("drg_code").is_null()
|
|
& (nw.col("drg_code").str.strip_chars() != "")
|
|
& ~nw.col("drg_code").str.strip_chars().is_in(["998", "999"])
|
|
)
|
|
.then(1)
|
|
.otherwise(0)
|
|
+ nw.when(~nw.col("paid_amount").is_null()).then(1).otherwise(0)
|
|
+ nw.when(
|
|
~nw.col("primary_diagnosis_code").is_null()
|
|
& (nw.col("primary_diagnosis_code").str.strip_chars() != "")
|
|
)
|
|
.then(1)
|
|
.otherwise(0)
|
|
).alias("completeness_score")
|
|
)
|
|
|
|
# Self-join to find overlapping encounters
|
|
e2 = enhanced.select(
|
|
nw.col("person_id").alias("_p2"),
|
|
nw.col("encounter_id").alias("_e2"),
|
|
nw.col("admit_date").alias("_a2"),
|
|
nw.col("discharge_date").alias("_d2"),
|
|
)
|
|
pairs = enhanced.join(
|
|
e2,
|
|
left_on=["person_id"],
|
|
right_on=["_p2"],
|
|
how="inner",
|
|
)
|
|
pairs = pairs.filter(
|
|
(nw.col("encounter_id") != nw.col("_e2"))
|
|
& (nw.col("admit_date") <= nw.col("_d2"))
|
|
& (nw.col("discharge_date") >= nw.col("_a2"))
|
|
)
|
|
has_overlap = (
|
|
pairs.select("encounter_id")
|
|
.unique()
|
|
.with_columns(nw.lit(1).alias("has_overlaps"))
|
|
)
|
|
|
|
# Overlap group: min of peer encounter_ids
|
|
grp = pairs.group_by("encounter_id").agg(nw.col("_e2").min().alias("_min_peer"))
|
|
grp = grp.with_columns(
|
|
nw.when(nw.col("_min_peer") < nw.col("encounter_id"))
|
|
.then(nw.col("_min_peer"))
|
|
.otherwise(nw.col("encounter_id"))
|
|
.alias("overlap_group_id")
|
|
).select("encounter_id", "overlap_group_id")
|
|
|
|
# Join back
|
|
result = enhanced.join(has_overlap, on=["encounter_id"], how="left").join(
|
|
grp, on=["encounter_id"], how="left"
|
|
)
|
|
result = result.with_columns(
|
|
nw.col("has_overlaps").fill_null(0),
|
|
nw.col("overlap_group_id").fill_null(nw.col("encounter_id")),
|
|
)
|
|
|
|
# Rank within overlap group
|
|
result = result.sort(
|
|
"person_id",
|
|
"overlap_group_id",
|
|
"source_type_priority",
|
|
"encounter_id",
|
|
)
|
|
result = result.with_columns(
|
|
nw.col("encounter_id")
|
|
.rank(method="ordinal")
|
|
.over("person_id", "overlap_group_id")
|
|
.alias("encounter_rank_in_group")
|
|
)
|
|
result = result.with_columns(
|
|
nw.when(nw.col("encounter_rank_in_group") == 1)
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("is_best_encounter"),
|
|
nw.when(
|
|
(nw.col("encounter_rank_in_group") == 1) & (nw.col("has_overlaps") == 1)
|
|
)
|
|
.then(nw.lit("Selected as best among overlapping"))
|
|
.otherwise(
|
|
nw.when(
|
|
(nw.col("encounter_rank_in_group") == 1) & (nw.col("has_overlaps") == 0)
|
|
)
|
|
.then(nw.lit("No overlapping encounters"))
|
|
.otherwise(nw.lit("Not selected - better exists"))
|
|
)
|
|
.alias("selection_reason"),
|
|
)
|
|
|
|
return result.select(
|
|
"encounter_id",
|
|
"person_id",
|
|
"admit_date",
|
|
"discharge_date",
|
|
"actual_length_of_stay",
|
|
"source_type_priority",
|
|
"completeness_score",
|
|
"overlap_group_id",
|
|
"has_overlaps",
|
|
"encounter_rank_in_group",
|
|
"is_best_encounter",
|
|
"selection_reason",
|
|
)
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_encounter_specialty_cohort(
|
|
readmissions___int_encounter: FrameT,
|
|
readmissions___int_encounter_with_ccs: FrameT,
|
|
readmissions___int_procedure_ccs: FrameT,
|
|
readmissions___value_set_specialty_cohort: FrameT,
|
|
readmissions___value_set_surgery_gynecology_cohort: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_encounter_specialty_cohort
|
|
|
|
Assigns each encounter to a specialty cohort based on procedure/diagnosis CCS
|
|
codes. Surgery/Gynecology is highest priority (rank 1), then Cardiorespiratory,
|
|
Cardiovascular, Neurology, and Medicine as default.
|
|
|
|
Operations: join, group_by, select
|
|
Dependencies: readmissions._int_encounter, readmissions._int_encounter_with_ccs,
|
|
readmissions._int_procedure_ccs,
|
|
readmissions._value_set_specialty_cohort,
|
|
readmissions._value_set_surgery_gynecology_cohort
|
|
"""
|
|
# Surgery/Gynecology encounters from procedure codes
|
|
surg_from_icd = readmissions___int_procedure_ccs.join(
|
|
readmissions___value_set_surgery_gynecology_cohort,
|
|
left_on=["procedure_code"],
|
|
right_on=["icd_10_pcs"],
|
|
how="inner",
|
|
).select(nw.col("encounter_id"), nw.lit(1).alias("c_rank"))
|
|
|
|
surg_from_ccs = readmissions___int_procedure_ccs.join(
|
|
readmissions___value_set_specialty_cohort.filter(
|
|
nw.col("specialty_cohort") == "Surgery/Gynecology"
|
|
),
|
|
left_on=["ccs_procedure_category"],
|
|
right_on=["ccs"],
|
|
how="inner",
|
|
).select(nw.col("encounter_id"), nw.lit(1).alias("c_rank"))
|
|
|
|
# Diagnosis-based cohorts
|
|
diag_cohorts = (
|
|
readmissions___int_encounter_with_ccs.join(
|
|
readmissions___value_set_specialty_cohort.filter(
|
|
nw.col("procedure_or_diagnosis") == "Diagnosis"
|
|
),
|
|
left_on=["ccs_diagnosis_category"],
|
|
right_on=["ccs"],
|
|
how="inner",
|
|
)
|
|
.with_columns(
|
|
nw.when(nw.col("specialty_cohort") == "Cardiorespiratory")
|
|
.then(2)
|
|
.otherwise(
|
|
nw.when(nw.col("specialty_cohort") == "Cardiovascular")
|
|
.then(3)
|
|
.otherwise(
|
|
nw.when(nw.col("specialty_cohort") == "Neurology")
|
|
.then(4)
|
|
.otherwise(5)
|
|
)
|
|
)
|
|
.alias("c_rank")
|
|
)
|
|
.select(nw.col("encounter_id"), nw.col("c_rank"))
|
|
)
|
|
|
|
# Combine all cohort assignments, pick min rank per encounter
|
|
all_cohorts = nw.concat([surg_from_icd, surg_from_ccs, diag_cohorts])
|
|
main_cohort = all_cohorts.group_by("encounter_id").agg(
|
|
nw.col("c_rank").min().alias("main_c_rank")
|
|
)
|
|
|
|
# Map rank back to cohort name, default to Medicine
|
|
result = readmissions___int_encounter.join(
|
|
main_cohort, on=["encounter_id"], how="left"
|
|
).with_columns(
|
|
nw.when(nw.col("main_c_rank") == 1)
|
|
.then(nw.lit("Surgery/Gynecology"))
|
|
.otherwise(
|
|
nw.when(nw.col("main_c_rank") == 2)
|
|
.then(nw.lit("Cardiorespiratory"))
|
|
.otherwise(
|
|
nw.when(nw.col("main_c_rank") == 3)
|
|
.then(nw.lit("Cardiovascular"))
|
|
.otherwise(
|
|
nw.when(nw.col("main_c_rank") == 4)
|
|
.then(nw.lit("Neurology"))
|
|
.otherwise(nw.lit("Medicine"))
|
|
)
|
|
)
|
|
)
|
|
.alias("specialty_cohort")
|
|
)
|
|
|
|
return result.select("encounter_id", "specialty_cohort")
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_encounter_with_ccs(
|
|
readmissions___int_encounter: FrameT,
|
|
readmissions___value_set_icd_10_cm_to_ccs: FrameT,
|
|
terminology__icd_10_cm: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_encounter_with_ccs
|
|
|
|
Joins encounters with ICD-10-CM terminology to validate diagnosis codes,
|
|
and with CCS mapping to get the diagnosis category.
|
|
|
|
Operations: join, select (CASE WHEN)
|
|
Dependencies: readmissions._int_encounter, readmissions._value_set_icd_10_cm_to_ccs,
|
|
terminology.icd_10_cm
|
|
"""
|
|
icd_cm = terminology__icd_10_cm.with_columns(nw.lit(1).alias("_icd_valid"))
|
|
result = readmissions___int_encounter.join(
|
|
icd_cm,
|
|
left_on=["primary_diagnosis_code"],
|
|
right_on=["icd_10_cm"],
|
|
how="left",
|
|
).join(
|
|
readmissions___value_set_icd_10_cm_to_ccs,
|
|
left_on=["primary_diagnosis_code"],
|
|
right_on=["icd_10_cm"],
|
|
how="left",
|
|
suffix="_ccs",
|
|
)
|
|
|
|
return result.select(
|
|
nw.col("encounter_id"),
|
|
nw.col("person_id"),
|
|
nw.col("admit_date"),
|
|
nw.col("discharge_date"),
|
|
nw.col("discharge_disposition_code"),
|
|
nw.col("facility_id"),
|
|
nw.col("drg_code_type"),
|
|
nw.col("drg_code"),
|
|
nw.col("paid_amount"),
|
|
nw.col("primary_diagnosis_code"),
|
|
nw.when(~nw.col("_icd_valid").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("valid_primary_diagnosis_code_flag"),
|
|
nw.col("ccs_diagnosis_category"),
|
|
)
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_exclusion(
|
|
readmissions___int_encounter_with_ccs: FrameT,
|
|
readmissions___value_set_exclusion_ccs_diagnosis_category: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_exclusion
|
|
|
|
Returns distinct encounter_ids whose CCS diagnosis category appears in the
|
|
exclusion value set (semi-join).
|
|
|
|
Operations: join (semi), select
|
|
Dependencies: readmissions._int_encounter_with_ccs,
|
|
readmissions._value_set_exclusion_ccs_diagnosis_category
|
|
"""
|
|
exclusion_categories = (
|
|
readmissions___value_set_exclusion_ccs_diagnosis_category.select(
|
|
"ccs_diagnosis_category"
|
|
).unique()
|
|
)
|
|
|
|
return (
|
|
readmissions___int_encounter_with_ccs.filter(
|
|
~nw.col("ccs_diagnosis_category").is_null()
|
|
)
|
|
.join(exclusion_categories, on=["ccs_diagnosis_category"], how="inner")
|
|
.select("encounter_id")
|
|
.unique()
|
|
)
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_index_admission(
|
|
readmissions___int_encounter: FrameT,
|
|
readmissions___int_index_time_requirement: FrameT,
|
|
readmissions___int_index_discharge_requirement: FrameT,
|
|
readmissions___int_exclusion: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_index_admission
|
|
|
|
An index admission passes both the time and discharge requirements and is
|
|
NOT in the exclusion list (anti-join on exclusion).
|
|
|
|
Operations: join, filter (anti-join)
|
|
Dependencies: readmissions._int_encounter, readmissions._int_index_time_requirement,
|
|
readmissions._int_index_discharge_requirement,
|
|
readmissions._int_exclusion
|
|
"""
|
|
excl = (
|
|
readmissions___int_exclusion.select("encounter_id")
|
|
.unique()
|
|
.with_columns(nw.lit(1).alias("_excl"))
|
|
)
|
|
result = (
|
|
readmissions___int_encounter.join(
|
|
readmissions___int_index_time_requirement.select("encounter_id"),
|
|
on=["encounter_id"],
|
|
how="inner",
|
|
)
|
|
.join(
|
|
readmissions___int_index_discharge_requirement.select("encounter_id"),
|
|
on=["encounter_id"],
|
|
how="inner",
|
|
)
|
|
.join(excl, on=["encounter_id"], how="left")
|
|
)
|
|
return result.filter(nw.col("_excl").is_null()).select("encounter_id").unique()
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_index_discharge_requirement(readmissions___int_encounter: FrameT) -> FrameT:
|
|
"""Build readmissions._int_index_discharge_requirement
|
|
|
|
Excludes encounters with invalid discharge disposition codes (02, 07, 20)
|
|
via anti-join pattern.
|
|
|
|
Operations: filter (anti-join)
|
|
Dependencies: readmissions._int_encounter
|
|
"""
|
|
invalid_ids = (
|
|
readmissions___int_encounter.filter(
|
|
nw.col("discharge_disposition_code").is_in(["02", "07", "20"])
|
|
)
|
|
.select("encounter_id")
|
|
.unique()
|
|
)
|
|
invalid_ids = invalid_ids.with_columns(nw.lit(1).alias("_invalid"))
|
|
result = readmissions___int_encounter.join(
|
|
invalid_ids, on=["encounter_id"], how="left"
|
|
)
|
|
return result.filter(nw.col("_invalid").is_null()).select("encounter_id")
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_index_time_requirement(readmissions___int_encounter: FrameT) -> FrameT:
|
|
"""Build readmissions._int_index_time_requirement
|
|
|
|
Keeps encounters whose discharge_date is at least 30 days before the
|
|
latest discharge in the dataset (so there's a window to observe readmission).
|
|
|
|
Operations: filter
|
|
Dependencies: readmissions._int_encounter
|
|
"""
|
|
# discharge_date <= max(discharge_date) - 30 days
|
|
max_discharge = readmissions___int_encounter.select(
|
|
nw.col("discharge_date").max().alias("max_discharge")
|
|
)
|
|
cutoff = readmissions___int_encounter.join(max_discharge, how="cross")
|
|
return cutoff.filter(
|
|
nw.col("discharge_date") <= nw.col("max_discharge") - nw.lit(timedelta(days=30))
|
|
).select("encounter_id")
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_planned_encounter(
|
|
readmissions___int_encounter_with_ccs: FrameT,
|
|
readmissions___int_procedure_ccs: FrameT,
|
|
readmissions___value_set_acute_diagnosis_ccs: FrameT,
|
|
readmissions___value_set_acute_diagnosis_icd_10_cm: FrameT,
|
|
readmissions___value_set_always_planned_ccs_diagnosis_category: FrameT,
|
|
readmissions___value_set_always_planned_ccs_procedure_category: FrameT,
|
|
readmissions___value_set_potentially_planned_ccs_procedure_category: FrameT,
|
|
readmissions___value_set_potentially_planned_icd_10_pcs: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_planned_encounter
|
|
|
|
Identifies encounters that are considered "planned" readmissions using
|
|
CMS methodology: always-planned procedures, always-planned diagnoses,
|
|
and potentially-planned procedures that don't have an acute diagnosis.
|
|
|
|
Operations: join, filter, union
|
|
Dependencies: readmissions._int_encounter_with_ccs, readmissions._int_procedure_ccs,
|
|
readmissions._value_set_* (6 value sets)
|
|
"""
|
|
# Always planned: procedure CCS category in always-planned list
|
|
always_planned_px = (
|
|
readmissions___int_procedure_ccs.join(
|
|
readmissions___value_set_always_planned_ccs_procedure_category,
|
|
on=["ccs_procedure_category"],
|
|
how="inner",
|
|
)
|
|
.select("encounter_id")
|
|
.unique()
|
|
)
|
|
|
|
# Always planned: diagnosis CCS category in always-planned list
|
|
always_planned_dx = (
|
|
readmissions___int_encounter_with_ccs.join(
|
|
readmissions___value_set_always_planned_ccs_diagnosis_category,
|
|
on=["ccs_diagnosis_category"],
|
|
how="inner",
|
|
)
|
|
.select("encounter_id")
|
|
.unique()
|
|
)
|
|
|
|
# Potentially planned: procedure CCS or ICD-10-PCS in potentially-planned list
|
|
pot_px_ccs = readmissions___int_procedure_ccs.join(
|
|
readmissions___value_set_potentially_planned_ccs_procedure_category,
|
|
on=["ccs_procedure_category"],
|
|
how="inner",
|
|
).select("encounter_id")
|
|
|
|
pot_px_icd = readmissions___int_procedure_ccs.join(
|
|
readmissions___value_set_potentially_planned_icd_10_pcs,
|
|
left_on=["procedure_code"],
|
|
right_on=["icd_10_pcs"],
|
|
how="inner",
|
|
).select("encounter_id")
|
|
|
|
potentially_planned = nw.concat([pot_px_ccs, pot_px_icd]).unique()
|
|
|
|
# Acute encounters: those with an acute diagnosis (ICD-10-CM or CCS)
|
|
acute_icd = readmissions___int_encounter_with_ccs.join(
|
|
readmissions___value_set_acute_diagnosis_icd_10_cm,
|
|
left_on=["primary_diagnosis_code"],
|
|
right_on=["icd_10_cm"],
|
|
how="inner",
|
|
).select("encounter_id")
|
|
|
|
acute_ccs = readmissions___int_encounter_with_ccs.join(
|
|
readmissions___value_set_acute_diagnosis_ccs,
|
|
on=["ccs_diagnosis_category"],
|
|
how="inner",
|
|
).select("encounter_id")
|
|
|
|
acute_encounters = nw.concat([acute_icd, acute_ccs]).unique()
|
|
|
|
# Potentially planned minus acute = actually planned
|
|
acute_marked = acute_encounters.with_columns(nw.lit(1).alias("_acute"))
|
|
pot_actually_planned = (
|
|
potentially_planned.join(acute_marked, on=["encounter_id"], how="left")
|
|
.filter(nw.col("_acute").is_null())
|
|
.select("encounter_id")
|
|
)
|
|
|
|
return nw.concat(
|
|
[always_planned_px, always_planned_dx, pot_actually_planned]
|
|
).unique()
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_procedure_ccs(
|
|
core__procedure: FrameT,
|
|
readmissions___value_set_icd_10_pcs_to_ccs: FrameT,
|
|
terminology__icd_10_pcs: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_procedure_ccs
|
|
|
|
Filters core.procedure to ICD-10-PCS, joins with terminology
|
|
for validation and CCS mapping for procedure categories.
|
|
|
|
Operations: filter, join, select
|
|
Dependencies: core.procedure,
|
|
readmissions._value_set_icd_10_pcs_to_ccs,
|
|
terminology.icd_10_pcs
|
|
"""
|
|
procs = core__procedure.filter(nw.col("normalized_code_type") == "icd-10-pcs")
|
|
icd_pcs = terminology__icd_10_pcs.with_columns(nw.lit(1).alias("_pcs_valid"))
|
|
result = procs.join(
|
|
icd_pcs,
|
|
left_on=["normalized_code"],
|
|
right_on=["icd_10_pcs"],
|
|
how="left",
|
|
)
|
|
result = result.join(
|
|
readmissions___value_set_icd_10_pcs_to_ccs,
|
|
left_on=["normalized_code"],
|
|
right_on=["icd_10_pcs"],
|
|
how="left",
|
|
suffix="_ccs",
|
|
)
|
|
return result.select(
|
|
nw.col("encounter_id"),
|
|
nw.col("normalized_code").alias("procedure_code"),
|
|
nw.when(~nw.col("_pcs_valid").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("valid_icd_10_pcs_flag"),
|
|
nw.col("ccs_procedure_category"),
|
|
)
|
|
|
|
|
|
@nw.narwhalify
|
|
def int_readmission_crude(
|
|
readmissions___int_encounter: FrameT,
|
|
readmissions___int_encounter_overlap: FrameT,
|
|
) -> FrameT:
|
|
"""Build readmissions._int_readmission_crude
|
|
|
|
Sequences valid, non-overlapping encounters per person by admit date,
|
|
then detects readmissions by joining consecutive encounters and checking
|
|
if the gap is <= 30 days.
|
|
|
|
Operations: filter, window, join (self), select
|
|
Dependencies: readmissions._int_encounter, readmissions._int_encounter_overlap
|
|
"""
|
|
# Anti-join: exclude overlap losers
|
|
losers = (
|
|
readmissions___int_encounter_overlap.filter(nw.col("is_best_encounter") == 0)
|
|
.select("encounter_id")
|
|
.unique()
|
|
.with_columns(nw.lit(1).alias("_loser"))
|
|
)
|
|
valid = (
|
|
readmissions___int_encounter.filter(
|
|
~nw.col("admit_date").is_null()
|
|
& ~nw.col("discharge_date").is_null()
|
|
& (nw.col("admit_date") <= nw.col("discharge_date"))
|
|
)
|
|
.join(losers, on=["encounter_id"], how="left")
|
|
.filter(nw.col("_loser").is_null())
|
|
.select(
|
|
"encounter_id",
|
|
"person_id",
|
|
"admit_date",
|
|
"discharge_date",
|
|
)
|
|
)
|
|
|
|
# Sequence encounters per person by admit_date
|
|
valid = valid.with_columns(
|
|
nw.col("admit_date")
|
|
.rank(method="ordinal")
|
|
.over("person_id")
|
|
.alias("encounter_seq")
|
|
)
|
|
|
|
# Self-join: current encounter (aa) to next (bb)
|
|
valid = valid.with_columns((nw.col("encounter_seq") + 1).alias("_next_seq"))
|
|
bb = valid.select(
|
|
nw.col("person_id").alias("_p"),
|
|
nw.col("encounter_seq").alias("_seq"),
|
|
nw.col("encounter_id").alias("_next_enc"),
|
|
nw.col("admit_date").alias("_next_admit"),
|
|
)
|
|
result = valid.join(
|
|
bb,
|
|
left_on=["person_id", "_next_seq"],
|
|
right_on=["_p", "_seq"],
|
|
how="left",
|
|
)
|
|
|
|
# Compute readmission flags
|
|
days_to = (
|
|
(
|
|
nw.col("_next_admit").cast(nw.Datetime)
|
|
- nw.col("discharge_date").cast(nw.Datetime)
|
|
).dt.total_seconds()
|
|
/ 86400
|
|
).cast(nw.Int64)
|
|
result = result.with_columns(
|
|
nw.when(~nw.col("_next_enc").is_null())
|
|
.then(1)
|
|
.otherwise(0)
|
|
.alias("had_readmission_flag"),
|
|
days_to.alias("days_to_readmit"),
|
|
nw.when(days_to <= 30).then(1).otherwise(0).alias("readmit_30_flag"),
|
|
)
|
|
return result.select(
|
|
"encounter_id",
|
|
"person_id",
|
|
"admit_date",
|
|
"discharge_date",
|
|
"had_readmission_flag",
|
|
"days_to_readmit",
|
|
"readmit_30_flag",
|
|
)
|