Bohndesliga Table Style
Contents
Bohndesliga Table Style#
Bohndesliga is a german online broadbast by Rocket Beans TV. In their show they discuss the recent games of the Bundesliga (in german). They have created a nice visual style to show the Bundesliga table that we are going to recreate with plottable (with a few details adjusted).
You can view an example of the table we are going to recreate at this timestamped video.
Imports#
%load_ext autoreload
%autoreload 2
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from plottable import ColDef, Table
from plottable.plots import image
Getting the data#
FIVETHIRTYEIGHT_URLS = {
"SPI_MATCHES": "https://projects.fivethirtyeight.com/soccer-api/club/spi_matches.csv",
"SPI_MATCHES_LATEST": "https://projects.fivethirtyeight.com/soccer-api/club/spi_matches_latest.csv",
}
df = pd.read_csv(FIVETHIRTYEIGHT_URLS["SPI_MATCHES_LATEST"])
bl = df.loc[df.league == "German Bundesliga"].dropna()
Data Processing#
def add_points(df: pd.DataFrame) -> pd.DataFrame:
df["pts_home"] = np.where(
df["score1"] > df["score2"], 3, np.where(df["score1"] == df["score2"], 1, 0)
)
df["pts_away"] = np.where(
df["score1"] < df["score2"], 3, np.where(df["score1"] == df["score2"], 1, 0)
)
return df
def get_wins_draws_losses(games: pd.DataFrame) -> pd.DataFrame:
return (
games.rename({"pts_home": "pts", "team1": "team"}, axis=1)
.groupby("team")["pts"]
.value_counts()
.add(
games.rename({"pts_away": "pts", "team2": "team"}, axis=1)
.groupby("team")["pts"]
.value_counts(),
fill_value=0,
)
.astype(int)
.rename("count")
.reset_index(level=1)
.pivot(columns="pts", values="count")
.rename({0: "L", 1: "D", 3: "W"}, axis=1)[["W", "D", "L"]]
)
bl = add_points(bl)
perform = (
bl.groupby("team1")[[
"pts_home",
"score1",
"score2",
"xg1",
"xg2",
]]
.sum()
.set_axis(
[
"pts",
"gf",
"ga",
"xgf",
"xga",
],
axis=1,
)
.add(
bl.groupby("team2")[[
"pts_away",
"score2",
"score1",
"xg2",
"xg1",
]]
.sum()
.set_axis(
[
"pts",
"gf",
"ga",
"xgf",
"xga",
],
axis=1,
)
)
)
perform.index.name = "team"
perform["gd"] = perform["gf"] - perform["ga"]
perform = perform[
[
"pts",
"gd",
"gf",
"ga",
"xgf",
"xga",
]
]
perform["games"] = bl.groupby("team1").size().add(bl.groupby("team2").size())
wins_draws_losses = get_wins_draws_losses(bl)
perform = pd.concat([perform, wins_draws_losses], axis=1)
perform
pts | gd | gf | ga | xgf | xga | games | W | D | L | |
---|---|---|---|---|---|---|---|---|---|---|
team | ||||||||||
1. FC Union Berlin | 27 | 7.0 | 23.0 | 16.0 | 13.85 | 14.29 | 14 | 8 | 3 | 3 |
Bayer Leverkusen | 18 | -1.0 | 25.0 | 26.0 | 22.98 | 23.41 | 15 | 5 | 3 | 7 |
Bayern Munich | 34 | 36.0 | 49.0 | 13.0 | 41.06 | 16.55 | 15 | 10 | 4 | 1 |
Borussia Dortmund | 25 | 4.0 | 25.0 | 21.0 | 26.56 | 18.72 | 15 | 8 | 1 | 6 |
Borussia Monchengladbach | 22 | 4.0 | 28.0 | 24.0 | 24.62 | 26.00 | 15 | 6 | 4 | 5 |
Eintracht Frankfurt | 26 | 8.0 | 31.0 | 23.0 | 24.69 | 18.11 | 14 | 8 | 2 | 4 |
FC Augsburg | 15 | -8.0 | 18.0 | 26.0 | 16.93 | 31.46 | 15 | 4 | 3 | 8 |
FC Cologne | 17 | -8.0 | 21.0 | 29.0 | 23.79 | 20.35 | 15 | 4 | 5 | 6 |
Hertha Berlin | 14 | -3.0 | 19.0 | 22.0 | 20.45 | 24.78 | 15 | 3 | 5 | 7 |
Mainz | 18 | -5.0 | 18.0 | 23.0 | 18.62 | 22.58 | 14 | 5 | 3 | 6 |
RB Leipzig | 28 | 9.0 | 30.0 | 21.0 | 30.96 | 17.72 | 15 | 8 | 4 | 3 |
SC Freiburg | 27 | 5.0 | 21.0 | 16.0 | 24.65 | 18.82 | 14 | 8 | 3 | 3 |
Schalke 04 | 9 | -19.0 | 13.0 | 32.0 | 18.39 | 26.38 | 15 | 2 | 3 | 10 |
TSG Hoffenheim | 18 | 0.0 | 22.0 | 22.0 | 24.01 | 23.31 | 15 | 5 | 3 | 7 |
VfB Stuttgart | 14 | -9.0 | 18.0 | 27.0 | 21.88 | 22.20 | 15 | 3 | 5 | 7 |
VfL Bochum | 13 | -22.0 | 14.0 | 36.0 | 15.39 | 34.90 | 15 | 4 | 1 | 10 |
VfL Wolfsburg | 23 | 4.0 | 24.0 | 20.0 | 20.12 | 24.34 | 15 | 6 | 5 | 4 |
Werder Bremen | 21 | -2.0 | 25.0 | 27.0 | 22.15 | 27.18 | 15 | 6 | 3 | 6 |
# mapping teamnames to logo paths
club_logo_path = Path("bundesliga_crests_22_23")
club_logo_files = list(club_logo_path.glob("*.png"))
club_logos_paths = {f.stem: f for f in club_logo_files}
perform = perform.reset_index()
# Add a column for crests
perform.insert(0, "crest", perform["team"])
perform["crest"] = perform["crest"].replace(club_logos_paths)
# sort by table standings
perform = perform.sort_values(by=["pts", "gd", "gf"], ascending=False)
for colname in ["gd", "gf", "ga"]:
perform[colname] = perform[colname].astype("int32")
perform["goal_difference"] = perform["gf"].astype(str) + ":" + perform["ga"].astype(str)
perform["rank"] = list(range(1, 19))
Building the Bohndesliga Table#
row_colors = {
"top4": "#2d3636",
"top6": "#516362",
"playoffs": "#8d9386",
"relegation": "#c8ab8d",
"even": "#627979",
"odd": "#68817e",
}
bg_color = row_colors["odd"]
text_color = "#e0e8df"
table_cols = ["crest", "team", "games", "W", "D", "L", "goal_difference", "gd", "pts"]
perform[table_cols]
crest | team | games | W | D | L | goal_difference | gd | pts | |
---|---|---|---|---|---|---|---|---|---|
2 | bundesliga_crests_22_23/Bayern Munich.png | Bayern Munich | 15 | 10 | 4 | 1 | 49:13 | 36 | 34 |
10 | bundesliga_crests_22_23/RB Leipzig.png | RB Leipzig | 15 | 8 | 4 | 3 | 30:21 | 9 | 28 |
0 | bundesliga_crests_22_23/1. FC Union Berlin.png | 1. FC Union Berlin | 14 | 8 | 3 | 3 | 23:16 | 7 | 27 |
11 | bundesliga_crests_22_23/SC Freiburg.png | SC Freiburg | 14 | 8 | 3 | 3 | 21:16 | 5 | 27 |
5 | bundesliga_crests_22_23/Eintracht Frankfurt.png | Eintracht Frankfurt | 14 | 8 | 2 | 4 | 31:23 | 8 | 26 |
3 | bundesliga_crests_22_23/Borussia Dortmund.png | Borussia Dortmund | 15 | 8 | 1 | 6 | 25:21 | 4 | 25 |
16 | bundesliga_crests_22_23/VfL Wolfsburg.png | VfL Wolfsburg | 15 | 6 | 5 | 4 | 24:20 | 4 | 23 |
4 | bundesliga_crests_22_23/Borussia Monchengladba... | Borussia Monchengladbach | 15 | 6 | 4 | 5 | 28:24 | 4 | 22 |
17 | bundesliga_crests_22_23/Werder Bremen.png | Werder Bremen | 15 | 6 | 3 | 6 | 25:27 | -2 | 21 |
13 | bundesliga_crests_22_23/TSG Hoffenheim.png | TSG Hoffenheim | 15 | 5 | 3 | 7 | 22:22 | 0 | 18 |
1 | bundesliga_crests_22_23/Bayer Leverkusen.png | Bayer Leverkusen | 15 | 5 | 3 | 7 | 25:26 | -1 | 18 |
9 | bundesliga_crests_22_23/Mainz.png | Mainz | 14 | 5 | 3 | 6 | 18:23 | -5 | 18 |
7 | bundesliga_crests_22_23/FC Cologne.png | FC Cologne | 15 | 4 | 5 | 6 | 21:29 | -8 | 17 |
6 | bundesliga_crests_22_23/FC Augsburg.png | FC Augsburg | 15 | 4 | 3 | 8 | 18:26 | -8 | 15 |
8 | bundesliga_crests_22_23/Hertha Berlin.png | Hertha Berlin | 15 | 3 | 5 | 7 | 19:22 | -3 | 14 |
14 | bundesliga_crests_22_23/VfB Stuttgart.png | VfB Stuttgart | 15 | 3 | 5 | 7 | 18:27 | -9 | 14 |
15 | bundesliga_crests_22_23/VfL Bochum.png | VfL Bochum | 15 | 4 | 1 | 10 | 14:36 | -22 | 13 |
12 | bundesliga_crests_22_23/Schalke 04.png | Schalke 04 | 15 | 2 | 3 | 10 | 13:32 | -19 | 9 |
Setting up the ColumnDefinitions#
With the ColumnDefinitions we we can adjust the tables style by supplying keywords such as title
, width
, formatters
and textprops
.
See also
You can view more details in the Using ColumnDefinition Notebook.
table_col_defs = [
ColDef("rank", width=0.5, title=""),
ColDef("crest", width=0.35, plot_fn=image, title=""),
ColDef("team", width=2.5, title="", textprops={"ha": "left"}),
ColDef("games", width=0.5, title="Games"),
ColDef("W", width=0.5),
ColDef("D", width=0.5),
ColDef("L", width=0.5),
ColDef("goal_difference", title="Goals"),
ColDef("gd", width=0.5, title="", formatter="{:+}"),
ColDef("pts", border="left", title="Points"),
]
Plotting the Table#
See also
Here we use various keywords of Table
to control its appearance. You can view more details in the Styling a Table Notebook.
fig, ax = plt.subplots(figsize=(14, 12))
plt.rcParams["text.color"] = text_color
plt.rcParams["font.family"] = "Roboto"
fig.set_facecolor(bg_color)
ax.set_facecolor(bg_color)
table = Table(
perform,
column_definitions=table_col_defs,
row_dividers=True,
col_label_divider=False,
footer_divider=True,
index_col="rank",
columns=table_cols,
even_row_color=row_colors["even"],
footer_divider_kw={"color": bg_color, "lw": 2},
row_divider_kw={"color": bg_color, "lw": 2},
column_border_kw={"color": bg_color, "lw": 2},
textprops={"fontsize": 16, "ha": "center", "fontname": "Roboto"},
)
for idx in [0, 1, 2, 3]:
table.rows[idx].set_facecolor(row_colors["top4"])
for idx in [4, 5]:
table.rows[idx].set_facecolor(row_colors["top6"])
table.rows[15].set_facecolor(row_colors["playoffs"])
for idx in [16, 17]:
table.rows[idx].set_facecolor(row_colors["relegation"])
table.rows[idx].set_fontcolor(row_colors["top4"])
fig.savefig(
"images/bohndesliga_table_recreation.png",
facecolor=fig.get_facecolor(),
dpi=200,
)
Note on the slightly adjusted details:
I chose to make the light text a little lighter than the original and used the background color of the top4 rows as the fontcolor for the bottom two rows because I think the higher contrast makes it a little bit easier to read.
I also chose to stick to the light text on the background for the header row to keep this consistent.
Finally I added a little clarity by adding titles to the points and goal difference columns. This also allowed me to not highlight the points additionally with a bold weight.