The EPL’s World Cup Footprint

sports
football
data-viz
world-cup
premier-league
The Premier League supplied more World Cup minutes than any other league. This post breaks that down by club: which EPL sides sent the most players, where those players came from internationally, and whether a club’s 2025/26 table finish predicted its World Cup contribution.
Author

Kivan Polimis

Published

July 28, 2026

While within Premier league rivalries burn hotter than a circle of hell in Dante’s Inferno, the 2026 World Cup presented the rare occasion where Premier League rivals combine on the field as teammates on a national team and in the stands singing for country. And for fans of the English Premier League (EPL) and international club soccer like myself, this World Cup was also an opportunity to test the relative quality of leagues in a different way from the normal Champions League competitions. At the end of the tournament, I saw the following screenshot in my social media feed that tickled my analytical brain:

Minutes by league at the 2026 World Cup: the screenshot that started this analysis.

The Premier League sent more minutes to the 2026 World Cup than La Liga and the Bundesliga combined. That is striking. But “the Premier League” is not a team. It is twenty clubs with different ownership models, scouting geographies, and squad philosophies. Arsenal’s international roster looks nothing like Sunderland’s. Manchester City’s player pool spans six continents. Brighton punches well above its wage bill. These differences matter, and a single aggregate obscures all of them.

This post breaks the EPL’s World Cup contribution down by club, looks at which national teams those minutes came from, and asks whether a club’s 2025/26 league finish predicted how internationally active its squad was. The league-level picture is in the previous post in this series. The 34-year EPL trajectory is in the EPL Standings post.


Which clubs contributed the most?

Code
top_clubs = merged[merged["wc_minutes"] > 0].nlargest(20, "wc_minutes").copy()
top_clubs["min_label"] = top_clubs["wc_minutes"].apply(lambda x: f"({x:,})")
sort_field = alt.EncodingSortField(field="wc_minutes", order="descending")

base = alt.Chart(top_clubs)

bars = base.mark_bar().encode(
    x=alt.X("wc_minutes:Q",
            axis=alt.Axis(title="Total World Cup minutes", format=",d")),
    y=alt.Y("display_name:N", sort=sort_field,
            axis=alt.Axis(title=None, labelPadding=28)),
    color=alt.Color("colour:N", scale=None, legend=None),
    tooltip=[
        alt.Tooltip("display_name:N", title="Club"),
        alt.Tooltip("wc_minutes:Q",   title="WC minutes",  format=",d"),
        alt.Tooltip("player_count:Q", title="Players"),
        alt.Tooltip("wc_goals:Q",     title="Goals"),
        alt.Tooltip("wc_assists:Q",   title="Assists"),
        alt.Tooltip("ga_per90:Q",     title="G+A per 90",  format=".2f"),
        alt.Tooltip("position:Q",     title="EPL position"),
    ],
)

# Logo next to y-axis label — outside bar on white background
axis_logos = base.mark_image(width=20, height=20, clip=False).encode(
    x=alt.value(-15),
    y=alt.Y("display_name:N", sort=sort_field),
    url="logo:N",
    tooltip=[
        alt.Tooltip("display_name:N", title="Club"),
        alt.Tooltip("wc_minutes:Q",   title="WC minutes", format=",d"),
    ],
)

# Minutes in parentheses at the bar end
end_labels = base.mark_text(align="left", dx=5, fontSize=10,
                             color="#333333").encode(
    x=alt.X("wc_minutes:Q"),
    y=alt.Y("display_name:N", sort=sort_field),
    text="min_label:N",
)

(bars + axis_logos + end_labels).properties(
    width=620, height=520,
    title="Manchester City and Arsenal Led the EPL's World Cup Contribution"
).interactive()

Total World Cup minutes by EPL club. Club logo centred in each bar; minutes shown in parentheses at the bar end.

Manchester City and Arsenal at the top looks right. Both clubs recruit globally and their squads reflect that. City’s Argentine spine, Arsenal’s Brazilian and Ivorian contingent, Liverpool’s South American core. These clubs built for sustained European competition, which means building rosters heavy with established internationals.

