Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import urllib.request
|
| 4 |
+
import xml.etree.ElementTree as ET
|
| 5 |
+
from email.utils import parsedate_to_datetime
|
| 6 |
+
from urllib.parse import quote
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import torch
|
| 11 |
+
from peft import PeftModel
|
| 12 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 13 |
+
|
| 14 |
+
# ================================================================
|
| 15 |
+
# CONFIG
|
| 16 |
+
# ================================================================
|
| 17 |
+
|
| 18 |
+
BASE_MODEL = "distilbert-base-uncased"
|
| 19 |
+
ADAPTER_PATH = "."
|
| 20 |
+
MAX_LENGTH = 128
|
| 21 |
+
BATCH_SIZE = 32
|
| 22 |
+
|
| 23 |
+
LABELS = {0: "negative", 1: "neutral", 2: "positive"}
|
| 24 |
+
|
| 25 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 26 |
+
|
| 27 |
+
# ================================================================
|
| 28 |
+
# MODEL LOADING (DistilBERT + LoRA)
|
| 29 |
+
# ================================================================
|
| 30 |
+
|
| 31 |
+
print("Device:", DEVICE)
|
| 32 |
+
|
| 33 |
+
tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH, use_fast=True)
|
| 34 |
+
|
| 35 |
+
base_model = AutoModelForSequenceClassification.from_pretrained(
|
| 36 |
+
BASE_MODEL,
|
| 37 |
+
num_labels=3,
|
| 38 |
+
id2label=LABELS,
|
| 39 |
+
label2id={v: k for k, v in LABELS.items()},
|
| 40 |
+
ignore_mismatched_sizes=True,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
|
| 44 |
+
try:
|
| 45 |
+
model = model.merge_and_unload()
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print("Merge skipped:", e)
|
| 48 |
+
|
| 49 |
+
model = model.to(DEVICE)
|
| 50 |
+
model.eval()
|
| 51 |
+
print("Model ready.")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ================================================================
|
| 55 |
+
# SENTIMENT (batch)
|
| 56 |
+
# ================================================================
|
| 57 |
+
|
| 58 |
+
def predict_batch(texts):
|
| 59 |
+
"""Her metin için (p_neg, p_neu, p_pos) döndürür."""
|
| 60 |
+
out = []
|
| 61 |
+
for i in range(0, len(texts), BATCH_SIZE):
|
| 62 |
+
chunk = texts[i:i + BATCH_SIZE]
|
| 63 |
+
inputs = tokenizer(
|
| 64 |
+
chunk,
|
| 65 |
+
return_tensors="pt",
|
| 66 |
+
truncation=True,
|
| 67 |
+
max_length=MAX_LENGTH,
|
| 68 |
+
padding=True,
|
| 69 |
+
)
|
| 70 |
+
inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
|
| 71 |
+
with torch.inference_mode():
|
| 72 |
+
probs = torch.softmax(model(**inputs).logits, dim=-1).cpu().tolist()
|
| 73 |
+
out.extend(probs)
|
| 74 |
+
return out
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ================================================================
|
| 78 |
+
# RISK KURALLARI (anahtar kelime tabanlı sinyaller)
|
| 79 |
+
# ================================================================
|
| 80 |
+
|
| 81 |
+
RISK_SIGNALS = {
|
| 82 |
+
"Hasar / Claim": [
|
| 83 |
+
"claim", "claims", "payout", "payouts", "loss", "losses",
|
| 84 |
+
"combined ratio", "reserve", "reserves",
|
| 85 |
+
],
|
| 86 |
+
"Doğal afet": [
|
| 87 |
+
"hurricane", "earthquake", "flood", "flooding", "wildfire", "storm",
|
| 88 |
+
"catastrophe", "cyclone", "typhoon", "tornado", "hailstorm", "drought",
|
| 89 |
+
],
|
| 90 |
+
"Büyük kaza": [
|
| 91 |
+
"crash", "explosion", "collapse", "derailment", "blast", "accident",
|
| 92 |
+
"fire", "sinking", "spill",
|
| 93 |
+
],
|
| 94 |
+
"Dava / Fraud": [
|
| 95 |
+
"lawsuit", "sued", "litigation", "fraud", "fine", "fined", "penalty",
|
| 96 |
+
"investigation", "probe", "settlement", "class action", "regulator",
|
| 97 |
+
],
|
| 98 |
+
"Finansal stres": [
|
| 99 |
+
"downgrade", "downgraded", "profit warning", "solvency", "writedown",
|
| 100 |
+
"write-down", "impairment", "insolvency", "bankruptcy", "capital shortfall",
|
| 101 |
+
"negative outlook", "slump", "plunge",
|
| 102 |
+
],
|
| 103 |
+
"Siber / Operasyonel": [
|
| 104 |
+
"cyberattack", "cyber attack", "data breach", "breach", "ransomware",
|
| 105 |
+
"outage",
|
| 106 |
+
],
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
TOPICS = {
|
| 110 |
+
"Motor insurance": ["motor", "auto insurance", "car insurance", "vehicle", "automobile"],
|
| 111 |
+
"Health insurance": ["health insurance", "health", "medical", "hospital"],
|
| 112 |
+
"Catastrophe risk": ["catastrophe", "hurricane", "earthquake", "flood", "wildfire",
|
| 113 |
+
"storm", "cyclone", "typhoon", "tornado", "hailstorm"],
|
| 114 |
+
"Life insurance": ["life insurance", "life insurer", "annuity", "annuities", "pension", "mortality"],
|
| 115 |
+
"Reinsurance": ["reinsur", "retrocession", "cat bond", "catastrophe bond"],
|
| 116 |
+
"Property": ["property insurance", "homeowner", "home insurance", "real estate", "fire"],
|
| 117 |
+
"Cyber": ["cyber", "ransomware", "data breach"],
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
COMPANIES = [
|
| 121 |
+
"Allianz", "AXA", "Zurich", "Generali", "Aviva", "Munich Re", "Swiss Re",
|
| 122 |
+
"Hannover Re", "Chubb", "AIG", "MetLife", "Prudential", "Allstate",
|
| 123 |
+
"Progressive", "State Farm", "Mapfre", "Ageas", "Lloyd's", "Travelers",
|
| 124 |
+
"Liberty Mutual", "Tokio Marine", "Ping An",
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _compile(words):
|
| 129 |
+
return re.compile(r"\b(?:%s)" % "|".join(re.escape(w) for w in words), re.I)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
SIGNAL_RE = {cat: _compile(words) for cat, words in RISK_SIGNALS.items()}
|
| 133 |
+
TOPIC_RE = {t: _compile(words) for t, words in TOPICS.items()}
|
| 134 |
+
COMPANY_RE = {c: re.compile(r"\b%s\b" % re.escape(c), re.I) for c in COMPANIES}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def find_signals(text):
|
| 138 |
+
hits = []
|
| 139 |
+
for cat, rx in SIGNAL_RE.items():
|
| 140 |
+
m = rx.findall(text)
|
| 141 |
+
if m:
|
| 142 |
+
hits.append((cat, sorted({x.lower() for x in m})[:2]))
|
| 143 |
+
return hits
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def find_topics(text):
|
| 147 |
+
return [t for t, rx in TOPIC_RE.items() if rx.search(text)]
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def find_companies(text):
|
| 151 |
+
return [c for c, rx in COMPANY_RE.items() if rx.search(text)]
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def risk_level(score, high=60, medium=35):
|
| 155 |
+
if score >= high:
|
| 156 |
+
return "🔴 HIGH"
|
| 157 |
+
if score >= medium:
|
| 158 |
+
return "🟠 MEDIUM"
|
| 159 |
+
return "🟢 LOW"
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# ================================================================
|
| 163 |
+
# TEK HABER ANALİZİ
|
| 164 |
+
# ================================================================
|
| 165 |
+
|
| 166 |
+
def score_item(text, probs):
|
| 167 |
+
p_neg, p_neu, p_pos = probs
|
| 168 |
+
sentiment = p_pos - p_neg # -1 .. +1
|
| 169 |
+
signals = find_signals(text)
|
| 170 |
+
n_sig = len(signals)
|
| 171 |
+
# Risk indeksi: %65 negatif olasılık + %35 risk sinyali yoğunluğu
|
| 172 |
+
risk = 100 * (0.65 * p_neg + 0.35 * min(n_sig, 3) / 3)
|
| 173 |
+
label = LABELS[max(range(3), key=lambda i: probs[i])]
|
| 174 |
+
reason = "; ".join(f"{c} ({', '.join(k)})" for c, k in signals) or "-"
|
| 175 |
+
return {
|
| 176 |
+
"label": label,
|
| 177 |
+
"sentiment": round(sentiment, 3),
|
| 178 |
+
"signals": n_sig,
|
| 179 |
+
"risk_score": round(risk, 1),
|
| 180 |
+
"risk": risk_level(risk),
|
| 181 |
+
"reason": reason,
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def analyze_news(text):
|
| 186 |
+
text = (text or "").strip()
|
| 187 |
+
if not text:
|
| 188 |
+
return "Lütfen bir sigorta haber başlığı girin.", "", ""
|
| 189 |
+
|
| 190 |
+
probs = predict_batch([text])[0]
|
| 191 |
+
s = score_item(text, probs)
|
| 192 |
+
|
| 193 |
+
companies = find_companies(text) or ["-"]
|
| 194 |
+
topics = find_topics(text) or ["-"]
|
| 195 |
+
|
| 196 |
+
result = (
|
| 197 |
+
f"Sentiment: {s['label'].upper()}\n"
|
| 198 |
+
f"Skor: {s['sentiment']:+.2f}\n"
|
| 199 |
+
f"Güven: {max(probs) * 100:.1f}%"
|
| 200 |
+
)
|
| 201 |
+
prob_text = (
|
| 202 |
+
f"Negative: {probs[0] * 100:.1f}%\n"
|
| 203 |
+
f"Neutral: {probs[1] * 100:.1f}%\n"
|
| 204 |
+
f"Positive: {probs[2] * 100:.1f}%"
|
| 205 |
+
)
|
| 206 |
+
risk_text = (
|
| 207 |
+
f"Şirket: {', '.join(companies)}\n"
|
| 208 |
+
f"Risk: {s['risk']} ({s['risk_score']}/100)\n"
|
| 209 |
+
f"Risk sinyali: {s['signals']}\n"
|
| 210 |
+
f"Neden: {s['reason']}\n"
|
| 211 |
+
f"Konu: {', '.join(topics)}"
|
| 212 |
+
)
|
| 213 |
+
return result, prob_text, risk_text
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# ================================================================
|
| 217 |
+
# TOPLU ANALİZ PIPELINE
|
| 218 |
+
# ================================================================
|
| 219 |
+
|
| 220 |
+
def analyze_items(items):
|
| 221 |
+
"""items: [{'headline':str, 'date':datetime|None, 'company':str|None}]"""
|
| 222 |
+
if not items:
|
| 223 |
+
return None, None, None
|
| 224 |
+
|
| 225 |
+
texts = [it["headline"] for it in items]
|
| 226 |
+
probs = predict_batch(texts)
|
| 227 |
+
|
| 228 |
+
rows = []
|
| 229 |
+
for it, p in zip(items, probs):
|
| 230 |
+
s = score_item(it["headline"], p)
|
| 231 |
+
companies = [it["company"]] if it.get("company") else find_companies(it["headline"])
|
| 232 |
+
rows.append({
|
| 233 |
+
"date": it.get("date"),
|
| 234 |
+
"headline": it["headline"],
|
| 235 |
+
"companies": companies or ["(Sektör)"],
|
| 236 |
+
"topics": find_topics(it["headline"]),
|
| 237 |
+
**s,
|
| 238 |
+
})
|
| 239 |
+
df = pd.DataFrame(rows)
|
| 240 |
+
|
| 241 |
+
has_dates = df["date"].notna().sum() >= 4
|
| 242 |
+
if has_dates:
|
| 243 |
+
df["date"] = pd.to_datetime(df["date"], utc=True, errors="coerce")
|
| 244 |
+
mid = df["date"].dropna().sort_values().iloc[len(df["date"].dropna()) // 2]
|
| 245 |
+
df["recent"] = df["date"] >= mid
|
| 246 |
+
else:
|
| 247 |
+
df["recent"] = False
|
| 248 |
+
|
| 249 |
+
# ---- Şirket tablosu ----
|
| 250 |
+
ex = df.explode("companies")
|
| 251 |
+
comp_rows = []
|
| 252 |
+
for comp, g in ex.groupby("companies"):
|
| 253 |
+
risk = g["risk_score"].mean()
|
| 254 |
+
trend = "—"
|
| 255 |
+
if has_dates:
|
| 256 |
+
r, e = g[g["recent"]], g[~g["recent"]]
|
| 257 |
+
if len(r) and len(e):
|
| 258 |
+
d = r["sentiment"].mean() - e["sentiment"].mean()
|
| 259 |
+
trend = ("Improving ↑" if d > 0.15
|
| 260 |
+
else "Deteriorating ↓" if d < -0.15 else "Stable →")
|
| 261 |
+
comp_rows.append({
|
| 262 |
+
"Company": comp,
|
| 263 |
+
"News": len(g),
|
| 264 |
+
"Avg sentiment": round(g["sentiment"].mean(), 2),
|
| 265 |
+
"Risk signals": int(g["signals"].sum()),
|
| 266 |
+
"High-risk news": int((g["risk_score"] >= 60).sum()),
|
| 267 |
+
"Risk score": round(risk, 1),
|
| 268 |
+
"Risk": risk_level(risk, high=55, medium=30),
|
| 269 |
+
"Trend": trend,
|
| 270 |
+
})
|
| 271 |
+
comp_df = pd.DataFrame(comp_rows).sort_values("Risk score", ascending=False)
|
| 272 |
+
|
| 273 |
+
# ---- Konu / trend tablosu ----
|
| 274 |
+
topic_rows = []
|
| 275 |
+
n_recent = max(int(df["recent"].sum()), 1)
|
| 276 |
+
n_early = max(int((~df["recent"]).sum()), 1)
|
| 277 |
+
for topic in TOPICS:
|
| 278 |
+
mask = df["topics"].apply(lambda t: topic in t)
|
| 279 |
+
if not mask.any():
|
| 280 |
+
continue
|
| 281 |
+
arrow = "—"
|
| 282 |
+
if has_dates:
|
| 283 |
+
share_r = (mask & df["recent"]).sum() / n_recent
|
| 284 |
+
share_e = (mask & ~df["recent"]).sum() / n_early
|
| 285 |
+
if share_e == 0:
|
| 286 |
+
arrow = "↑↑"
|
| 287 |
+
else:
|
| 288 |
+
ratio = share_r / share_e
|
| 289 |
+
arrow = ("↑↑" if ratio >= 1.5 else "↑" if ratio >= 1.15
|
| 290 |
+
else "↓" if ratio <= 0.8 else "→")
|
| 291 |
+
topic_rows.append({
|
| 292 |
+
"Topic": topic,
|
| 293 |
+
"News": int(mask.sum()),
|
| 294 |
+
"Avg sentiment": round(df.loc[mask, "sentiment"].mean(), 2),
|
| 295 |
+
"Trend": arrow,
|
| 296 |
+
})
|
| 297 |
+
topic_df = pd.DataFrame(topic_rows).sort_values("News", ascending=False) \
|
| 298 |
+
if topic_rows else pd.DataFrame(columns=["Topic", "News", "Avg sentiment", "Trend"])
|
| 299 |
+
|
| 300 |
+
# ---- Haber tablosu ----
|
| 301 |
+
news_df = df.assign(
|
| 302 |
+
companies=df["companies"].apply(", ".join),
|
| 303 |
+
topics=df["topics"].apply(", ".join),
|
| 304 |
+
date=df["date"].apply(lambda d: d.strftime("%Y-%m-%d") if pd.notna(d) else ""),
|
| 305 |
+
)[["date", "companies", "headline", "label", "sentiment", "risk", "risk_score", "reason", "topics"]]
|
| 306 |
+
news_df = news_df.sort_values("risk_score", ascending=False)
|
| 307 |
+
|
| 308 |
+
return news_df, comp_df, topic_df
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
# ================================================================
|
| 312 |
+
# TOPLU GİRİŞ (metin / CSV)
|
| 313 |
+
# ================================================================
|
| 314 |
+
|
| 315 |
+
def _pick(cols, names):
|
| 316 |
+
for n in names:
|
| 317 |
+
if n in cols:
|
| 318 |
+
return n
|
| 319 |
+
return None
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def batch_analyze(text, file):
|
| 323 |
+
items = []
|
| 324 |
+
|
| 325 |
+
path = file if isinstance(file, str) else getattr(file, "name", None)
|
| 326 |
+
if path:
|
| 327 |
+
df = pd.read_csv(path)
|
| 328 |
+
df.columns = [c.strip().lower() for c in df.columns]
|
| 329 |
+
h = _pick(df.columns, ["headline", "title", "text", "news", "haber", "baslik"])
|
| 330 |
+
d = _pick(df.columns, ["date", "published", "pubdate", "tarih"])
|
| 331 |
+
c = _pick(df.columns, ["company", "sirket", "şirket"])
|
| 332 |
+
if h is None:
|
| 333 |
+
raise gr.Error("CSV'de 'headline' (veya title/text/news) kolonu bulunamadı.")
|
| 334 |
+
for _, r in df.iterrows():
|
| 335 |
+
if pd.isna(r[h]) or not str(r[h]).strip():
|
| 336 |
+
continue
|
| 337 |
+
items.append({
|
| 338 |
+
"headline": str(r[h]).strip(),
|
| 339 |
+
"date": pd.to_datetime(r[d], utc=True, errors="coerce") if d else None,
|
| 340 |
+
"company": str(r[c]).strip() if c and pd.notna(r[c]) else None,
|
| 341 |
+
})
|
| 342 |
+
|
| 343 |
+
for line in (text or "").splitlines():
|
| 344 |
+
line = line.strip()
|
| 345 |
+
if line:
|
| 346 |
+
items.append({"headline": line, "date": None, "company": None})
|
| 347 |
+
|
| 348 |
+
if not items:
|
| 349 |
+
raise gr.Error("Haber girin (her satıra bir başlık) veya CSV yükleyin.")
|
| 350 |
+
return analyze_items(items)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
# ================================================================
|
| 354 |
+
# CANLI TARAMA (Google News RSS)
|
| 355 |
+
# ================================================================
|
| 356 |
+
|
| 357 |
+
def fetch_news(company, days, limit):
|
| 358 |
+
q = quote(f'"{company}" insurance when:{int(days)}d')
|
| 359 |
+
url = f"https://news.google.com/rss/search?q={q}&hl=en-US&gl=US&ceid=US:en"
|
| 360 |
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
| 361 |
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
| 362 |
+
root = ET.fromstring(resp.read())
|
| 363 |
+
|
| 364 |
+
items = []
|
| 365 |
+
for it in root.iter("item"):
|
| 366 |
+
title = (it.findtext("title") or "").strip()
|
| 367 |
+
title = re.sub(r"\s+-\s+[^-]+$", "", title) # " - Kaynak" kısmını at
|
| 368 |
+
try:
|
| 369 |
+
date = parsedate_to_datetime(it.findtext("pubDate"))
|
| 370 |
+
except Exception:
|
| 371 |
+
date = None
|
| 372 |
+
if title:
|
| 373 |
+
items.append({"headline": title, "date": date, "company": company})
|
| 374 |
+
if len(items) >= limit:
|
| 375 |
+
break
|
| 376 |
+
return items
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def live_scan(company_text, days, limit):
|
| 380 |
+
companies = [c.strip() for c in (company_text or "").split(",") if c.strip()]
|
| 381 |
+
if not companies:
|
| 382 |
+
raise gr.Error("En az bir şirket adı girin.")
|
| 383 |
+
|
| 384 |
+
items, errors = [], []
|
| 385 |
+
for c in companies:
|
| 386 |
+
try:
|
| 387 |
+
items += fetch_news(c, days, int(limit))
|
| 388 |
+
except Exception as e:
|
| 389 |
+
errors.append(f"{c}: {e}")
|
| 390 |
+
|
| 391 |
+
if not items:
|
| 392 |
+
raise gr.Error("Haber alınamadı. İnternet erişimini kontrol edin. " + " | ".join(errors))
|
| 393 |
+
return analyze_items(items)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
# ================================================================
|
| 397 |
+
# GRADIO UI
|
| 398 |
+
# ================================================================
|
| 399 |
+
|
| 400 |
+
EXAMPLES = [
|
| 401 |
+
["Insurance company reports record premium growth"],
|
| 402 |
+
["Allianz faces major claims increase after severe floods"],
|
| 403 |
+
["Zurich maintained stable operations in the third quarter."],
|
| 404 |
+
["AXA downgraded after reserve shortfall and fraud investigation"],
|
| 405 |
+
["Hurricane losses push reinsurance rates higher"],
|
| 406 |
+
]
|
| 407 |
+
|
| 408 |
+
BATCH_PLACEHOLDER = (
|
| 409 |
+
"Allianz reports record premium growth\n"
|
| 410 |
+
"Generali hit by wildfire claims surge\n"
|
| 411 |
+
"Zurich faces lawsuit over auto claims"
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
NEWS_COLS = ["date", "companies", "headline", "label", "sentiment", "risk", "risk_score", "reason", "topics"]
|
| 415 |
+
|
| 416 |
+
BRAND = "Goshawk_insurance_bert"
|
| 417 |
+
|
| 418 |
+
LOGO_SVG = """
|
| 419 |
+
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-label="Goshawk logo">
|
| 420 |
+
<defs>
|
| 421 |
+
<linearGradient id="gs" x1="0" y1="0" x2="0" y2="1">
|
| 422 |
+
<stop offset="0" stop-color="#1E4F9C"/><stop offset="1" stop-color="#0B1F3A"/>
|
| 423 |
+
</linearGradient>
|
| 424 |
+
</defs>
|
| 425 |
+
<path d="M32 3 L57 12 V32 C57 47 46 57 32 61 C18 57 7 47 7 32 V12 Z"
|
| 426 |
+
fill="url(#gs)" stroke="#F2B134" stroke-width="2.5" stroke-linejoin="round"/>
|
| 427 |
+
<path d="M15 37 C17 25 27 16 40 18 L51 24 L42 27 C45 32 43 38 38 41 L32 51 L28 43 C22 43 17 41 15 37 Z"
|
| 428 |
+
fill="#FFFFFF"/>
|
| 429 |
+
<path d="M51 24 L42 27 L45 33 Z" fill="#F2B134"/>
|
| 430 |
+
<path d="M22 27 L36 23 L35 26 L24 30 Z" fill="#0B1F3A" opacity=".85"/>
|
| 431 |
+
<circle cx="35" cy="29" r="2.2" fill="#F2B134"/>
|
| 432 |
+
<circle cx="35.4" cy="29" r="1" fill="#0B1F3A"/>
|
| 433 |
+
</svg>
|
| 434 |
+
"""
|
| 435 |
+
|
| 436 |
+
CSS = """
|
| 437 |
+
.gradio-container {max-width: 1100px !important; margin: auto;
|
| 438 |
+
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
|
| 439 |
+
#brand-header {display:flex; align-items:center; gap:16px; padding:20px 22px;
|
| 440 |
+
border-radius:18px; color:#fff; margin-bottom:10px;
|
| 441 |
+
background: linear-gradient(135deg, #0B1F3A 0%, #1E4F9C 70%, #2B6CC4 100%);
|
| 442 |
+
box-shadow: 0 6px 20px rgba(11,31,58,.25);}
|
| 443 |
+
#brand-header .logo {flex:0 0 auto; filter: drop-shadow(0 2px 6px rgba(0,0,0,.35));}
|
| 444 |
+
#brand-header .title {font-size:1.55rem; font-weight:800; letter-spacing:.2px; line-height:1.15;}
|
| 445 |
+
#brand-header .title span {color:#F2B134;}
|
| 446 |
+
#brand-header .sub {opacity:.85; font-size:.92rem; margin-top:4px;}
|
| 447 |
+
#brand-header .badges {margin-top:10px; display:flex; flex-wrap:wrap; gap:6px;}
|
| 448 |
+
#brand-header .badge {background:rgba(255,255,255,.14); border:1px solid rgba(255,255,255,.25);
|
| 449 |
+
padding:3px 10px; border-radius:999px; font-size:.74rem; backdrop-filter: blur(4px);}
|
| 450 |
+
.legend {display:flex; flex-wrap:wrap; gap:8px; margin:4px 0 12px;}
|
| 451 |
+
.legend .chip {padding:4px 12px; border-radius:999px; font-size:.8rem; font-weight:600;
|
| 452 |
+
border:1px solid rgba(128,128,128,.3);}
|
| 453 |
+
.chip.high {background:#fde8e8; color:#a31212;}
|
| 454 |
+
.chip.med {background:#fff1dc; color:#9a5a00;}
|
| 455 |
+
.chip.low {background:#e3f6e8; color:#14702d;}
|
| 456 |
+
button.primary {background: linear-gradient(135deg,#1E4F9C,#0B1F3A) !important;
|
| 457 |
+
border:none !important; color:#fff !important; font-weight:700 !important;
|
| 458 |
+
border-radius:12px !important;}
|
| 459 |
+
button.primary:hover {filter: brightness(1.15);}
|
| 460 |
+
.tab-nav button, button[role="tab"] {font-weight:600 !important;}
|
| 461 |
+
textarea, input[type="text"] {border-radius:10px !important;}
|
| 462 |
+
#footer {text-align:center; font-size:.82rem; opacity:.75; padding:10px 0 4px;}
|
| 463 |
+
@media (max-width: 640px) {
|
| 464 |
+
#brand-header {padding:14px; gap:12px;}
|
| 465 |
+
#brand-header .title {font-size:1.2rem;}
|
| 466 |
+
#brand-header .logo svg {width:48px; height:48px;}
|
| 467 |
+
}
|
| 468 |
+
"""
|
| 469 |
+
|
| 470 |
+
HEADER_HTML = f"""
|
| 471 |
+
<div id="brand-header">
|
| 472 |
+
<div class="logo">{LOGO_SVG}</div>
|
| 473 |
+
<div>
|
| 474 |
+
<div class="title">Goshawk<span>_</span>insurance<span>_</span>bert</div>
|
| 475 |
+
<div class="sub">Sigorta haberleri için sentiment, risk sinyali ve erken uyarı sistemi</div>
|
| 476 |
+
<div class="badges">
|
| 477 |
+
<span class="badge">DistilBERT + LoRA</span>
|
| 478 |
+
<span class="badge">Macro F1 78.06%</span>
|
| 479 |
+
<span class="badge">Early Warning</span>
|
| 480 |
+
<span class="badge">EN headlines</span>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
<div class="legend">
|
| 485 |
+
<span class="chip high">🔴 HIGH RISK</span>
|
| 486 |
+
<span class="chip med">🟠 MEDIUM RISK</span>
|
| 487 |
+
<span class="chip low">🟢 LOW RISK</span>
|
| 488 |
+
</div>
|
| 489 |
+
"""
|
| 490 |
+
|
| 491 |
+
with gr.Blocks(
|
| 492 |
+
title=BRAND,
|
| 493 |
+
theme=gr.themes.Soft(primary_hue="blue", secondary_hue="amber", neutral_hue="slate"),
|
| 494 |
+
css=CSS,
|
| 495 |
+
) as demo:
|
| 496 |
+
|
| 497 |
+
gr.HTML(HEADER_HTML)
|
| 498 |
+
|
| 499 |
+
# ---------------- TAB 1 ----------------
|
| 500 |
+
with gr.Tab("📰 Tek Haber"):
|
| 501 |
+
with gr.Row():
|
| 502 |
+
with gr.Column():
|
| 503 |
+
text_input = gr.Textbox(label="Insurance News", lines=4,
|
| 504 |
+
placeholder="Allianz faces major claims increase...")
|
| 505 |
+
btn = gr.Button("🔍 Analiz Et", variant="primary")
|
| 506 |
+
with gr.Column():
|
| 507 |
+
out_pred = gr.Textbox(label="Sentiment", lines=3)
|
| 508 |
+
out_prob = gr.Textbox(label="Olasılıklar", lines=3)
|
| 509 |
+
out_risk = gr.Textbox(label="Risk Raporu", lines=5)
|
| 510 |
+
outs = [out_pred, out_prob, out_risk]
|
| 511 |
+
btn.click(analyze_news, text_input, outs)
|
| 512 |
+
text_input.submit(analyze_news, text_input, outs)
|
| 513 |
+
gr.Examples(EXAMPLES, inputs=text_input)
|
| 514 |
+
|
| 515 |
+
# ---------------- TAB 2 ----------------
|
| 516 |
+
with gr.Tab("📚 Toplu Analiz"):
|
| 517 |
+
gr.Markdown(
|
| 518 |
+
"Her satıra bir başlık yazın **veya** CSV yükleyin "
|
| 519 |
+
"(kolonlar: `headline`, opsiyonel `date`, `company`). "
|
| 520 |
+
"Tarih varsa **trend** hesaplanır."
|
| 521 |
+
)
|
| 522 |
+
b_text = gr.Textbox(label="Başlıklar (satır başına bir haber)", lines=6,
|
| 523 |
+
placeholder=BATCH_PLACEHOLDER)
|
| 524 |
+
b_file = gr.File(label="CSV (opsiyonel)", file_types=[".csv"])
|
| 525 |
+
b_btn = gr.Button("🚀 Toplu Analiz", variant="primary")
|
| 526 |
+
gr.Markdown("### 🏢 Şirket Risk Tablosu (Underwriting / Rakip İstihbaratı)")
|
| 527 |
+
b_comp = gr.Dataframe(wrap=True)
|
| 528 |
+
gr.Markdown("### 📈 Sektör Konu Trendi")
|
| 529 |
+
b_topic = gr.Dataframe(wrap=True)
|
| 530 |
+
gr.Markdown("### 🗞️ Haber Detayı (riskten düşüğe)")
|
| 531 |
+
b_news = gr.Dataframe(wrap=True)
|
| 532 |
+
b_btn.click(batch_analyze, [b_text, b_file], [b_news, b_comp, b_topic])
|
| 533 |
+
|
| 534 |
+
# ---------------- TAB 3 ----------------
|
| 535 |
+
with gr.Tab("🌐 Canlı Tarama"):
|
| 536 |
+
gr.Markdown(
|
| 537 |
+
"Google News RSS'ten şirket haberlerini çekip otomatik analiz eder "
|
| 538 |
+
"(sunucunun internet erişimi gerekir)."
|
| 539 |
+
)
|
| 540 |
+
l_comp = gr.Textbox(label="Şirketler (virgülle)", value="Allianz, AXA, Zurich, Generali")
|
| 541 |
+
with gr.Row():
|
| 542 |
+
l_days = gr.Slider(7, 90, value=30, step=1, label="Son kaç gün")
|
| 543 |
+
l_limit = gr.Slider(5, 50, value=20, step=1, label="Şirket başına haber")
|
| 544 |
+
l_btn = gr.Button("🔎 Haberleri Tara", variant="primary")
|
| 545 |
+
gr.Markdown("### 🏢 Rakip Karşılaştırma")
|
| 546 |
+
l_compdf = gr.Dataframe(wrap=True)
|
| 547 |
+
gr.Markdown("### 📈 Konu Trendi")
|
| 548 |
+
l_topic = gr.Dataframe(wrap=True)
|
| 549 |
+
gr.Markdown("### 🗞️ Haberler")
|
| 550 |
+
l_news = gr.Dataframe(wrap=True)
|
| 551 |
+
l_btn.click(live_scan, [l_comp, l_days, l_limit], [l_news, l_compdf, l_topic])
|
| 552 |
+
|
| 553 |
+
gr.HTML('<div id="footer">🦅 <b>Goshawk_insurance_bert</b> · Karar destek aracıdır</div>')
|
| 554 |
+
gr.Markdown(
|
| 555 |
+
"**Not:** Risk skoru = %65 model negatif olasılığı + %35 anahtar-kelime risk sinyali. "
|
| 556 |
+
"Karar destek amaçlıdır; tek başına underwriting kararı için kullanılmamalıdır."
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
if __name__ == "__main__":
|
| 561 |
+
demo.launch(
|
| 562 |
+
server_name="0.0.0.0",
|
| 563 |
+
server_port=int(os.environ.get("PORT", 7860)),
|
| 564 |
+
show_error=True,
|
| 565 |
+
)
|