BaltasarDelle commited on
Commit
08a2b95
Β·
verified Β·
1 Parent(s): bacfdec

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +404 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib
5
+ matplotlib.use("Agg")
6
+ import matplotlib.pyplot as plt
7
+ import matplotlib.patches as mpatches
8
+ import io
9
+ import re
10
+ from datetime import datetime
11
+
12
+ # ── VADER sentiment (graceful fallback if not installed) ──────────────────────
13
+ try:
14
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
15
+ _analyzer = SentimentIntensityAnalyzer()
16
+ def vader_score(text):
17
+ return _analyzer.polarity_scores(str(text))
18
+ except ImportError:
19
+ def vader_score(text):
20
+ text = text.lower()
21
+ pos = sum(w in text for w in ["good","strong","growth","positive","gain","profit"])
22
+ neg = sum(w in text for w in ["breach","hack","lawsuit","strike","loss","fine","shut","miss","fraud","attack"])
23
+ compound = round((pos - neg) / max(pos + neg, 1), 3)
24
+ return {"neg": neg / max(pos+neg,1), "neu": 0.5, "pos": pos / max(pos+neg,1), "compound": compound}
25
+
26
+ # ── Styling constants ─────────────────────────────────────────────────────────
27
+ PALETTE = {
28
+ "critical response": "#C0392B",
29
+ "escalate": "#E67E22",
30
+ "review": "#2980B9",
31
+ "monitor": "#27AE60",
32
+ }
33
+ BG = "#0D0F14"
34
+ CARD_BG = "#141720"
35
+ ACCENT = "#4F8EF7"
36
+ TEXT = "#E8EAF0"
37
+
38
+ CRISIS_ICONS = {
39
+ "cybersecurity": "πŸ”",
40
+ "legal": "βš–οΈ",
41
+ "operations": "🏭",
42
+ "labor": "πŸ‘·",
43
+ "financial": "πŸ“‰",
44
+ }
45
+
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # HELPERS
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ def infer_crisis_type(text):
51
+ text = text.lower()
52
+ if any(w in text for w in ["hack","breach","cyber","ransomware","malware","data leak","phishing"]):
53
+ return "cybersecurity"
54
+ if any(w in text for w in ["lawsuit","antitrust","regulator","fine","court","SEC","penalty","sanction"]):
55
+ return "legal"
56
+ if any(w in text for w in ["factory","supply chain","shutdown","production","recall","outage"]):
57
+ return "operations"
58
+ if any(w in text for w in ["strike","worker","protest","union","layoff","walkout"]):
59
+ return "labor"
60
+ if any(w in text for w in ["profit warning","earnings miss","downgrade","revenue","loss","debt","bankruptcy"]):
61
+ return "financial"
62
+ return "general"
63
+
64
+ SEVERITY_BASE = {
65
+ "cybersecurity": 4, "legal": 4, "operations": 3,
66
+ "labor": 3, "financial": 5, "general": 3,
67
+ }
68
+ URGENCY_BASE = {
69
+ "cybersecurity": 5, "legal": 4, "operations": 4,
70
+ "labor": 3, "financial": 5, "general": 3,
71
+ }
72
+ MARKET_BASE = {
73
+ "cybersecurity": -2.8, "legal": -2.1, "operations": -1.8,
74
+ "labor": -1.2, "financial": -3.0, "general": -1.5,
75
+ }
76
+
77
+ def assign_priority(severity, urgency):
78
+ score = severity + urgency
79
+ if score >= 9: return "critical response"
80
+ if score >= 7: return "escalate"
81
+ if score >= 5: return "review"
82
+ return "monitor"
83
+
84
+ def sentiment_label(compound):
85
+ if compound >= 0.05: return "Positive 🟒"
86
+ if compound <= -0.05: return "Negative πŸ”΄"
87
+ return "Neutral βšͺ"
88
+
89
+ # ─────────────────────────────────────────────────────────────────────────────
90
+ # TAB 1 β€” Single headline analyser
91
+ # ─────────────────────────────────────────────────────────────────────────────
92
+
93
+ def analyse_headline(headline: str):
94
+ if not headline.strip():
95
+ return "⚠️ Please enter a headline.", None
96
+
97
+ scores = vader_score(headline)
98
+ compound = round(scores["compound"], 3)
99
+ crisis = infer_crisis_type(headline)
100
+ severity = SEVERITY_BASE[crisis]
101
+ urgency = URGENCY_BASE[crisis]
102
+ market = round(MARKET_BASE[crisis] + np.random.normal(0, 0.4), 2)
103
+ priority = assign_priority(severity, urgency)
104
+ sent_label = sentiment_label(compound)
105
+ icon = CRISIS_ICONS.get(crisis, "πŸ“°")
106
+ color = PALETTE.get(priority, "#888")
107
+
108
+ # ── bar chart ─────────────────────────────────────────────────────────────
109
+ fig, axes = plt.subplots(1, 2, figsize=(9, 3.5))
110
+ fig.patch.set_facecolor(BG)
111
+
112
+ # sentiment bars
113
+ ax = axes[0]
114
+ ax.set_facecolor(CARD_BG)
115
+ cats = ["Negative", "Neutral", "Positive"]
116
+ vals = [scores["neg"], scores["neu"], scores["pos"]]
117
+ colors = ["#C0392B", "#7F8C8D", "#27AE60"]
118
+ bars = ax.barh(cats, vals, color=colors, height=0.5)
119
+ ax.set_xlim(0, 1)
120
+ ax.set_title("Sentiment Breakdown", color=TEXT, fontsize=11, pad=8)
121
+ ax.tick_params(colors=TEXT, labelsize=9)
122
+ for spine in ax.spines.values(): spine.set_visible(False)
123
+ ax.xaxis.label.set_color(TEXT)
124
+ ax.set_xlabel("Score", color=TEXT, fontsize=8)
125
+ for bar, val in zip(bars, vals):
126
+ ax.text(val + 0.01, bar.get_y() + bar.get_height()/2,
127
+ f"{val:.2f}", va="center", color=TEXT, fontsize=8)
128
+
129
+ # severity / urgency gauge
130
+ ax2 = axes[1]
131
+ ax2.set_facecolor(CARD_BG)
132
+ metrics = ["Severity", "Urgency"]
133
+ mvals = [severity, urgency]
134
+ mcols = [ACCENT, color]
135
+ b2 = ax2.barh(metrics, mvals, color=mcols, height=0.5)
136
+ ax2.set_xlim(0, 5)
137
+ ax2.set_title("Risk Scores (out of 5)", color=TEXT, fontsize=11, pad=8)
138
+ ax2.tick_params(colors=TEXT, labelsize=9)
139
+ for spine in ax2.spines.values(): spine.set_visible(False)
140
+ ax2.set_xlabel("Score", color=TEXT, fontsize=8)
141
+ ax2.xaxis.label.set_color(TEXT)
142
+ for bar, val in zip(b2, mvals):
143
+ ax2.text(val + 0.05, bar.get_y() + bar.get_height()/2,
144
+ str(val), va="center", color=TEXT, fontsize=9, fontweight="bold")
145
+
146
+ plt.tight_layout(pad=1.5)
147
+
148
+ # ── markdown result card ───────────────────────────────────────────────────
149
+ md = f"""
150
+ ### {icon} Analysis Result
151
+
152
+ | Field | Value |
153
+ |---|---|
154
+ | **Crisis Type** | `{crisis.upper()}` |
155
+ | **Sentiment** | {sent_label} (compound: `{compound}`) |
156
+ | **Severity Score** | `{severity} / 5` |
157
+ | **Response Urgency** | `{urgency} / 5` |
158
+ | **Est. Market Impact** | `{market:+.2f}%` |
159
+ | **Priority Action** | <span style='color:{color};font-weight:bold'>{priority.upper()}</span> |
160
+
161
+ ---
162
+ **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."}
163
+ """
164
+ return md, fig
165
+
166
+
167
+ # ─────────────────────────────────────────────────────────────────────────────
168
+ # TAB 2 β€” CSV Dashboard
169
+ # ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ def build_dashboard(file):
172
+ if file is None:
173
+ return "⚠️ Please upload **crisis_news_enriched.csv** (output of Notebook 1).", None
174
+
175
+ try:
176
+ df = pd.read_csv(file.name)
177
+ except Exception as e:
178
+ return f"❌ Could not read file: {e}", None
179
+
180
+ required = {"crisis_type", "priority_action", "severity_score",
181
+ "estimated_market_impact_pct", "company"}
182
+ missing = required - set(df.columns)
183
+ if missing:
184
+ return f"❌ Missing columns: {missing}. Please upload `crisis_news_enriched.csv`.", None
185
+
186
+ # ── 4-panel figure ────────────────────────────────────────────────────────
187
+ fig, axes = plt.subplots(2, 2, figsize=(13, 9))
188
+ fig.patch.set_facecolor(BG)
189
+ fig.suptitle("Crisis Intelligence Dashboard", color=TEXT,
190
+ fontsize=16, fontweight="bold", y=0.98)
191
+
192
+ # 1. Crisis type distribution
193
+ ax = axes[0, 0]
194
+ ax.set_facecolor(CARD_BG)
195
+ ct = df["crisis_type"].value_counts()
196
+ bar_colors = [ACCENT] * len(ct)
197
+ bars = ax.bar(ct.index, ct.values, color=bar_colors, width=0.6)
198
+ ax.set_title("Headlines by Crisis Type", color=TEXT, fontsize=11)
199
+ ax.tick_params(colors=TEXT, labelsize=8)
200
+ ax.set_xlabel("Crisis Type", color=TEXT, fontsize=9)
201
+ ax.set_ylabel("Count", color=TEXT, fontsize=9)
202
+ for spine in ax.spines.values(): spine.set_color("#2A2D3A")
203
+ for bar in bars:
204
+ ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
205
+ str(int(bar.get_height())), ha="center", color=TEXT, fontsize=8)
206
+
207
+ # 2. Priority action distribution (donut)
208
+ ax2 = axes[0, 1]
209
+ ax2.set_facecolor(CARD_BG)
210
+ pa = df["priority_action"].value_counts()
211
+ cols = [PALETTE.get(k, "#888") for k in pa.index]
212
+ wedges, texts, autotexts = ax2.pie(
213
+ pa.values, labels=pa.index, colors=cols,
214
+ autopct="%1.0f%%", startangle=140,
215
+ wedgeprops=dict(width=0.55),
216
+ textprops={"color": TEXT, "fontsize": 8}
217
+ )
218
+ for at in autotexts: at.set_color(BG); at.set_fontweight("bold")
219
+ ax2.set_title("Priority Action Distribution", color=TEXT, fontsize=11)
220
+
221
+ # 3. Avg market impact by crisis type
222
+ ax3 = axes[1, 0]
223
+ ax3.set_facecolor(CARD_BG)
224
+ mi = df.groupby("crisis_type")["estimated_market_impact_pct"].mean().sort_values()
225
+ bar_c = ["#C0392B" if v < -2.5 else "#E67E22" if v < -1.5 else "#2980B9" for v in mi.values]
226
+ bars3 = ax3.barh(mi.index, mi.values, color=bar_c, height=0.5)
227
+ ax3.axvline(0, color=TEXT, linewidth=0.5, alpha=0.4)
228
+ ax3.set_title("Avg Market Impact % by Crisis Type", color=TEXT, fontsize=11)
229
+ ax3.tick_params(colors=TEXT, labelsize=8)
230
+ ax3.set_xlabel("Est. Impact (%)", color=TEXT, fontsize=9)
231
+ for spine in ax3.spines.values(): spine.set_color("#2A2D3A")
232
+ for bar, val in zip(bars3, mi.values):
233
+ ax3.text(val - 0.05, bar.get_y() + bar.get_height()/2,
234
+ f"{val:.2f}%", va="center", ha="right", color=TEXT, fontsize=8)
235
+
236
+ # 4. Severity heatmap (crisis type Γ— priority)
237
+ ax4 = axes[1, 1]
238
+ ax4.set_facecolor(CARD_BG)
239
+ pivot = df.groupby(["crisis_type", "priority_action"])["severity_score"].mean().unstack(fill_value=0)
240
+ im = ax4.imshow(pivot.values, cmap="RdYlGn_r", aspect="auto", vmin=1, vmax=5)
241
+ ax4.set_xticks(range(len(pivot.columns)))
242
+ ax4.set_yticks(range(len(pivot.index)))
243
+ ax4.set_xticklabels(pivot.columns, color=TEXT, fontsize=7, rotation=20, ha="right")
244
+ ax4.set_yticklabels(pivot.index, color=TEXT, fontsize=8)
245
+ ax4.set_title("Avg Severity: Crisis Type Γ— Priority", color=TEXT, fontsize=11)
246
+ for i in range(pivot.shape[0]):
247
+ for j in range(pivot.shape[1]):
248
+ val = pivot.values[i, j]
249
+ if val > 0:
250
+ ax4.text(j, i, f"{val:.1f}", ha="center", va="center",
251
+ color="white", fontsize=8, fontweight="bold")
252
+ cbar = fig.colorbar(im, ax=ax4, fraction=0.03)
253
+ cbar.ax.tick_params(colors=TEXT, labelsize=7)
254
+
255
+ plt.tight_layout(rect=[0, 0, 1, 0.96])
256
+
257
+ # ── summary stats ─────────────────────────────────────────────────────────
258
+ n = len(df)
259
+ n_critical = (df["priority_action"] == "critical response").sum()
260
+ top_type = df["crisis_type"].value_counts().idxmax()
261
+ top_co = df["company"].value_counts().idxmax()
262
+ avg_impact = df["estimated_market_impact_pct"].mean()
263
+
264
+ md = f"""
265
+ ### πŸ“Š Dataset Summary
266
+
267
+ | Metric | Value |
268
+ |---|---|
269
+ | Total headlines | `{n}` |
270
+ | Critical response alerts | `{n_critical}` ({100*n_critical/n:.0f}%) |
271
+ | Most common crisis type | `{top_type.upper()}` |
272
+ | Most exposed company | `{top_co}` |
273
+ | Avg estimated market impact | `{avg_impact:+.2f}%` |
274
+
275
+ Upload `crisis_news_enriched.csv` (generated by Notebook 1) to refresh.
276
+ """
277
+ return md, fig
278
+
279
+
280
+ # ─────────────────────────────────────────────────────────────────────────────
281
+ # BUILD UI
282
+ # ─────────────────────────────────────────────────────────────────────────────
283
+
284
+ CUSTOM_CSS = """
285
+ body, .gradio-container { background: #0D0F14 !important; color: #E8EAF0 !important; font-family: 'IBM Plex Mono', monospace; }
286
+ .gr-button-primary { background: #4F8EF7 !important; border: none !important; color: #0D0F14 !important; font-weight: 700 !important; }
287
+ .gr-button-primary:hover { background: #6FA3FA !important; }
288
+ h1, h2, h3 { color: #E8EAF0 !important; }
289
+ .gr-panel, .gr-box { background: #141720 !important; border-color: #2A2D3A !important; }
290
+ textarea, input[type=text] { background: #1C1F2B !important; color: #E8EAF0 !important; border-color: #2A2D3A !important; }
291
+ .gr-tab-nav button { color: #9AA0B4 !important; }
292
+ .gr-tab-nav button.selected { color: #4F8EF7 !important; border-bottom: 2px solid #4F8EF7 !important; }
293
+ """
294
+
295
+ ABOUT_MD = """
296
+ # πŸ” Crisis Monitor β€” AI-Powered Business Risk Intelligence
297
+
298
+ ## What this app does
299
+ 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.
300
+
301
+ It lets you:
302
+ - **Analyse any news headline** instantly β€” detecting crisis type, sentiment (VADER), severity, urgency, estimated market impact, and recommended action
303
+ - **Upload your enriched dataset** (`crisis_news_enriched.csv`) for a full visual dashboard
304
+
305
+ ## How the pipeline works
306
+ ```
307
+ Google News RSS ──► Notebook 1 ──► Enriched CSV ──► Notebook 2 ──► Analysis
308
+ (scraping, (severity, (VADER,
309
+ synthetic urgency, ARIMA,
310
+ enrichment) market decision
311
+ impact, support)
312
+ priority)
313
+ β”‚
314
+ β–Ό
315
+ This Hugging Face App
316
+ (real-time scanning +
317
+ dashboard visualisation)
318
+ ```
319
+
320
+ ## Crisis types monitored
321
+ | Type | Signal keywords |
322
+ |---|---|
323
+ | πŸ” Cybersecurity | breach, hack, ransomware, data leak |
324
+ | βš–οΈ Legal | lawsuit, antitrust, regulator, fine |
325
+ | 🏭 Operations | factory, supply chain, shutdown, recall |
326
+ | πŸ‘· Labor | strike, worker protest, union, layoff |
327
+ | πŸ“‰ Financial | profit warning, earnings miss, downgrade |
328
+
329
+ ## Priority framework
330
+ | Priority | Trigger |
331
+ |---|---|
332
+ | πŸ”΄ Critical Response | Severity + Urgency β‰₯ 9 |
333
+ | 🟠 Escalate | Score 7–8 |
334
+ | πŸ”΅ Review | Score 5–6 |
335
+ | 🟒 Monitor | Score < 5 |
336
+
337
+ ---
338
+ *Built with Python Β· Gradio Β· VADER Sentiment Β· Matplotlib*
339
+ """
340
+
341
+ with gr.Blocks(css=CUSTOM_CSS, title="Crisis Monitor") as demo:
342
+
343
+ gr.Markdown("""
344
+ # 🚨 Crisis Monitor
345
+ ### AI-Powered Business Risk Intelligence Β· ESCP Business School
346
+ """)
347
+
348
+ with gr.Tabs():
349
+
350
+ # ── TAB 1 ─────────────────────────────────────────────────────────────
351
+ with gr.Tab("πŸ” Headline Scanner"):
352
+ gr.Markdown("Paste any business news headline to get an instant risk assessment.")
353
+ with gr.Row():
354
+ with gr.Column(scale=2):
355
+ headline_input = gr.Textbox(
356
+ label="News Headline",
357
+ placeholder="e.g. Apple faces major data breach exposing 50M user records...",
358
+ lines=3,
359
+ )
360
+ scan_btn = gr.Button("⚑ Analyse Headline", variant="primary")
361
+ with gr.Column(scale=3):
362
+ result_md = gr.Markdown()
363
+ result_fig = gr.Plot()
364
+
365
+ scan_btn.click(
366
+ fn=analyse_headline,
367
+ inputs=headline_input,
368
+ outputs=[result_md, result_fig],
369
+ )
370
+
371
+ gr.Examples(
372
+ examples=[
373
+ ["Tesla hit with major ransomware attack, customer data leaked online"],
374
+ ["Amazon faces antitrust fine from EU regulators over pricing practices"],
375
+ ["Boeing factory workers go on strike, halting 737 MAX production"],
376
+ ["Intel issues profit warning, shares drop 12% after earnings miss"],
377
+ ["Apple supply chain disruption forces iPhone production cuts in China"],
378
+ ],
379
+ inputs=headline_input,
380
+ label="Try an example",
381
+ )
382
+
383
+ # ── TAB 2 ─────────────────────────────────────────────────────────────
384
+ with gr.Tab("πŸ“Š Crisis Dashboard"):
385
+ gr.Markdown("Upload `crisis_news_enriched.csv` (generated by Notebook 1) for a full portfolio view.")
386
+ with gr.Row():
387
+ csv_input = gr.File(label="Upload crisis_news_enriched.csv", file_types=[".csv"])
388
+ dash_btn = gr.Button("πŸ“ˆ Generate Dashboard", variant="primary")
389
+ with gr.Row():
390
+ dash_md = gr.Markdown()
391
+ with gr.Row():
392
+ dash_fig = gr.Plot()
393
+
394
+ dash_btn.click(
395
+ fn=build_dashboard,
396
+ inputs=csv_input,
397
+ outputs=[dash_md, dash_fig],
398
+ )
399
+
400
+ # ── TAB 3 ─────────────────────────────────────────────────────────────
401
+ with gr.Tab("ℹ️ About"):
402
+ gr.Markdown(ABOUT_MD)
403
+
404
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ pandas
3
+ numpy
4
+ matplotlib
5
+ vaderSentiment