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
Files changed (3) hide show
  1. app.py +156 -7
  2. openfda_client.py +283 -5
  3. plotting.py +125 -0
app.py CHANGED
@@ -1,6 +1,17 @@
1
  import gradio as gr
2
- from openfda_client import get_top_adverse_events, get_drug_event_pair_frequency
3
- from plotting import create_bar_chart
 
 
 
 
 
 
 
 
 
 
 
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
- data = get_top_adverse_events(drug_name)
 
 
 
 
 
 
 
 
 
 
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
- def get_top_adverse_events(drug_name: str, limit: int = 10) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=patient.drug.medicinalproduct:"{drug_name_processed}"'
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