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(""" """, 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('', 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('

🏃 RACE RESULTS EXPLORER

', unsafe_allow_html=True) st.markdown(f'

Showing {len(filtered)} of {len(df)} runners

', unsafe_allow_html=True) # ── Metric Cards ── c1, c2, c3, c4, c5 = st.columns(5) with c1: st.markdown(f"""
{len(filtered)}
Total
""", unsafe_allow_html=True) with c2: m_count = len(filtered[filtered["Gender"] == "M"]) st.markdown(f"""
{m_count}
Male
""", unsafe_allow_html=True) with c3: f_count = len(filtered[filtered["Gender"] == "F"]) st.markdown(f"""
{f_count}
Female
""", unsafe_allow_html=True) with c4: team_count = filtered[filtered["Team"] != ""]["Team"].nunique() st.markdown(f"""
{team_count}
Teams
""", unsafe_allow_html=True) with c5: state_count = filtered["State"].nunique() st.markdown(f"""
{state_count}
States
""", 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("""
Built with Streamlit • Race Results Explorer
""", unsafe_allow_html=True)