Show code
import os
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit
from collections import defaultdictFor the 60M in North America who participate in weekly, head-to-head fantasy football leagues, a certain genre of arguments rages weekly. The team that boasts the week’s second-highest score, but draws the short straw in facing the week’s juggernaut righly considers themselves ‘unlucky.’ Conversely, the team posting the week’s second-lowest score, but emerging victorious improbably by virtue of matching up with the week’s poorest offering has seen the fates smile upon them.
When these outcomes occur, the justifications, rationalizations, recriminations, and kernels of multi-year grudges begin heating. Or, as a famously narcissistic wide receiver once said, “get your popcorn ready!” Well, we want to put an end to that (the arguments, not the pontifications of narcissistic wide receivers, those are just too much fun!).
Read more about methods, philosophies, and inappropriate comments here.
The rise in popularity of daily fantasy football leagues exists, in part, because it allows thousands to compete, rather than 10-12. However, in so doing, the frenetic drafts and auctions, followed by season-long management disappears. A league of 1,000 cannot exist in a season-long format - there simply aren’t enough players roaming real-life gridirons on Sunday afternoon! However, given a standard league size and scoring system, and given some baseline managerial competence, we form divisions that all can compete for a title. The “real standings” tell a story independent of opponent. The scale of daily fantasy, the thrill of season-long drafts, auctions, and management!
| Points | Team | Win | Loss | |
|---|---|---|---|---|
| 0 | 170.76 | AgencyIncreasers | 1 | 0 |
| 1 | 165.76 | Burninators | 1 | 0 |
| 2 | 159.48 | Cantakerouses | 1 | 0 |
| 3 | 158.82 | Doomscrolls | 1 | 0 |
| 4 | 158.56 | Exorcists | 1 | 0 |
| ... | ... | ... | ... | ... |
| 379 | 103.44 | Hackathoners | 1 | 0 |
| 380 | 94.04 | Instigators | 0 | 1 |
| 381 | 50.54 | JQueries | 0 | 0 |
| 382 | 81.26 | Kodachromes | 0 | 1 |
| 383 | 125.28 | LowlyLlamas | 1 | 0 |
384 rows × 4 columns
def generate_standings(ffb_games):
"""Given an object containing team_names and game scores, run the sigmoid
model and return the binary and expected number of wins and losses for each roster"""
real_standings = defaultdict(list)
for team in pd.unique(ffb_games.Team):
real_standings['team'].append(team)
team_games = ffb_games[ffb_games.Team == team]
real_wins = np.sum(team_games.real_wins)
real_losses = len(team_games) - real_wins
wins = np.sum(team_games.Win)
losses = np.sum(team_games.Loss)
real_standings['real_wins'].append(real_wins)
real_standings['real_losses'].append(real_losses)
real_standings['wins'].append(wins)
real_standings['losses'].append(losses)
return pd.DataFrame(real_standings)def visualize_luck(standings):
"""Given an object containing team names and calculated luck values, generate a bar chart,
sorting from most to least lucky"""
fig = px.bar(standings, x='team', y='luck', color='luck',
color_continuous_scale=px.colors.sequential.Bluered[::-1],
labels={
"luck": "Wins Above Expectation (Luck)",
"team": "Rosters (Sorted from Luckiest to Unluckiest)"
},
title='Did you get lucky (well did you?)')
fig.update_coloraxes(showscale=False)
fig.show()| team | real_wins | real_losses | wins | losses | luck | |
|---|---|---|---|---|---|---|
| 0 | AgencyIncreasers | 18.92 | 13.08 | 18 | 12 | -0.92 |
| 1 | Burninators | 18.58 | 13.42 | 18 | 13 | -0.58 |
| 4 | Exorcists | 18.46 | 13.54 | 17 | 13 | -1.46 |
| 6 | GoodTrippers | 18.41 | 13.59 | 17 | 14 | -1.41 |
| 11 | LowlyLlamas | 16.12 | 15.88 | 13 | 18 | -3.12 |
| 8 | Instigators | 15.25 | 16.75 | 15 | 16 | -0.25 |
| 2 | Cantakerouses | 15.05 | 16.95 | 17 | 14 | 1.95 |
| 5 | Flagella | 14.48 | 17.52 | 17 | 14 | 2.52 |
| 3 | Doomscrolls | 14.45 | 17.55 | 14 | 15 | -0.45 |
| 10 | Kodachromes | 13.12 | 18.88 | 13 | 19 | -0.12 |
| 9 | JQueries | 11.37 | 20.63 | 12 | 19 | 0.63 |
| 7 | Hackathoners | 10.71 | 21.29 | 13 | 18 | 2.29 |
# Build figure
fig = go.Figure()
# Add scatter trace with medium sized markers
fig.add_trace(
go.Scatter(
mode='markers',
x=wins.Points,
y=wins.Win,
marker=dict(
color='Blue',
size=10,
opacity=0.2,
line=dict(
color='DarkBlue',
width=1
)
),
showlegend=False
)
)
# Add trace with large markers
fig.add_trace(
go.Scatter(
mode='markers',
x=losses.Points,
y=[0 for p in losses.Points],
marker=dict(
color='Red',
size=10,
opacity=0.2,
line=dict(
color='DarkRed',
width=1
)
),
showlegend=False
)
)
# Add trace with large markers
fig.add_trace(
go.Scatter(
x=xs,
y=sigmoid_ests,
mode='lines',
showlegend=False
)
)
fig.layout.update(title='Causes of Arguments and Insults', hovermode= 'closest',
xaxis = dict(title= 'Points Scored', range=[30,180], zeroline= False),
yaxis = dict(title= 'Win/Loss'))
fig.show()