Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as mpatches | |
| import io | |
| import re | |
| from datetime import datetime | |
| # ββ VADER sentiment (graceful fallback if not installed) ββββββββββββββββββββββ | |
| try: | |
| from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer | |
| _analyzer = SentimentIntensityAnalyzer() | |
| def vader_score(text): | |
| return _analyzer.polarity_scores(str(text)) | |
| except ImportError: | |
| def vader_score(text): | |
| text = text.lower() | |
| pos = sum(w in text for w in ["good","strong","growth","positive","gain","profit"]) | |
| neg = sum(w in text for w in ["breach","hack","lawsuit","strike","loss","fine","shut","miss","fraud","attack"]) | |
| compound = round((pos - neg) / max(pos + neg, 1), 3) | |
| return {"neg": neg / max(pos+neg,1), "neu": 0.5, "pos": pos / max(pos+neg,1), "compound": compound} | |
| # ββ Styling constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PALETTE = { | |
| "critical response": "#C0392B", | |
| "escalate": "#E67E22", | |
| "review": "#2980B9", | |
| "monitor": "#27AE60", | |
| } | |
| BG = "#0D0F14" | |
| CARD_BG = "#141720" | |
| ACCENT = "#4F8EF7" | |
| TEXT = "#E8EAF0" | |
| CRISIS_ICONS = { | |
| "cybersecurity": "π", | |
| "legal": "βοΈ", | |
| "operations": "π", | |
| "labor": "π·", | |
| "financial": "π", | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HELPERS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def infer_crisis_type(text): | |
| text = text.lower() | |
| if any(w in text for w in ["hack","breach","cyber","ransomware","malware","data leak","phishing"]): | |
| return "cybersecurity" | |
| if any(w in text for w in ["lawsuit","antitrust","regulator","fine","court","SEC","penalty","sanction"]): | |
| return "legal" | |
| if any(w in text for w in ["factory","supply chain","shutdown","production","recall","outage"]): | |
| return "operations" | |
| if any(w in text for w in ["strike","worker","protest","union","layoff","walkout"]): | |
| return "labor" | |
| if any(w in text for w in ["profit warning","earnings miss","downgrade","revenue","loss","debt","bankruptcy"]): | |
| return "financial" | |
| return "general" | |
| SEVERITY_BASE = { | |
| "cybersecurity": 4, "legal": 4, "operations": 3, | |
| "labor": 3, "financial": 5, "general": 3, | |
| } | |
| URGENCY_BASE = { | |
| "cybersecurity": 5, "legal": 4, "operations": 4, | |
| "labor": 3, "financial": 5, "general": 3, | |
| } | |
| MARKET_BASE = { | |
| "cybersecurity": -2.8, "legal": -2.1, "operations": -1.8, | |
| "labor": -1.2, "financial": -3.0, "general": -1.5, | |
| } | |
| def assign_priority(severity, urgency): | |
| score = severity + urgency | |
| if score >= 9: return "critical response" | |
| if score >= 7: return "escalate" | |
| if score >= 5: return "review" | |
| return "monitor" | |
| def sentiment_label(compound): | |
| if compound >= 0.05: return "Positive π’" | |
| if compound <= -0.05: return "Negative π΄" | |
| return "Neutral βͺ" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 1 β Single headline analyser | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyse_headline(headline: str): | |
| if not headline.strip(): | |
| return "β οΈ Please enter a headline.", None | |
| scores = vader_score(headline) | |
| compound = round(scores["compound"], 3) | |
| crisis = infer_crisis_type(headline) | |
| severity = SEVERITY_BASE[crisis] | |
| urgency = URGENCY_BASE[crisis] | |
| market = round(MARKET_BASE[crisis] + np.random.normal(0, 0.4), 2) | |
| priority = assign_priority(severity, urgency) | |
| sent_label = sentiment_label(compound) | |
| icon = CRISIS_ICONS.get(crisis, "π°") | |
| color = PALETTE.get(priority, "#888") | |
| # ββ bar chart βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fig, axes = plt.subplots(1, 2, figsize=(9, 3.5)) | |
| fig.patch.set_facecolor(BG) | |
| # sentiment bars | |
| ax = axes[0] | |
| ax.set_facecolor(CARD_BG) | |
| cats = ["Negative", "Neutral", "Positive"] | |
| vals = [scores["neg"], scores["neu"], scores["pos"]] | |
| colors = ["#C0392B", "#7F8C8D", "#27AE60"] | |
| bars = ax.barh(cats, vals, color=colors, height=0.5) | |
| ax.set_xlim(0, 1) | |
| ax.set_title("Sentiment Breakdown", color=TEXT, fontsize=11, pad=8) | |
| ax.tick_params(colors=TEXT, labelsize=9) | |
| for spine in ax.spines.values(): spine.set_visible(False) | |
| ax.xaxis.label.set_color(TEXT) | |
| ax.set_xlabel("Score", color=TEXT, fontsize=8) | |
| for bar, val in zip(bars, vals): | |
| ax.text(val + 0.01, bar.get_y() + bar.get_height()/2, | |
| f"{val:.2f}", va="center", color=TEXT, fontsize=8) | |
| # severity / urgency gauge | |
| ax2 = axes[1] | |
| ax2.set_facecolor(CARD_BG) | |
| metrics = ["Severity", "Urgency"] | |
| mvals = [severity, urgency] | |
| mcols = [ACCENT, color] | |
| b2 = ax2.barh(metrics, mvals, color=mcols, height=0.5) | |
| ax2.set_xlim(0, 5) | |
| ax2.set_title("Risk Scores (out of 5)", color=TEXT, fontsize=11, pad=8) | |
| ax2.tick_params(colors=TEXT, labelsize=9) | |
| for spine in ax2.spines.values(): spine.set_visible(False) | |
| ax2.set_xlabel("Score", color=TEXT, fontsize=8) | |
| ax2.xaxis.label.set_color(TEXT) | |
| for bar, val in zip(b2, mvals): | |
| ax2.text(val + 0.05, bar.get_y() + bar.get_height()/2, | |
| str(val), va="center", color=TEXT, fontsize=9, fontweight="bold") | |
| plt.tight_layout(pad=1.5) | |
| # ββ markdown result card βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| md = f""" | |
| ### {icon} Analysis Result | |
| | Field | Value | | |
| |---|---| | |
| | **Crisis Type** | `{crisis.upper()}` | | |
| | **Sentiment** | {sent_label} (compound: `{compound}`) | | |
| | **Severity Score** | `{severity} / 5` | | |
| | **Response Urgency** | `{urgency} / 5` | | |
| | **Est. Market Impact** | `{market:+.2f}%` | | |
| | **Priority Action** | <span style='color:{color};font-weight:bold'>{priority.upper()}</span> | | |
| --- | |
| **Recommended Response:** {"π¨ Immediate leadership escalation and cross-team crisis coordination required." if priority == "critical response" else "β‘ Escalate to risk and communications teams for coordinated response." if priority == "escalate" else "π Schedule a structured review in the next reporting cycle." if priority == "review" else "ποΈ Routine monitoring β flag if coverage increases."} | |
| """ | |
| return md, fig | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2 β CSV Dashboard | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_dashboard(file): | |
| if file is None: | |
| return "β οΈ Please upload **crisis_news_enriched.csv** (output of Notebook 1).", None | |
| try: | |
| df = pd.read_csv(file.name) | |
| except Exception as e: | |
| return f"β Could not read file: {e}", None | |
| required = {"crisis_type", "priority_action", "severity_score", | |
| "estimated_market_impact_pct", "company"} | |
| missing = required - set(df.columns) | |
| if missing: | |
| return f"β Missing columns: {missing}. Please upload `crisis_news_enriched.csv`.", None | |
| # ββ 4-panel figure ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fig, axes = plt.subplots(2, 2, figsize=(13, 9)) | |
| fig.patch.set_facecolor(BG) | |
| fig.suptitle("Crisis Intelligence Dashboard", color=TEXT, | |
| fontsize=16, fontweight="bold", y=0.98) | |
| # 1. Crisis type distribution | |
| ax = axes[0, 0] | |
| ax.set_facecolor(CARD_BG) | |
| ct = df["crisis_type"].value_counts() | |
| bar_colors = [ACCENT] * len(ct) | |
| bars = ax.bar(ct.index, ct.values, color=bar_colors, width=0.6) | |
| ax.set_title("Headlines by Crisis Type", color=TEXT, fontsize=11) | |
| ax.tick_params(colors=TEXT, labelsize=8) | |
| ax.set_xlabel("Crisis Type", color=TEXT, fontsize=9) | |
| ax.set_ylabel("Count", color=TEXT, fontsize=9) | |
| for spine in ax.spines.values(): spine.set_color("#2A2D3A") | |
| for bar in bars: | |
| ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, | |
| str(int(bar.get_height())), ha="center", color=TEXT, fontsize=8) | |
| # 2. Priority action distribution (donut) | |
| ax2 = axes[0, 1] | |
| ax2.set_facecolor(CARD_BG) | |
| pa = df["priority_action"].value_counts() | |
| cols = [PALETTE.get(k, "#888") for k in pa.index] | |
| wedges, texts, autotexts = ax2.pie( | |
| pa.values, labels=pa.index, colors=cols, | |
| autopct="%1.0f%%", startangle=140, | |
| wedgeprops=dict(width=0.55), | |
| textprops={"color": TEXT, "fontsize": 8} | |
| ) | |
| for at in autotexts: at.set_color(BG); at.set_fontweight("bold") | |
| ax2.set_title("Priority Action Distribution", color=TEXT, fontsize=11) | |
| # 3. Avg market impact by crisis type | |
| ax3 = axes[1, 0] | |
| ax3.set_facecolor(CARD_BG) | |
| mi = df.groupby("crisis_type")["estimated_market_impact_pct"].mean().sort_values() | |
| bar_c = ["#C0392B" if v < -2.5 else "#E67E22" if v < -1.5 else "#2980B9" for v in mi.values] | |
| bars3 = ax3.barh(mi.index, mi.values, color=bar_c, height=0.5) | |
| ax3.axvline(0, color=TEXT, linewidth=0.5, alpha=0.4) | |
| ax3.set_title("Avg Market Impact % by Crisis Type", color=TEXT, fontsize=11) | |
| ax3.tick_params(colors=TEXT, labelsize=8) | |
| ax3.set_xlabel("Est. Impact (%)", color=TEXT, fontsize=9) | |
| for spine in ax3.spines.values(): spine.set_color("#2A2D3A") | |
| for bar, val in zip(bars3, mi.values): | |
| ax3.text(val - 0.05, bar.get_y() + bar.get_height()/2, | |
| f"{val:.2f}%", va="center", ha="right", color=TEXT, fontsize=8) | |
| # 4. Severity heatmap (crisis type Γ priority) | |
| ax4 = axes[1, 1] | |
| ax4.set_facecolor(CARD_BG) | |
| pivot = df.groupby(["crisis_type", "priority_action"])["severity_score"].mean().unstack(fill_value=0) | |
| im = ax4.imshow(pivot.values, cmap="RdYlGn_r", aspect="auto", vmin=1, vmax=5) | |
| ax4.set_xticks(range(len(pivot.columns))) | |
| ax4.set_yticks(range(len(pivot.index))) | |
| ax4.set_xticklabels(pivot.columns, color=TEXT, fontsize=7, rotation=20, ha="right") | |
| ax4.set_yticklabels(pivot.index, color=TEXT, fontsize=8) | |
| ax4.set_title("Avg Severity: Crisis Type Γ Priority", color=TEXT, fontsize=11) | |
| for i in range(pivot.shape[0]): | |
| for j in range(pivot.shape[1]): | |
| val = pivot.values[i, j] | |
| if val > 0: | |
| ax4.text(j, i, f"{val:.1f}", ha="center", va="center", | |
| color="white", fontsize=8, fontweight="bold") | |
| cbar = fig.colorbar(im, ax=ax4, fraction=0.03) | |
| cbar.ax.tick_params(colors=TEXT, labelsize=7) | |
| plt.tight_layout(rect=[0, 0, 1, 0.96]) | |
| # ββ summary stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| n = len(df) | |
| n_critical = (df["priority_action"] == "critical response").sum() | |
| top_type = df["crisis_type"].value_counts().idxmax() | |
| top_co = df["company"].value_counts().idxmax() | |
| avg_impact = df["estimated_market_impact_pct"].mean() | |
| md = f""" | |
| ### π Dataset Summary | |
| | Metric | Value | | |
| |---|---| | |
| | Total headlines | `{n}` | | |
| | Critical response alerts | `{n_critical}` ({100*n_critical/n:.0f}%) | | |
| | Most common crisis type | `{top_type.upper()}` | | |
| | Most exposed company | `{top_co}` | | |
| | Avg estimated market impact | `{avg_impact:+.2f}%` | | |
| Upload `crisis_news_enriched.csv` (generated by Notebook 1) to refresh. | |
| """ | |
| return md, fig | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BUILD UI | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CUSTOM_CSS = """ | |
| body, .gradio-container { background: #0D0F14 !important; color: #E8EAF0 !important; font-family: 'IBM Plex Mono', monospace; } | |
| .gr-button-primary { background: #4F8EF7 !important; border: none !important; color: #0D0F14 !important; font-weight: 700 !important; } | |
| .gr-button-primary:hover { background: #6FA3FA !important; } | |
| h1, h2, h3 { color: #E8EAF0 !important; } | |
| .gr-panel, .gr-box { background: #141720 !important; border-color: #2A2D3A !important; } | |
| textarea, input[type=text] { background: #1C1F2B !important; color: #E8EAF0 !important; border-color: #2A2D3A !important; } | |
| .gr-tab-nav button { color: #9AA0B4 !important; } | |
| .gr-tab-nav button.selected { color: #4F8EF7 !important; border-bottom: 2px solid #4F8EF7 !important; } | |
| """ | |
| ABOUT_MD = """ | |
| # π Crisis Monitor β AI-Powered Business Risk Intelligence | |
| ## What this app does | |
| This tool is the interactive front-end of an end-to-end crisis monitoring pipeline built for the **AI for Big Data Management** course at ESCP Business School. | |
| It lets you: | |
| - **Analyse any news headline** instantly β detecting crisis type, sentiment (VADER), severity, urgency, estimated market impact, and recommended action | |
| - **Upload your enriched dataset** (`crisis_news_enriched.csv`) for a full visual dashboard | |
| ## How the pipeline works | |
| ``` | |
| Google News RSS βββΊ Notebook 1 βββΊ Enriched CSV βββΊ Notebook 2 βββΊ Analysis | |
| (scraping, (severity, (VADER, | |
| synthetic urgency, ARIMA, | |
| enrichment) market decision | |
| impact, support) | |
| priority) | |
| β | |
| βΌ | |
| This Hugging Face App | |
| (real-time scanning + | |
| dashboard visualisation) | |
| ``` | |
| ## Crisis types monitored | |
| | Type | Signal keywords | | |
| |---|---| | |
| | π Cybersecurity | breach, hack, ransomware, data leak | | |
| | βοΈ Legal | lawsuit, antitrust, regulator, fine | | |
| | π Operations | factory, supply chain, shutdown, recall | | |
| | π· Labor | strike, worker protest, union, layoff | | |
| | π Financial | profit warning, earnings miss, downgrade | | |
| ## Priority framework | |
| | Priority | Trigger | | |
| |---|---| | |
| | π΄ Critical Response | Severity + Urgency β₯ 9 | | |
| | π Escalate | Score 7β8 | | |
| | π΅ Review | Score 5β6 | | |
| | π’ Monitor | Score < 5 | | |
| --- | |
| *Built with Python Β· Gradio Β· VADER Sentiment Β· Matplotlib* | |
| """ | |
| with gr.Blocks(css=CUSTOM_CSS, title="Crisis Monitor") as demo: | |
| gr.Markdown(""" | |
| # π¨ Crisis Monitor | |
| ### AI-Powered Business Risk Intelligence Β· ESCP Business School | |
| """) | |
| with gr.Tabs(): | |
| # ββ TAB 1 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Headline Scanner"): | |
| gr.Markdown("Paste any business news headline to get an instant risk assessment.") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| headline_input = gr.Textbox( | |
| label="News Headline", | |
| placeholder="e.g. Apple faces major data breach exposing 50M user records...", | |
| lines=3, | |
| ) | |
| scan_btn = gr.Button("β‘ Analyse Headline", variant="primary") | |
| with gr.Column(scale=3): | |
| result_md = gr.Markdown() | |
| result_fig = gr.Plot() | |
| scan_btn.click( | |
| fn=analyse_headline, | |
| inputs=headline_input, | |
| outputs=[result_md, result_fig], | |
| ) | |
| gr.Examples( | |
| examples=[ | |
| ["Tesla hit with major ransomware attack, customer data leaked online"], | |
| ["Amazon faces antitrust fine from EU regulators over pricing practices"], | |
| ["Boeing factory workers go on strike, halting 737 MAX production"], | |
| ["Intel issues profit warning, shares drop 12% after earnings miss"], | |
| ["Apple supply chain disruption forces iPhone production cuts in China"], | |
| ], | |
| inputs=headline_input, | |
| label="Try an example", | |
| ) | |
| # ββ TAB 2 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Crisis Dashboard"): | |
| gr.Markdown("Upload `crisis_news_enriched.csv` (generated by Notebook 1) for a full portfolio view.") | |
| with gr.Row(): | |
| csv_input = gr.File(label="Upload crisis_news_enriched.csv", file_types=[".csv"]) | |
| dash_btn = gr.Button("π Generate Dashboard", variant="primary") | |
| with gr.Row(): | |
| dash_md = gr.Markdown() | |
| with gr.Row(): | |
| dash_fig = gr.Plot() | |
| dash_btn.click( | |
| fn=build_dashboard, | |
| inputs=csv_input, | |
| outputs=[dash_md, dash_fig], | |
| ) | |
| # ββ TAB 3 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("βΉοΈ About"): | |
| gr.Markdown(ABOUT_MD) | |
| demo.launch() | |