Spaces:
Sleeping
Sleeping
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 | |
| 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 βββββββββββββββββββββββββββββββββββββββββ | |
| 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" | |
| ) |