Spaces:
Sleeping
Sleeping
| """ | |
| Cross-Medical-System Drug Recommender v4.0 | |
| 4 Tabs: Search | Cross-Compare | FDA Live | Dashboard | |
| Symptoms + Medicine dropdown, Plotly dashboard, OpenFDA API | |
| """ | |
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import joblib, json, os, re, warnings, requests | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| from plotly.subplots import make_subplots | |
| warnings.filterwarnings("ignore") | |
| # ── Constants ──────────────────────────────────────────────────── | |
| OPENFDA_BASE = "https://api.fda.gov/drug" | |
| MODEL_DIR = os.path.join(os.path.dirname(__file__), "models") | |
| SYSTEM_COLORS = { | |
| "Allopathic": "#3b82f6", | |
| "Unani": "#f97316", | |
| "Ayurvedic": "#22c55e", | |
| "Homeopathic": "#a855f7", | |
| "Herbal": "#ef4444", | |
| } | |
| # ── Dropdown options: Symptoms AND Medicines ───────────────────── | |
| SEARCH_OPTIONS = { | |
| # ══ SYMPTOMS / CONDITIONS ════════════════════════════════════ | |
| "── SYMPTOMS & CONDITIONS ──": None, # separator (disabled) | |
| "🤒 Fever": "fever paracetamol tablet", | |
| "🩹 Body Pain / General Pain": "pain relief tablet", | |
| "🤕 Headache": "headache pain tablet", | |
| "😤 Cold & Runny Nose": "cold antihistamine tablet", | |
| "😮💨 Cough": "cough syrup liquid", | |
| "🫁 Asthma / Breathing Difficulty": "asthma bronchodilator tablet", | |
| "🤮 Nausea & Vomiting": "nausea vomiting tablet", | |
| "🫃 Acidity / Heartburn": "acidity antacid capsule", | |
| "💩 Diarrhea": "diarrhea tablet", | |
| "🤢 Stomach Pain": "stomach pain tablet", | |
| "😴 Anxiety / Sleeplessness": "anxiety sleep tablet", | |
| "🫀 High Blood Pressure": "hypertension blood pressure tablet", | |
| "🩸 High Blood Sugar (Diabetes)": "diabetes blood sugar tablet", | |
| "🦴 Joint Pain / Arthritis": "joint pain inflammation tablet", | |
| "🦷 Tooth / Ear Infection": "antibiotic infection capsule", | |
| "👁️ Eye Infection": "eye drops infection", | |
| "🩺 Urinary Tract Infection (UTI)": "urinary tract infection antibiotic tablet", | |
| "🌿 Worm / Parasite Infection": "deworming tablet", | |
| "🫧 Skin Fungal Infection": "antifungal cream tablet", | |
| "😵 Dizziness / Vertigo": "vertigo dizziness tablet", | |
| # ══ MEDICINES BY NAME ════════════════════════════════════════ | |
| "── MEDICINES BY NAME ──": None, # separator (disabled) | |
| # Antibiotics | |
| "🦠 Azithromycin — Antibiotic": "Azithromycin 500mg tablet", | |
| "🦠 Amoxicillin — Antibiotic": "Amoxicillin 500mg capsule", | |
| "🦠 Ciprofloxacin — Antibiotic": "Ciprofloxacin 500mg tablet", | |
| "🦠 Metronidazole — Antibiotic": "Metronidazole 400mg tablet", | |
| "🦠 Ceftriaxone — Injection": "Ceftriaxone 1gm injection", | |
| "🦠 Levofloxacin — Antibiotic": "Levofloxacin 500mg tablet", | |
| # Pain & Fever | |
| "🤒 Paracetamol — Fever/Pain": "Paracetamol 500mg tablet", | |
| "🤒 Diclofenac — Anti-inflammatory": "Diclofenac Sodium 50mg tablet", | |
| "🤒 Naproxen — Pain (Joints)": "Naproxen 250mg tablet", | |
| "🤒 Ibuprofen — Pain/Fever": "Ibuprofen 400mg tablet", | |
| # Heart & BP | |
| "💓 Amlodipine — Blood Pressure": "Amlodipine 5mg tablet", | |
| "💓 Atorvastatin — Cholesterol": "Atorvastatin 20mg tablet", | |
| "💓 Losartan — Hypertension": "Losartan Potassium 50mg tablet", | |
| "💓 Metoprolol — Beta Blocker": "Metoprolol 50mg tablet", | |
| # Diabetes | |
| "🩺 Metformin — Diabetes": "Metformin Hydrochloride 500mg tablet", | |
| "🩺 Glibenclamide — Blood Sugar": "Glibenclamide 5mg tablet", | |
| # Respiratory & Allergy | |
| "🫁 Salbutamol — Asthma": "Salbutamol 2mg tablet syrup", | |
| "🫁 Montelukast — Allergy/Asthma": "Montelukast 10mg tablet", | |
| "🫁 Fexofenadine — Antihistamine": "Fexofenadine 120mg tablet", | |
| "🫁 Cetirizine — Antihistamine": "Cetirizine 10mg tablet", | |
| # Neuro | |
| "🧠 Pregabalin — Nerve Pain": "Pregabalin 75mg capsule", | |
| "🧠 Clonazepam — Anxiety/Seizure": "Clonazepam 0.5mg tablet", | |
| # GI / Stomach | |
| "🫃 Omeprazole — Acid Reflux": "Omeprazole 20mg capsule", | |
| "🫃 Esomeprazole — GERD": "Esomeprazole 40mg capsule", | |
| "🫃 Domperidone — Nausea": "Domperidone 10mg tablet", | |
| "🫃 Ondansetron — Nausea": "Ondansetron 4mg tablet", | |
| # Antifungal / Deworming | |
| "🌿 Albendazole — Deworming": "Albendazole 400mg tablet", | |
| "🌿 Fluconazole — Antifungal": "Fluconazole 150mg capsule", | |
| # Vitamins | |
| "💊 Vitamin D3 — Bone/Immunity": "Cholecalciferol Vitamin D3 tablet", | |
| "💊 Zinc + Multivitamin": "Zinc Nicotinamide vitamin tablet", | |
| } | |
| # Remove separator entries to get valid choices | |
| ALL_LABELS = list(SEARCH_OPTIONS.keys()) | |
| # ── Load PKL Models ─────────────────────────────────────────────── | |
| def load_models(): | |
| vec = joblib.load(os.path.join(MODEL_DIR, "tfidf_vectorizer.pkl")) | |
| mat = joblib.load(os.path.join(MODEL_DIR, "tfidf_matrix.pkl")) | |
| db = pd.read_csv(os.path.join(MODEL_DIR, "drug_database.csv")) | |
| with open(os.path.join(MODEL_DIR, "model_metadata.json")) as f: | |
| meta = json.load(f) | |
| return vec, mat, db, meta | |
| try: | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| vectorizer, tfidf_matrix, drug_db, metadata = load_models() | |
| MEDICAL_SYSTEMS = ["All Systems"] + sorted(drug_db["medical_system"].unique().tolist()) | |
| MODEL_LOADED = True | |
| print(f"✅ Loaded {len(drug_db):,} drugs") | |
| except Exception as e: | |
| print(f"Model load failed: {e}") | |
| MODEL_LOADED = False | |
| drug_db = pd.DataFrame() | |
| metadata = {} | |
| MEDICAL_SYSTEMS = ["All Systems"] | |
| # ── Helpers ─────────────────────────────────────────────────────── | |
| def _clean(t): | |
| if pd.isna(t): return "" | |
| t = re.sub(r"[^a-z0-9\s\+\-\.]", " ", str(t).lower()) | |
| return re.sub(r"\s+", " ", t).strip() | |
| def _get_query(label): | |
| return SEARCH_OPTIONS.get(label) or label | |
| def _extract_generic(label): | |
| """Extract the best single search term from a dropdown label for OpenFDA queries. | |
| Examples | |
| -------- | |
| '🦠 Azithromycin — Antibiotic' → 'Azithromycin' | |
| '💊 Vitamin D3 — Bone/Immunity' → 'Cholecalciferol' (mapped) | |
| '🤒 Paracetamol — Fever/Pain' → 'Paracetamol' | |
| '🩺 Metformin — Diabetes' → 'Metformin' | |
| '🤒 Fever' (symptom) → uses query word | |
| """ | |
| # Hardcoded map for labels whose first real word is ambiguous for FDA search | |
| FDA_MAP = { | |
| "Vitamin D3": "Cholecalciferol", | |
| "Zinc": "Zinc", | |
| "Salbutamol": "Albuterol", # US FDA name | |
| "Paracetamol": "Acetaminophen", # US FDA name | |
| "Fexofenadine": "Fexofenadine", | |
| "Cetirizine": "Cetirizine", | |
| "Amlodipine": "Amlodipine", | |
| "Atorvastatin": "Atorvastatin", | |
| "Metformin": "Metformin", | |
| "Omeprazole": "Omeprazole", | |
| "Esomeprazole": "Esomeprazole", | |
| "Domperidone": "Domperidone", | |
| "Ondansetron": "Ondansetron", | |
| "Albendazole": "Albendazole", | |
| "Fluconazole": "Fluconazole", | |
| "Pregabalin": "Pregabalin", | |
| "Clonazepam": "Clonazepam", | |
| "Montelukast": "Montelukast", | |
| "Losartan": "Losartan", | |
| "Metoprolol": "Metoprolol", | |
| } | |
| # Strip everything after — | |
| raw = label.split("—")[0] | |
| cleaned = re.sub(r"[^\w\s]", "", raw).strip() | |
| words = [w for w in cleaned.split() if len(w) > 2] # skip short emoji fragments | |
| if not words: | |
| return cleaned | |
| # Try each word against the map | |
| for w in words: | |
| if w in FDA_MAP: | |
| return FDA_MAP[w] | |
| # Heuristic: return the longest word (most likely to be a pharmaceutical term) | |
| return max(words, key=len) | |
| def _openfda(endpoint, params, timeout=10): | |
| try: | |
| r = requests.get(f"{OPENFDA_BASE}/{endpoint}.json", params=params, | |
| timeout=timeout, headers={"User-Agent": "DrugRecommender/4.0"}) | |
| return r.json() if r.status_code == 200 else {"error": f"HTTP {r.status_code}", "message": r.text[:200]} | |
| except requests.exceptions.Timeout: | |
| return {"error": "timeout", "message": "OpenFDA timed out — try again."} | |
| except Exception as e: | |
| return {"error": "connection", "message": str(e)} | |
| # ── Core Recommender ────────────────────────────────────────────── | |
| def recommend(label, system_filter, top_n, min_score): | |
| if not MODEL_LOADED: | |
| return None, "❌ Models not loaded." | |
| query = _get_query(label) | |
| if not query: | |
| return None, "⚠️ Please select a valid option (not a section header)." | |
| q_vec = vectorizer.transform([_clean(query)]) | |
| sims = cosine_similarity(q_vec, tfidf_matrix).flatten() | |
| if system_filter != "All Systems": | |
| mask = drug_db["medical_system"] == system_filter | |
| work = sims.copy(); work[~mask] = 0 | |
| else: | |
| work = sims | |
| idx = [i for i in work.argsort()[-(top_n*4):][::-1] if sims[i] >= min_score][:top_n] | |
| if not idx: | |
| return None, f"⚠️ No results above score {min_score}. Lower the threshold." | |
| out = drug_db.iloc[idx][["brand_name","generic_name","dosage_form","strength","medical_system","manufacturer"]].copy() | |
| out["score"] = [round(float(sims[i]),4) for i in idx] | |
| out = out.sort_values("score", ascending=False).reset_index(drop=True) | |
| out.index = range(1, len(out)+1); out.index.name = "Rank" | |
| out.columns = ["Brand Name","Generic Name","Dosage Form","Strength","Medical System","Manufacturer","Score"] | |
| sys_str = " · ".join(f"**{k}** {v}" for k,v in out["Medical System"].value_counts().items()) | |
| label_short = re.sub(r"[^\w\s\-/]","",label).strip()[:40] | |
| summary = f"### ✅ {len(out)} results for **{label_short}**\n\n{sys_str}\n\n*Query: `{query}`*" | |
| return out, summary | |
| def cross_compare(label, top_per): | |
| if not MODEL_LOADED: return None, "❌ Models not loaded." | |
| query = _get_query(label) | |
| if not query: return None, "⚠️ Select a valid option." | |
| q_vec = vectorizer.transform([_clean(query)]) | |
| sims = cosine_similarity(q_vec, tfidf_matrix).flatten() | |
| rows = [] | |
| for sys in sorted(drug_db["medical_system"].unique()): | |
| mask = drug_db["medical_system"] == sys | |
| s = sims.copy(); s[~mask] = 0 | |
| for i in [x for x in s.argsort()[-top_per:][::-1] if sims[x] > 0.01]: | |
| r = drug_db.iloc[i] | |
| rows.append({"System": r["medical_system"], "Brand": r["brand_name"], | |
| "Generic": r["generic_name"], "Form": r["dosage_form"], | |
| "Strength": r["strength"], "Score": round(float(sims[i]),4)}) | |
| if not rows: return None, "No results found." | |
| df = pd.DataFrame(rows).sort_values(["System","Score"],ascending=[True,False]).reset_index(drop=True) | |
| df.index = range(1, len(df)+1); df.index.name = "Rank" | |
| label_short = re.sub(r"[^\w\s\-/]","",label).strip()[:40] | |
| return df, f"### 🌐 **{label_short}** — {len(df)} drugs across {df['System'].nunique()} systems" | |
| # ── OpenFDA ─────────────────────────────────────────────────────── | |
| def fda_label(label): | |
| g = _extract_generic(label) | |
| d = _openfda("label", {"search": f"openfda.generic_name:{g}", "limit": 1}) | |
| if "error" in d: | |
| return f"### ⚠️ {d['message']}\n\n*`{g}` may not be in US FDA records.*" | |
| res = d.get("results", []) | |
| if not res: return f"ℹ️ No FDA label for **{g}**." | |
| r = res[0]; ofd = r.get("openfda", {}) | |
| lines = [f"## 💊 {g.title()} — FDA Label", "_U.S. Food & Drug Administration · OpenFDA_\n"] | |
| for k,t in [("brand_name","Brand Names"),("manufacturer_name","Manufacturer"),("route","Route")]: | |
| v = ofd.get(k,[]) | |
| if v: lines.append(f"**{t}:** {', '.join(v[:5])}") | |
| lines.append("") | |
| for field, heading, lim in [ | |
| ("indications_and_usage","📋 Indications & Usage",700), | |
| ("warnings","⚠️ Warnings",500), | |
| ("dosage_and_administration","💉 Dosage",500), | |
| ("adverse_reactions","🔴 Adverse Reactions",400), | |
| ("drug_interactions","🔗 Drug Interactions",400), | |
| ]: | |
| v = r.get(field,[]) | |
| if v: lines += [f"### {heading}", v[0][:lim]+"…\n"] | |
| lines.append("---\n*[OpenFDA](https://open.fda.gov) · Research only · Not clinical advice*") | |
| return "\n".join(lines) | |
| def fda_adverse(label): | |
| g = _extract_generic(label) | |
| d = _openfda("event",{"search":f"patient.drug.medicinalproduct:{g}","count":"patient.reaction.reactionmeddrapt.exact","limit":15}) | |
| if "error" in d: return None, f"### ⚠️ {d['message']}" | |
| res = d.get("results",[]) | |
| if not res: return None, f"ℹ️ No FAERS data for **{g}**." | |
| df = pd.DataFrame(res, columns=["Adverse Reaction","Report Count"]) | |
| df = df.sort_values("Report Count",ascending=False).reset_index(drop=True) | |
| df.index = range(1,len(df)+1); df.index.name = "Rank" | |
| return df, f"### 📊 FAERS: **{g.title()}** · {df['Report Count'].sum():,} total reports" | |
| def fda_ndc(label): | |
| g = _extract_generic(label) | |
| d = _openfda("ndc",{"search":f"generic_name:{g}","limit":10}) | |
| if "error" in d: return None, f"### ⚠️ {d['message']}" | |
| res = d.get("results",[]) | |
| if not res: return None, f"ℹ️ No NDC data for **{g}**." | |
| df = pd.DataFrame([{"Brand":r.get("brand_name","—"),"Generic":r.get("generic_name","—"), | |
| "Form":r.get("dosage_form","—"),"Route":", ".join(r.get("route",[])),"Manufacturer":r.get("labeler_name","—"), | |
| "NDC":r.get("product_ndc","—")} for r in res]) | |
| df.index = range(1,len(df)+1); df.index.name = "#" | |
| return df, f"### 🏷️ NDC Registry: **{g.title()}** · {len(df)} products" | |
| # ── Dashboard Charts ────────────────────────────────────────────── | |
| if MODEL_LOADED and not drug_db.empty: | |
| _sys = drug_db["medical_system"].value_counts() | |
| _dos = drug_db["dosage_form"].value_counts().head(10) | |
| _mfr = drug_db["manufacturer"].value_counts().head(15) | |
| _cross = pd.crosstab(drug_db["medical_system"], drug_db["dosage_form"]) | |
| _cross = _cross[[c for c in _dos.index[:8] if c in _cross.columns]] | |
| else: | |
| _sys = pd.Series({"No data":1}); _dos = _sys.copy(); _mfr = _sys.copy(); _cross = pd.DataFrame() | |
| def _sc(labels): return [SYSTEM_COLORS.get(l,"#64748b") for l in labels] | |
| def make_dashboard(): | |
| # ── Row 1: KPI cards via annotations ────────────────────────── | |
| fig_kpi = go.Figure() | |
| kpis = [ | |
| ("53,581", "Total Drugs", "#3b82f6"), | |
| ("725", "Manufacturers", "#22c55e"), | |
| ("5", "Medical Systems", "#a855f7"), | |
| ("1,702", "Unique Compounds", "#f97316"), | |
| ] | |
| for i,(val,lbl,col) in enumerate(kpis): | |
| x = 0.13 + i*0.25 | |
| r2,g2,b2 = int(col[1:3],16), int(col[3:5],16), int(col[5:7],16) | |
| fill_rgba = f"rgba({r2},{g2},{b2},0.12)" | |
| fig_kpi.add_shape(type="rect", x0=x-0.11, x1=x+0.11, y0=0.05, y1=0.95, | |
| fillcolor=fill_rgba, line=dict(color=col, width=2), xref="paper", yref="paper") | |
| fig_kpi.add_annotation(x=x, y=0.62, text=f"<b>{val}</b>", showarrow=False, | |
| font=dict(size=28, color=col), xref="paper", yref="paper") | |
| fig_kpi.add_annotation(x=x, y=0.28, text=lbl, showarrow=False, | |
| font=dict(size=13, color="#475569"), xref="paper", yref="paper") | |
| fig_kpi.update_layout(height=130, margin=dict(t=10,b=10,l=10,r=10), | |
| paper_bgcolor="white", plot_bgcolor="white", | |
| xaxis=dict(visible=False), yaxis=dict(visible=False)) | |
| # ── Donut ────────────────────────────────────────────────────── | |
| fig_donut = go.Figure(go.Pie( | |
| labels=_sys.index.tolist(), values=_sys.values.tolist(), hole=0.58, | |
| marker=dict(colors=_sc(_sys.index), line=dict(color="#fff",width=2.5)), | |
| textinfo="label+percent", textfont=dict(size=12), | |
| hovertemplate="<b>%{label}</b><br>%{value:,} drugs · %{percent}<extra></extra>", | |
| )) | |
| fig_donut.update_layout( | |
| title=dict(text="<b>Drug Share by Medical System</b>", x=0.5, font=dict(size=15)), | |
| annotations=[dict(text=f"<b>53,581</b><br>Drugs", x=0.5, y=0.5, font=dict(size=13), showarrow=False)], | |
| legend=dict(orientation="h", y=-0.05, x=0.5, xanchor="center", font=dict(size=11)), | |
| height=340, margin=dict(t=50,b=30,l=10,r=10), paper_bgcolor="white", | |
| ) | |
| # ── H-Bar dosage ─────────────────────────────────────────────── | |
| labs = _dos.index.tolist()[::-1]; vals = _dos.values.tolist()[::-1] | |
| fig_bar = go.Figure(go.Bar( | |
| y=labs, x=vals, orientation="h", | |
| marker=dict(color=px.colors.sequential.Blues_r[:len(labs)]), | |
| text=[f" {v:,}" for v in vals], textposition="outside", | |
| hovertemplate="<b>%{y}</b>: %{x:,}<extra></extra>", | |
| )) | |
| fig_bar.update_layout( | |
| title=dict(text="<b>Top 10 Dosage Forms</b>", x=0.5, font=dict(size=15)), | |
| xaxis=dict(showgrid=True, gridcolor="#f0f0f0", title="Count"), | |
| yaxis=dict(title=""), height=340, | |
| margin=dict(t=50,b=30,l=160,r=60), paper_bgcolor="white", plot_bgcolor="white", | |
| ) | |
| # ── Grouped bar system × dosage ──────────────────────────────── | |
| fig_grp = go.Figure() | |
| palette = px.colors.qualitative.Pastel | |
| for j,col in enumerate([c for c in _cross.columns if not _cross.empty]): | |
| fig_grp.add_trace(go.Bar( | |
| name=col, x=_cross.index.tolist(), y=_cross[col].tolist(), | |
| marker_color=palette[j % len(palette)], | |
| hovertemplate=f"<b>{col}</b><br>%{{x}}: %{{y:,}}<extra></extra>", | |
| )) | |
| fig_grp.update_layout( | |
| barmode="group", | |
| title=dict(text="<b>Dosage Form per Medical System</b>", x=0.5, font=dict(size=15)), | |
| xaxis=dict(title=""), yaxis=dict(title="Drug Count", showgrid=True, gridcolor="#f0f0f0"), | |
| legend=dict(title="Form", orientation="h", y=-0.22, x=0.5, xanchor="center", font=dict(size=10)), | |
| height=380, margin=dict(t=50,b=100,l=60,r=20), paper_bgcolor="white", plot_bgcolor="white", | |
| ) | |
| # ── Treemap ──────────────────────────────────────────────────── | |
| tmdf = drug_db.groupby(["medical_system","manufacturer"]).size().reset_index(name="count") | |
| tmdf = tmdf.sort_values("count",ascending=False).groupby("medical_system").head(6).reset_index(drop=True) | |
| fig_tree = px.treemap(tmdf, path=["medical_system","manufacturer"], values="count", | |
| color="medical_system", color_discrete_map=SYSTEM_COLORS, | |
| custom_data=["count"]) | |
| fig_tree.update_traces(hovertemplate="<b>%{label}</b><br>Products: %{customdata[0]:,}<extra></extra>", | |
| textfont=dict(size=11)) | |
| fig_tree.update_layout( | |
| title=dict(text="<b>Top Manufacturers by Medical System</b>", x=0.5, font=dict(size=15)), | |
| height=420, margin=dict(t=50,b=10,l=10,r=10), paper_bgcolor="white", | |
| ) | |
| # ── Radar ────────────────────────────────────────────────────── | |
| cats = [c for c in ["Tablet","Capsule","Liquid","Injection","Syrup"] if not _cross.empty and c in _cross.columns] | |
| fig_radar = go.Figure() | |
| if cats: | |
| sub = _cross[cats] | |
| sub_n = sub.div(sub.max(axis=0),axis=1).fillna(0)*100 | |
| for sys in sub_n.index: | |
| vals = sub_n.loc[sys].tolist() | |
| col = SYSTEM_COLORS.get(sys,"#64748b") | |
| r2,g2,b2 = int(col[1:3],16), int(col[3:5],16), int(col[5:7],16) | |
| fill_rgba = f"rgba({r2},{g2},{b2},0.15)" | |
| fig_radar.add_trace(go.Scatterpolar( | |
| r=vals+[vals[0]], theta=cats+[cats[0]], | |
| fill="toself", fillcolor=fill_rgba, | |
| line=dict(color=col,width=2.5), name=sys, | |
| hovertemplate=f"<b>{sys}</b><br>%{{theta}}: %{{r:.0f}}%<extra></extra>", | |
| )) | |
| fig_radar.update_layout( | |
| polar=dict(radialaxis=dict(visible=True,range=[0,115],tickfont=dict(size=9),gridcolor="#e5e7eb"), | |
| angularaxis=dict(tickfont=dict(size=12)), bgcolor="white"), | |
| title=dict(text="<b>System Profile — Dosage Radar</b>", x=0.5, font=dict(size=15)), | |
| legend=dict(orientation="h", y=-0.1, x=0.5, xanchor="center"), | |
| height=400, margin=dict(t=50,b=60,l=50,r=50), paper_bgcolor="white", | |
| ) | |
| # ── Top manufacturers bar ────────────────────────────────────── | |
| fig_mfr = go.Figure(go.Bar( | |
| y=_mfr.index.tolist()[::-1], x=_mfr.values.tolist()[::-1], | |
| orientation="h", | |
| marker=dict(color=px.colors.sequential.Teal_r[:15]), | |
| text=[f" {v:,}" for v in _mfr.values.tolist()[::-1]], | |
| textposition="outside", | |
| hovertemplate="<b>%{y}</b>: %{x:,} products<extra></extra>", | |
| )) | |
| fig_mfr.update_layout( | |
| title=dict(text="<b>Top 15 Manufacturers by Product Count</b>", x=0.5, font=dict(size=15)), | |
| xaxis=dict(showgrid=True, gridcolor="#f0f0f0", title="Products"), | |
| yaxis=dict(title=""), height=450, | |
| margin=dict(t=50,b=30,l=250,r=80), paper_bgcolor="white", plot_bgcolor="white", | |
| ) | |
| return fig_kpi, fig_donut, fig_bar, fig_grp, fig_tree, fig_radar, fig_mfr | |
| # ═══════════════════════════════════════════════════════════════════ | |
| # GRADIO UI | |
| # ═══════════════════════════════════════════════════════════════════ | |
| CSS = """ | |
| .gradio-container { max-width:1080px !important; margin:auto !important; | |
| font-family:'Segoe UI',system-ui,sans-serif !important; } | |
| .hero { background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 55%,#0f172a 100%); | |
| border:1px solid rgba(99,102,241,.35); border-radius:16px; | |
| padding:28px 32px 22px; margin-bottom:18px; text-align:center; } | |
| .sbadge { display:inline-block; border-radius:999px; padding:4px 13px; | |
| font-size:12px; margin:3px; } | |
| .fix-note { background:rgba(34,197,94,.08); border:1px solid rgba(34,197,94,.25); | |
| border-radius:10px; padding:11px 16px; font-size:13px; margin:8px 0 12px; } | |
| footer { display:none !important; } | |
| """ | |
| HEADER = """ | |
| <div class="hero"> | |
| <h1 style="color:white;font-size:2em;margin:0 0 8px;font-weight:800;"> | |
| 💊 Cross-Medical-System Drug Recommender | |
| </h1> | |
| <p style="color:#94a3b8;margin:0 0 14px;font-size:1rem;"> | |
| 53,581 drugs · Search by Symptom or Medicine · NLP-Powered · Master's Thesis | |
| </p> | |
| <div> | |
| <span class="sbadge" style="background:rgba(59,130,246,.15);border:1px solid rgba(59,130,246,.3);color:#93c5fd;">🔵 Allopathic 36,251</span> | |
| <span class="sbadge" style="background:rgba(249,115,22,.12);border:1px solid rgba(249,115,22,.3);color:#fdba74;">🟠 Unani 8,460</span> | |
| <span class="sbadge" style="background:rgba(34,197,94,.12);border:1px solid rgba(34,197,94,.3);color:#86efac;">🟢 Ayurvedic 5,262</span> | |
| <span class="sbadge" style="background:rgba(168,85,247,.12);border:1px solid rgba(168,85,247,.3);color:#d8b4fe;">🟣 Homeopathic 2,580</span> | |
| <span class="sbadge" style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#fca5a5;">🔴 Herbal 1,028</span> | |
| <span class="sbadge" style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);color:#6ee7b7;">🇺🇸 OpenFDA API</span> | |
| </div> | |
| </div> | |
| """ | |
| FIX_NOTE = """ | |
| <div class="fix-note"> | |
| <strong>✅ Bug fix v3:</strong> Non-allopathic drugs no longer appear in Allopathic compound searches. | |
| Brand names like <em>"Feverfit"</em> are excluded from TF-IDF — only Generic Name drives Allopathic matching. | |
| </div> | |
| """ | |
| with gr.Blocks(css=CSS, title="💊 Drug Recommender", theme=gr.themes.Soft()) as demo: | |
| gr.HTML(HEADER) | |
| # ── Shared selector ────────────────────────────────────────── | |
| gr.Markdown("### 👇 Pick a symptom or medicine — then explore any tab") | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| selector = gr.Dropdown( | |
| choices=ALL_LABELS, | |
| value="🤒 Fever", | |
| label="🔍 Search by Symptom or Medicine Name", | |
| info="20 symptoms + 30 medicines = 50 options total", | |
| interactive=True, | |
| ) | |
| with gr.Column(scale=2): | |
| sys_filter = gr.Dropdown( | |
| choices=MEDICAL_SYSTEMS, value="All Systems", | |
| label="🏥 Filter by Medical System", | |
| ) | |
| # ── 4 Tabs ─────────────────────────────────────────────────── | |
| with gr.Tabs(): | |
| # ══════════════════════════════════════════════════════════ | |
| # TAB 1 — Recommendations | |
| # ══════════════════════════════════════════════════════════ | |
| with gr.TabItem("🔍 Find Drugs"): | |
| gr.HTML(FIX_NOTE) | |
| gr.Markdown("Finds similar drugs using **TF-IDF cosine similarity** across all 53,581 records.") | |
| with gr.Row(): | |
| top_n = gr.Slider(3, 25, value=10, step=1, label="📋 Results") | |
| min_score = gr.Slider(0.01, 0.50, value=0.05, step=0.01, label="🎯 Min Score") | |
| rec_btn = gr.Button("🚀 Get Recommendations", variant="primary", size="lg") | |
| rec_summary = gr.Markdown() | |
| rec_table = gr.DataFrame(wrap=True, interactive=False, | |
| label="📋 Recommended Drugs") | |
| rec_btn.click(fn=recommend, | |
| inputs=[selector, sys_filter, top_n, min_score], | |
| outputs=[rec_table, rec_summary]) | |
| gr.Markdown(""" | |
| --- | |
| **Matching logic:** | |
| - **Symptoms** (e.g. Fever) → maps to compound query like `"fever paracetamol tablet"` | |
| - **Allopathic** → matched by Generic Name compound ✅ | |
| - **Ayurvedic / Unani / Homeopathic / Herbal** → matched by dosage form + system | |
| """) | |
| # ══════════════════════════════════════════════════════════ | |
| # TAB 2 — Cross-System Compare | |
| # ══════════════════════════════════════════════════════════ | |
| with gr.TabItem("🌐 Cross-System Compare"): | |
| gr.Markdown(""" | |
| ### 🏆 Core Thesis Feature | |
| Best results from **every medical tradition** shown side by side for the same query. | |
| """) | |
| top_per = gr.Slider(1, 5, value=3, step=1, label="Results per System") | |
| cmp_btn = gr.Button("🔄 Compare All 5 Systems", variant="primary", size="lg") | |
| cmp_summary = gr.Markdown() | |
| cmp_table = gr.DataFrame(wrap=True, interactive=False, | |
| label="🌐 All 5 Systems Side by Side") | |
| cmp_btn.click(fn=cross_compare, inputs=[selector, top_per], | |
| outputs=[cmp_table, cmp_summary]) | |
| # ══════════════════════════════════════════════════════════ | |
| # TAB 3 — FDA Live API | |
| # ══════════════════════════════════════════════════════════ | |
| with gr.TabItem("🇺🇸 FDA Live Data"): | |
| gr.Markdown(""" | |
| ### Live data from the U.S. Food & Drug Administration | |
| > 🔌 **OpenFDA APIs** — Free · No key required · | |
| [open.fda.gov](https://open.fda.gov) | |
| """) | |
| with gr.Row(): | |
| fda_lbl_btn = gr.Button("📋 Drug Label", variant="primary") | |
| fda_ae_btn = gr.Button("⚠️ Adverse Events (FAERS)", variant="secondary") | |
| fda_ndc_btn = gr.Button("🏷️ NDC Registry", variant="secondary") | |
| fda_lbl_out = gr.Markdown() | |
| with gr.Row(): | |
| ae_summary = gr.Markdown() | |
| ndc_summary = gr.Markdown() | |
| with gr.Row(): | |
| ae_table = gr.DataFrame(label="⚠️ Adverse Reactions", wrap=True) | |
| ndc_table = gr.DataFrame(label="🏷️ NDC Products", wrap=True) | |
| fda_lbl_btn.click(fn=fda_label, inputs=[selector], outputs=[fda_lbl_out]) | |
| fda_ae_btn .click(fn=fda_adverse, inputs=[selector], outputs=[ae_table, ae_summary]) | |
| fda_ndc_btn.click(fn=fda_ndc, inputs=[selector], outputs=[ndc_table, ndc_summary]) | |
| gr.Markdown("⚠️ *FDA data covers US-approved drugs. Not all Bangladesh registry drugs appear here.*") | |
| # ══════════════════════════════════════════════════════════ | |
| # TAB 4 — Dataset Dashboard | |
| # ══════════════════════════════════════════════════════════ | |
| with gr.TabItem("📊 Dataset Dashboard"): | |
| gr.Markdown(""" | |
| ### 📊 Visual overview of the 53,581-drug Bangladesh registry | |
| Seven charts covering distribution, dosage forms, manufacturers, and system profiles. | |
| """) | |
| dash_btn = gr.Button("🎨 Load Dashboard", variant="primary", size="lg") | |
| kpi_plot = gr.Plot(label="📌 Key Metrics") | |
| with gr.Row(): | |
| donut_plot = gr.Plot(label="① Drug Share by System") | |
| bar_plot = gr.Plot(label="② Top 10 Dosage Forms") | |
| with gr.Row(): | |
| grp_plot = gr.Plot(label="③ Dosage per System") | |
| tree_plot = gr.Plot(label="④ Manufacturer Treemap") | |
| with gr.Row(): | |
| radar_plot = gr.Plot(label="⑤ System Profile Radar") | |
| mfr_plot = gr.Plot(label="⑥ Top 15 Manufacturers") | |
| dash_btn.click(fn=make_dashboard, inputs=[], | |
| outputs=[kpi_plot, donut_plot, bar_plot, | |
| grp_plot, tree_plot, radar_plot, mfr_plot]) | |
| gr.HTML(""" | |
| <div style="text-align:center;padding:14px;color:#94a3b8;font-size:12px; | |
| border-top:1px solid #e2e8f0;margin-top:12px;"> | |
| 💊 Drug Recommender v4.0 · Master's Thesis · | |
| 53,581 drugs · Symptoms + Medicines · TF-IDF · OpenFDA · Plotly Dashboard | |
| </div> | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True) | |