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,
)
../_images/e8d1fbc8adad079e0f4b9854873506539bd580747d14f617ec24b9eb329aea87.png

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.