I think the more interesting story is in the middle of this chart. Sunderland at sixth, Crystal Palace and Fulham in the top ten. Those clubs finished mid- to-lower table in 2025/26. Their World Cup contribution suggests that for some squads, the international pipeline runs somewhat independently of league performance. Worth looking at who those minutes actually came from (we will later).


Where did those minutes come from? National breakdown by club

The chart above shows volume per club. This one breaks each club’s contribution down by which national team the players represented. The five nations with the most World Cup minutes across EPL-based players are shown individually. Every other country is grouped as Other.

Code
flag_order = (
    [NATION_FLAGS.get(n, "🌐") + " " + n for n in
     sorted(top_nations,
            key=lambda n: -epl_players[epl_players["nation"]==n]["minutes"].sum())]
    + ["🌍 Other"]
)
# Only keep nations that actually appear in nbc_agg (after MIN_SEGMENT filter)
actual_flag_order = [f for f in flag_order if f in nbc_agg["nation_flag"].values]

# Club logo data for y-axis (one row per club)
_stacked_logo_df = (
    merged[merged["display_name"].isin(top_club_display)]
    [["display_name", "logo"]].drop_duplicates("display_name")
)

# Stacked bars — Vega-Lite handles the stacking; segments ordered largest-first
nation_bars = alt.Chart(nbc_agg).mark_bar().encode(
    x=alt.X("minutes:Q",
            axis=alt.Axis(title="World Cup minutes", format=",d")),
    y=alt.Y("display_name:N", sort=top_club_display,
            axis=alt.Axis(title=None, labelPadding=28)),
    color=alt.Color(
        "nation_flag:N",
        sort=actual_flag_order,
        scale=alt.Scale(scheme="tableau20"),
        legend=None,
    ),
    order=alt.Order("minutes:Q", sort="descending"),
    tooltip=[
        alt.Tooltip("display_name:N", title="Club"),
        alt.Tooltip("nation_flag:N",  title="Nation"),
        alt.Tooltip("minutes:Q",      title="Minutes", format=",d"),
        alt.Tooltip("player_count:Q", title="Players"),
    ],
)

# Flag emoji centred in each segment using pre-computed x_mid positions
flag_text = alt.Chart(nbc_sorted).mark_text(fontSize=13, baseline="middle").encode(
    x=alt.X("x_mid:Q"),
    y=alt.Y("display_name:N", sort=top_club_display),
    text="flag_only:N",
    opacity=alt.condition(
        alt.datum.minutes >= 200,
        alt.value(1), alt.value(0)
    ),
    tooltip=[
        alt.Tooltip("display_name:N", title="Club"),
        alt.Tooltip("nation_flag:N",  title="Nation"),
        alt.Tooltip("minutes:Q",      title="Minutes", format=",d"),
    ],
)

# Club logo next to each y-axis label
stacked_axis_logos = (
    alt.Chart(_stacked_logo_df)
    .mark_image(width=20, height=20, clip=False)
    .encode(
        x=alt.value(-15),
        y=alt.Y("display_name:N", sort=top_club_display),
        url="logo:N",
    )
)

main_stacked = (nation_bars + flag_text + stacked_axis_logos).properties(
    width=680, height=480,
    title="England Dominated Most Club Squads, but International Spread Varies"
).interactive()

# Standalone legend: flag emoji as the symbol, country name as the label
_flag_only    = [f.split(" ")[0] for f in actual_flag_order]
_country_only = [" ".join(f.split(" ")[1:]) for f in actual_flag_order]
_legend_df = pd.DataFrame({
    "flag":    _flag_only,
    "country": _country_only,
    "order":   list(range(len(actual_flag_order))),
})

legend_strip = (
    alt.Chart(_legend_df)
    .mark_text(fontSize=20, baseline="middle")
    .encode(
        x=alt.X("country:N",
                sort=_country_only,
                axis=alt.Axis(title=None, labelAngle=-30, labelFontSize=10,
                              ticks=False, domain=False, labelPadding=6)),
        text="flag:N",
    )
    .properties(width=680, height=52)
)

alt.vconcat(main_stacked, legend_strip, spacing=4)

