Spaces:
Sleeping
Sleeping
Jonas commited on
Commit ·
61fb024
1
Parent(s): f436a3a
Add new charting functions and enhance tools for serious outcomes and time-series analysis
Browse files- app.py +156 -7
- openfda_client.py +283 -5
- plotting.py +125 -0
app.py
CHANGED
|
@@ -1,6 +1,17 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from openfda_client import
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
|
| 6 |
# --- Formatting Functions ---
|
|
@@ -26,6 +37,27 @@ def format_top_events_results(data: dict, drug_name: str) -> str:
|
|
| 26 |
except Exception as e:
|
| 27 |
return f"An error occurred while formatting the data: {e}"
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def format_pair_frequency_results(data: dict, drug_name: str, event_name: str) -> str:
|
| 30 |
"""Formats the results for the drug-event pair frequency tool."""
|
| 31 |
if "error" in data:
|
|
@@ -43,21 +75,49 @@ def format_pair_frequency_results(data: dict, drug_name: str, event_name: str) -
|
|
| 43 |
|
| 44 |
# --- Tool Functions ---
|
| 45 |
|
| 46 |
-
def top_adverse_events_tool(drug_name: str):
|
| 47 |
"""
|
| 48 |
MCP Tool: Finds the top reported adverse events for a given drug.
|
| 49 |
|
| 50 |
Args:
|
| 51 |
drug_name (str): The name of the drug to search for.
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
Returns:
|
| 54 |
tuple: A Plotly figure and a formatted string with the top adverse events.
|
| 55 |
"""
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
chart = create_bar_chart(data, drug_name)
|
| 58 |
text_summary = format_top_events_results(data, drug_name)
|
| 59 |
return chart, text_summary
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def drug_event_stats_tool(drug_name: str, event_name: str):
|
| 62 |
"""
|
| 63 |
MCP Tool: Gets the total number of reports for a specific drug and adverse event pair.
|
|
@@ -72,6 +132,37 @@ def drug_event_stats_tool(drug_name: str, event_name: str):
|
|
| 72 |
data = get_drug_event_pair_frequency(drug_name, event_name)
|
| 73 |
return format_pair_frequency_results(data, drug_name, event_name)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# --- Gradio Interface ---
|
| 76 |
|
| 77 |
interface1 = gr.Interface(
|
|
@@ -80,7 +171,24 @@ interface1 = gr.Interface(
|
|
| 80 |
gr.Textbox(
|
| 81 |
label="Drug Name",
|
| 82 |
info="Enter a brand or generic drug name (e.g., 'Aspirin', 'Lisinopril')."
|
| 83 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
],
|
| 85 |
outputs=[
|
| 86 |
gr.Plot(label="Top Adverse Events Chart"),
|
|
@@ -91,6 +199,23 @@ interface1 = gr.Interface(
|
|
| 91 |
examples=[["Lisinopril"], ["Ozempic"], ["Metformin"]],
|
| 92 |
)
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
interface2 = gr.Interface(
|
| 95 |
fn=drug_event_stats_tool,
|
| 96 |
inputs=[
|
|
@@ -103,9 +228,33 @@ interface2 = gr.Interface(
|
|
| 103 |
examples=[["Lisinopril", "Cough"], ["Ozempic", "Nausea"]],
|
| 104 |
)
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
demo = gr.TabbedInterface(
|
| 107 |
-
[interface1, interface2],
|
| 108 |
-
["Top Events", "Event Frequency"],
|
| 109 |
title="Medication Adverse-Event Explorer"
|
| 110 |
)
|
| 111 |
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from openfda_client import (
|
| 3 |
+
get_top_adverse_events,
|
| 4 |
+
get_drug_event_pair_frequency,
|
| 5 |
+
get_serious_outcomes,
|
| 6 |
+
get_time_series_data,
|
| 7 |
+
get_report_source_data
|
| 8 |
+
)
|
| 9 |
+
from plotting import (
|
| 10 |
+
create_bar_chart,
|
| 11 |
+
create_outcome_chart,
|
| 12 |
+
create_time_series_chart,
|
| 13 |
+
create_pie_chart
|
| 14 |
+
)
|
| 15 |
import pandas as pd
|
| 16 |
|
| 17 |
# --- Formatting Functions ---
|
|
|
|
| 37 |
except Exception as e:
|
| 38 |
return f"An error occurred while formatting the data: {e}"
|
| 39 |
|
| 40 |
+
def format_serious_outcomes_results(data: dict, drug_name: str) -> str:
|
| 41 |
+
"""Formats the results for the serious outcomes tool."""
|
| 42 |
+
if "error" in data:
|
| 43 |
+
return f"An error occurred: {data['error']}"
|
| 44 |
+
|
| 45 |
+
if "results" not in data or not data["results"]:
|
| 46 |
+
return f"No serious outcome data found for '{drug_name}'. The drug may not be in the database or it might be misspelled."
|
| 47 |
+
|
| 48 |
+
header = f"Top Serious Outcomes for '{drug_name.title()}'\n"
|
| 49 |
+
header += "Source: FDA FAERS via OpenFDA\n"
|
| 50 |
+
header += "Disclaimer: Spontaneous reports do not prove causation. Consult a healthcare professional.\n"
|
| 51 |
+
header += "---------------------------------------------------\n"
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
df = pd.DataFrame(data["results"])
|
| 55 |
+
df = df.rename(columns={"term": "Serious Outcome", "count": "Report Count"})
|
| 56 |
+
result_string = df.to_string(index=False)
|
| 57 |
+
return header + result_string
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return f"An error occurred while formatting the data: {e}"
|
| 60 |
+
|
| 61 |
def format_pair_frequency_results(data: dict, drug_name: str, event_name: str) -> str:
|
| 62 |
"""Formats the results for the drug-event pair frequency tool."""
|
| 63 |
if "error" in data:
|
|
|
|
| 75 |
|
| 76 |
# --- Tool Functions ---
|
| 77 |
|
| 78 |
+
def top_adverse_events_tool(drug_name: str, patient_sex: str = "all", min_age: int = 0, max_age: int = 120):
|
| 79 |
"""
|
| 80 |
MCP Tool: Finds the top reported adverse events for a given drug.
|
| 81 |
|
| 82 |
Args:
|
| 83 |
drug_name (str): The name of the drug to search for.
|
| 84 |
+
patient_sex (str): The patient's sex to filter by.
|
| 85 |
+
min_age (int): The minimum age for the filter.
|
| 86 |
+
max_age (int): The maximum age for the filter.
|
| 87 |
|
| 88 |
Returns:
|
| 89 |
tuple: A Plotly figure and a formatted string with the top adverse events.
|
| 90 |
"""
|
| 91 |
+
sex_code = None
|
| 92 |
+
if patient_sex == "Male":
|
| 93 |
+
sex_code = "1"
|
| 94 |
+
elif patient_sex == "Female":
|
| 95 |
+
sex_code = "2"
|
| 96 |
+
|
| 97 |
+
age_range = None
|
| 98 |
+
if min_age > 0 or max_age < 120:
|
| 99 |
+
age_range = (min_age, max_age)
|
| 100 |
+
|
| 101 |
+
data = get_top_adverse_events(drug_name, patient_sex=sex_code, age_range=age_range)
|
| 102 |
chart = create_bar_chart(data, drug_name)
|
| 103 |
text_summary = format_top_events_results(data, drug_name)
|
| 104 |
return chart, text_summary
|
| 105 |
|
| 106 |
+
def serious_outcomes_tool(drug_name: str):
|
| 107 |
+
"""
|
| 108 |
+
MCP Tool: Finds the top reported serious outcomes for a given drug.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
drug_name (str): The name of the drug to search for.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
tuple: A Plotly figure and a formatted string with the top serious outcomes.
|
| 115 |
+
"""
|
| 116 |
+
data = get_serious_outcomes(drug_name)
|
| 117 |
+
chart = create_outcome_chart(data, drug_name)
|
| 118 |
+
text_summary = format_serious_outcomes_results(data, drug_name)
|
| 119 |
+
return chart, text_summary
|
| 120 |
+
|
| 121 |
def drug_event_stats_tool(drug_name: str, event_name: str):
|
| 122 |
"""
|
| 123 |
MCP Tool: Gets the total number of reports for a specific drug and adverse event pair.
|
|
|
|
| 132 |
data = get_drug_event_pair_frequency(drug_name, event_name)
|
| 133 |
return format_pair_frequency_results(data, drug_name, event_name)
|
| 134 |
|
| 135 |
+
def time_series_tool(drug_name: str, event_name: str, aggregation: str):
|
| 136 |
+
"""
|
| 137 |
+
MCP Tool: Creates a time-series plot for a drug-event pair.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
drug_name (str): The name of the drug.
|
| 141 |
+
event_name (str): The name of the adverse event.
|
| 142 |
+
aggregation (str): Time aggregation ('Yearly' or 'Quarterly').
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
A Plotly figure.
|
| 146 |
+
"""
|
| 147 |
+
agg_code = 'Y' if aggregation == 'Yearly' else 'Q'
|
| 148 |
+
data = get_time_series_data(drug_name, event_name)
|
| 149 |
+
chart = create_time_series_chart(data, drug_name, event_name, time_aggregation=agg_code)
|
| 150 |
+
return chart
|
| 151 |
+
|
| 152 |
+
def report_source_tool(drug_name: str):
|
| 153 |
+
"""
|
| 154 |
+
MCP Tool: Creates a pie chart of report sources for a given drug.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
drug_name (str): The name of the drug.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
A Plotly figure.
|
| 161 |
+
"""
|
| 162 |
+
data = get_report_source_data(drug_name)
|
| 163 |
+
chart = create_pie_chart(data, drug_name)
|
| 164 |
+
return chart
|
| 165 |
+
|
| 166 |
# --- Gradio Interface ---
|
| 167 |
|
| 168 |
interface1 = gr.Interface(
|
|
|
|
| 171 |
gr.Textbox(
|
| 172 |
label="Drug Name",
|
| 173 |
info="Enter a brand or generic drug name (e.g., 'Aspirin', 'Lisinopril')."
|
| 174 |
+
),
|
| 175 |
+
gr.Radio(
|
| 176 |
+
["All", "Male", "Female"],
|
| 177 |
+
label="Patient Sex",
|
| 178 |
+
value="All"
|
| 179 |
+
),
|
| 180 |
+
gr.Slider(
|
| 181 |
+
0, 120,
|
| 182 |
+
value=0,
|
| 183 |
+
label="Minimum Age",
|
| 184 |
+
step=1
|
| 185 |
+
),
|
| 186 |
+
gr.Slider(
|
| 187 |
+
0, 120,
|
| 188 |
+
value=120,
|
| 189 |
+
label="Maximum Age",
|
| 190 |
+
step=1
|
| 191 |
+
),
|
| 192 |
],
|
| 193 |
outputs=[
|
| 194 |
gr.Plot(label="Top Adverse Events Chart"),
|
|
|
|
| 199 |
examples=[["Lisinopril"], ["Ozempic"], ["Metformin"]],
|
| 200 |
)
|
| 201 |
|
| 202 |
+
interface3 = gr.Interface(
|
| 203 |
+
fn=serious_outcomes_tool,
|
| 204 |
+
inputs=[
|
| 205 |
+
gr.Textbox(
|
| 206 |
+
label="Drug Name",
|
| 207 |
+
info="Enter a brand or generic drug name (e.g., 'Aspirin', 'Lisinopril')."
|
| 208 |
+
)
|
| 209 |
+
],
|
| 210 |
+
outputs=[
|
| 211 |
+
gr.Plot(label="Top Serious Outcomes Chart"),
|
| 212 |
+
gr.Textbox(label="Top Serious Outcomes (Raw Data)", lines=15)
|
| 213 |
+
],
|
| 214 |
+
title="Serious Outcome Analysis",
|
| 215 |
+
description="Find the most frequently reported serious outcomes (e.g., hospitalization, death) for a specific medication.",
|
| 216 |
+
examples=[["Lisinopril"], ["Ozempic"], ["Metformin"]],
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
interface2 = gr.Interface(
|
| 220 |
fn=drug_event_stats_tool,
|
| 221 |
inputs=[
|
|
|
|
| 228 |
examples=[["Lisinopril", "Cough"], ["Ozempic", "Nausea"]],
|
| 229 |
)
|
| 230 |
|
| 231 |
+
interface4 = gr.Interface(
|
| 232 |
+
fn=time_series_tool,
|
| 233 |
+
inputs=[
|
| 234 |
+
gr.Textbox(label="Drug Name", info="e.g., 'Ibuprofen'"),
|
| 235 |
+
gr.Textbox(label="Adverse Event", info="e.g., 'Headache'"),
|
| 236 |
+
gr.Radio(["Yearly", "Quarterly"], label="Aggregation", value="Yearly")
|
| 237 |
+
],
|
| 238 |
+
outputs=[gr.Plot(label="Report Trends")],
|
| 239 |
+
title="Time-Series Trend Plotting",
|
| 240 |
+
description="Plot the number of adverse event reports over time for a specific drug-event pair.",
|
| 241 |
+
examples=[["Lisinopril", "Cough", "Yearly"], ["Ozempic", "Nausea", "Quarterly"]],
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
interface5 = gr.Interface(
|
| 245 |
+
fn=report_source_tool,
|
| 246 |
+
inputs=[
|
| 247 |
+
gr.Textbox(label="Drug Name", info="e.g., 'Aspirin', 'Lisinopril'")
|
| 248 |
+
],
|
| 249 |
+
outputs=[gr.Plot(label="Report Source Breakdown")],
|
| 250 |
+
title="Report Source Breakdown",
|
| 251 |
+
description="Show a pie chart breaking down the source of the reports (e.g., Consumer, Physician).",
|
| 252 |
+
examples=[["Lisinopril"], ["Ozempic"]],
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
demo = gr.TabbedInterface(
|
| 256 |
+
[interface1, interface3, interface2, interface4, interface5],
|
| 257 |
+
["Top Events", "Serious Outcomes", "Event Frequency", "Time-Series Trends", "Report Sources"],
|
| 258 |
title="Medication Adverse-Event Explorer"
|
| 259 |
)
|
| 260 |
|
openfda_client.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import requests
|
| 2 |
from cachetools import TTLCache, cached
|
| 3 |
import time
|
|
|
|
| 4 |
|
| 5 |
API_BASE_URL = "https://api.fda.gov/drug/event.json"
|
| 6 |
# Cache with a TTL of 10 minutes (600 seconds)
|
|
@@ -9,13 +10,122 @@ cache = TTLCache(maxsize=256, ttl=600)
|
|
| 9 |
# 240 requests per minute per IP. A 0.25s delay is a simple way to stay under.
|
| 10 |
REQUEST_DELAY_SECONDS = 0.25
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"""
|
| 14 |
Query OpenFDA to get the top adverse events for a given drug.
|
| 15 |
|
| 16 |
Args:
|
| 17 |
drug_name (str): The name of the drug to search for (brand or generic).
|
| 18 |
limit (int): The maximum number of adverse events to return.
|
|
|
|
|
|
|
| 19 |
|
| 20 |
Returns:
|
| 21 |
dict: The JSON response from the API, or an error dictionary.
|
|
@@ -24,15 +134,26 @@ def get_top_adverse_events(drug_name: str, limit: int = 10) -> dict:
|
|
| 24 |
return {"error": "Drug name cannot be empty."}
|
| 25 |
|
| 26 |
drug_name_processed = drug_name.lower().strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
# Using a simple cache key
|
| 29 |
-
cache_key = f"top_events_{drug_name_processed}_{limit}"
|
| 30 |
|
| 31 |
if cache_key in cache:
|
| 32 |
return cache[cache_key]
|
| 33 |
|
| 34 |
query = (
|
| 35 |
-
f'search=
|
| 36 |
f'&count=patient.reaction.reactionmeddrapt.exact&limit={limit}'
|
| 37 |
)
|
| 38 |
|
|
@@ -72,6 +193,7 @@ def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict:
|
|
| 72 |
return {"error": "Drug name and event name cannot be empty."}
|
| 73 |
|
| 74 |
drug_name_processed = drug_name.lower().strip()
|
|
|
|
| 75 |
event_name_processed = event_name.lower().strip()
|
| 76 |
|
| 77 |
cache_key = f"pair_freq_{drug_name_processed}_{event_name_processed}"
|
|
@@ -100,4 +222,160 @@ def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict:
|
|
| 100 |
except requests.exceptions.RequestException as req_err:
|
| 101 |
return {"error": f"A network request error occurred: {req_err}"}
|
| 102 |
except Exception as e:
|
| 103 |
-
return {"error": f"An unexpected error occurred: {e}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import requests
|
| 2 |
from cachetools import TTLCache, cached
|
| 3 |
import time
|
| 4 |
+
from typing import Optional, Tuple
|
| 5 |
|
| 6 |
API_BASE_URL = "https://api.fda.gov/drug/event.json"
|
| 7 |
# Cache with a TTL of 10 minutes (600 seconds)
|
|
|
|
| 10 |
# 240 requests per minute per IP. A 0.25s delay is a simple way to stay under.
|
| 11 |
REQUEST_DELAY_SECONDS = 0.25
|
| 12 |
|
| 13 |
+
DRUG_SYNONYM_MAPPING = {
|
| 14 |
+
"tylenol": "acetaminophen",
|
| 15 |
+
"advil": "ibuprofen",
|
| 16 |
+
"motrin": "ibuprofen",
|
| 17 |
+
"aleve": "naproxen",
|
| 18 |
+
"benadryl": "diphenhydramine",
|
| 19 |
+
"claritin": "loratadine",
|
| 20 |
+
"zyrtec": "cetirizine",
|
| 21 |
+
"allegra": "fexofenadine",
|
| 22 |
+
"zantac": "ranitidine",
|
| 23 |
+
"pepcid": "famotidine",
|
| 24 |
+
"prilosec": "omeprazole",
|
| 25 |
+
"lipitor": "atorvastatin",
|
| 26 |
+
"zocor": "simvastatin",
|
| 27 |
+
"norvasc": "amlodipine",
|
| 28 |
+
"hydrochlorothiazide": "hctz",
|
| 29 |
+
"glucophage": "metformin",
|
| 30 |
+
"synthroid": "levothyroxine",
|
| 31 |
+
"ambien": "zolpidem",
|
| 32 |
+
"xanax": "alprazolam",
|
| 33 |
+
"prozac": "fluoxetine",
|
| 34 |
+
"zoloft": "sertraline",
|
| 35 |
+
"paxil": "paroxetine",
|
| 36 |
+
"lexapro": "escitalopram",
|
| 37 |
+
"cymbalta": "duloxetine",
|
| 38 |
+
"wellbutrin": "bupropion",
|
| 39 |
+
"desyrel": "trazodone",
|
| 40 |
+
"eliquis": "apixaban",
|
| 41 |
+
"xarelto": "rivaroxaban",
|
| 42 |
+
"pradaxa": "dabigatran",
|
| 43 |
+
"coumadin": "warfarin",
|
| 44 |
+
"januvia": "sitagliptin",
|
| 45 |
+
"tradjenta": "linagliptin",
|
| 46 |
+
"jardiance": "empagliflozin",
|
| 47 |
+
"farxiga": "dapagliflozin",
|
| 48 |
+
"invokana": "canagliflozin",
|
| 49 |
+
"ozempic": "semaglutide",
|
| 50 |
+
"victoza": "liraglutide",
|
| 51 |
+
"trulicity": "dulaglutide",
|
| 52 |
+
"humira": "adalimumab",
|
| 53 |
+
"enbrel": "etanercept",
|
| 54 |
+
"remicade": "infliximab",
|
| 55 |
+
"stelara": "ustekinumab",
|
| 56 |
+
"keytruda": "pembrolizumab",
|
| 57 |
+
"opdivo": "nivolumab",
|
| 58 |
+
"revlimid": "lenalidomide",
|
| 59 |
+
"rituxan": "rituximab",
|
| 60 |
+
"herceptin": "trastuzumab",
|
| 61 |
+
"avastin": "bevacizumab",
|
| 62 |
+
"spiriva": "tiotropium",
|
| 63 |
+
"advair": "fluticasone/salmeterol",
|
| 64 |
+
"symbicort": "budesonide/formoterol",
|
| 65 |
+
"singulair": "montelukast",
|
| 66 |
+
"lyrica": "pregabalin",
|
| 67 |
+
"neurontin": "gabapentin",
|
| 68 |
+
"topamax": "topiramate",
|
| 69 |
+
"lamictal": "lamotrigine",
|
| 70 |
+
"keppra": "levetiracetam",
|
| 71 |
+
"dilantin": "phenytoin",
|
| 72 |
+
"tegretol": "carbamazepine",
|
| 73 |
+
"depakote": "divalproex",
|
| 74 |
+
"vyvanse": "lisdexamfetamine",
|
| 75 |
+
"adderall": "amphetamine/dextroamphetamine",
|
| 76 |
+
"ritalin": "methylphenidate",
|
| 77 |
+
"concerta": "methylphenidate",
|
| 78 |
+
"focalin": "dexmethylphenidate",
|
| 79 |
+
"strattera": "atomoxetine",
|
| 80 |
+
"viagra": "sildenafil",
|
| 81 |
+
"cialis": "tadalafil",
|
| 82 |
+
"levitra": "vardenafil",
|
| 83 |
+
"bactrim": "sulfamethoxazole/trimethoprim",
|
| 84 |
+
"keflex": "cephalexin",
|
| 85 |
+
"augmentin": "amoxicillin/clavulanate",
|
| 86 |
+
"zithromax": "azithromycin",
|
| 87 |
+
"levaquin": "levofloxacin",
|
| 88 |
+
"cipro": "ciprofloxacin",
|
| 89 |
+
"diflucan": "fluconazole",
|
| 90 |
+
"tamiflu": "oseltamivir",
|
| 91 |
+
"valtrex": "valacyclovir",
|
| 92 |
+
"zofran": "ondansetron",
|
| 93 |
+
"phenergan": "promethazine",
|
| 94 |
+
"imitrex": "sumatriptan",
|
| 95 |
+
"flexeril": "cyclobenzaprine",
|
| 96 |
+
"soma": "carisoprodol",
|
| 97 |
+
"valium": "diazepam",
|
| 98 |
+
"ativan": "lorazepam",
|
| 99 |
+
"klonopin": "clonazepam",
|
| 100 |
+
"restoril": "temazepam",
|
| 101 |
+
"ultram": "tramadol",
|
| 102 |
+
"percocet": "oxycodone/acetaminophen",
|
| 103 |
+
"vicodin": "hydrocodone/acetaminophen",
|
| 104 |
+
"oxycontin": "oxycodone",
|
| 105 |
+
"dilaudid": "hydromorphone",
|
| 106 |
+
"morphine": "ms contin",
|
| 107 |
+
"fentanyl": "duragesic"
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
OUTCOME_MAPPING = {
|
| 111 |
+
"1": "Life-Threatening",
|
| 112 |
+
"2": "Hospitalization - Initial or Prolonged",
|
| 113 |
+
"3": "Disability",
|
| 114 |
+
"4": "Congenital Anomaly",
|
| 115 |
+
"5": "Required Intervention to Prevent Permanent Impairment/Damage",
|
| 116 |
+
"6": "Death",
|
| 117 |
+
"7": "Other Serious (Important Medical Event)",
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
def get_top_adverse_events(drug_name: str, limit: int = 10, patient_sex: Optional[str] = None, age_range: Optional[Tuple[int, int]] = None) -> dict:
|
| 121 |
"""
|
| 122 |
Query OpenFDA to get the top adverse events for a given drug.
|
| 123 |
|
| 124 |
Args:
|
| 125 |
drug_name (str): The name of the drug to search for (brand or generic).
|
| 126 |
limit (int): The maximum number of adverse events to return.
|
| 127 |
+
patient_sex (str): The patient's sex to filter by ('1' for Male, '2' for Female).
|
| 128 |
+
age_range (tuple): A tuple containing min and max age, e.g., (20, 50).
|
| 129 |
|
| 130 |
Returns:
|
| 131 |
dict: The JSON response from the API, or an error dictionary.
|
|
|
|
| 134 |
return {"error": "Drug name cannot be empty."}
|
| 135 |
|
| 136 |
drug_name_processed = drug_name.lower().strip()
|
| 137 |
+
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed)
|
| 138 |
+
|
| 139 |
+
# Build the search query
|
| 140 |
+
search_terms = [f'patient.drug.medicinalproduct:"{drug_name_processed}"']
|
| 141 |
+
if patient_sex and patient_sex in ["1", "2"]:
|
| 142 |
+
search_terms.append(f'patient.patientsex:"{patient_sex}"')
|
| 143 |
+
if age_range and len(age_range) == 2:
|
| 144 |
+
min_age, max_age = age_range
|
| 145 |
+
search_terms.append(f'patient.patientonsetage:[{min_age} TO {max_age}]')
|
| 146 |
+
|
| 147 |
+
search_query = "+AND+".join(search_terms)
|
| 148 |
|
| 149 |
+
# Using a simple cache key that includes filters
|
| 150 |
+
cache_key = f"top_events_{drug_name_processed}_{limit}_{patient_sex}_{age_range}"
|
| 151 |
|
| 152 |
if cache_key in cache:
|
| 153 |
return cache[cache_key]
|
| 154 |
|
| 155 |
query = (
|
| 156 |
+
f'search={search_query}'
|
| 157 |
f'&count=patient.reaction.reactionmeddrapt.exact&limit={limit}'
|
| 158 |
)
|
| 159 |
|
|
|
|
| 193 |
return {"error": "Drug name and event name cannot be empty."}
|
| 194 |
|
| 195 |
drug_name_processed = drug_name.lower().strip()
|
| 196 |
+
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed)
|
| 197 |
event_name_processed = event_name.lower().strip()
|
| 198 |
|
| 199 |
cache_key = f"pair_freq_{drug_name_processed}_{event_name_processed}"
|
|
|
|
| 222 |
except requests.exceptions.RequestException as req_err:
|
| 223 |
return {"error": f"A network request error occurred: {req_err}"}
|
| 224 |
except Exception as e:
|
| 225 |
+
return {"error": f"An unexpected error occurred: {e}"}
|
| 226 |
+
|
| 227 |
+
def get_serious_outcomes(drug_name: str, limit: int = 10) -> dict:
|
| 228 |
+
"""
|
| 229 |
+
Query OpenFDA to get the most frequent serious outcomes for a given drug.
|
| 230 |
+
Outcomes include: death, disability, hospitalization, etc.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
drug_name (str): The name of the drug to search for.
|
| 234 |
+
limit (int): The maximum number of outcomes to return.
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
dict: The JSON response from the API, or an error dictionary.
|
| 238 |
+
"""
|
| 239 |
+
if not drug_name:
|
| 240 |
+
return {"error": "Drug name cannot be empty."}
|
| 241 |
+
|
| 242 |
+
drug_name_processed = drug_name.lower().strip()
|
| 243 |
+
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed)
|
| 244 |
+
cache_key = f"serious_outcomes_{drug_name_processed}_{limit}"
|
| 245 |
+
|
| 246 |
+
if cache_key in cache:
|
| 247 |
+
return cache[cache_key]
|
| 248 |
+
|
| 249 |
+
query = (
|
| 250 |
+
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 251 |
+
f'&count=patient.reaction.reactionoutcome.exact&limit={limit}'
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
time.sleep(REQUEST_DELAY_SECONDS)
|
| 256 |
+
|
| 257 |
+
response = requests.get(f"{API_BASE_URL}?{query}")
|
| 258 |
+
response.raise_for_status()
|
| 259 |
+
|
| 260 |
+
data = response.json()
|
| 261 |
+
|
| 262 |
+
# Map outcome codes to human-readable names
|
| 263 |
+
if "results" in data:
|
| 264 |
+
for item in data["results"]:
|
| 265 |
+
item["term"] = OUTCOME_MAPPING.get(item["term"], f"Unknown ({item['term']})")
|
| 266 |
+
|
| 267 |
+
cache[cache_key] = data
|
| 268 |
+
return data
|
| 269 |
+
|
| 270 |
+
except requests.exceptions.HTTPError as http_err:
|
| 271 |
+
if response.status_code == 404:
|
| 272 |
+
return {"error": f"No data found for drug: '{drug_name}'. It might be misspelled or not in the database."}
|
| 273 |
+
return {"error": f"HTTP error occurred: {http_err}"}
|
| 274 |
+
except requests.exceptions.RequestException as req_err:
|
| 275 |
+
return {"error": f"A network request error occurred: {req_err}"}
|
| 276 |
+
except Exception as e:
|
| 277 |
+
return {"error": f"An unexpected error occurred: {e}"}
|
| 278 |
+
|
| 279 |
+
def get_time_series_data(drug_name: str, event_name: str) -> dict:
|
| 280 |
+
"""
|
| 281 |
+
Query OpenFDA to get the time series data for a drug-event pair.
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
drug_name (str): The name of the drug.
|
| 285 |
+
event_name (str): The name of the adverse event.
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
dict: The JSON response from the API, or an error dictionary.
|
| 289 |
+
"""
|
| 290 |
+
if not drug_name or not event_name:
|
| 291 |
+
return {"error": "Drug name and event name cannot be empty."}
|
| 292 |
+
|
| 293 |
+
drug_name_processed = drug_name.lower().strip()
|
| 294 |
+
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed)
|
| 295 |
+
event_name_processed = event_name.lower().strip()
|
| 296 |
+
|
| 297 |
+
cache_key = f"time_series_{drug_name_processed}_{event_name_processed}"
|
| 298 |
+
if cache_key in cache:
|
| 299 |
+
return cache[cache_key]
|
| 300 |
+
|
| 301 |
+
query = (
|
| 302 |
+
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 303 |
+
f'+AND+patient.reaction.reactionmeddrapt:"{event_name_processed}"'
|
| 304 |
+
f'&count=receivedate'
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
try:
|
| 308 |
+
time.sleep(REQUEST_DELAY_SECONDS)
|
| 309 |
+
|
| 310 |
+
response = requests.get(f"{API_BASE_URL}?{query}")
|
| 311 |
+
response.raise_for_status()
|
| 312 |
+
|
| 313 |
+
data = response.json()
|
| 314 |
+
cache[cache_key] = data
|
| 315 |
+
return data
|
| 316 |
+
|
| 317 |
+
except requests.exceptions.HTTPError as http_err:
|
| 318 |
+
if response.status_code == 404:
|
| 319 |
+
return {"error": f"No data found for drug '{drug_name}' and event '{event_name}'. They may be misspelled or not in the database."}
|
| 320 |
+
return {"error": f"HTTP error occurred: {http_err}"}
|
| 321 |
+
except requests.exceptions.RequestException as req_err:
|
| 322 |
+
return {"error": f"A network request error occurred: {req_err}"}
|
| 323 |
+
except Exception as e:
|
| 324 |
+
return {"error": f"An unexpected error occurred: {e}"}
|
| 325 |
+
|
| 326 |
+
def get_report_source_data(drug_name: str) -> dict:
|
| 327 |
+
"""
|
| 328 |
+
Query OpenFDA to get the breakdown of report sources for a given drug.
|
| 329 |
+
|
| 330 |
+
Args:
|
| 331 |
+
drug_name (str): The name of the drug to search for.
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
dict: The JSON response from the API, or an error dictionary.
|
| 335 |
+
"""
|
| 336 |
+
if not drug_name:
|
| 337 |
+
return {"error": "Drug name cannot be empty."}
|
| 338 |
+
|
| 339 |
+
drug_name_processed = drug_name.lower().strip()
|
| 340 |
+
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed)
|
| 341 |
+
|
| 342 |
+
cache_key = f"report_source_{drug_name_processed}"
|
| 343 |
+
if cache_key in cache:
|
| 344 |
+
return cache[cache_key]
|
| 345 |
+
|
| 346 |
+
query = (
|
| 347 |
+
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"'
|
| 348 |
+
f'&count=primarysource.qualification.exact'
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
try:
|
| 352 |
+
time.sleep(REQUEST_DELAY_SECONDS)
|
| 353 |
+
|
| 354 |
+
response = requests.get(f"{API_BASE_URL}?{query}")
|
| 355 |
+
response.raise_for_status()
|
| 356 |
+
|
| 357 |
+
data = response.json()
|
| 358 |
+
|
| 359 |
+
if "results" in data:
|
| 360 |
+
for item in data["results"]:
|
| 361 |
+
item["term"] = QUALIFICATION_MAPPING.get(item["term"], f"Unknown ({item['term']})")
|
| 362 |
+
|
| 363 |
+
cache[cache_key] = data
|
| 364 |
+
return data
|
| 365 |
+
|
| 366 |
+
except requests.exceptions.HTTPError as http_err:
|
| 367 |
+
if response.status_code == 404:
|
| 368 |
+
return {"error": f"No data found for drug: '{drug_name}'."}
|
| 369 |
+
return {"error": f"HTTP error occurred: {http_err}"}
|
| 370 |
+
except requests.exceptions.RequestException as req_err:
|
| 371 |
+
return {"error": f"A network request error occurred: {req_err}"}
|
| 372 |
+
except Exception as e:
|
| 373 |
+
return {"error": f"An unexpected error occurred: {e}"}
|
| 374 |
+
|
| 375 |
+
QUALIFICATION_MAPPING = {
|
| 376 |
+
"1": "Physician",
|
| 377 |
+
"2": "Pharmacist",
|
| 378 |
+
"3": "Other Health Professional",
|
| 379 |
+
"4": "Lawyer",
|
| 380 |
+
"5": "Consumer or non-health professional",
|
| 381 |
+
}
|
plotting.py
CHANGED
|
@@ -44,4 +44,129 @@ def create_bar_chart(data: dict, drug_name: str):
|
|
| 44 |
|
| 45 |
return fig
|
| 46 |
except Exception:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
return None
|
|
|
|
| 44 |
|
| 45 |
return fig
|
| 46 |
except Exception:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
def create_outcome_chart(data: dict, drug_name: str):
|
| 50 |
+
"""
|
| 51 |
+
Creates a Plotly bar chart for serious outcomes from OpenFDA data.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
data (dict): The data from the OpenFDA client.
|
| 55 |
+
drug_name (str): The name of the drug.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
A Plotly Figure object if data is valid, otherwise None.
|
| 59 |
+
"""
|
| 60 |
+
if "error" in data or "results" not in data or not data["results"]:
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
df = pd.DataFrame(data["results"])
|
| 65 |
+
df = df.rename(columns={"term": "Serious Outcome", "count": "Report Count"})
|
| 66 |
+
|
| 67 |
+
df['Report Count'] = pd.to_numeric(df['Report Count'])
|
| 68 |
+
df = df.sort_values(by="Report Count", ascending=True)
|
| 69 |
+
|
| 70 |
+
fig = go.Figure(
|
| 71 |
+
go.Bar(
|
| 72 |
+
x=df["Report Count"],
|
| 73 |
+
y=df["Serious Outcome"],
|
| 74 |
+
orientation='h',
|
| 75 |
+
marker=dict(color='crimson') # Different color for distinction
|
| 76 |
+
)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
fig.update_layout(
|
| 80 |
+
title_text=f"Top Serious Outcomes for {drug_name.title()}",
|
| 81 |
+
xaxis_title="Number of Reports",
|
| 82 |
+
yaxis_title="Serious Outcome",
|
| 83 |
+
yaxis=dict(automargin=True),
|
| 84 |
+
height=max(400, len(df) * 40)
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
return fig
|
| 88 |
+
except Exception:
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
def create_time_series_chart(data: dict, drug_name: str, event_name: str, time_aggregation: str = 'Y'):
|
| 92 |
+
"""
|
| 93 |
+
Creates a Plotly time-series chart from OpenFDA data.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
data (dict): The data from the OpenFDA client.
|
| 97 |
+
drug_name (str): The name of the drug.
|
| 98 |
+
event_name (str): The name of the adverse event.
|
| 99 |
+
time_aggregation (str): The time unit for aggregation ('Y' for year, 'Q' for quarter).
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
A Plotly Figure object if data is valid, otherwise None.
|
| 103 |
+
"""
|
| 104 |
+
if "error" in data or "results" not in data or not data["results"]:
|
| 105 |
+
return None
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
df = pd.DataFrame(data["results"])
|
| 109 |
+
df['time'] = pd.to_datetime(df['time'], format='%Y%m%d')
|
| 110 |
+
|
| 111 |
+
# Resample data
|
| 112 |
+
df = df.set_index('time').resample(time_aggregation)['count'].sum().reset_index()
|
| 113 |
+
|
| 114 |
+
aggregation_label = "Year" if time_aggregation == 'Y' else "Quarter"
|
| 115 |
+
|
| 116 |
+
fig = go.Figure(
|
| 117 |
+
go.Scatter(
|
| 118 |
+
x=df["time"],
|
| 119 |
+
y=df["count"],
|
| 120 |
+
mode='lines+markers',
|
| 121 |
+
line=dict(color='royalblue'),
|
| 122 |
+
)
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
fig.update_layout(
|
| 126 |
+
title_text=f"Report Trend for {drug_name.title()} and {event_name.title()}",
|
| 127 |
+
xaxis_title=f"Report {aggregation_label}",
|
| 128 |
+
yaxis_title="Number of Reports",
|
| 129 |
+
yaxis=dict(automargin=True),
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
return fig
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f"Error creating time-series chart: {e}")
|
| 135 |
+
return None
|
| 136 |
+
|
| 137 |
+
def create_pie_chart(data: dict, drug_name: str):
|
| 138 |
+
"""
|
| 139 |
+
Creates a Plotly pie chart for report source breakdown.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
data (dict): The data from the OpenFDA client.
|
| 143 |
+
drug_name (str): The name of the drug.
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
A Plotly Figure object if data is valid, otherwise None.
|
| 147 |
+
"""
|
| 148 |
+
if "error" in data or "results" not in data or not data["results"]:
|
| 149 |
+
return None
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
df = pd.DataFrame(data["results"])
|
| 153 |
+
df = df.rename(columns={"term": "Source", "count": "Count"})
|
| 154 |
+
|
| 155 |
+
fig = go.Figure(
|
| 156 |
+
go.Pie(
|
| 157 |
+
labels=df["Source"],
|
| 158 |
+
values=df["Count"],
|
| 159 |
+
hole=.3,
|
| 160 |
+
pull=[0.05] * len(df) # Explode slices slightly
|
| 161 |
+
)
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
fig.update_layout(
|
| 165 |
+
title_text=f"Report Sources for {drug_name.title()}",
|
| 166 |
+
showlegend=True
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
return fig
|
| 170 |
+
except Exception as e:
|
| 171 |
+
print(f"Error creating pie chart: {e}")
|
| 172 |
return None
|