pycomorb 0.5.2__tar.gz → 0.6__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.
Files changed (37) hide show
  1. {pycomorb-0.5.2/src/pycomorb.egg-info → pycomorb-0.6}/PKG-INFO +1 -1
  2. {pycomorb-0.5.2 → pycomorb-0.6}/pyproject.toml +4 -2
  3. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/CharlsonComorbidityIndex.py +0 -9
  4. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/CustomComorbidityIndex.py +0 -9
  5. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/ElixhauserComorbidityIndex.py +0 -9
  6. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/GagneComorbidityIndex.py +0 -9
  7. pycomorb-0.6/src/pycomorb/ICDModifications.py +128 -0
  8. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/__init__.py +56 -13
  9. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_QUAN.csv +1 -1
  10. {pycomorb-0.5.2/src/pycomorb/germanmodification → pycomorb-0.6/src/pycomorb/modification_DE}/germanmodification.py +0 -1
  11. pycomorb-0.6/src/pycomorb/modification_DE/icd10gm.csv +2514 -0
  12. pycomorb-0.6/src/pycomorb/modification_US/clinicalmodification.py +152 -0
  13. pycomorb-0.6/src/pycomorb/modification_US/icd10cm.csv +5695 -0
  14. {pycomorb-0.5.2 → pycomorb-0.6/src/pycomorb.egg-info}/PKG-INFO +1 -1
  15. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb.egg-info/SOURCES.txt +4 -1
  16. pycomorb-0.5.2/src/pycomorb/ICDModifications.py +0 -61
  17. {pycomorb-0.5.2 → pycomorb-0.6}/LICENSE +0 -0
  18. {pycomorb-0.5.2 → pycomorb-0.6}/README.md +0 -0
  19. {pycomorb-0.5.2 → pycomorb-0.6}/setup.cfg +0 -0
  20. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/HospitalFrailtyRiskScore.py +0 -0
  21. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_AUSTRALIA.csv +0 -0
  22. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_DEYO.csv +0 -0
  23. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_DHOORE.csv +0 -0
  24. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_RCS.csv +0 -0
  25. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_ROMANO.csv +0 -0
  26. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_SWEDEN.csv +0 -0
  27. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/CHARLSON_UK_SHMI_v1.55.csv +0 -0
  28. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/ELIXHAUSER.csv +0 -0
  29. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/ELIXHAUSER_AHRQ_ICD10.csv +0 -0
  30. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/ELIXHAUSER_AHRQ_ICD9.csv +0 -0
  31. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/ELIXHAUSER_QUAN.csv +0 -0
  32. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/ELIXHAUSER_WEIGHTS.csv +0 -0
  33. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/GAGNE.csv +0 -0
  34. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb/common/HFRS.csv +0 -0
  35. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb.egg-info/dependency_links.txt +0 -0
  36. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb.egg-info/requires.txt +0 -0
  37. {pycomorb-0.5.2 → pycomorb-0.6}/src/pycomorb.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycomorb
3
- Version: 0.5.2
3
+ Version: 0.6
4
4
  Summary: Comorbidity and frailty score calculation utilities (Charlson, Elixhauser, Gagne, HFRS) using polars
5
5
  Author-email: Finn Fassbender <finn.fassbender@charite.de>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pycomorb"
3
- version = "0.5.2"
3
+ version = "0.6"
4
4
  authors = [
5
5
  { name="Finn Fassbender", email="finn.fassbender@charite.de" },
6
6
  ]
@@ -23,4 +23,6 @@ requires = ["setuptools >= 77.0.3"]
23
23
  build-backend = "setuptools.build_meta"
24
24
 
25
25
  [tool.setuptools.package-data]
26
- "pycomorb.common" = ["*.csv"]
26
+ "pycomorb.common" = ["*.csv"]
27
+ "pycomorb.modification_DE" = ["*.csv"]
28
+ "pycomorb.modification_US" = ["*.csv"]
@@ -28,7 +28,6 @@
28
28
 
29
29
  from pathlib import Path
30
30
 
31
- import pandas as pd
32
31
  import polars as pl
33
32
 
34
33
  # Import the generalized function