World Cup minutes by EPL club, stacked by national team. Flag emoji centred in each segment; legend shows flags only (no colour blocks).

A few things stand out here. England leads for most clubs, as you’d expect from domestic players who qualified. For instance, a club like Sunderland with a lower wage bill may still have a cluster of English or Northern European internationals that pushes its total up. Argentina, Netherlands, and Brazil appear across multiple clubs, which reflects how broadly those nations recruit talent at the top of the market.

A caution: the breakdown reflects squad composition at the time of the tournament, not a deliberate diversification strategy. Clubs do not pick players to maximize nation coverage. The pattern emerges from transfer decisions made years earlier.


Who are these players?

Code
top60 = player_detail.head(60).copy()
p_sort = alt.EncodingSortField(field="Min", order="descending")

p_bars = (
    alt.Chart(top60)
    .mark_bar()
    .encode(
        x=alt.X("Min:Q",
                axis=alt.Axis(title="World Cup minutes", format=",d")),
        y=alt.Y("Player_flag:N", sort=p_sort,
                axis=alt.Axis(title=None, labelLimit=200)),
        color=alt.Color("colour:N", scale=None, legend=None),
        tooltip=[
            alt.Tooltip("Player:N",  title="Player"),
            alt.Tooltip("Nation:N",  title="Nation"),
            alt.Tooltip("Club:N",    title="Club"),
            alt.Tooltip("Pos:N",     title="Position"),
            alt.Tooltip("Min:Q",     title="Minutes", format=",d"),
            alt.Tooltip("G:Q",       title="Goals"),
            alt.Tooltip("A:Q",       title="Assists"),
            alt.Tooltip("G+A:Q",     title="G+A"),
        ],
    )
)

p_logos = (
    alt.Chart(top60)
    .mark_image(width=18, height=18, stroke="white", strokeWidth=1.5)
    .encode(
        x=alt.value(9),
        y=alt.Y("Player_flag:N", sort=p_sort),
        url="logo:N",
        tooltip=[
            alt.Tooltip("Player:N", title="Player"),
            alt.Tooltip("Club:N",   title="Club"),
            alt.Tooltip("Nation:N", title="Nation"),
        ],
    )
)

(p_bars + p_logos).properties(
    width=640, height=980,
    title="EPL Players at the 2026 World Cup (top 60 by minutes)"
).interactive()

EPL players at the 2026 World Cup, sorted by minutes. Bar colour and logo are the club’s.

Hover any bar to see the player’s nation, club, position, and goal contributions. The bar colour and logo are the club’s. Bars sharing the same colour came from the same side.


Does league position predict World Cup contribution?

Better clubs should send more World Cup players. Richer squads (in talent and finances) will probably have deeper international pipelines. Or at least that’s the intuition. The chart below tests it, with each club shown as its own badge.

Code
scatter_data = _reg_data.copy()

logos_scatter = (
    alt.Chart(scatter_data)
    .mark_image(width=32, height=32)
    .encode(
        x=alt.X("position:Q",
                scale=alt.Scale(domain=[0, 21]),
                axis=alt.Axis(title="2025/26 EPL final position (1 = champion)",
                              values=list(range(1, 21)))),
        y=alt.Y("wc_minutes:Q",
                axis=alt.Axis(title="World Cup minutes", format=",d")),
        url="logo:N",
        tooltip=[
            alt.Tooltip("display_name:N", title="Club"),
            alt.Tooltip("position:Q",     title="EPL position"),
            alt.Tooltip("wc_minutes:Q",   title="WC minutes",  format=",d"),
            alt.Tooltip("player_count:Q", title="Players"),
            alt.Tooltip("ga_per90:Q",     title="G+A per 90",  format=".2f"),
        ],
    )
)

trend = (
    alt.Chart(_reg_line)
    .mark_line(color="#CC0000", strokeDash=[5, 3], opacity=0.7)
    .encode(x=alt.X("position:Q"), y=alt.Y("wc_minutes:Q"))
)

_ols_caption = (
    f"OLS: slope = {_slope:.0f} min per table place  ·  "
    f"R² = {_r2:.2f}  ·  p = {_p:.3f}"
)

