Download app.py from Umesh1608/biointel: direct link, hf CLI and curl.
- Browser
- Download file 27.2 kB
-
https://huggingface.co/spaces/Umesh1608/biointel/resolve/main/app.py
- Command line
-
hf download hf://spaces/Umesh1608/biointel/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Umesh1608/biointel/resolve/main/app.py
27.2 kB
| """BioIntel β biotech catalyst and wet-lab demand intelligence. | |
| Two public sources, joined into one view: | |
| * SEC EDGAR 8-K full-text search β catalysts that have already happened | |
| * ClinicalTrials.gov v2 β dated catalysts that have not happened yet | |
| Data is produced by the ingest pipeline and published as parquet to a Hugging | |
| Face Dataset repo; this app downloads it once at startup and queries it in | |
| memory. Not investment advice. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| import gradio as gr | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| DATASET_REPO = os.environ.get("BIOINTEL_DATASET", "Umesh1608/biointel-data") | |
| # Dossiers live in a separate *static* Space: it renders HTML natively, never | |
| # sleeps, and costs nothing against the PRO allowance. | |
| REPORTS_BASE = os.environ.get( | |
| "BIOINTEL_REPORTS", "https://umesh1608-biointel-reports.static.hf.space") | |
| LOCAL_DATA = Path(os.environ.get("BIOINTEL_DATA", | |
| Path.home() / "biointel" / "data")) | |
| # A restrained palette: one accent, one alarm colour, everything else neutral. | |
| INK, ACCENT, ALARM, MUTED = "#0F172A", "#0D9488", "#DC2626", "#64748B" | |
| SERIES = ["#0D9488", "#0EA5E9", "#8B5CF6", "#F59E0B", "#DC2626", "#10B981", | |
| "#6366F1", "#EC4899", "#14B8A6", "#F97316", "#64748B", "#84CC16"] | |
| NEGATIVE_SIGNALS = {"endpoint_missed", "crl", "clinical_hold", "discontinue"} | |
| # -------------------------------------------------------------------------- | |
| # data | |
| # -------------------------------------------------------------------------- | |
| def _load(name: str, parquet: bool = True): | |
| local = LOCAL_DATA / (f"{name}.parquet" if parquet else name) | |
| if local.exists(): | |
| return pd.read_parquet(local) if parquet else json.loads(local.read_text()) | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| p = hf_hub_download(DATASET_REPO, | |
| f"data/{name}.parquet" if parquet else f"data/{name}", | |
| repo_type="dataset") | |
| return pd.read_parquet(p) if parquet else json.loads(Path(p).read_text()) | |
| except Exception as e: # noqa: BLE001 β render the shell even with no data | |
| print(f"[warn] could not load {name}: {e}") | |
| return pd.DataFrame() if parquet else {} | |
| CATALYSTS = _load("catalysts") | |
| READOUTS = _load("readouts") | |
| MANIFEST = _load("manifest.json", parquet=False) | |
| PROC_DATE = (MANIFEST.get("generated_at") or "unknown")[:10] | |
| for _df, _col in ((CATALYSTS, "filed_date"), (READOUTS, "primary_completion")): | |
| if not _df.empty: | |
| _df["_dt"] = pd.to_datetime(_df[_col], errors="coerce") | |
| def _style(fig, height=380): | |
| """One chart style everywhere: no chartjunk, muted grid, honest axes.""" | |
| fig.update_layout( | |
| height=height, margin=dict(l=8, r=8, t=48, b=8), | |
| font=dict(family="Inter, ui-sans-serif, system-ui", size=12.5), | |
| title=dict(font=dict(size=15), x=0, xanchor="left"), | |
| plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)", | |
| legend=dict(title_text="", orientation="h", y=-0.18, font=dict(size=11)), | |
| colorway=SERIES, hoverlabel=dict(font_size=12), | |
| ) | |
| fig.update_xaxes(showgrid=False, linecolor="rgba(100,116,139,.25)", title=None) | |
| fig.update_yaxes(gridcolor="rgba(100,116,139,.15)", zeroline=False, | |
| linecolor="rgba(0,0,0,0)", title=None) | |
| return fig | |
| def _empty(msg="No data loaded"): | |
| f = go.Figure() | |
| f.add_annotation(text=msg, showarrow=False, font=dict(size=14, color=MUTED)) | |
| f.update_layout(height=320, plot_bgcolor="rgba(0,0,0,0)", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| xaxis=dict(visible=False), yaxis=dict(visible=False)) | |
| return f | |
| # -------------------------------------------------------------------------- | |
| # header | |
| # -------------------------------------------------------------------------- | |
| def kpis() -> str: | |
| if CATALYSTS.empty and READOUTS.empty: | |
| return "<div class='bi-kpis'><div class='bi-card'>No data</div></div>" | |
| n_cat = len(CATALYSTS) | |
| n_neg = int(CATALYSTS.negative.sum()) if not CATALYSTS.empty else 0 | |
| n_co = CATALYSTS.company.nunique() if not CATALYSTS.empty else 0 | |
| n_ro = len(READOUTS) | |
| n_ph3 = int(READOUTS.phase.str.contains("Ph3", na=False).sum()) if not READOUTS.empty else 0 | |
| cards = [ | |
| ("Catalyst events", f"{n_cat:,}", "8-K filings Β· last 90 days"), | |
| ("Companies", f"{n_co:,}", "distinct issuers"), | |
| ("Negative signals", f"{n_neg:,}", f"{n_neg / max(n_cat, 1):.0%} of events"), | |
| ("Upcoming readouts", f"{n_ro:,}", "industry trials Β· next 12 mo"), | |
| ("Phase 3 ahead", f"{n_ph3:,}", "highest-stakes subset"), | |
| ] | |
| html = "".join( | |
| f"<div class='bi-card'><div class='bi-label'>{lbl}</div>" | |
| f"<div class='bi-value'>{val}</div><div class='bi-sub'>{sub}</div></div>" | |
| for lbl, val, sub in cards) | |
| return f"<div class='bi-kpis'>{html}</div>" | |
| def freshness() -> str: | |
| ts = MANIFEST.get("generated_at") | |
| if not ts: | |
| return "<div class='bi-stale'>β No manifest found β data may be missing.</div>" | |
| try: | |
| gen = datetime.fromisoformat(ts) | |
| age = (datetime.now(timezone.utc) - gen).total_seconds() / 3600 | |
| except ValueError: | |
| return f"<div class='bi-fresh'>Data generated {ts}</div>" | |
| cls = "bi-stale" if age > 48 else "bi-fresh" | |
| note = " β stale, the refresh job may have failed" if age > 48 else "" | |
| return (f"<div class='{cls}'>β Data generated " | |
| f"<b>{gen:%d %b %Y, %H:%M} UTC</b> Β· {age:.0f}h ago{note}</div>") | |
| # -------------------------------------------------------------------------- | |
| # tab 1 β catalyst feed | |
| # -------------------------------------------------------------------------- | |
| CAT_COLS = ["filed_date", "ticker", "company", "signal", "negative", "url"] | |
| def filter_catalysts(signals, neg_only, search, limit): | |
| df = CATALYSTS | |
| if df.empty: | |
| return pd.DataFrame({"note": ["No catalyst data loaded."]}) | |
| if signals: | |
| df = df[df.signal.isin(signals)] | |
| if neg_only: | |
| df = df[df.negative] | |
| if search: | |
| s = search.strip().lower() | |
| df = df[df.company.str.lower().str.contains(s, na=False) | |
| | df.ticker.fillna("").str.lower().str.contains(s)] | |
| out = df.sort_values("filed_date", ascending=False)[CAT_COLS].head(int(limit)).copy() | |
| out["negative"] = out.negative.map({True: "βΌ negative", False: ""}) | |
| return out.rename(columns={"filed_date": "Filed", "ticker": "Ticker", | |
| "company": "Company", "signal": "Signal", | |
| "negative": "Tone", "url": "Filing"}) | |
| def catalyst_trend(signals): | |
| df = CATALYSTS | |
| if df.empty: | |
| return _empty() | |
| if signals: | |
| df = df[df.signal.isin(signals)] | |
| if df.empty: | |
| return _empty("No events match these filters") | |
| d = df.copy() | |
| d["week"] = d._dt.dt.to_period("W").dt.start_time | |
| g = d.groupby(["week", "signal"]).size().reset_index(name="filings") | |
| return _style(px.bar(g, x="week", y="filings", color="signal", | |
| title="Catalyst filings per week")) | |
| def catalyst_mix(signals): | |
| df = CATALYSTS | |
| if df.empty: | |
| return _empty() | |
| if signals: | |
| df = df[df.signal.isin(signals)] | |
| if df.empty: | |
| return _empty("No events match these filters") | |
| g = df.signal.value_counts().reset_index() | |
| g.columns = ["signal", "events"] | |
| g["tone"] = g.signal.isin(NEGATIVE_SIGNALS).map( | |
| {True: "negative", False: "neutral / positive"}) | |
| fig = px.bar(g, x="events", y="signal", orientation="h", color="tone", | |
| color_discrete_map={"negative": ALARM, | |
| "neutral / positive": ACCENT}, | |
| title="Which signals are firing") | |
| fig.update_layout(yaxis=dict(autorange="reversed")) | |
| return _style(fig) | |
| # -------------------------------------------------------------------------- | |
| # tab 2 β readout calendar | |
| # -------------------------------------------------------------------------- | |
| RO_COLS = ["primary_completion", "date_precision", "phase", "sponsor", | |
| "modality", "conditions", "enrollment", "url"] | |
| def filter_readouts(phases, modalities, sponsor, months, exact_only, limit): | |
| df = READOUTS | |
| if df.empty: | |
| return pd.DataFrame({"note": ["No readout data loaded."]}) | |
| if phases: | |
| df = df[df.phase.isin(phases)] | |
| if modalities: | |
| df = df[df.modality.isin(modalities)] | |
| if sponsor: | |
| df = df[df.sponsor.str.lower().str.contains(sponsor.strip().lower(), na=False)] | |
| if exact_only and "date_precision" in df.columns: | |
| df = df[df.date_precision == "day"] | |
| if months: | |
| cutoff = pd.Timestamp.today() + pd.DateOffset(months=int(months)) | |
| df = df[df._dt <= cutoff] | |
| cols = [c for c in RO_COLS if c in df.columns] | |
| out = df.sort_values("primary_completion")[cols].head(int(limit)).copy() | |
| if "date_precision" in out.columns: | |
| out["date_precision"] = out.date_precision.map( | |
| {"day": "exact", "month": "month only", | |
| "year": "year only"}).fillna("β") | |
| return out.rename(columns={"primary_completion": "Readout due", | |
| "date_precision": "Date precision", | |
| "phase": "Phase", "sponsor": "Sponsor", | |
| "modality": "Modality", "conditions": "Condition", | |
| "enrollment": "N", "url": "Registry"}) | |
| def readout_chart(phases, modalities): | |
| df = READOUTS | |
| if df.empty: | |
| return _empty() | |
| if phases: | |
| df = df[df.phase.isin(phases)] | |
| if modalities: | |
| df = df[df.modality.isin(modalities)] | |
| if df.empty: | |
| return _empty("No trials match these filters") | |
| d = df.copy() | |
| d["month"] = d._dt.dt.to_period("M").dt.start_time | |
| d["precision"] = (d.date_precision.map({"day": "exact date", | |
| "month": "month only"}) | |
| .fillna("month only") | |
| if "date_precision" in d.columns else "unknown") | |
| g = d.groupby(["month", "precision"]).size().reset_index(name="trials") | |
| fig = px.bar(g, x="month", y="trials", color="precision", | |
| color_discrete_map={"exact date": ACCENT, | |
| "month only": "#CBD5E1"}, | |
| title="Readouts by month β split by how precise the date is") | |
| return _style(fig) | |
| def modality_chart(phases): | |
| df = READOUTS | |
| if df.empty: | |
| return _empty() | |
| if phases: | |
| df = df[df.phase.isin(phases)] | |
| if df.empty: | |
| return _empty("No trials match these filters") | |
| g = df.modality.value_counts().reset_index() | |
| g.columns = ["modality", "trials"] | |
| fig = px.bar(g.head(12), x="trials", y="modality", orientation="h", | |
| title="Modality mix of the forward pipeline") | |
| fig.update_traces(marker_color=ACCENT) | |
| fig.update_layout(yaxis=dict(autorange="reversed")) | |
| return _style(fig) | |
| def phase_chart(): | |
| if READOUTS.empty: | |
| return _empty() | |
| g = READOUTS.phase.value_counts().reset_index() | |
| g.columns = ["phase", "trials"] | |
| fig = px.pie(g.head(8), names="phase", values="trials", hole=0.62, | |
| title="Phase mix") | |
| fig.update_traces(textposition="outside", textinfo="label+percent") | |
| return _style(fig) | |
| def _choices(df, col): | |
| return sorted(df[col].dropna().unique().tolist()) if not df.empty else [] | |
| # -------------------------------------------------------------------------- | |
| # tab 3 β pivotal (Phase 3) readouts | |
| # -------------------------------------------------------------------------- | |
| # "Pivotal" = anything carrying a Phase 3 label, so Ph2/Ph3 seamless designs | |
| # are included: they are registrational too, and excluding them would hide | |
| # real risk. | |
| PIVOTAL = (READOUTS[READOUTS.phase.str.contains("Ph3", na=False)].copy() | |
| if not READOUTS.empty else pd.DataFrame()) | |
| PIV_COLS = ["primary_completion", "date_precision", "phase", "sponsor", | |
| "enrollment", "modality", "conditions", "url"] | |
| def pivotal_kpis() -> str: | |
| if PIVOTAL.empty: | |
| return "<div class='bi-kpis'><div class='bi-card'>No pivotal trials</div></div>" | |
| soon = int((PIVOTAL._dt <= pd.Timestamp.today() + pd.DateOffset(days=90)).sum()) | |
| exact = int((PIVOTAL.date_precision == "day").sum()) | |
| med = PIVOTAL.enrollment.median() | |
| cards = [ | |
| ("Pivotal readouts", f"{len(PIVOTAL):,}", "Phase 3 or Ph2/Ph3"), | |
| ("Due in 90 days", f"{soon:,}", "nearest-term risk"), | |
| ("Exact-dated", f"{exact / len(PIVOTAL):.0%}", f"{exact:,} of {len(PIVOTAL):,}"), | |
| ("Sponsors", f"{PIVOTAL.sponsor.nunique():,}", "distinct companies"), | |
| ("Median enrolment", f"{med:,.0f}" if pd.notna(med) else "β", "patients per trial"), | |
| ] | |
| html = "".join( | |
| f"<div class='bi-card'><div class='bi-label'>{lbl}</div>" | |
| f"<div class='bi-value'>{val}</div><div class='bi-sub'>{sub}</div></div>" | |
| for lbl, val, sub in cards) | |
| return f"<div class='bi-kpis'>{html}</div>" | |
| def filter_pivotal(days, exact_only, sponsor, modalities, min_n, limit): | |
| df = PIVOTAL | |
| if df.empty: | |
| return pd.DataFrame({"note": ["No pivotal trial data loaded."]}) | |
| if days: | |
| df = df[df._dt <= pd.Timestamp.today() + pd.DateOffset(days=int(days))] | |
| if exact_only: | |
| df = df[df.date_precision == "day"] | |
| if sponsor: | |
| df = df[df.sponsor.str.lower().str.contains(sponsor.strip().lower(), na=False)] | |
| if modalities: | |
| df = df[df.modality.isin(modalities)] | |
| if min_n: | |
| df = df[df.enrollment.fillna(0) >= int(min_n)] | |
| out = df.sort_values("primary_completion")[PIV_COLS + ["nct_id"]].head(int(limit)).copy() | |
| out["date_precision"] = out.date_precision.map( | |
| {"day": "exact", "month": "month only", "year": "year only"}).fillna("β") | |
| # Markdown link so the dossier opens in its own window. | |
| out["dossier"] = out.nct_id.map( | |
| lambda n: f"[open report]({REPORTS_BASE}/{n}.html)") | |
| out = out.drop(columns=["nct_id"]) | |
| return out.rename(columns={"dossier": "Dossier", | |
| "primary_completion": "Readout due", | |
| "date_precision": "Date precision", | |
| "phase": "Phase", "sponsor": "Sponsor", | |
| "enrollment": "Patients", "modality": "Modality", | |
| "conditions": "Condition", "url": "Registry"}) | |
| def pivotal_month_chart(exact_only): | |
| df = PIVOTAL | |
| if df.empty: | |
| return _empty() | |
| if exact_only: | |
| df = df[df.date_precision == "day"] | |
| if df.empty: | |
| return _empty("No pivotal trials match") | |
| d = df.copy() | |
| d["month"] = d._dt.dt.to_period("M").dt.start_time | |
| d["precision"] = d.date_precision.map( | |
| {"day": "exact date", "month": "month only"}).fillna("month only") | |
| g = d.groupby(["month", "precision"]).size().reset_index(name="trials") | |
| fig = px.bar(g, x="month", y="trials", color="precision", | |
| color_discrete_map={"exact date": ACCENT, "month only": "#CBD5E1"}, | |
| title="Pivotal readouts by month") | |
| return _style(fig) | |
| def pivotal_sponsor_chart(days): | |
| df = PIVOTAL | |
| if df.empty: | |
| return _empty() | |
| if days: | |
| df = df[df._dt <= pd.Timestamp.today() + pd.DateOffset(days=int(days))] | |
| if df.empty: | |
| return _empty("No pivotal trials in this window") | |
| g = df.sponsor.value_counts().head(12).reset_index() | |
| g.columns = ["sponsor", "trials"] | |
| g["sponsor"] = g.sponsor.str.slice(0, 34) | |
| fig = px.bar(g, x="trials", y="sponsor", orientation="h", | |
| title="Most pivotal readouts ahead, by sponsor") | |
| fig.update_traces(marker_color=SERIES[1]) | |
| fig.update_layout(yaxis=dict(autorange="reversed")) | |
| return _style(fig) | |
| # -------------------------------------------------------------------------- | |
| # styling | |
| # -------------------------------------------------------------------------- | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); | |
| .gradio-container { font-family:'Inter',ui-sans-serif,system-ui !important; | |
| max-width:1460px !important; } | |
| #bi-hero { padding:6px 0 0; } | |
| #bi-hero h1 { font-size:2.1rem; font-weight:700; letter-spacing:-.028em; | |
| margin:0; line-height:1.1; } | |
| #bi-hero p { margin:.35rem 0 0; color:#64748B; font-size:1.02rem; } | |
| .bi-kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); | |
| gap:12px; margin:14px 0 8px; } | |
| .bi-card { background:var(--block-background-fill); border:1px solid var(--border-color-primary); | |
| border-radius:14px; padding:14px 16px; } | |
| .bi-label { font-size:.7rem; font-weight:600; letter-spacing:.08em; | |
| text-transform:uppercase; color:#64748B; } | |
| .bi-value { font-size:1.9rem; font-weight:700; line-height:1.15; margin:.2rem 0; | |
| font-variant-numeric:tabular-nums; letter-spacing:-.02em; } | |
| .bi-sub { font-size:.75rem; color:#94A3B8; } | |
| .bi-fresh, .bi-stale { display:inline-block; font-size:.8rem; padding:5px 12px; | |
| border-radius:999px; margin:6px 0 2px; font-weight:500; } | |
| .bi-fresh { background:#ECFDF5; color:#047857; border:1px solid #A7F3D0; } | |
| .bi-stale { background:#FEF2F2; color:#B91C1C; border:1px solid #FECACA; } | |
| .dark .bi-fresh { background:#06281F; border-color:#115E4A; color:#5EEAD4; } | |
| .dark .bi-stale { background:#2A1113; border-color:#7F1D1D; color:#FCA5A5; } | |
| .bi-note { font-size:.84rem; color:#64748B; border-left:3px solid #0D9488; | |
| padding:3px 0 3px 12px; margin:6px 0 14px; line-height:1.5; } | |
| footer, .built-with { display:none !important; } | |
| .tab-nav button { font-weight:600 !important; font-size:.95rem !important; } | |
| """ | |
| ABOUT = f""" | |
| ## What this shows | |
| Two public sources, joined into one view of biotech catalysts. | |
| **SEC EDGAR 8-K full-text search** β *what already happened.* An 8-K is how a | |
| US-listed company must announce a material event, so it is the closest public | |
| thing to a ground-truth catalyst feed. Twelve signal types are matched on | |
| phrase groups chosen by **measurement, not guesswork**: over a sample window | |
| *"met its primary endpoint"* appeared in 54 filings where *"met the primary | |
| endpoint"* appeared in 19, so both are searched. EDGAR accepts uppercase `OR` | |
| between quoted phrases β a `|` silently returns zero results. | |
| **ClinicalTrials.gov API v2** β *what is coming.* A trial whose primary | |
| completion date falls next quarter is a dated, public, not-yet-happened | |
| catalyst. Only industry-sponsored trials are kept. | |
| ## Read these caveats before trusting a number | |
| * **40% of readout dates are month-only.** Sponsors often register just | |
| `2026-12`. Converting that to a day manufactures precision: in one sample | |
| 46% of trials landed on day 1 purely as an artifact of that conversion, and | |
| a further 28% sat on month-end placeholders. A naive chart shows a December | |
| "catalyst wall" of 913 trials that is mostly rounding. The calendar | |
| therefore **labels every date's precision** and can filter to exact dates. | |
| * **Signals co-occur, and that is real.** One 8-K can match both *endpoint | |
| met* and *endpoint missed* β a release reporting a hit on one endpoint and a | |
| miss on another. Treat the columns as flags, not a verdict. | |
| * **Trial dates slip.** A primary completion date is a plan, not a commitment. | |
| * **Modality is inferred**, not a registry field. It reports `unknown` rather | |
| than guessing, because a wrong label is worse than a missing one. | |
| * **Not every catalyst is an 8-K.** Conference presentations and releases not | |
| deemed material never appear. | |
| * **Both APIs truncate silently.** One EDGAR call returns at most 100 hits | |
| regardless of the reported total, and filtering trials client-side after a | |
| truncated fetch drops most of the result set. Both are paged and filtered | |
| server-side here. | |
| ## What is *not* here, and why | |
| The thesis this was modelled on promises CRO lab occupancy, lease renewal | |
| spreads and options flow. **Those are not obtainable from public data.** There | |
| is no free, redistributable source of options or intraday price data β OPRA | |
| licensing runs \\$650β1,500/month β so that signal is **not replicated** rather | |
| than approximated from a prohibited source. Public proxies for lab demand do | |
| exist and are the natural next addition. Facility-level utilisation is not | |
| among them: Charles River's own 10-K says it tracks room utilisation and | |
| prints no figure. | |
| ## Attribution and licensing | |
| **ClinicalTrials.gov.** Trial data courtesy of the U.S. National Library of | |
| Medicine, retrieved from the ClinicalTrials.gov API v2. **Processing date: | |
| {PROC_DATE}.** The data has been **modified** from source: filtered to | |
| industry-sponsored studies with a future primary completion date, dates | |
| normalised for sorting with their original precision retained in a separate | |
| column, and a `modality` label **inferred** by this project from intervention | |
| names and titles. NLM does not endorse this reuse. | |
| **SEC EDGAR.** Filings are public information and may be copied or further | |
| distributed without the SEC's permission. Accessed within the published | |
| 10 requests/second limit with a descriptive User-Agent. | |
| Sources whose terms forbid redistribution or automated retrieval are | |
| deliberately **excluded** β PMC full text, EPO OPS, and every commercial | |
| market-data free tier. The PatentsView API was retired in March 2026. | |
| **An information tool built on public filings. Not investment advice.** | |
| """ | |
| with gr.Blocks(title="BioIntel", css=CSS, fill_width=True) as demo: | |
| gr.HTML("<div id='bi-hero'><h1>BioIntel</h1><p>Biotech catalyst " | |
| "intelligence from public filings and trial registries</p></div>") | |
| gr.HTML(freshness()) | |
| gr.HTML(kpis()) | |
| with gr.Tab("Catalyst feed"): | |
| gr.HTML("<div class='bi-note'>8-K filings matching clinical and " | |
| "regulatory catalyst language. <b>Tone</b> marks signals that are " | |
| "usually bad news β though a single filing can carry both.</div>") | |
| with gr.Row(): | |
| f_sig = gr.Dropdown(_choices(CATALYSTS, "signal"), multiselect=True, | |
| label="Signal", scale=3) | |
| f_q = gr.Textbox(label="Company or ticker", | |
| placeholder="e.g. BridgeBio, EYPT", scale=2) | |
| f_neg = gr.Checkbox(label="Negative only", scale=1) | |
| f_lim = gr.Slider(25, 1000, value=150, step=25, label="Rows", scale=2) | |
| t_cat = gr.Dataframe(value=filter_catalysts([], False, "", 150), | |
| wrap=True, max_height=430) | |
| with gr.Row(): | |
| p_trend = gr.Plot(catalyst_trend([])) | |
| p_mix = gr.Plot(catalyst_mix([])) | |
| cins = [f_sig, f_neg, f_q, f_lim] | |
| for c in cins: | |
| c.change(filter_catalysts, cins, t_cat) | |
| f_sig.change(catalyst_trend, f_sig, p_trend) | |
| f_sig.change(catalyst_mix, f_sig, p_mix) | |
| with gr.Tab("Readout calendar"): | |
| gr.HTML("<div class='bi-note'>Industry trials with a primary completion " | |
| "date ahead. <b>40% of registry dates are month-only</b> β the " | |
| "precision column says which, and the chart separates them so a " | |
| "rounding artifact cannot read as a catalyst cluster.</div>") | |
| with gr.Row(): | |
| r_ph = gr.Dropdown(_choices(READOUTS, "phase"), multiselect=True, | |
| label="Phase", scale=2) | |
| r_mod = gr.Dropdown(_choices(READOUTS, "modality"), multiselect=True, | |
| label="Modality", scale=2) | |
| r_sp = gr.Textbox(label="Sponsor contains", | |
| placeholder="e.g. Novartis", scale=2) | |
| r_mo = gr.Slider(1, 12, value=6, step=1, label="Within months", scale=2) | |
| r_ex = gr.Checkbox(label="Exact dates only", scale=1) | |
| r_lim = gr.Slider(25, 1000, value=150, step=25, label="Rows", scale=1) | |
| t_ro = gr.Dataframe(value=filter_readouts([], [], "", 6, False, 150), | |
| wrap=True, max_height=430) | |
| with gr.Row(): | |
| p_ro = gr.Plot(readout_chart([], [])) | |
| p_mod = gr.Plot(modality_chart([])) | |
| rins = [r_ph, r_mod, r_sp, r_mo, r_ex, r_lim] | |
| for c in rins: | |
| c.change(filter_readouts, rins, t_ro) | |
| for c in (r_ph, r_mod): | |
| c.change(readout_chart, [r_ph, r_mod], p_ro) | |
| r_ph.change(modality_chart, r_ph, p_mod) | |
| with gr.Tab("Pivotal readouts"): | |
| gr.HTML("<div class='bi-note'>Phase 3 and Ph2/Ph3 trials only β the " | |
| "readouts that reprice a company. <b>Exact dates only</b> is on " | |
| "by default here: a pivotal date that is really just a month is " | |
| "not something you can plan around. Each row links to a " | |
| "<b>dossier</b> β design, endpoint, scale and sponsor context " | |
| "β which opens in its own window.</div>") | |
| gr.HTML(pivotal_kpis()) | |
| with gr.Row(): | |
| v_days = gr.Slider(30, 365, value=180, step=30, label="Within days", scale=2) | |
| v_sp = gr.Textbox(label="Sponsor contains", | |
| placeholder="e.g. AstraZeneca", scale=2) | |
| v_mod = gr.Dropdown(_choices(PIVOTAL, "modality"), multiselect=True, | |
| label="Modality", scale=2) | |
| v_n = gr.Slider(0, 2000, value=0, step=50, label="Min patients", scale=2) | |
| v_ex = gr.Checkbox(label="Exact dates only", value=True, scale=1) | |
| v_lim = gr.Slider(25, 1000, value=200, step=25, label="Rows", scale=1) | |
| t_piv = gr.Dataframe( | |
| value=filter_pivotal(180, True, "", [], 0, 200), | |
| wrap=True, max_height=430, | |
| datatype=["str", "str", "str", "str", "number", "str", "str", | |
| "markdown", "markdown"]) | |
| with gr.Row(): | |
| p_pm = gr.Plot(pivotal_month_chart(True)) | |
| p_ps = gr.Plot(pivotal_sponsor_chart(180)) | |
| vins = [v_days, v_ex, v_sp, v_mod, v_n, v_lim] | |
| for c in vins: | |
| c.change(filter_pivotal, vins, t_piv) | |
| v_ex.change(pivotal_month_chart, v_ex, p_pm) | |
| v_days.change(pivotal_sponsor_chart, v_days, p_ps) | |
| with gr.Tab("Pipeline mix"): | |
| gr.HTML("<div class='bi-note'>Modality and phase mix of the forward " | |
| "pipeline β the closest public read on what kind of wet-lab and " | |
| "manufacturing capacity will be needed.</div>") | |
| with gr.Row(): | |
| gr.Plot(modality_chart([])) | |
| gr.Plot(phase_chart()) | |
| with gr.Tab("About & caveats"): | |
| gr.Markdown(ABOUT) | |
| gr.HTML("<div style='margin-top:16px;font-size:.78rem;color:#94A3B8'>" | |
| "Trial data courtesy of the U.S. National Library of Medicine " | |
| "(ClinicalTrials.gov), modified β see About. SEC filings are public " | |
| "domain. Not investment advice.</div>") | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="slate"), | |
| server_name="0.0.0.0") | |