bk1210's picture
Create app.py
5514350 verified
Raw
History Blame Contribute Delete
9.32 kB
import streamlit as st
import pandas as pd
import numpy as np
import faiss
import shap
import os
from sklearn.linear_model import LinearRegression
from sentence_transformers import SentenceTransformer
from groq import Groq
st.set_page_config(page_title="⚽ Explainable Match Summaries", page_icon="⚽", layout="centered")
# ── HuggingFace API Token ─────────────────────────────────────
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") or os.environ.get("Token", "")
# ── Load Data ─────────────────────────────────────────────────
@st.cache_data
def load_data():
df = pd.read_excel("ucl_stats.xlsx")
df = df.dropna(subset=["Home Team", "Away Team", "Home Team Goals", "Away Team Goals"])
df["Home Team Goals"] = pd.to_numeric(df["Home Team Goals"], errors="coerce").fillna(0)
df["Away Team Goals"] = pd.to_numeric(df["Away Team Goals"], errors="coerce").fillna(0)
def outcome(row):
if row["Home Team Goals"] > row["Away Team Goals"]: return "Home Win"
elif row["Home Team Goals"] < row["Away Team Goals"]: return "Away Win"
return "Draw"
df["Outcome"] = df.apply(outcome, axis=1)
return df
@st.cache_data
def build_documents(df):
docs = []
for _, row in df.iterrows():
try:
home = row["Home Team"]
away = row["Away Team"]
hg = int(row["Home Team Goals"])
ag = int(row["Away Team Goals"])
phase = row.get("Phase", "UCL 2025")
winner = row.get("Winner", "Unknown")
# optional stats
def safe(col): return row[col] if col in df.columns and pd.notna(row.get(col)) else None
shots_h = safe("Home Team Total shots attempts")
shots_a = safe("Away Team Total shots attempts")
poss_col = next((c for c in df.columns if "Possession" in c and "Home" in c), None)
poss_h = safe(poss_col) if poss_col else None
corners_h = safe("Home Corners taken")
corners_a = safe("Away Corners taken")
parts = [f"{phase}: {home} vs {away} β€” Final score {hg}-{ag}. Winner: {winner}."]
if shots_h and shots_a:
parts.append(f"{home} had {int(shots_h)} shots; {away} had {int(shots_a)} shots.")
if poss_h:
parts.append(f"{home} ball possession: {poss_h}%.")
if corners_h and corners_a:
parts.append(f"Corners: {home} {int(corners_h)}, {away} {int(corners_a)}.")
docs.append(" ".join(parts))
except Exception:
continue
return docs
# ── Embedding + FAISS ─────────────────────────────────────────
@st.cache_resource
def build_index(docs):
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(docs, show_progress_bar=False)
idx = faiss.IndexFlatL2(embeddings.shape[1])
idx.add(np.array(embeddings))
return model, idx, embeddings
def retrieve(query, model, index, docs, top_k=3):
q_emb = model.encode([query])
_, indices = index.search(np.array(q_emb), top_k)
return [docs[i] for i in indices[0]]
# ── LLM via HuggingFace API ───────────────────────────────────
def generate_summary(query, evidence, groq_key):
evidence_text = "\n".join([f"- {e}" for e in evidence])
try:
client = Groq(api_key=groq_key)
response = client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{
"role": "system",
"content": "You are a UEFA Champions League analyst. Generate concise factual match summaries using ONLY the evidence provided. Do NOT invent facts. Keep it under 100 words."
},
{
"role": "user",
"content": f"QUERY: {query}\n\nEVIDENCE:\n{evidence_text}\n\nWrite a concise factual summary:"
}
],
max_tokens=150,
temperature=0.2,
)
return response.choices[0].message.content.strip()
except Exception as e:
return f"⚠️ API Error: {str(e)}"
# ── SHAP Explainability ───────────────────────────────────────
def compute_shap(df, home_team, away_team):
try:
shap_df = pd.DataFrame({
"goals_scored": pd.to_numeric(df["Home Team Goals"], errors="coerce").fillna(0),
"goals_conceded": pd.to_numeric(df["Away Team Goals"], errors="coerce").fillna(0),
})
shap_df["goal_difference"] = shap_df["goals_scored"] - shap_df["goals_conceded"]
shap_df["is_win"] = (shap_df["goal_difference"] > 0).astype(int)
shap_df = shap_df.dropna()
X = shap_df[["goals_scored", "goals_conceded", "goal_difference", "is_win"]]
y = shap_df["goal_difference"]
model = LinearRegression()
model.fit(X, y)
explainer = shap.LinearExplainer(model, X)
shap_values = explainer.shap_values(X)
mean_shap = np.abs(shap_values).mean(axis=0)
return dict(zip(X.columns, mean_shap))
except Exception:
return {}
# ── UI ────────────────────────────────────────────────────────
st.title("⚽ Explainable UCL Match Summaries")
st.markdown(
"RAG + **Mistral-7B** + **SHAP** explainability on the self-curated "
"UCL 2025 dataset (189 matches). Grounded summaries β€” no hallucination."
)
st.divider()
# Load
with st.spinner("Loading UCL 2025 dataset..."):
df = load_data()
docs = build_documents(df)
with st.spinner("Building FAISS index with Sentence-BERT..."):
emb_model, faiss_index, _ = build_index(docs)
st.success(f"βœ… {len(docs)} match records indexed | {len(df['Home Team'].unique())} teams")
# Token input
st.subheader("πŸ”‘ Groq API Key")
if GROQ_API_KEY:
st.info("βœ… Groq API key loaded from Space secrets (GROQ_API_KEY)")
token = GROQ_API_KEY
else:
token = st.text_input(
"Enter your Groq API key (free at console.groq.com):",
type="password",
placeholder="gsk_..."
)
st.divider()
# Query input
st.subheader("πŸ” Ask About a Match")
teams = sorted(set(df["Home Team"].dropna()) | set(df["Away Team"].dropna()))
col1, col2 = st.columns(2)
with col1:
team1 = st.selectbox("Team 1", teams, index=teams.index("Real Madrid") if "Real Madrid" in teams else 0)
with col2:
team2 = st.selectbox("Team 2", teams, index=teams.index("Liverpool") if "Liverpool" in teams else 1)
query_type = st.selectbox("Query type", [
"Match summary",
"Who won?",
"Goals and shots analysis",
"Possession and corners breakdown",
])
query_map = {
"Match summary": f"Summarize the match between {team1} and {team2}",
"Who won?": f"Who won the match between {team1} and {team2} and by how many goals?",
"Goals and shots analysis": f"Analyze the goals and shots for {team1} vs {team2}",
"Possession and corners breakdown": f"Describe the possession and corners for {team1} vs {team2}",
}
custom_query = st.text_input("Or type your own query:", placeholder=f"e.g. How did {team1} perform against {team2}?")
final_query = custom_query if custom_query.strip() else query_map[query_type]
if st.button("πŸš€ Generate Summary", type="primary"):
if not token:
st.error("❌ Please enter your Groq API key above!")
else:
with st.spinner("πŸ” Retrieving evidence from FAISS..."):
evidence = retrieve(final_query, emb_model, faiss_index, docs, top_k=3)
with st.spinner("πŸ€– Generating summary with Mistral-7B..."):
summary = generate_summary(final_query, evidence, token)
st.divider()
st.subheader("πŸ“‹ Generated Summary")
st.success(summary)
st.subheader("πŸ“š Retrieved Evidence (RAG)")
for i, ev in enumerate(evidence, 1):
st.info(f"**Evidence {i}:** {ev}")
# SHAP
st.subheader("πŸ” SHAP Feature Importance")
shap_scores = compute_shap(df, team1, team2)
if shap_scores:
shap_df_display = pd.DataFrame({
"Feature": list(shap_scores.keys()),
"SHAP Value": [round(v, 4) for v in shap_scores.values()]
}).sort_values("SHAP Value", ascending=False)
st.bar_chart(shap_df_display.set_index("Feature"))
st.caption("SHAP values show which features most influenced the match outcome prediction.")
else:
st.warning("SHAP computation unavailable for this query.")
st.divider()
st.markdown(
"Built by **Bharath Kesav R** Β· "
"[GitHub](https://github.com/bk1210) Β· "
"[Portfolio](https://bk1210.github.io/portfolio) Β· "
"Model: Mistral-7B-Instruct via HuggingFace API Β· RAG: Sentence-BERT + FAISS"
)