gajjukhan's picture
Upload 3 files
299934e verified
import streamlit as st
import pandas as pd
# ── Page Config ──
st.set_page_config(
page_title="Race Results Explorer",
page_icon="πŸƒ",
layout="wide",
initial_sidebar_state="expanded",
)
# ── Custom CSS ──
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@700&display=swap');
.stApp {
background: linear-gradient(145deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
}
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
}
.main-title {
font-family: 'Space Mono', monospace;
font-size: 2.4rem;
font-weight: 700;
background: linear-gradient(90deg, #f97316, #fb923c, #fbbf24);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
margin-bottom: 0;
letter-spacing: -1px;
}
.sub-title {
text-align: center;
color: #94a3b8;
font-size: 0.95rem;
margin-top: 0;
}
.metric-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 16px 20px;
text-align: center;
}
.metric-val {
font-family: 'Space Mono', monospace;
font-size: 1.8rem;
font-weight: 700;
}
.metric-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
letter-spacing: 2px;
margin-top: 2px;
}
.sidebar-header {
font-family: 'Space Mono', monospace;
font-size: 1.1rem;
color: #f97316;
letter-spacing: 1px;
margin-bottom: 8px;
}
div[data-testid="stDataFrame"] {
border-radius: 12px;
overflow: hidden;
}
</style>
""", unsafe_allow_html=True)
# ── Data Loading ──
@st.cache_data
def load_data():
df = pd.read_csv("new_data_01__2_.csv")
# Drop the junk row at the bottom (non-numeric Place)
df = df[pd.to_numeric(df["Place"], errors="coerce").notna()].copy()
# Clean columns
df["Place"] = df["Place"].astype(int)
df["Bib"] = pd.to_numeric(df["Bib"], errors="coerce").astype("Int64")
df["Gender"] = df["Gender"].str.strip().str.upper()
df["City"] = df["City"].fillna("NOT SPECIFIED").str.strip().str.upper()
df["State"] = df["State"].fillna("NOT SPECIFIED").str.strip().str.upper()
df["Team"] = df["Team"].fillna("").str.strip()
df["Time"] = df["Time"].astype(str).str.strip()
df["Gun Time"] = df["Gun Time"].astype(str).str.strip()
# Keep only valid genders
df = df[df["Gender"].isin(["M", "F"])].copy()
# Drop unnamed index column if present
if "Unnamed: 0" in df.columns:
df.drop(columns=["Unnamed: 0"], inplace=True)
return df
df = load_data()
# ── Helper: parse time string to total seconds for filtering ──
def time_to_seconds(t):
try:
parts = str(t).split(":")
if len(parts) == 3:
h, m, s = parts
return int(h) * 3600 + int(m) * 60 + int(s)
elif len(parts) == 2:
m, s = parts
return int(m) * 60 + int(s)
except Exception:
return None
return None
df["_time_sec"] = df["Time"].apply(time_to_seconds)
df["_gun_sec"] = df["Gun Time"].apply(time_to_seconds)
min_time = int(df["_time_sec"].min()) if df["_time_sec"].notna().any() else 0
max_time = int(df["_time_sec"].max()) if df["_time_sec"].notna().any() else 7200
min_gun = int(df["_gun_sec"].min()) if df["_gun_sec"].notna().any() else 0
max_gun = int(df["_gun_sec"].max()) if df["_gun_sec"].notna().any() else 7200
def seconds_to_label(s):
h = s // 3600
m = (s % 3600) // 60
sec = s % 60
if h > 0:
return f"{h}:{m:02d}:{sec:02d}"
return f"{m}:{sec:02d}"
# ══════════════════════════════════════
# SIDEBAR – FILTER CHAMBER
# ══════════════════════════════════════
with st.sidebar:
st.markdown('<div class="sidebar-header">🎯 FILTER CHAMBER</div>', unsafe_allow_html=True)
st.markdown("---")
# ── 1. Gender ──
st.markdown("##### πŸ‘€ Gender")
gender_opts = ["All"] + sorted(df["Gender"].unique().tolist())
gender_sel = st.selectbox("Select Gender", gender_opts, index=0, label_visibility="collapsed")
# ── 2. City ──
st.markdown("##### πŸ™οΈ City")
city_list = sorted(df["City"].unique().tolist())
city_sel = st.multiselect("Select City", city_list, default=[], placeholder="All cities")
# ── 3. State ──
st.markdown("##### πŸ“ State")
state_list = sorted(df["State"].unique().tolist())
state_sel = st.multiselect("Select State", state_list, default=[], placeholder="All states")
# ── 4. Team ──
st.markdown("##### πŸ… Team")
team_mode = st.radio("Team filter mode", ["All Runners", "Team Members Only", "Independent Only", "Specific Team"], index=0, label_visibility="collapsed")
specific_team = None
if team_mode == "Specific Team":
team_list = sorted([t for t in df["Team"].unique() if t])
specific_team = st.selectbox("Choose team", team_list, label_visibility="collapsed")
st.markdown("---")
# ── 5. Chip Time Range ──
st.markdown("##### ⏱️ Chip Time Range")
time_range = st.slider(
"Chip Time",
min_value=min_time,
max_value=max_time,
value=(min_time, max_time),
step=60,
format="%d sec",
label_visibility="collapsed",
)
st.caption(f"From **{seconds_to_label(time_range[0])}** to **{seconds_to_label(time_range[1])}**")
# ── 6. Gun Time Range ──
st.markdown("##### πŸ”« Gun Time Range")
gun_range = st.slider(
"Gun Time",
min_value=min_gun,
max_value=max_gun,
value=(min_gun, max_gun),
step=60,
format="%d sec",
label_visibility="collapsed",
)
st.caption(f"From **{seconds_to_label(gun_range[0])}** to **{seconds_to_label(gun_range[1])}**")
st.markdown("---")
# ── 7. Name Search ──
st.markdown("##### πŸ” Search by Name")
name_search = st.text_input("Name", placeholder="e.g. JARED WILSON", label_visibility="collapsed")
# ── Reset ──
if st.button("πŸ”„ Reset All Filters", use_container_width=True):
st.rerun()
# ══════════════════════════════════════
# APPLY FILTERS
# ══════════════════════════════════════
filtered = df.copy()
# Gender
if gender_sel != "All":
filtered = filtered[filtered["Gender"] == gender_sel]
# City
if city_sel:
filtered = filtered[filtered["City"].isin(city_sel)]
# State
if state_sel:
filtered = filtered[filtered["State"].isin(state_sel)]
# Team
if team_mode == "Team Members Only":
filtered = filtered[filtered["Team"] != ""]
elif team_mode == "Independent Only":
filtered = filtered[filtered["Team"] == ""]
elif team_mode == "Specific Team" and specific_team:
filtered = filtered[filtered["Team"] == specific_team]
# Chip Time
filtered = filtered[
(filtered["_time_sec"] >= time_range[0]) & (filtered["_time_sec"] <= time_range[1])
| filtered["_time_sec"].isna()
]
# Gun Time
filtered = filtered[
(filtered["_gun_sec"] >= gun_range[0]) & (filtered["_gun_sec"] <= gun_range[1])
| filtered["_gun_sec"].isna()
]
# Name
if name_search:
filtered = filtered[filtered["Name"].str.contains(name_search.upper(), case=False, na=False)]
# ══════════════════════════════════════
# MAIN CONTENT
# ══════════════════════════════════════
st.markdown('<h1 class="main-title">πŸƒ RACE RESULTS EXPLORER</h1>', unsafe_allow_html=True)
st.markdown(f'<p class="sub-title">Showing <strong>{len(filtered)}</strong> of <strong>{len(df)}</strong> runners</p>', unsafe_allow_html=True)
# ── Metric Cards ──
c1, c2, c3, c4, c5 = st.columns(5)
with c1:
st.markdown(f"""
<div class="metric-card">
<div class="metric-val" style="color:#f97316">{len(filtered)}</div>
<div class="metric-label">Total</div>
</div>""", unsafe_allow_html=True)
with c2:
m_count = len(filtered[filtered["Gender"] == "M"])
st.markdown(f"""
<div class="metric-card">
<div class="metric-val" style="color:#3b82f6">{m_count}</div>
<div class="metric-label">Male</div>
</div>""", unsafe_allow_html=True)
with c3:
f_count = len(filtered[filtered["Gender"] == "F"])
st.markdown(f"""
<div class="metric-card">
<div class="metric-val" style="color:#ec4899">{f_count}</div>
<div class="metric-label">Female</div>
</div>""", unsafe_allow_html=True)
with c4:
team_count = filtered[filtered["Team"] != ""]["Team"].nunique()
st.markdown(f"""
<div class="metric-card">
<div class="metric-val" style="color:#10b981">{team_count}</div>
<div class="metric-label">Teams</div>
</div>""", unsafe_allow_html=True)
with c5:
state_count = filtered["State"].nunique()
st.markdown(f"""
<div class="metric-card">
<div class="metric-val" style="color:#a78bfa">{state_count}</div>
<div class="metric-label">States</div>
</div>""", unsafe_allow_html=True)
st.markdown("")
# ── Data Table ──
display_cols = ["Place", "Bib", "Name", "Gender", "City", "State", "Time", "Gun Time", "Team"]
display_df = filtered[display_cols].reset_index(drop=True)
st.dataframe(
display_df,
use_container_width=True,
height=520,
column_config={
"Place": st.column_config.NumberColumn("πŸ† Place", width="small"),
"Bib": st.column_config.NumberColumn("πŸ”’ Bib", width="small"),
"Name": st.column_config.TextColumn("πŸ‘€ Name", width="medium"),
"Gender": st.column_config.TextColumn("⚧ Gender", width="small"),
"City": st.column_config.TextColumn("πŸ™οΈ City", width="medium"),
"State": st.column_config.TextColumn("πŸ“ State", width="small"),
"Time": st.column_config.TextColumn("⏱️ Chip Time", width="small"),
"Gun Time": st.column_config.TextColumn("πŸ”« Gun Time", width="small"),
"Team": st.column_config.TextColumn("πŸ… Team", width="medium"),
},
)
# ── Charts Row ──
st.markdown("")
st.markdown("### πŸ“Š Distribution Insights")
tab1, tab2, tab3, tab4 = st.tabs(["Gender Split", "Top Cities", "Top Teams", "Time Distribution"])
with tab1:
gender_counts = filtered["Gender"].value_counts().reset_index()
gender_counts.columns = ["Gender", "Count"]
gender_counts["Gender"] = gender_counts["Gender"].map({"M": "Male", "F": "Female"})
st.bar_chart(gender_counts.set_index("Gender"), color="#f97316", horizontal=True)
with tab2:
city_counts = filtered["City"].value_counts().head(10).reset_index()
city_counts.columns = ["City", "Count"]
st.bar_chart(city_counts.set_index("City"), color="#3b82f6")
with tab3:
team_data = filtered[filtered["Team"] != ""]["Team"].value_counts().reset_index()
team_data.columns = ["Team", "Count"]
if not team_data.empty:
st.bar_chart(team_data.set_index("Team"), color="#10b981")
else:
st.info("No team members in current filter.")
with tab4:
time_data = filtered[filtered["_time_sec"].notna()].copy()
if not time_data.empty:
time_data["Minutes"] = time_data["_time_sec"] / 60
st.bar_chart(
time_data["Minutes"].value_counts(bins=20).sort_index().reset_index().rename(
columns={"index": "Time Bin", "count": "Runners"}
).set_index("Time Bin"),
color="#fbbf24",
)
else:
st.info("No time data available for current filter.")
# ── Download ──
st.markdown("---")
col_dl1, col_dl2, _ = st.columns([1, 1, 3])
with col_dl1:
csv_out = display_df.to_csv(index=False).encode("utf-8")
st.download_button("πŸ“₯ Download Filtered CSV", csv_out, "filtered_results.csv", "text/csv", use_container_width=True)
with col_dl2:
st.download_button("πŸ“₯ Download Full CSV", df[display_cols].to_csv(index=False).encode("utf-8"), "full_results.csv", "text/csv", use_container_width=True)
st.markdown("""
<div style="text-align:center; color:#475569; font-size:0.8rem; margin-top:24px;">
Built with Streamlit β€’ Race Results Explorer
</div>
""", unsafe_allow_html=True)