(logos_scatter + trend).properties(
    width=620, height=440,
    title=alt.TitleParams(
        "Higher EPL Finish Correlates with Greater World Cup Contribution",
        subtitle=[_ols_caption],
        subtitleColor="#CC0000",
        subtitleFontSize=10,
        subtitleFontStyle="italic",
    )
).interactive()

EPL final position vs. World Cup minutes per club. Each logo is one club.

The trend line direction tells one story. The outlier logos tell a better one. A badge sitting far above the trend at a high position number (lower in the table) has a squad built around a cluster of internationals from a nation that ran deep in the tournament. A top-four badge sitting low on the y-axis likely leaned on English players who either did not qualify or exited early.

Worth noting: the trend captures correlation, not causation. Top clubs recruit established internationals partly because they can pay for them, and those players tend to stay on international rosters. The league position may be a proxy for wage bill rather than an independent signal.


Production, not just presence

All men are created equal, but all minutes are not. Goals and assists per 90 are contribution.

Code
ga_clubs = merged[merged["wc_minutes"] >= 200].nlargest(15, "ga_per90").copy()
ga_clubs["ga_label"] = ga_clubs["ga_per90"].apply(lambda x: f"{x:.2f}")
ga_sort = alt.EncodingSortField(field="ga_per90", order="descending")

ga_base = alt.Chart(ga_clubs)

ga_bars = ga_base.mark_bar().encode(
    x=alt.X("ga_per90:Q",
            axis=alt.Axis(title="Goals + assists per 90 min")),
    y=alt.Y("display_name:N", sort=ga_sort, axis=alt.Axis(title=None)),
    color=alt.Color("colour:N", scale=None, legend=None),
    tooltip=[
        alt.Tooltip("display_name:N", title="Club"),
        alt.Tooltip("ga_per90:Q",     title="G+A per 90",  format=".2f"),
        alt.Tooltip("wc_goals:Q",     title="Goals"),
        alt.Tooltip("wc_assists:Q",   title="Assists"),
        alt.Tooltip("wc_minutes:Q",   title="WC minutes",  format=",d"),
        alt.Tooltip("player_count:Q", title="Players"),
    ],
)

ga_logos = ga_base.mark_image(width=22, height=22, stroke="white", strokeWidth=1.5).encode(
    x=alt.value(11),
    y=alt.Y("display_name:N", sort=ga_sort),
    url="logo:N",
    tooltip=[alt.Tooltip("display_name:N", title="Club")],
)

(ga_bars + ga_logos).properties(
    width=620, height=420,
    title="EPL Clubs Vary Widely in World Cup Production (min. 200 minutes)"
).interactive()

G+A per 90 by EPL club at the 2026 World Cup. Minimum 200 WC minutes to suppress small-sample noise. Club logo at bar start.

Code
club_opts = ["Arsenal"] + sorted(
    c for c in ga_players["Club"].dropna().unique() if c != "Arsenal"
)
club_sel = alt.selection_point(
    fields=["Club"],
    bind=alt.binding_select(options=club_opts, name="Club: "),
    value="Arsenal",
)

ga_p_sort = alt.EncodingSortField(field="ga_per90", order="descending")

ga_p_bars = (
    alt.Chart(ga_players)
    .mark_bar()
    .encode(
        x=alt.X("ga_per90:Q",
                axis=alt.Axis(title="G+A per 90 min")),
        y=alt.Y("Player_flag:N", sort=ga_p_sort,
                axis=alt.Axis(title=None, labelLimit=200)),
        color=alt.Color("colour:N", scale=None, legend=None),
        tooltip=[
            alt.Tooltip("Player:N",   title="Player"),
            alt.Tooltip("Nation:N",   title="Nation"),
            alt.Tooltip("ga_per90:Q", title="G+A per 90",  format=".2f"),
            alt.Tooltip("G+A:Q",      title="G+A"),
            alt.Tooltip("G:Q",        title="Goals"),
            alt.Tooltip("A:Q",        title="Assists"),
            alt.Tooltip("Min:Q",      title="Minutes",      format=",d"),
        ],
    )
    .add_params(club_sel)
    .transform_filter(club_sel)
)

