pycomorb 0.4__py3-none-any.whl

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.
@@ -0,0 +1,262 @@
1
+ # Reference for CCI:
2
+ # 1. Charlson ME, Pompei P, Ales KL, MacKenzie CR.
3
+ # A new method of classifying prognostic comorbidity in longitudinal studies: development and validation.
4
+ # J Chronic Dis. 1987;40(5):373-83.
5
+ # doi: 10.1016/0021-9681(87)90171-8. PMID: 3558716.
6
+ # 2. Charlson M, Szatrowski TP, Peterson J, Gold J.
7
+ # Validation of a combined comorbidity index.
8
+ # J Clin Epidemiol. 1994 Nov;47(11):1245-51.
9
+ # doi: 10.1016/0895-4356(94)90129-5. PMID: 7722560.
10
+ #
11
+ # Reference for ICD-9-CM and ICD-10 Coding Algorithms for Charlson Comorbidities:
12
+ # 3. Deyo RA, Cherkin DC, Ciol MA.
13
+ # Adapting a clinical comorbidity index for use with ICD-9-CM administrative databases.
14
+ # J Clin Epidemiol. 1992 Jun;45(6):613-9.
15
+ # doi: 10.1016/0895-4356(92)90133-8. PMID: 1607900.
16
+ # 4. Romano PS, Roos LL, Jollis JG.
17
+ # Adapting a clinical comorbidity index for use with ICD-9-CM administrative data: differing perspectives.
18
+ # J Clin Epidemiol. 1993 Oct;46(10):1075-9; discussion 1081-90.
19
+ # doi: 10.1016/0895-4356(93)90103-8. PMID: 8410092.
20
+ # 5. Quan H, Sundararajan V, Halfon P, Fong A, Burnand B, Luthi JC, Saunders LD, Beck CA, Feasby TE, Ghali WA.
21
+ # Coding algorithms for defining comorbidities in ICD-9-CM and ICD-10 administrative data.
22
+ # Med Care. 2005 Nov;43(11):1130-9.
23
+ # doi: 10.1097/01.mlr.0000182534.19832.83. PMID: 16224307.
24
+
25
+ from pathlib import Path
26
+
27
+ import pandas as pd
28
+ import polars as pl
29
+
30
+ # Import the generalized function
31
+ from .CustomComorbidityIndex import CustomComorbidityIndex
32
+
33
+
34
+ def CharlsonComorbidityIndex(
35
+ df: pl.DataFrame,
36
+ id_col: str = "id",
37
+ code_col: str = "code",
38
+ age_col: str = "age",
39
+ icd_version: str = "icd10",
40
+ icd_version_col: str = None,
41
+ implementation: str = "quan",
42
+ return_categories: bool = False,
43
+ ):
44
+ """
45
+ Calculate the Charlson Comorbidity Index (CCI) using ICD codes and age.
46
+
47
+ Args:
48
+ df (pl.DataFrame): DataFrame with at least columns [id_col, code_col, age_col].
49
+ id_col (str): Name of the column containing unique identifier. Default: "id".
50
+ code_col (str): Name of the column containing ICD codes. Default: "code".
51
+ age_col (str): Name of the column containing patient ages. Default: "age".
52
+ icd_version (str): ICD version ('icd9', 'icd10', or 'icd9_10'). Default: "icd10".
53
+ icd_version_col (str, optional): Name of the column with ICD version for 'icd9_10'. Default: None.
54
+ implementation (str): CCI implementation ('quan', 'deyo', 'romano', 'australia', 'sweden', 'uk_shmi'). Default: "quan".
55
+ return_categories (bool): If True, also return presence indicators for each CCI category.
56
+
57
+ Returns:
58
+ - DataFrame with [id_col, "Charlson Score"].
59
+ - DataFrame with category indicators if return_categories is True, else None.
60
+ """
61
+
62
+ # Check if input is pandas DataFrame and convert to polars
63
+ is_pandas = pd and isinstance(df, pd.DataFrame)
64
+ if is_pandas:
65
+ df = pl.from_pandas(df)
66
+
67
+ # Change ICD to ICD-9 for Deyo, D'Hoore and Romano
68
+ if icd_version == "icd10" and implementation in [
69
+ "deyo",
70
+ "dhoore",
71
+ "romano",
72
+ ]:
73
+ print(f"Warning: Implementation '{implementation}' only uses ICD-9. Setting ICD version to 'icd9'.") # fmt: skip
74
+ icd_version = "icd9"
75
+ # Change ICD to ICD-10 for Australian, Swedish and UK versions
76
+ elif icd_version == "icd9" and implementation in [
77
+ "australia",
78
+ "sweden",
79
+ "uk_shmi",
80
+ ]:
81
+ print(f"Warning: Implementation '{implementation}' only uses ICD-10. Setting ICD version to 'icd10'.") # fmt: skip
82
+ icd_version = "icd10"
83
+
84
+ # Input validation specific to Charlson
85
+ assert implementation in [
86
+ "quan",
87
+ "deyo",
88
+ "romano",
89
+ "australia",
90
+ "sweden",
91
+ "uk_shmi",
92
+ ], (
93
+ "implementation must be one of: "
94
+ "'quan', 'deyo', 'romano', 'australia', 'sweden', or 'uk_shmi'."
95
+ )
96
+ assert (
97
+ age_col in df.columns
98
+ ), f"Column '{age_col}' (age) must be present in input DataFrame."
99
+ assert (
100
+ id_col in df.columns
101
+ ), f"Column '{id_col}' (ID) must be present in input DataFrame."
102
+
103
+ # STEP 0: select relevant columns and rename diagnosis code column
104
+ # diagnoses handled by CustomComorbidityIndex
105
+ patient_ages = df.select(id_col, age_col)
106
+
107
+ # STEP 1: Calculate Age Score separately
108
+ # <= 50: 0
109
+ # <= 60: 1
110
+ # <= 70: 2
111
+ # <= 80: 3
112
+ # > 80: 4
113
+ age_scores = (
114
+ patient_ages.with_columns(
115
+ pl.col(age_col)
116
+ .cut(
117
+ breaks=[50, 60, 70, 80],
118
+ labels=["0", "1", "2", "3", "4"],
119
+ left_closed=False, # Age > 50 gets score 1, etc.
120
+ )
121
+ .fill_null("0") # Assume age 0 if null
122
+ .cast(int)
123
+ .alias("Age Score")
124
+ )
125
+ .group_by(id_col)
126
+ .agg(pl.col("Age Score").max().alias("Age Score"))
127
+ )
128
+
129
+ # STEP 2: Calculate Comorbidity Score using generalized function
130
+ # Determine definition file based on implementation
131
+ if implementation == "quan":
132
+ definition_file = "CHARLSON_QUAN.csv"
133
+ elif implementation == "deyo":
134
+ definition_file = "CHARLSON_DEYO.csv"
135
+ elif implementation == "dhoore":
136
+ definition_file = "CHARLSON_DHOORE.csv"
137
+ elif implementation == "romano":
138
+ definition_file = "CHARLSON_ROMANO.csv"
139
+ elif implementation == "australia":
140
+ definition_file = "CHARLSON_AUSTRALIA.csv"
141
+ elif implementation == "sweden":
142
+ definition_file = "CHARLSON_SWEDEN.csv"
143
+ elif implementation == "uk_shmi":
144
+ definition_file = "CHARLSON_UK_SHMI_v1.55.csv"
145
+ else:
146
+ # Should be caught by assert earlier
147
+ raise ValueError(f"Unsupported implementation: {implementation}")
148
+
149
+ definition_file_path = Path(__file__).parent / f"common/{definition_file}"
150
+ weight_col_name = "weights"
151
+
152
+ # Use a temporary score name before adding age score
153
+ score_col_name = "Charlson Comorbidity Score"
154
+
155
+ # Define mutual exclusion rules for Charlson
156
+ # These category names are common across Quan, Romano, Deyo
157
+ mutual_exclusion_rules = [
158
+ (
159
+ "Diabetes with chronic complication",
160
+ "Diabetes without chronic complication",
161
+ ),
162
+ ("Moderate or severe liver disease", "Mild liver disease"),
163
+ ]
164
+ # Adjust rules for specific implementations if category names differ
165
+ if implementation == "australia" or implementation == "uk_shmi":
166
+ mutual_exclusion_rules = [
167
+ ("Diabetes complications", "Diabetes"),
168
+ ("Severe liver disease", "Liver disease"),
169
+ ]
170
+ elif implementation == "sweden":
171
+ mutual_exclusion_rules = [
172
+ ("Diabetes with end organ damage", "Diabetes"),
173
+ # Sweden uses 'Moderate or severe kidney disease' - no specific liver exclusion rule needed based on provided names
174
+ # Sweden splits Pulmonary disease - no exclusion needed between them
175
+ ]
176
+
177
+ df_charlson = CustomComorbidityIndex(
178
+ df=df,
179
+ id_col=id_col,
180
+ code_col=code_col,
181
+ icd_version=icd_version,
182
+ icd_version_col=icd_version_col,
183
+ definition_data=definition_file_path,
184
+ weight_col_name=weight_col_name,
185
+ score_col_name=score_col_name,
186
+ mutual_exclusion_rules=mutual_exclusion_rules,
187
+ return_categories=return_categories,
188
+ )
189
+
190
+ # STEP 3: Combine Age Score and Comorbidity Score
191
+ final_score_col_name = "Charlson Score"
192
+ df_charlson = df_charlson.join(
193
+ age_scores, on=id_col, how="left", coalesce=True
194
+ ).with_columns(
195
+ (pl.col(score_col_name) + pl.col("Age Score")).alias(
196
+ final_score_col_name
197
+ )
198
+ )
199
+
200
+ # Drop Age Score column if not needed
201
+ if not return_categories:
202
+ df_charlson = df_charlson.drop("Age Score")
203
+
204
+ if is_pandas:
205
+ return df_charlson.to_pandas()
206
+
207
+ return df_charlson
208
+
209
+
210
+ # region 10y survival
211
+ def CharlsonComorbidity_10year_survival(
212
+ df: pl.DataFrame = None,
213
+ id_col: str = "id",
214
+ code_col: str = "code",
215
+ age_col: str = "age",
216
+ icd_version: str = "icd10",
217
+ icd_version_col: str = None,
218
+ implementation: str = "quan",
219
+ precalculated_df: bool = False,
220
+ ) -> pl.DataFrame:
221
+ """
222
+ Calculate the 10-year survival probability based on the Charlson Comorbidity Index.
223
+
224
+ Args:
225
+ df (pl.DataFrame): DataFrame with at least columns [id_col, code_col, age_col] or [id_col, "Charlson Score"] if precalculated_df is True.
226
+ id_col (str): Name of the column containing unique identifier. Default: "id".
227
+ code_col (str): Name of the column containing ICD codes. Default: "code".
228
+ age_col (str): Name of the column containing patient ages. Default: "age".
229
+ icd_version (str): ICD version ('icd9', 'icd10', or 'icd9_10'). Default: "icd10".
230
+ icd_version_col (str, optional): Name of the column with ICD version for 'icd9_10'. Default: None.
231
+ implementation (str): The Charlson implementation. Default: "quan".
232
+ precalculated_df (bool): If True, df is expected to contain a "Charlson Score" column instead of ICD codes and age.
233
+
234
+ Returns:
235
+ pl.DataFrame: DataFrame with columns [id_col, "10-year survival probability"].
236
+ """
237
+
238
+ if not precalculated_df:
239
+ df, _ = CharlsonComorbidityIndex(
240
+ df=df,
241
+ id_col=id_col,
242
+ code_col=code_col,
243
+ age_col=age_col,
244
+ icd_version=icd_version,
245
+ icd_version_col=icd_version_col,
246
+ implementation=implementation,
247
+ return_categories=False,
248
+ )
249
+ else:
250
+ assert (
251
+ "Charlson Score" in df.columns
252
+ ), "Input DataFrame must contain column 'Charlson Score'."
253
+ assert (
254
+ id_col in df.columns
255
+ ), f"Input DataFrame must contain column '{id_col}'."
256
+
257
+ # Formula: 0.983 ^ (CCI Score * 0.9)
258
+ return df.with_columns(
259
+ (pl.lit(0.983) ** (pl.col("Charlson Score").clip(lower_bound=0) * 0.9))
260
+ .round(3)
261
+ .alias("10-year survival probability")
262
+ ).select(id_col, "10-year survival probability")
@@ -0,0 +1,255 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional, Tuple, Union
3
+
4
+ import pandas as pd
5
+ import polars as pl
6
+
7
+
8
+ def CustomComorbidityIndex(
9
+ df: pl.DataFrame,
10
+ id_col: str = "id",
11
+ code_col: str = "code",
12
+ icd_version: str = "icd10",
13
+ icd_version_col: str = None,
14
+ definition_data: Union[Path, pl.DataFrame] = None,
15
+ weight_col_name: str = None,
16
+ score_col_name: str = None,
17
+ mutual_exclusion_rules: Optional[List[Tuple[str, str]]] = None,
18
+ return_categories=False,
19
+ ):
20
+ """
21
+ Generic comorbidity index calculation from ICD codes.
22
+
23
+ Args:
24
+ df (pl.DataFrame): DataFrame with at least columns [id_col, code_col].
25
+ id_col (str): Name of the column containing unique identifier. Default: "id".
26
+ code_col (str): Name of the column containing ICD codes. Default: "code".
27
+ icd_version (str): ICD version ('icd9', 'icd10', or 'icd9_10'). Default: "icd10".
28
+ icd_version_col (str, optional): Name of the column with ICD version for 'icd9_10'. Default: None.
29
+ definition_data (Path or pl.DataFrame): Path to CSV or DataFrame with ICD definitions and weights.
30
+ weight_col_name (str): Name of the column with weights in definition_data.
31
+ score_col_name (str): Name for the calculated score column.
32
+ mutual_exclusion_rules (list of tuple, optional): List of (complicated, uncomplicated) category pairs.
33
+ return_categories (bool): If True, also return presence indicators for each category.
34
+
35
+ Returns:
36
+ - DataFrame with [id_col, score_col_name].
37
+ - DataFrame with category indicators if return_categories is True, else None.
38
+ """
39
+
40
+ # Check if input is pandas DataFrame and convert to polars
41
+ is_pandas = pd and isinstance(df, pd.DataFrame)
42
+ if is_pandas:
43
+ df = pl.from_pandas(df)
44
+
45
+ # Input validation
46
+ assert icd_version in [
47
+ "icd9",
48
+ "icd10",
49
+ "icd9_10",
50
+ ], f"icd_version must be one of: 'icd9', 'icd10', or 'icd9_10'. Got '{icd_version}'."
51
+ assert (
52
+ code_col in df.columns
53
+ ), f"Column '{code_col}' (ICD code) must be present in input DataFrame."
54
+ assert (
55
+ id_col in df.columns
56
+ ), f"Column '{id_col}' (ID) must be present in input DataFrame."
57
+ if icd_version == "icd9_10":
58
+ assert (
59
+ icd_version_col is not None and icd_version_col in df.columns
60
+ ), f"For icd9_10, '{icd_version_col}' (ICD version) column must be present in input DataFrame."
61
+
62
+ # Load definitions
63
+ if isinstance(definition_data, Path):
64
+ definitions = pl.read_csv(definition_data, separator=",")
65
+ else:
66
+ definitions = definition_data
67
+
68
+ assert (
69
+ "category" in definitions.columns
70
+ ), "'category' column not found in definition file."
71
+ assert (
72
+ weight_col_name in definitions.columns
73
+ ), f"'{weight_col_name}' column not found in definition file."
74
+
75
+ # Drop rows from df with missing codes
76
+ df = df.filter(pl.col(code_col).is_not_null())
77
+
78
+ def process_single_icd(df, icd_col_name):
79
+ # Prepare mapping DataFrame: code_col, category
80
+ code_map_rows = []
81
+ for row in definitions.iter_rows(named=True):
82
+ if row["category"] is None or row.get(icd_col_name) is None:
83
+ continue
84
+ for code in row[icd_col_name].split("|"):
85
+ code_map_rows.append(
86
+ {"prefix": code.strip(), "category": row["category"]}
87
+ )
88
+ code_map = pl.DataFrame(code_map_rows)
89
+
90
+ # Create dataframe with presence indicators
91
+ categories = code_map.select("category").unique()
92
+ categories_list = categories.to_series().to_list()
93
+ categories_df = categories.with_columns(
94
+ pl.when(pl.col("category") == category)
95
+ .then(pl.lit(1))
96
+ .otherwise(pl.lit(0))
97
+ .alias(category)
98
+ for category in categories_list
99
+ )
100
+
101
+ # Get all unique ICD codes in the data
102
+ code_prefixes = code_map.select("prefix").to_series()
103
+ unique_codes = df.select(code_col).unique()
104
+ longest_code = max(unique_codes.to_series().to_list(), key=len)
105
+
106
+ # Create a DataFrame with all possible prefixes, then find the best match
107
+ best_prefix = (
108
+ unique_codes.with_columns(
109
+ pl.col(code_col).str.slice(0, n).alias(f"prefix_{n}")
110
+ for n in range(1, len(longest_code) + 1)
111
+ )
112
+ .with_columns(
113
+ pl.when(pl.col(f"prefix_{n}").is_in(code_prefixes))
114
+ .then(pl.col(f"prefix_{n}"))
115
+ .otherwise(None)
116
+ .alias(f"prefix_{n}")
117
+ for n in range(1, len(longest_code) + 1)
118
+ )
119
+ # Count ICD code as present for each comorbidity it matches
120
+ .with_columns(
121
+ pl.concat_list(
122
+ pl.col(f"prefix_{n}")
123
+ for n in range(1, len(longest_code) + 1)
124
+ )
125
+ .list.drop_nulls()
126
+ .list.unique()
127
+ .alias("best_prefixes")
128
+ )
129
+ .explode("best_prefixes")
130
+ .rename({"best_prefixes": "best_prefix"})
131
+ .select(code_col, "best_prefix")
132
+ .drop_nulls("best_prefix")
133
+ )
134
+
135
+ (
136
+ unique_codes.with_columns(
137
+ pl.col(code_col).str.slice(0, n).alias(f"prefix_{n}")
138
+ for n in range(1, len(longest_code) + 1)
139
+ )
140
+ .with_columns(
141
+ pl.when(pl.col(f"prefix_{n}").is_in(code_prefixes))
142
+ .then(pl.col(f"prefix_{n}"))
143
+ .otherwise(None)
144
+ .alias(f"prefix_{n}")
145
+ for n in range(1, len(longest_code) + 1)
146
+ )
147
+ .with_columns(
148
+ pl.concat_list(
149
+ pl.col(f"prefix_{n}")
150
+ for n in range(1, len(longest_code) + 1)
151
+ )
152
+ .list.drop_nulls()
153
+ .list.unique()
154
+ .list.join(", ")
155
+ .alias("best_prefixes")
156
+ )
157
+ .write_csv("best_prefix.csv")
158
+ )
159
+
160
+ code_to_category = (
161
+ best_prefix.join(
162
+ code_map, left_on="best_prefix", right_on="prefix", how="left"
163
+ )
164
+ .select(code_col, "category")
165
+ .join(categories_df, on="category", how="left")
166
+ .select(code_col, *categories_list)
167
+ )
168
+
169
+ # Join diagnoses with code_to_category to get the best matching category for each diagnosis
170
+ df_presence_absence = (
171
+ df.join(code_to_category, on=code_col, how="left")
172
+ .group_by(id_col)
173
+ .agg(pl.max(cat).fill_null(0).alias(cat) for cat in categories_list)
174
+ )
175
+
176
+ # Add missing columns
177
+ for cat in categories_list:
178
+ if cat not in df_presence_absence.columns:
179
+ df_presence_absence = df_presence_absence.with_columns(
180
+ pl.lit(0).alias(cat)
181
+ )
182
+ df_presence_absence = df_presence_absence.select(
183
+ [id_col] + categories_list
184
+ )
185
+ return df_presence_absence, categories_list
186
+
187
+ if icd_version == "icd9_10":
188
+ # Split by icd_version_col
189
+ df_icd9 = df.filter(pl.col(icd_version_col).cast(str) == "ICD-9")
190
+ df_icd10 = df.filter(pl.col(icd_version_col).cast(str) != "ICD-9")
191
+
192
+ # Process each ICD version separately
193
+ df9, cats9 = process_single_icd(df_icd9, "icd9_codes")
194
+ df10, cats10 = process_single_icd(df_icd10, "icd10_codes")
195
+
196
+ # Combine: outer join on id_col, take max for each category
197
+ all_categories = sorted(set(cats9) | set(cats10))
198
+ df_combined = df9.join(df10, on=id_col, how="outer", suffix="_right")
199
+ for cat in all_categories:
200
+ left = cat
201
+ right = f"{cat}_right"
202
+ if left in df_combined.columns and right in df_combined.columns:
203
+ df_combined = df_combined.with_columns(
204
+ pl.max_horizontal(pl.col(left), pl.col(right)).alias(cat)
205
+ )
206
+ elif right in df_combined.columns:
207
+ df_combined = df_combined.rename({right: cat})
208
+ df_presence_absence = df_combined.select([id_col] + all_categories)
209
+ else:
210
+ icd_col_name = f"{icd_version}_codes"
211
+ df_presence_absence, all_categories = process_single_icd(
212
+ df, icd_col_name
213
+ )
214
+
215
+ # STEP 2: calculate score
216
+ category_weights = dict(
217
+ zip(definitions["category"], definitions[weight_col_name])
218
+ )
219
+ category_weights = {
220
+ k: v
221
+ for k, v in category_weights.items()
222
+ if k is not None and v is not None
223
+ }
224
+
225
+ # Apply mutual exclusion rules
226
+ if mutual_exclusion_rules:
227
+ for complicated, uncomplicated in mutual_exclusion_rules:
228
+ if (
229
+ complicated in df_presence_absence.columns
230
+ and uncomplicated in df_presence_absence.columns
231
+ ):
232
+ df_presence_absence = df_presence_absence.with_columns(
233
+ pl.when(pl.col(complicated) == 1)
234
+ .then(0)
235
+ .otherwise(pl.col(uncomplicated))
236
+ .alias(uncomplicated)
237
+ )
238
+
239
+ # calculate scores
240
+ # Ensure weights are applied only to existing category columns that have weights
241
+ score_expr = pl.sum_horizontal(
242
+ pl.col(cat) * category_weights.get(cat, 0)
243
+ for cat in all_categories
244
+ if cat in df_presence_absence.columns and cat in category_weights
245
+ ).alias(score_col_name)
246
+
247
+ cols = [id_col] + all_categories if return_categories else [id_col]
248
+ comorbidity_df = df_presence_absence.select(*cols, score_expr).cast(
249
+ {score_col_name: int}
250
+ )
251
+
252
+ if is_pandas:
253
+ comorbidity_df = comorbidity_df.to_pandas()
254
+
255
+ return comorbidity_df
@@ -0,0 +1,153 @@
1
+ # Reference for ECI:
2
+ # 1. van Walraven C, Austin PC, Jennings A, Quan H, Forster AJ.
3
+ # A modification of the Elixhauser comorbidity measures into a point system for hospital death using administrative data.
4
+ # Med Care. 2009 Jun;47(6):626-33.
5
+ # doi: 10.1097/MLR.0b013e31819432e5. PMID: 19433995.
6
+ # 2. Elixhauser A, Steiner C, Harris DR, Coffey RM.
7
+ # Comorbidity measures for use with administrative data.
8
+ # Med Care. 1998 Jan;36(1):8-27.
9
+ # doi: 10.1097/00005650-199801000-00004. PMID: 9431328.
10
+ #
11
+ # Reference for ICD-9-CM and ICD-10 Coding Algorithms for Elixhauser Comorbidities:
12
+ # 3. Quan H, Sundararajan V, Halfon P, Fong A, Burnand B, Luthi JC, Saunders LD, Beck CA, Feasby TE, Ghali WA.
13
+ # Coding algorithms for defining comorbidities in ICD-9-CM and ICD-10 administrative data.
14
+ # Med Care. 2005 Nov;43(11):1130-9.
15
+ # doi: 10.1097/01.mlr.0000182534.19832.83. PMID: 16224307.
16
+ # 4. Original Elixhauser definitions (used when implementation='elixhauser')
17
+ # 5. AHRQ definitions (used when implementation='ahrq_icd9' or 'ahrq_icd10')
18
+
19
+ from pathlib import Path
20
+
21
+ import pandas as pd
22
+ import polars as pl
23
+
24
+ # Import the generalized function
25
+ from .CustomComorbidityIndex import CustomComorbidityIndex
26
+
27
+
28
+ def ElixhauserComorbidityIndex(
29
+ df: pl.DataFrame,
30
+ id_col: str = "id",
31
+ code_col: str = "code",
32
+ icd_version: str = "icd10",
33
+ icd_version_col: str = None,
34
+ implementation: str = "quan",
35
+ weights: str = "van_walraven",
36
+ return_categories=False,
37
+ ):
38
+ """
39
+ Calculate the Elixhauser Comorbidity Index (ECI) using ICD codes.
40
+
41
+ Args:
42
+ df (pl.DataFrame): DataFrame with at least columns [id_col, code_col].
43
+ id_col (str): Name of the column containing unique identifier. Default: "id".
44
+ code_col (str): Name of the column containing ICD codes. Default: "code".
45
+ icd_version (str): ICD version ('icd9', 'icd10', or 'icd9_10'). Default: "icd10".
46
+ icd_version_col (str, optional): Name of the column with ICD version for 'icd9_10'. Default: None.
47
+ implementation (str): Coding algorithm ('quan' or 'elixhauser'). Default: "quan".
48
+ weights (str): Weighting scheme ('van_walraven', 'thompson_30', 'thompson_29', 'ahrq'). Default: "van_walraven".
49
+ return_categories (bool): If True, also return presence indicators for each ECI category.
50
+
51
+ Returns:
52
+ - DataFrame with [id_col, score_col_name].
53
+ - DataFrame with category indicators if return_categories is True, else None.
54
+ """
55
+
56
+ # Check if input is pandas DataFrame and convert to polars
57
+ is_pandas = pd and isinstance(df, pd.DataFrame)
58
+ if is_pandas:
59
+ df = pl.from_pandas(df)
60
+
61
+ # Change ICD to ICD-9 for original Elixhauser and AHRQ ICD-9
62
+ if icd_version == "icd10" and implementation in [
63
+ "elixhauser",
64
+ "ahrq_icd9",
65
+ ]:
66
+ print(f"Warning: Implementation '{implementation}' only uses ICD-9. Setting ICD version to 'icd9'.") # fmt: skip
67
+ icd_version = "icd9"
68
+ # Change ICD to ICD-10 for AHRQ ICD-10
69
+ if icd_version == "icd9" and implementation in ["ahrq_icd10"]:
70
+ print(f"Warning: Implementation '{implementation}' only uses ICD-10. Setting ICD version to 'icd10'.") # fmt: skip
71
+ icd_version = "icd10"
72
+
73
+ # Input validation specific to Elixhauser
74
+ assert implementation in [
75
+ "quan",
76
+ "elixhauser",
77
+ ], "implementation must be one of: 'quan', 'elixhauser'."
78
+ assert weights in [
79
+ "van_walraven",
80
+ "thompson_30",
81
+ "thompson_29",
82
+ "ahrq",
83
+ ], "weights must be one of: 'van_walraven', 'thompson_30', 'thompson_29', or 'ahrq'."
84
+
85
+ # Determine definition file based on implementation
86
+ if implementation == "quan":
87
+ definition_file = "ELIXHAUSER_QUAN.csv"
88
+ elif implementation == "elixhauser":
89
+ definition_file = "ELIXHAUSER.csv"
90
+ else:
91
+ # Should be caught by assert earlier, but as a safeguard
92
+ raise ValueError(f"Unsupported implementation: {implementation}")
93
+
94
+ # Determine weight column and score column names based on weights argument
95
+ if weights == "van_walraven":
96
+ weight_col_name = "van_walraven_weights"
97
+ score_col_name = "Elixhauser van Walraven Score"
98
+ elif weights == "thompson_30":
99
+ weight_col_name = "Thompson_30_weights"
100
+ score_col_name = "Elixhauser Thompson(30) Score"
101
+ elif weights == "thompson_29":
102
+ weight_col_name = "Thompson_29_weights"
103
+ score_col_name = "Elixhauser Thompson(29) Score"
104
+ elif weights == "ahrq":
105
+ weight_col_name = "AHRQ_weights"
106
+ score_col_name = "Elixhauser AHRQ Score"
107
+ else:
108
+ # Should be caught by assert earlier, but as a safeguard
109
+ raise ValueError(f"Unsupported weights scheme: {weights}")
110
+
111
+ # Load definition and weight files
112
+ base_path = Path(__file__).parent / "common/"
113
+ definition_file_path = base_path / definition_file
114
+ weights_file_path = base_path / "ELIXHAUSER_WEIGHTS.csv"
115
+
116
+ df_definitions = pl.read_csv(definition_file_path)
117
+ df_weights = pl.read_csv(weights_file_path).drop("index")
118
+
119
+ # Join definitions and weights
120
+ # Ensure 'category' column exists in both for joining
121
+ if (
122
+ "category" not in df_definitions.columns
123
+ or "category" not in df_weights.columns
124
+ ):
125
+ raise ValueError("Both definition and weight files must contain a 'category' column.") # fmt: skip
126
+
127
+ # Perform the join
128
+ df_combined = df_definitions.join(df_weights, on="category", how="left")
129
+
130
+ # Define mutual exclusion rules for Elixhauser
131
+ mutual_exclusion_rules = [
132
+ ("Diabetes complicated", "Diabetes uncomplicated"),
133
+ ("Hypertension complicated", "Hypertension uncomplicated"),
134
+ ]
135
+
136
+ # Call the generalized function with the combined DataFrame
137
+ df_elixhauser = CustomComorbidityIndex(
138
+ df=df,
139
+ id_col=id_col,
140
+ code_col=code_col,
141
+ icd_version=icd_version,
142
+ icd_version_col=icd_version_col,
143
+ definition_data=df_combined,
144
+ weight_col_name=weight_col_name,
145
+ score_col_name=score_col_name,
146
+ mutual_exclusion_rules=mutual_exclusion_rules,
147
+ return_categories=return_categories,
148
+ )
149
+
150
+ if is_pandas:
151
+ return df_elixhauser.to_pandas()
152
+
153
+ return df_elixhauser