Jonas
commited on
Commit
·
c2b5b0b
1
Parent(s):
e382e80
final fixes to add top_n. fix presentation of about section. add relative even frequency.
Browse files- app.py +136 -126
- openfda_client.py +40 -13
app.py
CHANGED
|
@@ -22,13 +22,23 @@ def format_pair_frequency_results(data: dict, drug_name: str, event_name: str) -
|
|
| 22 |
if "error" in data:
|
| 23 |
return f"An error occurred: {data['error']}"
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
result = (
|
| 28 |
-
f"Found {total_reports:,} reports for the combination of "
|
| 29 |
-
f"'{drug_name.title()}' and '{event_name.title()}'
|
| 30 |
-
"Source
|
| 31 |
-
"Disclaimer
|
| 32 |
)
|
| 33 |
return result
|
| 34 |
|
|
@@ -48,6 +58,8 @@ def top_adverse_events_tool(drug_name: str, top_n: int = 10, patient_sex: str =
|
|
| 48 |
Returns:
|
| 49 |
tuple: A Plotly figure, a Pandas DataFrame, and a summary string.
|
| 50 |
"""
|
|
|
|
|
|
|
| 51 |
if patient_sex is None:
|
| 52 |
patient_sex = "all"
|
| 53 |
if min_age is None:
|
|
@@ -105,6 +117,8 @@ def serious_outcomes_tool(drug_name: str, top_n: int = 6):
|
|
| 105 |
Returns:
|
| 106 |
tuple: A Plotly figure, a Pandas DataFrame, and a summary string.
|
| 107 |
"""
|
|
|
|
|
|
|
| 108 |
data = get_serious_outcomes(drug_name, limit=top_n)
|
| 109 |
|
| 110 |
if "error" in data:
|
|
@@ -181,6 +195,8 @@ def report_source_tool(drug_name: str, top_n: int = 5):
|
|
| 181 |
Returns:
|
| 182 |
tuple: A Plotly figure, a Pandas DataFrame, and a summary string.
|
| 183 |
"""
|
|
|
|
|
|
|
| 184 |
data = get_report_source_data(drug_name, limit=top_n)
|
| 185 |
|
| 186 |
if "error" in data:
|
|
@@ -213,127 +229,121 @@ def report_source_tool(drug_name: str, top_n: int = 5):
|
|
| 213 |
with open("gradio_readme.md", "r") as f:
|
| 214 |
readme_content = f.read()
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
)
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
)
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
)
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
)
|
| 331 |
-
|
| 332 |
-
demo = gr.TabbedInterface(
|
| 333 |
-
[interface_about, interface1, interface3, interface2, interface4, interface5],
|
| 334 |
-
["About", "Top Events", "Serious Outcomes", "Event Frequency", "Time-Series Trends", "Report Sources"],
|
| 335 |
-
title="Medication Adverse-Event Explorer"
|
| 336 |
-
)
|
| 337 |
|
| 338 |
if __name__ == "__main__":
|
| 339 |
demo.launch(mcp_server=True, server_name="0.0.0.0")
|
|
|
|
| 22 |
if "error" in data:
|
| 23 |
return f"An error occurred: {data['error']}"
|
| 24 |
|
| 25 |
+
results = data.get("meta", {}).get("results", {})
|
| 26 |
+
total_reports = results.get("total", 0)
|
| 27 |
+
total_for_drug = results.get("total_for_drug", 0)
|
| 28 |
+
|
| 29 |
+
percentage_string = ""
|
| 30 |
+
if total_for_drug > 0:
|
| 31 |
+
percentage = (total_reports / total_for_drug) * 100
|
| 32 |
+
percentage_string = (
|
| 33 |
+
f"\n\nThis combination accounts for **{percentage:.2f}%** of the **{total_for_drug:,}** "
|
| 34 |
+
f"total adverse event reports for '{drug_name.title()}' in the database."
|
| 35 |
+
)
|
| 36 |
|
| 37 |
result = (
|
| 38 |
+
f"Found **{total_reports:,}** reports for the combination of "
|
| 39 |
+
f"'{drug_name.title()}' and '{event_name.title()}'.{percentage_string}\n\n"
|
| 40 |
+
"**Source**: FDA FAERS via OpenFDA\n"
|
| 41 |
+
"**Disclaimer**: Spontaneous reports do not prove causation. Consult a healthcare professional."
|
| 42 |
)
|
| 43 |
return result
|
| 44 |
|
|
|
|
| 58 |
Returns:
|
| 59 |
tuple: A Plotly figure, a Pandas DataFrame, and a summary string.
|
| 60 |
"""
|
| 61 |
+
if top_n is None:
|
| 62 |
+
top_n = 10
|
| 63 |
if patient_sex is None:
|
| 64 |
patient_sex = "all"
|
| 65 |
if min_age is None:
|
|
|
|
| 117 |
Returns:
|
| 118 |
tuple: A Plotly figure, a Pandas DataFrame, and a summary string.
|
| 119 |
"""
|
| 120 |
+
if top_n is None:
|
| 121 |
+
top_n = 6
|
| 122 |
data = get_serious_outcomes(drug_name, limit=top_n)
|
| 123 |
|
| 124 |
if "error" in data:
|
|
|
|
| 195 |
Returns:
|
| 196 |
tuple: A Plotly figure, a Pandas DataFrame, and a summary string.
|
| 197 |
"""
|
| 198 |
+
if top_n is None:
|
| 199 |
+
top_n = 5
|
| 200 |
data = get_report_source_data(drug_name, limit=top_n)
|
| 201 |
|
| 202 |
if "error" in data:
|
|
|
|
| 229 |
with open("gradio_readme.md", "r") as f:
|
| 230 |
readme_content = f.read()
|
| 231 |
|
| 232 |
+
with gr.Blocks(title="Medication Adverse-Event Explorer") as demo:
|
| 233 |
+
gr.Markdown("# Medication Adverse-Event Explorer")
|
| 234 |
+
|
| 235 |
+
with gr.Tabs():
|
| 236 |
+
with gr.TabItem("About"):
|
| 237 |
+
gr.Markdown(readme_content)
|
| 238 |
+
|
| 239 |
+
with gr.TabItem("Top Events"):
|
| 240 |
+
gr.Interface(
|
| 241 |
+
fn=top_adverse_events_tool,
|
| 242 |
+
inputs=[
|
| 243 |
+
gr.Textbox(
|
| 244 |
+
label="Drug Name",
|
| 245 |
+
info="Enter a brand or generic drug name (e.g., 'Aspirin', 'Lisinopril')."
|
| 246 |
+
),
|
| 247 |
+
gr.Slider(
|
| 248 |
+
5, 50,
|
| 249 |
+
value=10,
|
| 250 |
+
label="Number of Events to Show",
|
| 251 |
+
step=1
|
| 252 |
+
),
|
| 253 |
+
gr.Radio(
|
| 254 |
+
["All", "Male", "Female"],
|
| 255 |
+
label="Patient Sex",
|
| 256 |
+
value="All"
|
| 257 |
+
),
|
| 258 |
+
gr.Slider(
|
| 259 |
+
0, 120,
|
| 260 |
+
value=0,
|
| 261 |
+
label="Minimum Age",
|
| 262 |
+
step=1
|
| 263 |
+
),
|
| 264 |
+
gr.Slider(
|
| 265 |
+
0, 120,
|
| 266 |
+
value=120,
|
| 267 |
+
label="Maximum Age",
|
| 268 |
+
step=1
|
| 269 |
+
),
|
| 270 |
+
],
|
| 271 |
+
outputs=[
|
| 272 |
+
gr.Plot(label="Top Adverse Events Chart"),
|
| 273 |
+
gr.DataFrame(label="Top Adverse Events", interactive=False),
|
| 274 |
+
gr.Markdown()
|
| 275 |
+
],
|
| 276 |
+
title="Top Adverse Events by Drug",
|
| 277 |
+
description="Find the most frequently reported adverse events for a specific medication.",
|
| 278 |
+
examples=[["Lisinopril"], ["Ozempic"], ["Metformin"]],
|
| 279 |
+
allow_flagging="never",
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
with gr.TabItem("Serious Outcomes"):
|
| 283 |
+
gr.Interface(
|
| 284 |
+
fn=serious_outcomes_tool,
|
| 285 |
+
inputs=[
|
| 286 |
+
gr.Textbox(
|
| 287 |
+
label="Drug Name",
|
| 288 |
+
info="Enter a brand or generic drug name (e.g., 'Aspirin', 'Lisinopril')."
|
| 289 |
+
),
|
| 290 |
+
gr.Slider(1, 6, value=6, label="Number of Outcomes to Show", step=1),
|
| 291 |
+
],
|
| 292 |
+
outputs=[
|
| 293 |
+
gr.Plot(label="Top Serious Outcomes Chart"),
|
| 294 |
+
gr.DataFrame(label="Top Serious Outcomes", interactive=False),
|
| 295 |
+
gr.Markdown()
|
| 296 |
+
],
|
| 297 |
+
title="Serious Outcome Analysis",
|
| 298 |
+
description="Find the most frequently reported serious outcomes (e.g., hospitalization, death) for a specific medication.",
|
| 299 |
+
examples=[["Lisinopril"], ["Ozempic"], ["Metformin"]],
|
| 300 |
+
allow_flagging="never",
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
with gr.TabItem("Event Frequency"):
|
| 304 |
+
gr.Interface(
|
| 305 |
+
fn=drug_event_stats_tool,
|
| 306 |
+
inputs=[
|
| 307 |
+
gr.Textbox(label="Drug Name", info="e.g., 'Ibuprofen'"),
|
| 308 |
+
gr.Textbox(label="Adverse Event", info="e.g., 'Headache'")
|
| 309 |
+
],
|
| 310 |
+
outputs=[gr.Textbox(label="Report Count", lines=5)],
|
| 311 |
+
title="Drug/Event Pair Frequency",
|
| 312 |
+
description="Get the total number of reports for a specific drug and adverse event combination.",
|
| 313 |
+
examples=[["Lisinopril", "Cough"], ["Ozempic", "Nausea"]],
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
with gr.TabItem("Time-Series Trends"):
|
| 317 |
+
gr.Interface(
|
| 318 |
+
fn=time_series_tool,
|
| 319 |
+
inputs=[
|
| 320 |
+
gr.Textbox(label="Drug Name", info="e.g., 'Ibuprofen'"),
|
| 321 |
+
gr.Textbox(label="Adverse Event", info="e.g., 'Headache'"),
|
| 322 |
+
gr.Radio(["Yearly", "Quarterly"], label="Aggregation", value="Yearly")
|
| 323 |
+
],
|
| 324 |
+
outputs=[gr.Plot(label="Report Trends")],
|
| 325 |
+
title="Time-Series Trend Plotting",
|
| 326 |
+
description="Plot the number of adverse event reports over time for a specific drug-event pair.",
|
| 327 |
+
examples=[["Lisinopril", "Cough", "Yearly"], ["Ozempic", "Nausea", "Quarterly"]],
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
with gr.TabItem("Report Sources"):
|
| 331 |
+
gr.Interface(
|
| 332 |
+
fn=report_source_tool,
|
| 333 |
+
inputs=[
|
| 334 |
+
gr.Textbox(label="Drug Name", info="e.g., 'Aspirin', 'Lisinopril'"),
|
| 335 |
+
gr.Slider(1, 5, value=5, label="Number of Sources to Show", step=1),
|
| 336 |
+
],
|
| 337 |
+
outputs=[
|
| 338 |
+
gr.Plot(label="Report Source Breakdown"),
|
| 339 |
+
gr.DataFrame(label="Report Source Data", interactive=False),
|
| 340 |
+
gr.Markdown()
|
| 341 |
+
],
|
| 342 |
+
title="Report Source Breakdown",
|
| 343 |
+
description="Show a pie chart breaking down the source of the reports (e.g., Consumer, Physician).",
|
| 344 |
+
examples=[["Lisinopril"], ["Ibuprofen"]],
|
| 345 |
+
allow_flagging="never",
|
| 346 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
if __name__ == "__main__":
|
| 349 |
demo.launch(mcp_server=True, server_name="0.0.0.0")
|
openfda_client.py
CHANGED
|
@@ -210,14 +210,15 @@ def get_top_adverse_events(drug_name: str, limit: int = 10, patient_sex: Optiona
|
|
| 210 |
def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict:
|
| 211 |
"""
|
| 212 |
Query OpenFDA to get the total number of reports for a specific
|
| 213 |
-
drug-adverse event pair.
|
| 214 |
|
| 215 |
Args:
|
| 216 |
drug_name (str): The name of the drug.
|
| 217 |
event_name (str): The name of the adverse event.
|
| 218 |
|
| 219 |
Returns:
|
| 220 |
-
dict:
|
|
|
|
| 221 |
"""
|
| 222 |
if not drug_name or not event_name:
|
| 223 |
return {"error": "Drug name and event name cannot be empty."}
|
|
@@ -229,25 +230,51 @@ def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict:
|
|
| 229 |
cache_key = f"pair_freq_{drug_name_processed}_{event_name_processed}"
|
| 230 |
if cache_key in cache:
|
| 231 |
return cache[cache_key]
|
| 232 |
-
|
| 233 |
-
query = (
|
| 234 |
-
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 235 |
-
f'+AND+patient.reaction.reactionmeddrapt:"{event_name_processed}"'
|
| 236 |
-
)
|
| 237 |
-
|
| 238 |
try:
|
|
|
|
|
|
|
| 239 |
time.sleep(REQUEST_DELAY_SECONDS)
|
|
|
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
-
data = response.json()
|
| 245 |
cache[cache_key] = data
|
| 246 |
return data
|
| 247 |
|
| 248 |
except requests.exceptions.HTTPError as http_err:
|
| 249 |
-
if response.status_code == 404:
|
| 250 |
-
return {"error": f"No data found for drug '{drug_name}'
|
| 251 |
return {"error": f"HTTP error occurred: {http_err}"}
|
| 252 |
except requests.exceptions.RequestException as req_err:
|
| 253 |
return {"error": f"A network request error occurred: {req_err}"}
|
|
|
|
| 210 |
def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict:
|
| 211 |
"""
|
| 212 |
Query OpenFDA to get the total number of reports for a specific
|
| 213 |
+
drug-adverse event pair, and the total reports for the drug alone.
|
| 214 |
|
| 215 |
Args:
|
| 216 |
drug_name (str): The name of the drug.
|
| 217 |
event_name (str): The name of the adverse event.
|
| 218 |
|
| 219 |
Returns:
|
| 220 |
+
dict: A dictionary containing the results or an error message.
|
| 221 |
+
Includes `total` (for the pair) and `total_for_drug`.
|
| 222 |
"""
|
| 223 |
if not drug_name or not event_name:
|
| 224 |
return {"error": "Drug name and event name cannot be empty."}
|
|
|
|
| 230 |
cache_key = f"pair_freq_{drug_name_processed}_{event_name_processed}"
|
| 231 |
if cache_key in cache:
|
| 232 |
return cache[cache_key]
|
| 233 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
try:
|
| 235 |
+
# First, get total reports for the drug to see if it exists
|
| 236 |
+
drug_query = f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 237 |
time.sleep(REQUEST_DELAY_SECONDS)
|
| 238 |
+
drug_response = requests.get(f"{API_BASE_URL}?{drug_query}")
|
| 239 |
|
| 240 |
+
# This is a critical failure if the drug isn't found
|
| 241 |
+
drug_response.raise_for_status()
|
| 242 |
+
|
| 243 |
+
drug_data = drug_response.json()
|
| 244 |
+
total_for_drug = drug_data.get("meta", {}).get("results", {}).get("total", 0)
|
| 245 |
+
|
| 246 |
+
# Second, get reports for the specific drug-event pair
|
| 247 |
+
pair_query = (
|
| 248 |
+
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 249 |
+
f'+AND+patient.reaction.reactionmeddrapt:"{event_name_processed}"'
|
| 250 |
+
)
|
| 251 |
+
time.sleep(REQUEST_DELAY_SECONDS)
|
| 252 |
+
pair_response = requests.get(f"{API_BASE_URL}?{pair_query}")
|
| 253 |
+
|
| 254 |
+
total_for_pair = 0
|
| 255 |
+
if pair_response.status_code == 200:
|
| 256 |
+
pair_data = pair_response.json()
|
| 257 |
+
total_for_pair = pair_data.get("meta", {}).get("results", {}).get("total", 0)
|
| 258 |
+
# We don't raise for 404 on the pair, as it just means 0 results
|
| 259 |
+
elif pair_response.status_code != 404:
|
| 260 |
+
pair_response.raise_for_status()
|
| 261 |
+
|
| 262 |
+
# Construct a consistent response structure
|
| 263 |
+
data = {
|
| 264 |
+
"meta": {
|
| 265 |
+
"results": {
|
| 266 |
+
"total": total_for_pair,
|
| 267 |
+
"total_for_drug": total_for_drug
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
|
|
|
|
| 272 |
cache[cache_key] = data
|
| 273 |
return data
|
| 274 |
|
| 275 |
except requests.exceptions.HTTPError as http_err:
|
| 276 |
+
if http_err.response.status_code == 404:
|
| 277 |
+
return {"error": f"No data found for drug '{drug_name}'. It may be misspelled or not in the database."}
|
| 278 |
return {"error": f"HTTP error occurred: {http_err}"}
|
| 279 |
except requests.exceptions.RequestException as req_err:
|
| 280 |
return {"error": f"A network request error occurred: {req_err}"}
|