ga_p_logos = (
    alt.Chart(ga_players)
    .mark_image(width=18, height=18, stroke="white", strokeWidth=1.5)
    .encode(
        x=alt.value(9),
        y=alt.Y("Player_flag:N", sort=ga_p_sort),
        url="logo:N",
        tooltip=[alt.Tooltip("Player:N", title="Player"),
                 alt.Tooltip("Club:N",   title="Club")],
    )
    .add_params(club_sel)
    .transform_filter(club_sel)
)

(ga_p_bars + ga_p_logos).properties(
    width=500, height=300,
    title="Which Players Drove Their Club's Production? (select club)"
).interactive()

G+A per 90 for individual players, by EPL club. Select a club from the dropdown.

High G+A per 90 with few total minutes often means one player ran very hot in a short run. For instance, a club whose only World Cup contributor is a striker who scored twice in the group stage before their nation exited will look dominant on this metric while barely registering on total minutes.

Low G+A per 90 with many total minutes suggests defenders and defensive midfielders carrying the bulk of a club’s allocation. Neither reading is better on its own. They are different things.


Full circle

This series started with a screenshot.

The original chart that kicked this off: minutes by league at the 2026 World Cup.

That image showed the EPL at 42,301 minutes, nearly double its nearest rival. Our analysis lands at 48,660 minutes, somewhat higher. The gap is likely methodological: our club-to-league mapping uses footy’s historical league assignments, which can include clubs that were in the EPL in earlier seasons even if they were in the Championship in 2025/26. For instance, Sunderland returned to the top flight and appears here correctly; a handful of others might be overcounted.

A note on bar colors: each club’s bar uses its official brand color. Eight of the twenty clubs shown use reds that are difficult to distinguish under deuteranopia or protanopia simulation. Notably, Bournemouth and Manchester United share the exact same official hex (#DA291C). Bar color here is a brand reference, not a primary data encoding. Each bar is independently labeled by club name and logo on the y-axis, so the chart is readable without color differentiation. Liverpool’s, Fulham’s, and Nottingham Forest’s bars are lightened slightly to improve badge legibility against the bar background.

One methodological note worth flagging: player club affiliations here are as of the final day of the 2025/26 EPL season (specifically, whenever FBref’s squad data was last updated before the tournament). Players who transferred mid-tournament (or between the end of the domestic season and the World Cup) are attributed to their EPL club rather than their new one. Marc Cucurella, for instance, moved from Chelsea to Real Madrid but is counted under Chelsea in this data. The same logic applies in reverse for inbound transfers. I think this is the right call: it measures where players developed and played leading into the tournament. Worth keeping in mind when interpreting totals for clubs in active transfer windows.

Still, the core finding holds up either way. The EPL is not slightly ahead of the competition. It is categorically ahead. La Liga and the Bundesliga are the second tier. The gap between them and everyone else is meaningful too.

What the original chart prompted was a set of follow-on questions: which EPL clubs drove that total, what nations those minutes came from, and whether the table finish said anything about a club’s international depth. I think the answers here are genuinely interesting. Manchester City and Arsenal lead by volume. On production, among clubs with at least 200 World Cup minutes, Arsenal ranks near the top at 0.53 G+A per 90, ahead of Liverpool, Crystal Palace, and everyone else with substantial minutes. Sunderland appears in the top six by volume, which looks surprising until you see the national breakdown. The league position and World Cup contribution correlation exists but is weaker than intuition would suggest.

That is probably the right place to end. The data confirms the headline, adds texture to the story, and raises at least a few questions worth investigating further.


What this connects to

  1. EPL Standings (1992–2026): 34 seasons of finishes, animated as a bump chart, with k-means cluster analysis.

  2. Which Leagues Powered the 2026 World Cup?: league-level breakdown of minutes, G+A per 90, and tournament depth.

  3. The EPL’s World Cup Footprint (this post): club-level breakdown, national team composition, and the relationship between league position and World Cup contribution.


Player data from FBref via soccerdata. EPL standings from football-data.co.uk. Club colours and logos from footy. Code: github.com/kpolimis/kpolimis.github.io.