Spaces:
Running
Running
Chris McMaster
commited on
Commit
·
04e78e6
1
Parent(s):
8b42c1d
Improved DBI
Browse files- app.py +48 -2
- dbi_mcp.py +247 -19
- dbi_reference_by_route.csv +4 -0
app.py
CHANGED
@@ -3,7 +3,7 @@ from typing import Dict, Any
|
|
3 |
from datetime import datetime
|
4 |
|
5 |
from brand_to_generic import brand_lookup
|
6 |
-
from dbi_mcp import dbi_mcp
|
7 |
from clinical_calculators import (
|
8 |
cockcroft_gault_creatinine_clearance,
|
9 |
ckd_epi_egfr,
|
@@ -53,6 +53,12 @@ def _dbi_mcp_gradio(text_block: str, route: str = "oral"):
|
|
53 |
return standardize_response(result, "dbi_calculator")
|
54 |
|
55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
@with_error_handling
|
57 |
def _cockcroft_gault_gradio(
|
58 |
age: int, weight_kg: float, serum_creatinine: float, is_female: bool
|
@@ -249,6 +255,29 @@ def calculate_drug_burden_index_mcp(drug_list: str, route: str = "oral") -> str:
|
|
249 |
return format_json_output(result)
|
250 |
|
251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
@with_error_handling
|
253 |
def calculate_creatinine_clearance_mcp(
|
254 |
age: str, weight_kg: str, serum_creatinine: str, is_female: str
|
@@ -739,11 +768,26 @@ dbi_calculator_ui = gr.Interface(
|
|
739 |
gr.Text(label="Route of Administration", value="oral"),
|
740 |
],
|
741 |
outputs=gr.JSON(label="DBI Calculation"),
|
742 |
-
title="DBI Calculator",
|
743 |
api_name="dbi_calculator",
|
744 |
description="Calculate Drug Burden Index (DBI) from a list of medications. Supports PRN and various dose formats.",
|
745 |
)
|
746 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
747 |
cockcroft_gault_ui = gr.Interface(
|
748 |
fn=calculate_creatinine_clearance_mcp,
|
749 |
inputs=[
|
@@ -850,6 +894,7 @@ demo = gr.TabbedInterface(
|
|
850 |
livertox_ui,
|
851 |
brand_generic_ui,
|
852 |
dbi_calculator_ui,
|
|
|
853 |
cockcroft_gault_ui,
|
854 |
ckd_epi_ui,
|
855 |
child_pugh_ui,
|
@@ -868,6 +913,7 @@ demo = gr.TabbedInterface(
|
|
868 |
"LiverTox",
|
869 |
"Brand to Generic",
|
870 |
"DBI Calculator",
|
|
|
871 |
"Creatinine CL",
|
872 |
"eGFR",
|
873 |
"Child-Pugh",
|
|
|
3 |
from datetime import datetime
|
4 |
|
5 |
from brand_to_generic import brand_lookup
|
6 |
+
from dbi_mcp import dbi_mcp, dbi_mcp_mixed_routes
|
7 |
from clinical_calculators import (
|
8 |
cockcroft_gault_creatinine_clearance,
|
9 |
ckd_epi_egfr,
|
|
|
53 |
return standardize_response(result, "dbi_calculator")
|
54 |
|
55 |
|
56 |
+
@with_error_handling
|
57 |
+
def _dbi_mcp_mixed_routes_gradio(text_block: str):
|
58 |
+
result = dbi_mcp_mixed_routes(text_block, ref_csv="dbi_reference_by_route.csv")
|
59 |
+
return standardize_response(result, "dbi_calculator_mixed_routes")
|
60 |
+
|
61 |
+
|
62 |
@with_error_handling
|
63 |
def _cockcroft_gault_gradio(
|
64 |
age: int, weight_kg: float, serum_creatinine: float, is_female: bool
|
|
|
255 |
return format_json_output(result)
|
256 |
|
257 |
|
258 |
+
@with_error_handling
|
259 |
+
def calculate_drug_burden_index_mixed_routes_mcp(drug_list: str) -> str:
|
260 |
+
"""
|
261 |
+
Calculate Drug Burden Index (DBI) from a list of medications with automatic route detection.
|
262 |
+
|
263 |
+
This enhanced version automatically detects the route of administration for each medication
|
264 |
+
(oral, transdermal patches, parenteral injections, etc.) and uses the appropriate reference
|
265 |
+
data for each route. Perfect for mixed medication lists.
|
266 |
+
|
267 |
+
Args:
|
268 |
+
drug_list (str): Drug list (one per line, include dose and frequency - also write "prn" if the drug is a PRN medication)
|
269 |
+
Examples:
|
270 |
+
- "Fentanyl 25mcg/hr patch daily"
|
271 |
+
- "Amitriptyline 25mg tablet twice daily"
|
272 |
+
- "Morphine 10mg injection PRN"
|
273 |
+
|
274 |
+
Returns:
|
275 |
+
str: JSON string with DBI calculation results broken down by route and individual drug contributions
|
276 |
+
"""
|
277 |
+
result = _dbi_mcp_mixed_routes_gradio(drug_list)
|
278 |
+
return format_json_output(result)
|
279 |
+
|
280 |
+
|
281 |
@with_error_handling
|
282 |
def calculate_creatinine_clearance_mcp(
|
283 |
age: str, weight_kg: str, serum_creatinine: str, is_female: str
|
|
|
768 |
gr.Text(label="Route of Administration", value="oral"),
|
769 |
],
|
770 |
outputs=gr.JSON(label="DBI Calculation"),
|
771 |
+
title="DBI Calculator (Single Route)",
|
772 |
api_name="dbi_calculator",
|
773 |
description="Calculate Drug Burden Index (DBI) from a list of medications. Supports PRN and various dose formats.",
|
774 |
)
|
775 |
|
776 |
+
dbi_mixed_routes_ui = gr.Interface(
|
777 |
+
fn=calculate_drug_burden_index_mixed_routes_mcp,
|
778 |
+
inputs=[
|
779 |
+
gr.Textbox(
|
780 |
+
label="Drug List (one per line, include dose and frequency)",
|
781 |
+
lines=10,
|
782 |
+
placeholder="e.g., Fentanyl 25mcg/hr patch daily\nAmitriptyline 25mg tablet twice daily\nMorphine 10mg injection PRN",
|
783 |
+
),
|
784 |
+
],
|
785 |
+
outputs=gr.JSON(label="DBI Calculation with Route Detection"),
|
786 |
+
title="DBI Calculator (Mixed Routes)",
|
787 |
+
api_name="dbi_calculator_mixed_routes",
|
788 |
+
description="Enhanced DBI calculator that automatically detects routes (oral, patches, injections, etc.) and uses appropriate reference data for each medication.",
|
789 |
+
)
|
790 |
+
|
791 |
cockcroft_gault_ui = gr.Interface(
|
792 |
fn=calculate_creatinine_clearance_mcp,
|
793 |
inputs=[
|
|
|
894 |
livertox_ui,
|
895 |
brand_generic_ui,
|
896 |
dbi_calculator_ui,
|
897 |
+
dbi_mixed_routes_ui,
|
898 |
cockcroft_gault_ui,
|
899 |
ckd_epi_ui,
|
900 |
child_pugh_ui,
|
|
|
913 |
"LiverTox",
|
914 |
"Brand to Generic",
|
915 |
"DBI Calculator",
|
916 |
+
"DBI Mixed Routes",
|
917 |
"Creatinine CL",
|
918 |
"eGFR",
|
919 |
"Child-Pugh",
|
dbi_mcp.py
CHANGED
@@ -18,9 +18,13 @@ except ImportError:
|
|
18 |
|
19 |
__all__ = [
|
20 |
"load_reference",
|
|
|
21 |
"load_patient_meds",
|
22 |
"calculate_dbi",
|
23 |
"print_report",
|
|
|
|
|
|
|
24 |
]
|
25 |
|
26 |
PatientInput = Union[
|
@@ -29,12 +33,69 @@ PatientInput = Union[
|
|
29 |
Mapping[str, float],
|
30 |
]
|
31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
def _normalise_name(name: str) -> str:
|
34 |
"""Strip/-lower a drug name for key matching."""
|
35 |
return name.strip().lower()
|
36 |
|
37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
def load_reference(
|
39 |
ref_path: Path,
|
40 |
*,
|
@@ -72,6 +133,51 @@ def load_reference(
|
|
72 |
|
73 |
return ref
|
74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
def calculate_dbi(
|
76 |
patient_meds: Mapping[str, float],
|
77 |
reference: Mapping[str, Tuple[float, str]],
|
@@ -141,7 +247,7 @@ def _freq_to_per_day(token: str) -> float:
|
|
141 |
return 24 / hrs if hrs else 1
|
142 |
return 1
|
143 |
|
144 |
-
Parsed = Tuple[str, float, bool]
|
145 |
|
146 |
@functools.lru_cache(maxsize=2048)
|
147 |
def _parse_line(line: str) -> Optional[Parsed]:
|
@@ -150,13 +256,15 @@ def _parse_line(line: str) -> Optional[Parsed]:
|
|
150 |
return None
|
151 |
|
152 |
is_prn = "prn" in original.lower()
|
|
|
153 |
|
154 |
m_patch = PATCH_PAT.search(original)
|
155 |
if m_patch:
|
156 |
mcg_hr = float(m_patch.group("val").replace(",", "."))
|
157 |
mg_day = (mcg_hr * 24) / 1_000 # µg/hr → mg/day
|
158 |
name_part = PATCH_PAT.sub("", original).split()[0]
|
159 |
-
|
|
|
160 |
|
161 |
m_conc = CONC_PAT.search(original)
|
162 |
m_vol = VOL_PAT.search(original)
|
@@ -175,7 +283,7 @@ def _parse_line(line: str) -> Optional[Parsed]:
|
|
175 |
freq = _freq_to_per_day(m_freq.group(0))
|
176 |
mg_day = mg_per_dose * freq
|
177 |
name_part = CONC_PAT.split(original)[0].strip()
|
178 |
-
return (name_part, mg_day, is_prn)
|
179 |
|
180 |
m = UNIT_PAT.search(original)
|
181 |
if m:
|
@@ -192,30 +300,35 @@ def _parse_line(line: str) -> Optional[Parsed]:
|
|
192 |
name_part = original[:m.start()].strip()
|
193 |
name_part = re.sub(r"[^A-Za-z0-9\s]", " ", name_part)
|
194 |
name_part = re.sub(r"\s+", " ", name_part).strip()
|
195 |
-
return (name_part, mg_day, is_prn)
|
196 |
|
197 |
logger.debug("unhandled line: %s", original)
|
198 |
return None
|
199 |
|
200 |
-
def _smart_drug_lookup(raw_name: str,
|
201 |
"""
|
202 |
Smart drug name resolution that avoids unnecessary API calls.
|
|
|
203 |
|
204 |
-
1. First checks if the name (or close variant) exists in reference data
|
205 |
2. Only calls brand_lookup API if not found in reference
|
206 |
3. Returns the best generic name match
|
207 |
"""
|
208 |
clean_name = raw_name.strip().lower()
|
209 |
|
210 |
-
|
211 |
-
|
212 |
-
|
|
|
|
|
213 |
|
214 |
-
for
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
|
|
|
|
219 |
|
220 |
common_variations = {
|
221 |
'acetaminophen': 'paracetamol',
|
@@ -228,9 +341,11 @@ def _smart_drug_lookup(raw_name: str, reference_data: dict) -> str:
|
|
228 |
|
229 |
if clean_name in common_variations:
|
230 |
alt_name = common_variations[clean_name]
|
231 |
-
|
232 |
-
|
233 |
-
|
|
|
|
|
234 |
|
235 |
logger.debug(f"'{raw_name}' not found in reference data, trying brand lookup API")
|
236 |
try:
|
@@ -263,8 +378,11 @@ def dbi_mcp(text_block: str, *, ref_csv: Union[str, Path] = "dbi_reference_by_ro
|
|
263 |
meds_with: Dict[str, float] = {}
|
264 |
meds_without: Dict[str, float] = {}
|
265 |
|
266 |
-
for
|
267 |
-
|
|
|
|
|
|
|
268 |
|
269 |
meds_with[generic] = meds_with.get(generic, 0.0) + mg_day
|
270 |
if not is_prn:
|
@@ -286,6 +404,116 @@ def dbi_mcp(text_block: str, *, ref_csv: Union[str, Path] = "dbi_reference_by_ro
|
|
286 |
}
|
287 |
|
288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
if __name__ == "__main__":
|
290 |
import sys
|
291 |
import pprint
|
|
|
18 |
|
19 |
__all__ = [
|
20 |
"load_reference",
|
21 |
+
"load_all_routes_reference",
|
22 |
"load_patient_meds",
|
23 |
"calculate_dbi",
|
24 |
"print_report",
|
25 |
+
"detect_route_from_text",
|
26 |
+
"dbi_mcp",
|
27 |
+
"dbi_mcp_mixed_routes",
|
28 |
]
|
29 |
|
30 |
PatientInput = Union[
|
|
|
33 |
Mapping[str, float],
|
34 |
]
|
35 |
|
36 |
+
# Route detection patterns
|
37 |
+
ROUTE_PATTERNS = {
|
38 |
+
'transdermal': [
|
39 |
+
r'\bpatch(es)?\b',
|
40 |
+
r'\btransdermal\b',
|
41 |
+
r'\bmcg/hr\b',
|
42 |
+
r'\bμg/hr\b',
|
43 |
+
r'\bmicrograms?/hr\b',
|
44 |
+
r'\bmicrograms?/hour\b',
|
45 |
+
],
|
46 |
+
'parenteral': [
|
47 |
+
r'\binjection\b',
|
48 |
+
r'\biv\b',
|
49 |
+
r'\bim\b',
|
50 |
+
r'\bsc\b',
|
51 |
+
r'\bsubcut\b',
|
52 |
+
r'\bsubcutaneous\b',
|
53 |
+
r'\bintravenous\b',
|
54 |
+
r'\bintramuscular\b',
|
55 |
+
r'\bparenteral\b',
|
56 |
+
],
|
57 |
+
'sublingual_buccal': [
|
58 |
+
r'\bsublingual\b',
|
59 |
+
r'\bbuccal\b',
|
60 |
+
r'\bsl\b',
|
61 |
+
r'\bunder.?tongue\b',
|
62 |
+
],
|
63 |
+
'oral': [
|
64 |
+
r'\btab(let)?s?\b',
|
65 |
+
r'\bcap(sule)?s?\b',
|
66 |
+
r'\boral\b',
|
67 |
+
r'\bpo\b',
|
68 |
+
r'\bby.?mouth\b',
|
69 |
+
r'\bliquid\b',
|
70 |
+
r'\bsyrup\b',
|
71 |
+
r'\bsolution\b',
|
72 |
+
r'\bsuspension\b',
|
73 |
+
]
|
74 |
+
}
|
75 |
+
|
76 |
|
77 |
def _normalise_name(name: str) -> str:
|
78 |
"""Strip/-lower a drug name for key matching."""
|
79 |
return name.strip().lower()
|
80 |
|
81 |
|
82 |
+
def detect_route_from_text(text: str) -> str:
|
83 |
+
"""
|
84 |
+
Detect the most likely route of administration from medication text.
|
85 |
+
Returns the detected route or 'oral' as default.
|
86 |
+
"""
|
87 |
+
text_lower = text.lower()
|
88 |
+
|
89 |
+
# Check each route pattern
|
90 |
+
for route, patterns in ROUTE_PATTERNS.items():
|
91 |
+
for pattern in patterns:
|
92 |
+
if re.search(pattern, text_lower):
|
93 |
+
return route
|
94 |
+
|
95 |
+
# Default to oral if no specific route detected
|
96 |
+
return 'oral'
|
97 |
+
|
98 |
+
|
99 |
def load_reference(
|
100 |
ref_path: Path,
|
101 |
*,
|
|
|
133 |
|
134 |
return ref
|
135 |
|
136 |
+
|
137 |
+
def load_all_routes_reference(
|
138 |
+
ref_path: Path,
|
139 |
+
*,
|
140 |
+
use_pandas: bool | None = None,
|
141 |
+
) -> Dict[str, Dict[str, Tuple[float, str]]]:
|
142 |
+
"""
|
143 |
+
Load reference data for all routes.
|
144 |
+
Returns mapping: route → {generic → (δ, drug_class)}
|
145 |
+
"""
|
146 |
+
if use_pandas is None:
|
147 |
+
use_pandas = pd is not None
|
148 |
+
|
149 |
+
all_routes: Dict[str, Dict[str, Tuple[float, str]]] = {}
|
150 |
+
|
151 |
+
if use_pandas:
|
152 |
+
df = pd.read_csv(ref_path)
|
153 |
+
for _, row in df.iterrows():
|
154 |
+
route = row["route"].strip().lower()
|
155 |
+
generic = _normalise_name(row["generic_name"])
|
156 |
+
|
157 |
+
if route not in all_routes:
|
158 |
+
all_routes[route] = {}
|
159 |
+
|
160 |
+
all_routes[route][generic] = (
|
161 |
+
float(row["min_daily_dose_mg"]),
|
162 |
+
row["drug_class"].strip().lower(),
|
163 |
+
)
|
164 |
+
else:
|
165 |
+
with ref_path.open(newline="") as f:
|
166 |
+
rdr = csv.DictReader(f)
|
167 |
+
for row in rdr:
|
168 |
+
route = row["route"].strip().lower()
|
169 |
+
generic = _normalise_name(row["generic_name"])
|
170 |
+
|
171 |
+
if route not in all_routes:
|
172 |
+
all_routes[route] = {}
|
173 |
+
|
174 |
+
all_routes[route][generic] = (
|
175 |
+
float(row["min_daily_dose_mg"]),
|
176 |
+
row["drug_class"].strip().lower(),
|
177 |
+
)
|
178 |
+
|
179 |
+
return all_routes
|
180 |
+
|
181 |
def calculate_dbi(
|
182 |
patient_meds: Mapping[str, float],
|
183 |
reference: Mapping[str, Tuple[float, str]],
|
|
|
247 |
return 24 / hrs if hrs else 1
|
248 |
return 1
|
249 |
|
250 |
+
Parsed = Tuple[str, float, bool, str] # Added route detection
|
251 |
|
252 |
@functools.lru_cache(maxsize=2048)
|
253 |
def _parse_line(line: str) -> Optional[Parsed]:
|
|
|
256 |
return None
|
257 |
|
258 |
is_prn = "prn" in original.lower()
|
259 |
+
detected_route = detect_route_from_text(original)
|
260 |
|
261 |
m_patch = PATCH_PAT.search(original)
|
262 |
if m_patch:
|
263 |
mcg_hr = float(m_patch.group("val").replace(",", "."))
|
264 |
mg_day = (mcg_hr * 24) / 1_000 # µg/hr → mg/day
|
265 |
name_part = PATCH_PAT.sub("", original).split()[0]
|
266 |
+
# Override route detection for patches
|
267 |
+
return (name_part, mg_day, is_prn, "transdermal")
|
268 |
|
269 |
m_conc = CONC_PAT.search(original)
|
270 |
m_vol = VOL_PAT.search(original)
|
|
|
283 |
freq = _freq_to_per_day(m_freq.group(0))
|
284 |
mg_day = mg_per_dose * freq
|
285 |
name_part = CONC_PAT.split(original)[0].strip()
|
286 |
+
return (name_part, mg_day, is_prn, detected_route)
|
287 |
|
288 |
m = UNIT_PAT.search(original)
|
289 |
if m:
|
|
|
300 |
name_part = original[:m.start()].strip()
|
301 |
name_part = re.sub(r"[^A-Za-z0-9\s]", " ", name_part)
|
302 |
name_part = re.sub(r"\s+", " ", name_part).strip()
|
303 |
+
return (name_part, mg_day, is_prn, detected_route)
|
304 |
|
305 |
logger.debug("unhandled line: %s", original)
|
306 |
return None
|
307 |
|
308 |
+
def _smart_drug_lookup(raw_name: str, all_routes_reference: Dict[str, Dict[str, Tuple[float, str]]]) -> str:
|
309 |
"""
|
310 |
Smart drug name resolution that avoids unnecessary API calls.
|
311 |
+
Works with multi-route reference data.
|
312 |
|
313 |
+
1. First checks if the name (or close variant) exists in any route's reference data
|
314 |
2. Only calls brand_lookup API if not found in reference
|
315 |
3. Returns the best generic name match
|
316 |
"""
|
317 |
clean_name = raw_name.strip().lower()
|
318 |
|
319 |
+
# Check all routes for direct match
|
320 |
+
for route_data in all_routes_reference.values():
|
321 |
+
if clean_name in route_data:
|
322 |
+
logger.debug(f"Direct match found for '{raw_name}' in reference data")
|
323 |
+
return clean_name
|
324 |
|
325 |
+
# Check all routes for partial match
|
326 |
+
for route_data in all_routes_reference.values():
|
327 |
+
for ref_name in route_data.keys():
|
328 |
+
if len(clean_name) >= 4 and len(ref_name) >= 4:
|
329 |
+
if clean_name in ref_name or ref_name in clean_name:
|
330 |
+
logger.debug(f"Partial match found: '{raw_name}' -> '{ref_name}' in reference data")
|
331 |
+
return ref_name
|
332 |
|
333 |
common_variations = {
|
334 |
'acetaminophen': 'paracetamol',
|
|
|
341 |
|
342 |
if clean_name in common_variations:
|
343 |
alt_name = common_variations[clean_name]
|
344 |
+
# Check all routes for the alternative name
|
345 |
+
for route_data in all_routes_reference.values():
|
346 |
+
if alt_name in route_data:
|
347 |
+
logger.debug(f"Found common variation: '{raw_name}' -> '{alt_name}' in reference data")
|
348 |
+
return alt_name
|
349 |
|
350 |
logger.debug(f"'{raw_name}' not found in reference data, trying brand lookup API")
|
351 |
try:
|
|
|
378 |
meds_with: Dict[str, float] = {}
|
379 |
meds_without: Dict[str, float] = {}
|
380 |
|
381 |
+
# Load all routes for smart lookup (backward compatibility)
|
382 |
+
all_routes_ref = load_all_routes_reference(Path(ref_csv))
|
383 |
+
|
384 |
+
for raw_name, mg_day, is_prn, detected_route in parsed:
|
385 |
+
generic = _smart_drug_lookup(raw_name, all_routes_ref)
|
386 |
|
387 |
meds_with[generic] = meds_with.get(generic, 0.0) + mg_day
|
388 |
if not is_prn:
|
|
|
404 |
}
|
405 |
|
406 |
|
407 |
+
def dbi_mcp_mixed_routes(text_block: str, *, ref_csv: Union[str, Path] = "dbi_reference_by_route.csv") -> dict:
|
408 |
+
"""
|
409 |
+
Enhanced DBI calculator that handles mixed routes automatically.
|
410 |
+
|
411 |
+
This function:
|
412 |
+
1. Detects the route for each medication from the text
|
413 |
+
2. Uses the appropriate reference data for each route
|
414 |
+
3. Provides detailed breakdown by route and medication
|
415 |
+
"""
|
416 |
+
all_routes_ref = load_all_routes_reference(Path(ref_csv))
|
417 |
+
|
418 |
+
parsed: List[Parsed] = []
|
419 |
+
unmatched: List[str] = []
|
420 |
+
route_stats: Dict[str, int] = {}
|
421 |
+
|
422 |
+
for ln in text_block.splitlines():
|
423 |
+
res = _parse_line(ln)
|
424 |
+
if res:
|
425 |
+
parsed.append(res)
|
426 |
+
route = res[3] # detected route
|
427 |
+
route_stats[route] = route_stats.get(route, 0) + 1
|
428 |
+
else:
|
429 |
+
unmatched.append(ln)
|
430 |
+
|
431 |
+
# Organize medications by route and PRN status
|
432 |
+
meds_by_route_with: Dict[str, Dict[str, float]] = {}
|
433 |
+
meds_by_route_without: Dict[str, Dict[str, float]] = {}
|
434 |
+
medication_details: List[Dict] = []
|
435 |
+
|
436 |
+
for raw_name, mg_day, is_prn, detected_route in parsed:
|
437 |
+
generic = _smart_drug_lookup(raw_name, all_routes_ref)
|
438 |
+
|
439 |
+
# Initialize route dictionaries if needed
|
440 |
+
if detected_route not in meds_by_route_with:
|
441 |
+
meds_by_route_with[detected_route] = {}
|
442 |
+
meds_by_route_without[detected_route] = {}
|
443 |
+
|
444 |
+
# Add to appropriate dictionaries
|
445 |
+
meds_by_route_with[detected_route][generic] = meds_by_route_with[detected_route].get(generic, 0.0) + mg_day
|
446 |
+
if not is_prn:
|
447 |
+
meds_by_route_without[detected_route][generic] = meds_by_route_without[detected_route].get(generic, 0.0) + mg_day
|
448 |
+
|
449 |
+
# Store medication details
|
450 |
+
medication_details.append({
|
451 |
+
"original_text": f"{raw_name} {mg_day}mg/day",
|
452 |
+
"generic_name": generic,
|
453 |
+
"dose_mg_day": mg_day,
|
454 |
+
"detected_route": detected_route,
|
455 |
+
"is_prn": is_prn
|
456 |
+
})
|
457 |
+
|
458 |
+
# Calculate DBI for each route
|
459 |
+
route_results = {}
|
460 |
+
total_dbi_with = 0.0
|
461 |
+
total_dbi_without = 0.0
|
462 |
+
all_details_with = []
|
463 |
+
all_details_without = []
|
464 |
+
|
465 |
+
for route in meds_by_route_with.keys():
|
466 |
+
if route in all_routes_ref:
|
467 |
+
route_ref = all_routes_ref[route]
|
468 |
+
|
469 |
+
# Calculate DBI for this route
|
470 |
+
dbi_with, details_with = calculate_dbi(meds_by_route_with[route], route_ref)
|
471 |
+
dbi_without, details_without = calculate_dbi(meds_by_route_without[route], route_ref)
|
472 |
+
|
473 |
+
total_dbi_with += dbi_with
|
474 |
+
total_dbi_without += dbi_without
|
475 |
+
|
476 |
+
# Format details
|
477 |
+
def _format_details(details, route_name):
|
478 |
+
formatted = []
|
479 |
+
for g, d, delta, dbi in details:
|
480 |
+
formatted.append({
|
481 |
+
"generic_name": g,
|
482 |
+
"dose_mg_day": d,
|
483 |
+
"delta_mg": delta,
|
484 |
+
"dbi_component": dbi,
|
485 |
+
"route": route_name
|
486 |
+
})
|
487 |
+
return formatted
|
488 |
+
|
489 |
+
route_details_with = _format_details(details_with, route)
|
490 |
+
route_details_without = _format_details(details_without, route)
|
491 |
+
|
492 |
+
all_details_with.extend(route_details_with)
|
493 |
+
all_details_without.extend(route_details_without)
|
494 |
+
|
495 |
+
route_results[route] = {
|
496 |
+
"dbi_with_prn": round(dbi_with, 2),
|
497 |
+
"dbi_without_prn": round(dbi_without, 2),
|
498 |
+
"details_with_prn": route_details_with,
|
499 |
+
"details_without_prn": route_details_without,
|
500 |
+
"medication_count": route_stats.get(route, 0)
|
501 |
+
}
|
502 |
+
|
503 |
+
return {
|
504 |
+
"mixed_routes": True,
|
505 |
+
"total_dbi_without_prn": round(total_dbi_without, 2),
|
506 |
+
"total_dbi_with_prn": round(total_dbi_with, 2),
|
507 |
+
"routes_detected": list(route_stats.keys()),
|
508 |
+
"route_statistics": route_stats,
|
509 |
+
"route_breakdown": route_results,
|
510 |
+
"all_details_without_prn": all_details_without,
|
511 |
+
"all_details_with_prn": all_details_with,
|
512 |
+
"medication_details": medication_details,
|
513 |
+
"unmatched_input": unmatched,
|
514 |
+
}
|
515 |
+
|
516 |
+
|
517 |
if __name__ == "__main__":
|
518 |
import sys
|
519 |
import pprint
|
dbi_reference_by_route.csv
CHANGED
@@ -117,3 +117,7 @@ trimipramine,oral,37.5,both
|
|
117 |
triprolidine,oral,10,both
|
118 |
zuclopenthixol,oral,20,both
|
119 |
zuclopenthixol,parenteral,11.4,both
|
|
|
|
|
|
|
|
|
|
117 |
triprolidine,oral,10,both
|
118 |
zuclopenthixol,oral,20,both
|
119 |
zuclopenthixol,parenteral,11.4,both
|
120 |
+
fentanyl,transdermal,0.3,sedative
|
121 |
+
buprenorphine,transdermal,0.12,sedative
|
122 |
+
clonidine,transdermal,0.1,both
|
123 |
+
scopolamine,transdermal,0.5,both
|