This commit is contained in:
John Gatward
2026-03-19 21:07:16 +00:00
commit a69ef51825
18 changed files with 1000 additions and 0 deletions

257
src/app.py Normal file
View File

@@ -0,0 +1,257 @@
from flask import Flask, render_template
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import statsmodels.api as sm
import numpy as np
import datetime
from sklearn.linear_model import LinearRegression
import json
from stats import generate_stats
from player_table import generate_player_table
import constants
app = Flask(__name__)
def get_data_frame(filename):
df = pd.read_csv(filename)
df["Date"] = pd.to_datetime(df["Date"], dayfirst=True)
df = df.sort_values("Date").reset_index(drop=True)
return df
def build_hovertext(df, attendance_columns):
return df[attendance_columns].apply(
lambda row: ", ".join(
[
player
for player in attendance_columns
if row[player] == 1
]
) or "No attendance",
axis=1
)
def generate_weekly_attendance_calendar(df):
# Compute ISO year/week and attendance
df["Year"] = df["Date"].dt.isocalendar().year
df["Week"] = df["Date"].dt.isocalendar().week
attendee_columns = [
col for col in df.columns if col not in {
"Date", "Relative Position", "Number of Players",
"Number of Teams", "Attendees", "Year", "Week", "Year-Week"
}
]
df["Attended"] = df[attendee_columns].sum(axis=1) > 0
weekly_attendance = df.groupby(["Year", "Week"])[
"Attended"].any().astype(int).reset_index()
# Build full year/week grid
all_years = sorted(df["Year"].unique())
all_weeks = list(range(1, 53))
grid = []
for year in all_years:
for week in all_weeks:
grid.append({"Year": year, "Week": week})
calendar = pd.DataFrame(grid)
calendar = calendar.merge(weekly_attendance, on=[
"Year", "Week"], how="left").fillna(0)
calendar["Attended"] = calendar["Attended"].astype(int)
# Plot
fig = go.Figure(data=go.Heatmap(
x=calendar["Week"],
y=calendar["Year"],
z=calendar["Attended"],
colorscale=constants.ATTENDANCE_COLORSCHEME,
zmin=0,
zmax=1,
showscale=False
))
fig.update_layout(
title="Pub Quiz Attendance Calendar (Weekly)",
xaxis_title="Week Number",
yaxis_title="Year",
xaxis=dict(tickmode="linear", dtick=4),
template="plotly_white",
height=180 + len(all_years) * 40
)
return fig
def generate_relative_position_over_time(df):
df["Date_ordinal"] = df["Date"].map(pd.Timestamp.toordinal)
X = sm.add_constant(df["Date_ordinal"])
y = df["Relative Position"]
model = sm.OLS(y, X).fit()
df["BestFit"] = model.predict(X)
intercept = model.params["const"]
slope = model.params["Date_ordinal"]
target_value = 0.08
predicted_ordinal = (target_value - intercept) / slope
min_ordinal = df["Date_ordinal"].min()
max_ordinal = df["Date_ordinal"].max()
if predicted_ordinal > max_ordinal:
extended_ordinals = np.linspace(min_ordinal, predicted_ordinal, 100)
else:
extended_ordinals = np.linspace(min_ordinal, max_ordinal, 100)
extended_bestfit = intercept + slope * extended_ordinals
extended_dates = [datetime.date.fromordinal(
int(x)) for x in extended_ordinals]
df["Attendees"] = build_hovertext(df, constants.PLAYER_NAME_COLUMNS)
fig = px.line(
df,
x="Date",
y="Relative Position",
title="Quiz Position Over Time with Extended Trendline",
hover_data={"Attendees": True}
)
fig.add_scatter(
x=extended_dates,
y=extended_bestfit,
mode="lines",
name="Extended Trendline",
line=dict(dash="dot", color="red")
)
fig.update_yaxes(range=[0, 1], tickformat=".2f")
return fig
def generate_visualisations(df):
feature_columns = [
col
for col in df.columns
if col in constants.FEATURE_COLUMNS
]
x = df[feature_columns]
y = df["Relative Position"]
model = LinearRegression()
model.fit(x, y)
plots = {}
plots["relative_pos_over_time"] = json.dumps(
generate_relative_position_over_time(df),
cls=plotly.utils.PlotlyJSONEncoder
)
df_line = df.melt(
id_vars="Date",
value_vars=["Absolute Position", "Number of Teams"],
var_name="Metric",
value_name="Value"
)
fig11 = px.line(
df_line,
x='Date',
y='Value',
color='Metric',
title='Absolute Position and Total Number of Teams Over Time'
)
plots["absolute_pos_over_time"] = json.dumps(
fig11, cls=plotly.utils.PlotlyJSONEncoder
)
# 2. Number of players vs position with regression line
fig2 = px.scatter(
df,
x="Number of Players",
y="Relative Position",
trendline="ols",
title="Players vs Position (%)",
)
fig2.update_xaxes(dtick=1)
plots["players_vs_position"] = json.dumps(
fig2, cls=plotly.utils.PlotlyJSONEncoder)
# 3. Player participation heatmap
df_players = df[constants.PLAYER_NAME_COLUMNS]
fig3 = px.imshow(
df_players.T,
labels=dict(x="Games", y="Player", color="Present"),
title="Player Participation Heatmap",
color_continuous_scale=constants.ATTENDANCE_COLORSCHEME,
zmin=0,
zmax=1,
aspect="auto"
)
fig3.update_coloraxes(
colorbar=dict(
tickvals=[0, 1],
ticktext=["Absent", "Present"],
lenmode="pixels",
len=300,
)
)
fig3.update_layout(
template="seaborn",
height=600,
yaxis=dict(
tickmode="array",
tickvals=list(range(len(df_players.columns))),
ticktext=df_players.columns
)
)
plots["player_participation"] = json.dumps(
fig3, cls=plotly.utils.PlotlyJSONEncoder)
# 4. Calendar view
plots["calendar"] = json.dumps(
generate_weekly_attendance_calendar(df),
cls=plotly.utils.PlotlyJSONEncoder
)
# 5. Coefficient bar chart
coefficients = pd.Series(model.coef_, index=x.columns).sort_values()
fig5 = px.bar(
coefficients,
orientation="h",
labels={"value": "Coefficient", "index": "Feature"},
title="Linear Regression Coefficients",
)
plots["coefficients"] = json.dumps(
fig5, cls=plotly.utils.PlotlyJSONEncoder)
return plots
@app.route("/")
def index():
df = get_data_frame("data.csv")
stats = generate_stats(df)
player_table = generate_player_table(df)
plots = generate_visualisations(df)
return render_template(
"index.html",
plots=plots,
stats=stats,
player_table=player_table
)
if __name__ == "__main__":
import plotly
app.run(debug=True)