@@ -63,11 +62,6 @@ def CharlsonComorbidityIndex(
63
62
  - DataFrame with category indicators if return_categories is True, else None.
64
63
  """
65
64
 
66
- # Check if input is pandas DataFrame and convert to polars
67
- is_pandas = pd and isinstance(df, pd.DataFrame)
68
- if is_pandas:
69
- df = pl.from_pandas(df)
70
-
71
65
  # Change ICD to ICD-9 for Deyo, D'Hoore and Romano
72
66
  if icd_version == "icd10" and implementation in [
73
67
  "deyo",
@@ -210,9 +204,6 @@ def CharlsonComorbidityIndex(
210
204
  if not return_categories:
211
205
  df_charlson = df_charlson.drop("Age Score")
212
206
 
213
- if is_pandas:
214
- return df_charlson.to_pandas()
215
-
216
207
  return df_charlson
217
208
 
218
209
 
@@ -1,7 +1,6 @@
1
1
  from pathlib import Path
2
2
  from typing import List, Optional, Tuple, Union
3
3
 
4
- import pandas as pd
5
4
  import polars as pl
6
5
 
7
6
 
@@ -37,11 +36,6 @@ def CustomComorbidityIndex(
37
36
  - DataFrame with category indicators if return_categories is True, else None.
38
37
  """
39
38
 
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
39
  # Input validation
46
40
  assert icd_version in [
47
41
  "icd9",
@@ -226,7 +220,4 @@ def CustomComorbidityIndex(
226
220
  {score_col_name: int}
227
221
  )
228
222
 
229
- if is_pandas:
230
- comorbidity_df = comorbidity_df.to_pandas()
231
-
232
223
  return comorbidity_df
@@ -18,7 +18,6 @@
18
18
 
19
19
  from pathlib import Path
20
20
 
21
- import pandas as pd
22
21
  import polars as pl
23
22
 
24
23
  # Import the generalized function
@@ -53,11 +52,6 @@ def ElixhauserComorbidityIndex(
53
52
  - DataFrame with category indicators if return_categories is True, else None.
54
53
  """
55
54
 
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
55
  # Change ICD to ICD-9 for original Elixhauser and AHRQ ICD-9
62
56
  if icd_version == "icd10" and implementation in [
63
57
  "elixhauser",
@@ -147,7 +141,4 @@ def ElixhauserComorbidityIndex(
147
141
  return_categories=return_categories,
148
142
  )
149
143
 
150
- if is_pandas:
151
- return df_elixhauser.to_pandas()
152
-
153
144
  return df_elixhauser
@@ -22,7 +22,6 @@
22
22
 
23
23
  from pathlib import Path
24
24
 
25
- import pandas as pd
26
25
  import polars as pl
27
26
 
28
27
  # Import the generalized function
@@ -54,11 +53,6 @@ def GagneComorbidityIndex(
54
53
  - DataFrame with category indicators if return_categories is True, else None.
55
54
  """
56
55
 
57
- # Check if input is pandas DataFrame and convert to polars
58
- is_pandas = pd and isinstance(df, pd.DataFrame)
59
- if is_pandas:
60
- df = pl.from_pandas(df)
61
-
62
56
  assert (
63
57
  id_col in df.columns
64
58
  ), f"Column '{id_col}' (ID) must be present in input DataFrame."
@@ -90,7 +84,4 @@ def GagneComorbidityIndex(
90
84
  return_categories=return_categories,
91
85
  )
92
86
 
93
- if is_pandas:
94
- return df_gagne.to_pandas()
95
-
96
87
  return df_gagne
@@ -0,0 +1,128 @@
1
+ # Reference for ICD-10-CM modifications:
2
+ # CMS: https://www.cms.gov/medicare/coding-billing/icd-10-codes
3
+
4
+ # Reference for ICD-10-GM modifications:
5
+ # BfArM: https://www.bfarm.de/DE/Kodiersysteme/Services/Downloads/_node.html
6
+
7
+ from pathlib import Path
8
+
9
+ import polars as pl
10
+
11
+
12
+ def get_icdmodification(
13
+ data: pl.DataFrame,
14
+ transfer_file_path: str,
15
+ code_col="code",
16
+ year_col="year",
17
+ target_year=2024,
18
+ ) -> pl.DataFrame:
19
+ """
20
+ Map ICD codes to the target year using official transfer files.
21
+
22
+ Args:
23
+ data (pl.DataFrame): Input DataFrame with at least columns [code_col, year_col].
24
+ transfer_file_path (str): Path to the CSV file containing the transfer mappings.
25
+ code_col (str): Name of the column containing ICD codes. Default: "code".
26
+ year_col (str): Name of the column containing the year of the ICD code. Default: "year".
27
+ target_year (int): Target year for ICD codes (e.g., 2024). Default: 2024.
28
+
29
+ Returns:
30
+ pl.DataFrame: DataFrame with original columns plus a new column 'icdmod_{target_year}' with mapped codes.
31
+ """
32
+
33
+ assert (
34
+ code_col in data.columns
35
+ ), f"Column '{code_col}' (ICD code) must be present in input DataFrame."
36
+ assert (
37
+ data.height
38
+ == data.filter(pl.col(code_col).str.contains(r"^[A-Z]")).height
39
+ ), f"All values in column '{code_col}' must start with an uppercase letter (A-Z)."
40
+ assert (
41
+ year_col in data.columns
42
+ ), f"Column '{year_col}' (year) must be present in input DataFrame."
43
+
44
+ # Unpivot the data to long format
45
+ transfer_data = (
46
+ pl.read_csv(transfer_file_path, null_values=["", " "], separator=",")
47
+ .unpivot(
48
+ index=str(target_year),
49
+ variable_name=year_col,
50
+ value_name=code_col,
51
+ )
52
+ .cast({year_col: int})
53
+ .rename({str(target_year): f"icd_{target_year}"})
54
+ )
55
+
56
+ return (
57
+ data.join(transfer_data, on=[code_col, year_col], how="left")
58
+ # Prefer mapped code, but fall back to original if no mapping available
59
+ .with_columns(
60
+ pl.coalesce(pl.col(f"icd_{target_year}"), pl.col(code_col)).alias(
61
+ f"icd_{target_year}"
62
+ )
63
+ )
64
+ )
65
+
66
+
67
+ def get_icd10gm(
68
+ data: pl.DataFrame, code_col="code", year_col="year", target_year=2024
69
+ ) -> pl.DataFrame:
70
+ """
71
+ Map ICD-10-GM codes to the target year using official transfer files.
72
+
73
+ Args:
74
+ data (pl.DataFrame): Input DataFrame with at least columns [code_col, year_col].
75
+ code_col (str): Name of the column containing ICD codes. Default: "code".
76
+ year_col (str): Name of the column containing the year of the ICD code. Default: "year".
77
+ target_year (int): Target year for ICD-10-GM codes (e.g., 2024). Default: 2024.
78
+
79
+ Returns:
80
+ pl.DataFrame: DataFrame with original columns plus a new column 'icd10gm_{target_year}' with mapped codes.
81
+ """
82
+
83
+ assert target_year in range(
84
+ 2004, 2026
85
+ ), "Target year must be between 2004 and 2025."
86
+
87
+ # Load all transfer files into a dictionary of DataFrames
88
+ transfer_file_path = Path(__file__).parent / "modification_DE/icd10gm.csv" # fmt: skip
89
+
90
+ return get_icdmodification(
91
+ data=data,
92
+ transfer_file_path=transfer_file_path,
93
+ code_col=code_col,
94
+ year_col=year_col,
95
+ target_year=target_year,
96
+ )
97
+
98
+
99
+ def get_icd10cm(
100
+ data: pl.DataFrame, code_col="code", year_col="year", target_year=2024
101
+ ) -> pl.DataFrame:
102
+ """
103
+ Map ICD-10-CM codes to the target year using official transfer files.
104
+
105
+ Args:
106
+ data (pl.DataFrame): Input DataFrame with at least columns [code_col, year_col].
107
+ code_col (str): Name of the column containing ICD codes. Default: "code".
108
+ year_col (str): Name of the column containing the year of the ICD code. Default: "year".
109
+ target_year (int): Target year for ICD-10-CM codes (e.g., 2024). Default: 2024.
110
+
111
+ Returns:
112
+ pl.DataFrame: DataFrame with original columns plus a new column 'icd10gm_{target_year}' with mapped codes.
113
+ """
114
+
115
+ assert target_year in range(
116
+ 2015, 2026
117
+ ), "Target year must be between 2004 and 2025."
118
+
119
+ # Load all transfer files into a dictionary of DataFrames
120
+ transfer_file_path = Path(__file__).parent / "modification_US/icd10cm.csv" # fmt: skip
121
+
122
+ return get_icdmodification(
123
+ data=data,
124
+ transfer_file_path=transfer_file_path,
125
+ code_col=code_col,
126
+ year_col=year_col,
127
+ target_year=target_year,
128
+ )
@@ -2,14 +2,17 @@
2
2
 
3
3
  __author__ = """Finn Fassbender"""
4
4
  __email__ = "finn.fassbender@charite.de"
5
- __version__ = "0.5.2"
5
+ __version__ = "0.6"
6
+
7
+ import pandas as pd
8
+ import polars as pl
6
9
 
7
10
  from .CharlsonComorbidityIndex import CharlsonComorbidityIndex
11
+ from .CustomComorbidityIndex import CustomComorbidityIndex
8
12
  from .ElixhauserComorbidityIndex import ElixhauserComorbidityIndex
9
13
  from .GagneComorbidityIndex import GagneComorbidityIndex
10
- from .CustomComorbidityIndex import CustomComorbidityIndex
11
14
  from .HospitalFrailtyRiskScore import HospitalFrailtyRiskScore
12
- from .ICDModifications import get_icd10gm
15
+ from .ICDModifications import get_icd10cm, get_icd10gm
13
16
 
14
17
 
15
18
  def comorbidity(
@@ -31,6 +34,7 @@ def comorbidity(
31
34
  score_col_name: str = "Custom Comorbidity Score",
32
35
  mutual_exclusion_rules: list[tuple[str, str]] = None,
33
36
  return_categories: bool = False,
37
+ fix_dot_in_icd_code: bool = False,
34
38
  ):
35
39
  """
36
40
  Unified wrapper to calculate a comorbidity or frailty score.
@@ -42,7 +46,7 @@ def comorbidity(
42
46
  code_col (str): Name of the column containing ICD codes. Default: "code".
43
47
  age_col (str): Name of the column containing patient ages (Charlson only). Default: "age".
44
48
  year_col (str): Name of the column containing the year of the ICD code (for ICD history modifications). Default: "year".
45
- icd_version (str): ICD version. Default: "icd10".
49
+ icd_version (str): ICD version. Must be one of: 'icd9', 'icd10', or 'icd9_10'. Default: "icd10".
46
50
  icd_version_col (str, optional): Name of the column with ICD version for 'icd9_10'. Default: None.
47
51
  icd_modification (str, optional): ICD modification to apply ('icd10gm'). Default: None.
48
52
  icd_modification_target_year (int): Target year for ICD modification (if applicable). Default: 2004.
@@ -54,23 +58,57 @@ def comorbidity(
54
58
  score_col_name (str): Name for the calculated score column (Custom only).
55
59
  mutual_exclusion_rules (list of tuple, optional): List of mutually exclusive category pairs (Custom only).
56
60
  return_categories (bool): Whether to return category indicators.
61
+ fix_dot_in_icd_code (bool): Whether to remove dots from ICD codes before processing. Default: False.
57
62
 
58
63
  Returns:
59
64
  - DataFrame with [id_col, score].
60
65
  - DataFrame with category indicators if return_categories is True, else None.
61
66
  """
67
+
68
+ # Check if input is pandas DataFrame and convert to polars
69
+ is_pandas = pd and isinstance(df, pd.DataFrame)
70
+ if is_pandas:
71
+ df = pl.from_pandas(df)
72
+
73
+ # remove dots from ICD codes if requested
74
+ if fix_dot_in_icd_code:
75
+ df = df.with_columns(
76
+ pl.col(code_col).str.replace(".", "", literal=True)
77
+ )
78
+
79
+ # check that no dots are present in ICD codes (Polars-native)
80
+ contains_dot = df.select(
81
+ pl.col(code_col).str.contains(r"\.").any().alias("contains_dot")
82
+ ).to_dicts()[0]["contains_dot"]
83
+ assert (
84
+ not contains_dot
85
+ ), f"All values in column '{code_col}' must not contain dots ('.'). Consider setting fix_dot_in_icd_code=True."
86
+
87
+ # strip whitespace from ICD codes, and make uppercase
88
+ df = df.with_columns(
89
+ pl.col(code_col).str.strip_chars().str.to_uppercase()
90
+ )
91
+
62
92
  # apply ICD modification if requested
63
- if icd_modification is not None:
64
- if icd_modification.lower() == "icd10gm":
93
+ if icd_modification is not None and "10" in icd_version:
94
+ if icd_modification.lower() == "icd10cm":
95
+ df = get_icd10cm(
96
+ data=df,
97
+ code_col=code_col,
98
+ year_col=year_col,
99
+ target_year=icd_modification_target_year,
100
+ )
101
+ code_col = f"icd_{icd_modification_target_year}"
102
+ elif icd_modification.lower() == "icd10gm":
65
103
  df = get_icd10gm(
66
104
  data=df,
67
105
  code_col=code_col,
68
106
  year_col=year_col,
69
107
  target_year=icd_modification_target_year,
70
108
  )
71
- code_col = f"icd10gm_{icd_modification_target_year}"
109
+ code_col = f"icd_{icd_modification_target_year}"
72
110
  else:
73
- raise ValueError(f"Unknown icd_modification: '{icd_modification}'. Currently, only 'icd10gm' is supported.") # fmt: skip
111
+ raise ValueError(f"Unknown icd_modification: '{icd_modification}'. Currently, only 'icd10gm' and 'icd10cm' are supported.") # fmt: skip
74
112
 
75
113
  # calculate scores
76
114
  score = score.lower()
@@ -82,7 +120,7 @@ def comorbidity(
82
120
  ):
83
121
  if age_col not in df.columns:
84
122
  raise ValueError(f"Column '{age_col}' (age) must be present in input DataFrame for Charlson calculation.") # fmt: skip
85
- return CharlsonComorbidityIndex(
123
+ return_df = CharlsonComorbidityIndex(
86
124
  df=df,
87
125
  id_col=id_col,
88
126
  code_col=code_col,
@@ -98,7 +136,7 @@ def comorbidity(
98
136
  "elixhausercomorbidityindex",
99
137
  "elixhauser_comorbidity_index",
100
138
  ):
101
- return ElixhauserComorbidityIndex(
139
+ return_df = ElixhauserComorbidityIndex(
102
140
  df=df,
103
141
  id_col=id_col,
104
142
  code_col=code_col,
@@ -117,7 +155,7 @@ def comorbidity(
117
155
  "combinedcomorbidityindex",
118
156
  "combined_comorbidity_index",
119
157
  ):
120
- return GagneComorbidityIndex(
158
+ return_df = GagneComorbidityIndex(
121
159
  df=df,
122
160
  id_col=id_col,
123
161
  code_col=code_col,
@@ -131,7 +169,7 @@ def comorbidity(
131
169
  "hospitalfrailtyriskscore",
132
170
  "hospital_frailty_risk_score",
133
171
  ):
134
- return HospitalFrailtyRiskScore(
172
+ return_df = HospitalFrailtyRiskScore(
135
173
  df=df,
136
174
  id_col=id_col,
137
175
  code_col=code_col,
@@ -143,7 +181,7 @@ def comorbidity(
143
181
  "customcomorbidityindex",
144
182
  "custom_comorbidity_index",
145
183
  ):
146
- return CustomComorbidityIndex(
184
+ return_df = CustomComorbidityIndex(
147
185
  df=df,
148
186
  id_col=id_col,
149
187
  code_col=code_col,
@@ -160,3 +198,8 @@ def comorbidity(
160
198
  f"Unknown score: '{score}'. Must be one of: "
161
199
  "'charlson', 'elixhauser', 'gagne', 'hfrs' or 'custom'."
162
200
  )
201
+
202
+ if is_pandas:
203
+ return return_df.to_pandas()
204
+
205
+ return return_df
@@ -1,7 +1,7 @@
1
1
  index,category,icd9_codes,icd10_codes,weights
2
2
  0,Age,XXXX|XXXX,YYYY|YYYY,1
3
3
  1,Myocardial infarction,410|412,I21|I22|I252,1
4
- 2,Congestive heart failure,428|4254|4259|39891|40201|40211|40291|40401|40403|40411|40413|40491|40493,I43|I50|I099|I110|I130|I132|I255|I420|I425|I426|I427|I428|I429|P290,1
4
+ 2,Congestive heart failure,428|4254|4255|4256|4257|4258|4259|39891|40201|40211|40291|40401|40403|40411|40413|40491|40493,I43|I50|I099|I110|I130|I132|I255|I420|I425|I426|I427|I428|I429|P290,1
5
5
  3,Peripheral vascular disease,440|441|0930|4373|4471|5571|5579|V434|4431|4432|4433|4434|4435|4436|4437|4438|4439,I70|I71|I731|I738|I739|I771|I790|I792|K551|K558|K559|Z958|Z959,1
6
6
  4,Cerebrovascular disease,430|431|432|433|434|435|436|437|438|36234,G45|G46|I60|I61|I62|I63|I64|I65|I66|I67|I68|I69|H340,1
7
7
  5,Dementia,290|2941|3312,F00|F01|F02|F03|G30|F051|G311,1
@@ -3,7 +3,6 @@
3
3
  # 3. take only the transfer files (e.g. icd10gm2013syst_umsteiger_2012_2013.txt for 2012->2013) and put them into a separate folder, e.g. data/icd10gm/transfers
4
4
  # 4. run this script in that folder
5
5
 
6
- import os
7
6
  import polars as pl
8
7
 
9
8
  files = [