Spaces:
Sleeping
Sleeping
Jonas
final fixes to add top_n. fix presentation of about section. add relative even frequency.
c2b5b0b
import requests | |
from cachetools import TTLCache, cached | |
import time | |
from typing import Optional, Tuple | |
API_BASE_URL = "https://api.fda.gov/drug/event.json" | |
# Cache with a TTL of 10 minutes (600 seconds) | |
cache = TTLCache(maxsize=256, ttl=600) | |
# 240 requests per minute per IP. A 0.25s delay is a simple way to stay under. | |
REQUEST_DELAY_SECONDS = 0.25 | |
DRUG_SYNONYM_MAPPING = { | |
"tylenol": "acetaminophen", | |
"advil": "ibuprofen", | |
"motrin": "ibuprofen", | |
"aleve": "naproxen", | |
"benadryl": "diphenhydramine", | |
"claritin": "loratadine", | |
"zyrtec": "cetirizine", | |
"allegra": "fexofenadine", | |
"zantac": "ranitidine", | |
"pepcid": "famotidine", | |
"prilosec": "omeprazole", | |
"lipitor": "atorvastatin", | |
"zocor": "simvastatin", | |
"norvasc": "amlodipine", | |
"hydrochlorothiazide": "hctz", | |
"glucophage": "metformin", | |
"synthroid": "levothyroxine", | |
"ambien": "zolpidem", | |
"xanax": "alprazolam", | |
"prozac": "fluoxetine", | |
"zoloft": "sertraline", | |
"paxil": "paroxetine", | |
"lexapro": "escitalopram", | |
"cymbalta": "duloxetine", | |
"wellbutrin": "bupropion", | |
"desyrel": "trazodone", | |
"eliquis": "apixaban", | |
"xarelto": "rivaroxaban", | |
"pradaxa": "dabigatran", | |
"coumadin": "warfarin", | |
"januvia": "sitagliptin", | |
"tradjenta": "linagliptin", | |
"jardiance": "empagliflozin", | |
"farxiga": "dapagliflozin", | |
"invokana": "canagliflozin", | |
"ozempic": "semaglutide", | |
"victoza": "liraglutide", | |
"trulicity": "dulaglutide", | |
"humira": "adalimumab", | |
"enbrel": "etanercept", | |
"remicade": "infliximab", | |
"stelara": "ustekinumab", | |
"keytruda": "pembrolizumab", | |
"opdivo": "nivolumab", | |
"revlimid": "lenalidomide", | |
"rituxan": "rituximab", | |
"herceptin": "trastuzumab", | |
"avastin": "bevacizumab", | |
"spiriva": "tiotropium", | |
"advair": "fluticasone/salmeterol", | |
"symbicort": "budesonide/formoterol", | |
"singulair": "montelukast", | |
"lyrica": "pregabalin", | |
"neurontin": "gabapentin", | |
"topamax": "topiramate", | |
"lamictal": "lamotrigine", | |
"keppra": "levetiracetam", | |
"dilantin": "phenytoin", | |
"tegretol": "carbamazepine", | |
"depakote": "divalproex", | |
"vyvanse": "lisdexamfetamine", | |
"adderall": "amphetamine/dextroamphetamine", | |
"ritalin": "methylphenidate", | |
"concerta": "methylphenidate", | |
"focalin": "dexmethylphenidate", | |
"strattera": "atomoxetine", | |
"viagra": "sildenafil", | |
"cialis": "tadalafil", | |
"levitra": "vardenafil", | |
"bactrim": "sulfamethoxazole/trimethoprim", | |
"keflex": "cephalexin", | |
"augmentin": "amoxicillin/clavulanate", | |
"zithromax": "azithromycin", | |
"levaquin": "levofloxacin", | |
"cipro": "ciprofloxacin", | |
"diflucan": "fluconazole", | |
"tamiflu": "oseltamivir", | |
"valtrex": "valacyclovir", | |
"zofran": "ondansetron", | |
"phenergan": "promethazine", | |
"imitrex": "sumatriptan", | |
"flexeril": "cyclobenzaprine", | |
"soma": "carisoprodol", | |
"valium": "diazepam", | |
"ativan": "lorazepam", | |
"klonopin": "clonazepam", | |
"restoril": "temazepam", | |
"ultram": "tramadol", | |
"percocet": "oxycodone/acetaminophen", | |
"vicodin": "hydrocodone/acetaminophen", | |
"oxycontin": "oxycodone", | |
"dilaudid": "hydromorphone", | |
"morphine": "ms contin", | |
"fentanyl": "duragesic" | |
} | |
OUTCOME_MAPPING = { | |
"1": "Recovered/Resolved", | |
"2": "Recovering/Resolving", | |
"3": "Not Recovered/Not Resolved", | |
"4": "Recovered/Resolved with Sequelae", | |
"5": "Fatal", | |
"6": "Unknown", | |
} | |
QUALIFICATION_MAPPING = { | |
"1": "Physician", | |
"2": "Pharmacist", | |
"3": "Other Health Professional", | |
"4": "Lawyer", | |
"5": "Consumer or Non-Health Professional", | |
} | |
SERIOUS_OUTCOME_FIELDS = [ | |
"seriousnessdeath", | |
"seriousnesslifethreatening", | |
"seriousnesshospitalization", | |
"seriousnessdisabling", | |
"seriousnesscongenitalanomali", | |
"seriousnessother", | |
] | |
def get_top_adverse_events(drug_name: str, limit: int = 10, patient_sex: Optional[str] = None, age_range: Optional[Tuple[int, int]] = None) -> dict: | |
""" | |
Query OpenFDA to get the top adverse events for a given drug. | |
Args: | |
drug_name (str): The name of the drug to search for (brand or generic). | |
limit (int): The maximum number of adverse events to return. | |
patient_sex (str): The patient's sex to filter by ('1' for Male, '2' for Female). | |
age_range (tuple): A tuple containing min and max age, e.g., (20, 50). | |
Returns: | |
dict: The JSON response from the API, or an error dictionary. | |
""" | |
if not drug_name: | |
return {"error": "Drug name cannot be empty."} | |
drug_name_processed = drug_name.lower().strip() | |
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed) | |
# Build the search query | |
search_terms = [f'patient.drug.medicinalproduct:"{drug_name_processed}"'] | |
if patient_sex and patient_sex in ["1", "2"]: | |
search_terms.append(f'patient.patientsex:"{patient_sex}"') | |
if age_range and len(age_range) == 2: | |
min_age, max_age = age_range | |
search_terms.append(f'patient.patientonsetage:[{min_age} TO {max_age}]') | |
search_query = "+AND+".join(search_terms) | |
# Using a simple cache key that includes filters | |
cache_key = f"top_events_{drug_name_processed}_{limit}_{patient_sex}_{age_range}" | |
if cache_key in cache: | |
return cache[cache_key] | |
# Query for top events by count | |
count_query_url = ( | |
f'{API_BASE_URL}?search={search_query}' | |
f'&count=patient.reaction.reactionmeddrapt.exact&limit={limit}' | |
) | |
try: | |
# Respect rate limits | |
time.sleep(REQUEST_DELAY_SECONDS) | |
response = requests.get(count_query_url) | |
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) | |
data = response.json() | |
# Query for total reports matching the filters | |
total_query_url = f'{API_BASE_URL}?search={search_query}' | |
time.sleep(REQUEST_DELAY_SECONDS) | |
total_response = requests.get(total_query_url) | |
total_response.raise_for_status() | |
total_data = total_response.json() | |
total_reports = total_data.get("meta", {},).get("results", {}).get("total", 0) | |
# Add total to the main data object | |
if 'meta' not in data: | |
data['meta'] = {} | |
data['meta']['total_reports_for_query'] = total_reports | |
cache[cache_key] = data | |
return data | |
except requests.exceptions.HTTPError as http_err: | |
if response.status_code == 404: | |
return {"error": f"No data found for '{drug_name}' with the specified filters. The drug may not be in the database, or there may be no reports matching the filter criteria."} | |
return {"error": f"HTTP error occurred: {http_err}"} | |
except requests.exceptions.RequestException as req_err: | |
return {"error": f"A network request error occurred: {req_err}"} | |
except Exception as e: | |
return {"error": f"An unexpected error occurred: {e}"} | |
def get_drug_event_pair_frequency(drug_name: str, event_name: str) -> dict: | |
""" | |
Query OpenFDA to get the total number of reports for a specific | |
drug-adverse event pair, and the total reports for the drug alone. | |
Args: | |
drug_name (str): The name of the drug. | |
event_name (str): The name of the adverse event. | |
Returns: | |
dict: A dictionary containing the results or an error message. | |
Includes `total` (for the pair) and `total_for_drug`. | |
""" | |
if not drug_name or not event_name: | |
return {"error": "Drug name and event name cannot be empty."} | |
drug_name_processed = drug_name.lower().strip() | |
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed) | |
event_name_processed = event_name.lower().strip() | |
cache_key = f"pair_freq_{drug_name_processed}_{event_name_processed}" | |
if cache_key in cache: | |
return cache[cache_key] | |
try: | |
# First, get total reports for the drug to see if it exists | |
drug_query = f'search=patient.drug.medicinalproduct:"{drug_name_processed}"' | |
time.sleep(REQUEST_DELAY_SECONDS) | |
drug_response = requests.get(f"{API_BASE_URL}?{drug_query}") | |
# This is a critical failure if the drug isn't found | |
drug_response.raise_for_status() | |
drug_data = drug_response.json() | |
total_for_drug = drug_data.get("meta", {}).get("results", {}).get("total", 0) | |
# Second, get reports for the specific drug-event pair | |
pair_query = ( | |
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"' | |
f'+AND+patient.reaction.reactionmeddrapt:"{event_name_processed}"' | |
) | |
time.sleep(REQUEST_DELAY_SECONDS) | |
pair_response = requests.get(f"{API_BASE_URL}?{pair_query}") | |
total_for_pair = 0 | |
if pair_response.status_code == 200: | |
pair_data = pair_response.json() | |
total_for_pair = pair_data.get("meta", {}).get("results", {}).get("total", 0) | |
# We don't raise for 404 on the pair, as it just means 0 results | |
elif pair_response.status_code != 404: | |
pair_response.raise_for_status() | |
# Construct a consistent response structure | |
data = { | |
"meta": { | |
"results": { | |
"total": total_for_pair, | |
"total_for_drug": total_for_drug | |
} | |
} | |
} | |
cache[cache_key] = data | |
return data | |
except requests.exceptions.HTTPError as http_err: | |
if http_err.response.status_code == 404: | |
return {"error": f"No data found for drug '{drug_name}'. It may be misspelled or not in the database."} | |
return {"error": f"HTTP error occurred: {http_err}"} | |
except requests.exceptions.RequestException as req_err: | |
return {"error": f"A network request error occurred: {req_err}"} | |
except Exception as e: | |
return {"error": f"An unexpected error occurred: {e}"} | |
def get_serious_outcomes(drug_name: str, limit: int = 6) -> dict: | |
""" | |
Query OpenFDA to get the most frequent serious outcomes for a given drug. | |
This function makes multiple API calls to count different outcome fields. | |
Args: | |
drug_name (str): The name of the drug to search for. | |
limit (int): The maximum number of outcomes to return. | |
Returns: | |
dict: A dictionary containing aggregated results or an error. | |
""" | |
if not drug_name: | |
return {"error": "Drug name cannot be empty."} | |
drug_name_processed = drug_name.lower().strip() | |
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed) | |
# Use a cache key for the aggregated result | |
cache_key = f"serious_outcomes_aggregated_{drug_name_processed}_{limit}" | |
if cache_key in cache: | |
return cache[cache_key] | |
aggregated_results = {} | |
# Base search for all serious reports | |
base_search_query = f'patient.drug.medicinalproduct:"{drug_name_processed}"+AND+serious:1' | |
# Get total number of serious reports | |
total_serious_reports = 0 | |
try: | |
total_query_url = f"{API_BASE_URL}?search={base_search_query}" | |
time.sleep(REQUEST_DELAY_SECONDS) | |
response = requests.get(total_query_url) | |
if response.status_code == 200: | |
total_data = response.json() | |
total_serious_reports = total_data.get("meta", {}).get("results", {}).get("total", 0) | |
elif response.status_code != 404: | |
# If this call fails, we can still proceed, the total will just be 0. | |
pass | |
except requests.exceptions.RequestException: | |
# If fetching total fails, proceed without it. | |
pass | |
for field in SERIOUS_OUTCOME_FIELDS: | |
try: | |
# Each query counts reports where the specific seriousness field exists | |
query = f"search={base_search_query}+AND+_exists_:{field}" | |
time.sleep(REQUEST_DELAY_SECONDS) | |
response = requests.get(f"{API_BASE_URL}?{query}") | |
if response.status_code == 200: | |
data = response.json() | |
total_count = data.get("meta", {}).get("results", {}).get("total", 0) | |
if total_count > 0: | |
# Use a more readable name for the outcome | |
outcome_name = field.replace("seriousness", "").replace("anomali", "anomaly").title() | |
aggregated_results[outcome_name] = total_count | |
# Ignore 404s, as they just mean no results for that specific outcome | |
elif response.status_code != 404: | |
response.raise_for_status() | |
except requests.exceptions.RequestException as e: | |
return {"error": f"A network request error occurred for field {field}: {e}"} | |
if not aggregated_results: | |
return {"error": f"No serious outcome data found for drug: '{drug_name}'."} | |
# Format the results to match the expected structure for plotting | |
final_data = { | |
"results": [{"term": k, "count": v} for k, v in aggregated_results.items()], | |
"meta": {"total_reports_for_query": total_serious_reports} | |
} | |
# Sort results by count, descending, and then limit | |
final_data["results"] = sorted(final_data["results"], key=lambda x: x['count'], reverse=True) | |
if limit: | |
final_data["results"] = final_data["results"][:limit] | |
cache[cache_key] = final_data | |
return final_data | |
def get_time_series_data(drug_name: str, event_name: str) -> dict: | |
""" | |
Query OpenFDA to get the time series data for a drug-event pair. | |
Args: | |
drug_name (str): The name of the drug. | |
event_name (str): The name of the adverse event. | |
Returns: | |
dict: The JSON response from the API, or an error dictionary. | |
""" | |
if not drug_name or not event_name: | |
return {"error": "Drug name and event name cannot be empty."} | |
drug_name_processed = drug_name.lower().strip() | |
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed) | |
event_name_processed = event_name.lower().strip() | |
cache_key = f"time_series_{drug_name_processed}_{event_name_processed}" | |
if cache_key in cache: | |
return cache[cache_key] | |
query = ( | |
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"' | |
f'+AND+patient.reaction.reactionmeddrapt:"{event_name_processed}"' | |
f'&count=receiptdate' | |
) | |
try: | |
time.sleep(REQUEST_DELAY_SECONDS) | |
response = requests.get(f"{API_BASE_URL}?{query}") | |
response.raise_for_status() | |
data = response.json() | |
cache[cache_key] = data | |
return data | |
except requests.exceptions.HTTPError as http_err: | |
if response.status_code == 404: | |
return {"error": f"No data found for drug '{drug_name}' and event '{event_name}'. They may be misspelled or not in the database."} | |
return {"error": f"HTTP error occurred: {http_err}"} | |
except requests.exceptions.RequestException as req_err: | |
return {"error": f"A network request error occurred: {req_err}"} | |
except Exception as e: | |
return {"error": f"An unexpected error occurred: {e}"} | |
def get_report_source_data(drug_name: str, limit: int = 5) -> dict: | |
""" | |
Query OpenFDA to get the breakdown of report sources for a given drug. | |
Args: | |
drug_name (str): The name of the drug to search for. | |
limit (int): The maximum number of sources to return. | |
Returns: | |
dict: The JSON response from the API, or an error dictionary. | |
""" | |
if not drug_name: | |
return {"error": "Drug name cannot be empty."} | |
drug_name_processed = drug_name.lower().strip() | |
drug_name_processed = DRUG_SYNONYM_MAPPING.get(drug_name_processed, drug_name_processed) | |
cache_key = f"report_source_{drug_name_processed}_{limit}" | |
if cache_key in cache: | |
return cache[cache_key] | |
query = ( | |
f'search=patient.drug.medicinalproduct:"{drug_name_processed}"' | |
f'&count=primarysource.qualification' | |
) | |
try: | |
time.sleep(REQUEST_DELAY_SECONDS) | |
response = requests.get(f"{API_BASE_URL}?{query}") | |
response.raise_for_status() | |
data = response.json() | |
# Translate the qualification codes and calculate total before limiting | |
if "results" in data: | |
# Sort by count first | |
data['results'] = sorted(data['results'], key=lambda x: x['count'], reverse=True) | |
# Calculate total from all results before limiting | |
total_with_source = sum(item['count'] for item in data['results']) | |
if 'meta' not in data: | |
data['meta'] = {} | |
data['meta']['total_reports_for_query'] = total_with_source | |
# Translate codes after processing | |
for item in data["results"]: | |
term_str = str(item["term"]) | |
item["term"] = QUALIFICATION_MAPPING.get(term_str, f"Unknown ({term_str})") | |
# Apply limit | |
if limit: | |
data['results'] = data['results'][:limit] | |
cache[cache_key] = data | |
return data | |
except requests.exceptions.HTTPError as http_err: | |
if response.status_code == 404: | |
return {"error": f"No data found for drug: '{drug_name}'."} | |
return {"error": f"HTTP error occurred: {http_err}"} | |
except requests.exceptions.RequestException as req_err: | |
return {"error": f"A network request error occurred: {req_err}"} | |
except Exception as e: | |
return {"error": f"An unexpected error occurred: {e}"} |