Location: bets/vignettes/38.fifa_club_world_cup_25
https://www.transfermarkt.com/fifa-klub-wm/gesamtspielplan/pokalwettbewerb/KLUB/saison_id/2024
rm(list = ls())
set.seed(999)
invisible(sapply(c('tidyverse', "brms", "readr",
"janitor", "lubridate", "stringi",
"DT"),
require, character.only = TRUE))
options(dplyr.summarise.inform = FALSE)
# load functions
invisible(sapply(
list.files(pattern = "[.]R$",
path = "~/Documents/bets/R",
full.names = TRUE), source))
Introduction
The tournament is played in five rounds (R), the 32 participating
teams are grouped into 8 groups, each group consisting of four teams
that compete in a round-robin, the first two of each
group qualify for the round of 16, the winners of the
round of 16 matches qualify for the quarter-finals, the
winners of the quarter-finals qualify for the
semi-finals and finally the championship
game is decided between the winners of the semi-finals.
Round dates
- R1 Round-robin: Jun 14, 2025 to Jun 27, 2025
- R2 Round 16: Jun 28, 2025 to Jul 2, 2025
- R3 Quarter-final: Jul 4, 2025 to Jul 5, 2025
- R4 Semi-finals: Jul 8, 2025 to Jul 9, 2025
- R5 Championship game: Jul 13, 2025
Work flow
setwd("~/Documents/bets/data")
flow <- read_csv("fifa_world_cup25_sch.csv")
PrintDT(flow, 7, "Work flow", 0)
Data for model
date, round, group, home_team, home_goal, away_goal &
away_team
setwd("~/Documents/bets/data")
dm <- read_csv("fifa_world_cup25.csv", col_names = TRUE) %>%
mutate(home_team = stri_trans_general(
home_team, id = "Latin-ASCII"),
away_team = stri_trans_general(
away_team, id = "Latin-ASCII")) %>%
mutate(home_goal = as.numeric(home_goal),
away_goal = as.numeric(away_goal),
home_gd = home_goal - away_goal,
away_gd = away_goal - home_goal,
date = mdy(date)) %>%
select(date, round, group, home_team, away_team, home_goal,
away_goal, home_gd, away_gd) %>%
arrange(date, home_team)
PrintDT(dm, 4, "Input data", 1)
In the round-robin phase, each team plays its matches in three
sub-rounds. After each sub-round ends, a Bayesian model is calculated
and used to “predict” the results of the following sub-rounds. This
process is repeated for each sub-round or round until the tournament
ends.
Round to be
analized
- DM3: Data for model
- M3: Bayes model
- DP3: Data used to predict outcomes
- PR3: Prediction results
- MQ3: Model quality
DM3: Data for
model
Select data from games that have already finished
# data for model
dm3 <- dm %>% filter(round == "r11" | round == "r12" | round == "r13" | round == "r2")
M3: Bayes model
The Bayesian model uses data from finished games and allows the
results of future games to be predicted.
DP3: Data used to
predict outcomes
Define the games you want to be predicted, generally referring to
matches that have not yet taken place.
# data for prediction
dp3 <- dm %>%
filter(round == "r3") %>%
select(date, group, home_team, away_team,
hg = home_goal, ag = away_goal)
PR3: Prediction
results
The Bayesian model estimates the probabilities (winning, drawing or
losing the match) and additionally calculates the number of expected
goals for each team.
# Using "allow_new_levels" = TRUE
# Include games after certain dates
# run predict_matches function
pr3 <- predict_matches(dm, dp3, m3_bmh, m3_bma)
# model results
pr3a <- model_results(pr3, dp3)
# add group to model results
pr3b <- full_join(dp3, pr3a, by = join_by(date, group, home_team, away_team, hg, ag))
PrintDT(pr3b, 20, "R1: model results", 2)
MQ3: Model
quality
The quality of the model essentially compares the expected results
vs. the actual results of the matches.
# model quality
pr3b %>%
group_by(model_quality) %>%
summarise(freq = n()) %>%
ungroup() %>%
mutate("model quality %" = round(100*freq/sum(freq), 1))
pr3b %>% mutate(ph_pa = ph - pa)
Tournament results
# league_results
PrintDT(league_results(
dm %>% select(home_team, away_team,
home_score = home_goal,
away_score = away_goal)),
4, "Tournament results", 1)
teams <- c("Real Madrid", "Bayern Munich", "Paris SG",
"Fluminense", "Palmeiras", "Chelsea",
"Bor. Dortmund", "Al-Hilal")
ht_hg_ag <- dm %>%
group_by(home_team) %>%
drop_na(home_goal) %>%
summarise(ng_ht = n(),
hg_ht = mean(home_goal, na.rm = T),
ag_ht = mean(away_goal, na.rm = T)) %>%
ungroup() %>%
filter(home_team %in% teams)
at_hg_ag <- dm %>%
group_by(away_team) %>%
drop_na(home_goal) %>%
summarise(ng_at = n(),
hg_at = mean(home_goal, na.rm = T),
ag_at = mean(away_goal, na.rm = T)) %>%
ungroup() %>%
filter(away_team %in% teams)
da <- cbind(ht_hg_ag, at_hg_ag) %>%
mutate_if(is.numeric, round, 1) %>%
mutate(sht = hg_ht - ag_ht,
sat = ag_at - hg_at)
da1 <- full_join(pr3b, da,
by = join_by(home_team, away_team)) %>%
select(home_team:away_team, ph:wbyp, ng_ht:sat)
LS0tCnRpdGxlOiAiRklGQSBjbHViIHdvcmxkIGN1cCAyMDI1IgpzdWJ0aXRsZTogIkZvcmVjYXN0aW5nIGdhbWUgcmVzdWx0cyB1c2luZyBCYXllc2lhbiByZWdyZXNzaW9uIG1vZGVscyIKYWJzdHJhY3Q6ICIiCmF1dGhvcjogIkFuZHJlcyBQRU5BIgpkYXRlOiAyMDI1LTA2LTI1Cm91dHB1dDogCiAgaHRtbF9ub3RlYm9vazogCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwogICAgaGlnaGxpZ2h0OiBrYXRlCiAgICB0aGVtZTogcmVhZGFibGUKICAgIG51bWJlcl9zZWN0aW9uczogeWVzCi0tLQoKTG9jYXRpb246IGJldHMvdmlnbmV0dGVzLzM4LmZpZmFfY2x1Yl93b3JsZF9jdXBfMjUKCjxodHRwczovL3d3dy50cmFuc2Zlcm1hcmt0LmNvbS9maWZhLWtsdWItd20vZ2VzYW10c3BpZWxwbGFuL3Bva2Fsd2V0dGJld2VyYi9LTFVCL3NhaXNvbl9pZC8yMDI0PgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0Kcm0obGlzdCA9IGxzKCkpCnNldC5zZWVkKDk5OSkKaW52aXNpYmxlKHNhcHBseShjKCd0aWR5dmVyc2UnLCAiYnJtcyIsICJyZWFkciIsIAogICAgICAgICAgICAgICAgICAgImphbml0b3IiLCAibHVicmlkYXRlIiwgInN0cmluZ2kiLCAKICAgICAgICAgICAgICAgICAgICJEVCIpLCAKICAgICAgICAgICAgICAgICByZXF1aXJlLCBjaGFyYWN0ZXIub25seSA9IFRSVUUpKQoKb3B0aW9ucyhkcGx5ci5zdW1tYXJpc2UuaW5mb3JtID0gRkFMU0UpCgojIGxvYWQgZnVuY3Rpb25zIAppbnZpc2libGUoc2FwcGx5KAogICAgbGlzdC5maWxlcyhwYXR0ZXJuID0gIlsuXVIkIiwgCiAgICAgICAgICAgICAgIHBhdGggPSAifi9Eb2N1bWVudHMvYmV0cy9SIiwgCiAgICAgICAgICAgICAgIGZ1bGwubmFtZXMgPSBUUlVFKSwgc291cmNlKSkKYGBgCgojIEludHJvZHVjdGlvbiAKClRoZSB0b3VybmFtZW50IGlzIHBsYXllZCBpbiBmaXZlIHJvdW5kcyAoUiksIHRoZSAzMiBwYXJ0aWNpcGF0aW5nIHRlYW1zIGFyZSBncm91cGVkIGludG8gOCBncm91cHMsIGVhY2ggZ3JvdXAgY29uc2lzdGluZyBvZiBmb3VyIHRlYW1zIHRoYXQgY29tcGV0ZSBpbiBhICoqcm91bmQtcm9iaW4qKiwgdGhlIGZpcnN0IHR3byBvZiBlYWNoIGdyb3VwIHF1YWxpZnkgZm9yIHRoZSAqKnJvdW5kIG9mIDE2KiosIHRoZSB3aW5uZXJzIG9mIHRoZSByb3VuZCBvZiAxNiBtYXRjaGVzIHF1YWxpZnkgZm9yIHRoZSAqKnF1YXJ0ZXItZmluYWxzKiosIHRoZSB3aW5uZXJzIG9mIHRoZSBxdWFydGVyLWZpbmFscyBxdWFsaWZ5IGZvciB0aGUgKipzZW1pLWZpbmFscyoqIGFuZCBmaW5hbGx5IHRoZSAqKmNoYW1waW9uc2hpcCBnYW1lKiogaXMgZGVjaWRlZCBiZXR3ZWVuIHRoZSB3aW5uZXJzIG9mIHRoZSBzZW1pLWZpbmFscy4KCiMgUm91bmQgZGF0ZXMKCiAgLSBSMSBSb3VuZC1yb2JpbjogSnVuIDE0LCAyMDI1IHRvIEp1biAyNywgMjAyNQogIC0gUjIgUm91bmQgMTY6IEp1biAyOCwgMjAyNSB0byBKdWwgMiwgMjAyNSAgCiAgLSBSMyBRdWFydGVyLWZpbmFsOiBKdWwgNCwgMjAyNSB0byBKdWwgNSwgMjAyNSAgCiAgLSBSNCBTZW1pLWZpbmFsczogSnVsIDgsIDIwMjUgdG8gSnVsIDksIDIwMjUgICAKICAtIFI1IENoYW1waW9uc2hpcCBnYW1lOiBKdWwgMTMsIDIwMjUKICAKIyBXb3JrIGZsb3cgCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpzZXR3ZCgifi9Eb2N1bWVudHMvYmV0cy9kYXRhIikKZmxvdyA8LSByZWFkX2NzdigiZmlmYV93b3JsZF9jdXAyNV9zY2guY3N2IikKClByaW50RFQoZmxvdywgNywgIldvcmsgZmxvdyIsIDApCmBgYAoKIyBEYXRhIGZvciBtb2RlbCAKCmRhdGUsIHJvdW5kLCBncm91cCwgaG9tZV90ZWFtLCBob21lX2dvYWwsIGF3YXlfZ29hbCAmCWF3YXlfdGVhbQoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0Kc2V0d2QoIn4vRG9jdW1lbnRzL2JldHMvZGF0YSIpCmRtIDwtIHJlYWRfY3N2KCJmaWZhX3dvcmxkX2N1cDI1LmNzdiIsIGNvbF9uYW1lcyA9IFRSVUUpICU+JQogIG11dGF0ZShob21lX3RlYW0gPSBzdHJpX3RyYW5zX2dlbmVyYWwoCiAgICBob21lX3RlYW0sIGlkID0gIkxhdGluLUFTQ0lJIiksCiAgICBhd2F5X3RlYW0gPSBzdHJpX3RyYW5zX2dlbmVyYWwoCiAgICAgIGF3YXlfdGVhbSwgaWQgPSAiTGF0aW4tQVNDSUkiKSkgJT4lCiAgbXV0YXRlKGhvbWVfZ29hbCA9IGFzLm51bWVyaWMoaG9tZV9nb2FsKSwgCiAgICAgICAgIGF3YXlfZ29hbCA9IGFzLm51bWVyaWMoYXdheV9nb2FsKSwKICAgICAgICAgaG9tZV9nZCA9IGhvbWVfZ29hbCAtIGF3YXlfZ29hbCwKICAgICAgICAgYXdheV9nZCA9IGF3YXlfZ29hbCAtIGhvbWVfZ29hbCwKICAgICAgICAgZGF0ZSA9IG1keShkYXRlKSkgJT4lIAogIHNlbGVjdChkYXRlLCByb3VuZCwgZ3JvdXAsIGhvbWVfdGVhbSwgYXdheV90ZWFtLCBob21lX2dvYWwsIAogICAgICAgICBhd2F5X2dvYWwsIGhvbWVfZ2QsIGF3YXlfZ2QpICU+JQogIGFycmFuZ2UoZGF0ZSwgaG9tZV90ZWFtKQoKUHJpbnREVChkbSwgNCwgIklucHV0IGRhdGEiLCAxKQpgYGAKCkluIHRoZSByb3VuZC1yb2JpbiBwaGFzZSwgZWFjaCB0ZWFtIHBsYXlzIGl0cyBtYXRjaGVzIGluIHRocmVlIHN1Yi1yb3VuZHMuIEFmdGVyIGVhY2ggc3ViLXJvdW5kIGVuZHMsIGEgQmF5ZXNpYW4gbW9kZWwgaXMgY2FsY3VsYXRlZCBhbmQgdXNlZCB0byAicHJlZGljdCIgdGhlIHJlc3VsdHMgb2YgdGhlIGZvbGxvd2luZyBzdWItcm91bmRzLiBUaGlzIHByb2Nlc3MgaXMgcmVwZWF0ZWQgZm9yIGVhY2ggc3ViLXJvdW5kIG9yIHJvdW5kIHVudGlsIHRoZSB0b3VybmFtZW50IGVuZHMuCgojIFJvdW5kIHRvIGJlIGFuYWxpemVkCgotIERNMzogRGF0YSBmb3IgbW9kZWwgIAotIE0zOiBCYXllcyBtb2RlbCAKLSBEUDM6IERhdGEgdXNlZCB0byBwcmVkaWN0IG91dGNvbWVzICAgCi0gUFIzOiBQcmVkaWN0aW9uIHJlc3VsdHMgCi0gTVEzOiBNb2RlbCBxdWFsaXR5ICAKCiMjIERNMzogRGF0YSBmb3IgbW9kZWwKClNlbGVjdCBkYXRhIGZyb20gZ2FtZXMgdGhhdCBoYXZlIGFscmVhZHkgZmluaXNoZWQKCmBgYHtyfQojIGRhdGEgZm9yIG1vZGVsIApkbTMgPC0gZG0gJT4lIGZpbHRlcihyb3VuZCA9PSAicjExIiB8IHJvdW5kID09ICJyMTIiIHwgcm91bmQgPT0gInIxMyIgfCByb3VuZCA9PSAicjIiKQpgYGAKCiMjIE0zOiBCYXllcyBtb2RlbAoKVGhlIEJheWVzaWFuIG1vZGVsIHVzZXMgZGF0YSBmcm9tIGZpbmlzaGVkIGdhbWVzIGFuZCBhbGxvd3MgdGhlIHJlc3VsdHMgb2YgZnV0dXJlIGdhbWVzIHRvIGJlIHByZWRpY3RlZC4KCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CiMgUnVuIEJheWVzIG1vZGVsIGhvbWUgKGJtaCkgdXNpbmcgZG0xIGRhdGEKc3lzdGVtLnRpbWUoe20zX2JtaCA8LSBiYXllc19tb2RlbF9ob21lKGRtMyl9KQoKIyBSdW4gQmF5ZXMgbW9kZWwgYXdheSAoYm1hKSB1c2luZyBkbTEgZGF0YQpzeXN0ZW0udGltZSh7bTNfYm1hIDwtIGJheWVzX21vZGVsX2F3YXkoZG0zKX0pCmBgYAoKIyMgRFAzOiBEYXRhIHVzZWQgdG8gcHJlZGljdCBvdXRjb21lcwoKRGVmaW5lIHRoZSBnYW1lcyB5b3Ugd2FudCB0byBiZSBwcmVkaWN0ZWQsIGdlbmVyYWxseSByZWZlcnJpbmcgdG8gbWF0Y2hlcyB0aGF0IGhhdmUgbm90IHlldCB0YWtlbiBwbGFjZS4KCmBgYHtyfQojIGRhdGEgZm9yIHByZWRpY3Rpb24KZHAzIDwtIGRtICU+JSAKICBmaWx0ZXIocm91bmQgPT0gInIzIikgJT4lCiAgc2VsZWN0KGRhdGUsIGdyb3VwLCBob21lX3RlYW0sIGF3YXlfdGVhbSwgCiAgICAgICAgIGhnID0gaG9tZV9nb2FsLCBhZyA9IGF3YXlfZ29hbCkKYGBgCgojIyBQUjM6IFByZWRpY3Rpb24gcmVzdWx0cwoKVGhlIEJheWVzaWFuIG1vZGVsIGVzdGltYXRlcyB0aGUgcHJvYmFiaWxpdGllcyAod2lubmluZywgZHJhd2luZyBvciBsb3NpbmcgdGhlIG1hdGNoKSBhbmQgYWRkaXRpb25hbGx5IGNhbGN1bGF0ZXMgdGhlIG51bWJlciBvZiBleHBlY3RlZCBnb2FscyBmb3IgZWFjaCB0ZWFtLgoKYGBge3J9CiMgVXNpbmcgImFsbG93X25ld19sZXZlbHMiID0gVFJVRSAgCiMgSW5jbHVkZSBnYW1lcyBhZnRlciBjZXJ0YWluIGRhdGVzIAojIHJ1biBwcmVkaWN0X21hdGNoZXMgZnVuY3Rpb24gCnByMyA8LSBwcmVkaWN0X21hdGNoZXMoZG0sIGRwMywgbTNfYm1oLCBtM19ibWEpCgojIG1vZGVsIHJlc3VsdHMKcHIzYSA8LSBtb2RlbF9yZXN1bHRzKHByMywgZHAzKQoKIyBhZGQgZ3JvdXAgdG8gbW9kZWwgcmVzdWx0cyAKcHIzYiA8LSBmdWxsX2pvaW4oZHAzLCBwcjNhLCBieSA9IGpvaW5fYnkoZGF0ZSwgZ3JvdXAsIGhvbWVfdGVhbSwgYXdheV90ZWFtLCBoZywgYWcpKQoKUHJpbnREVChwcjNiLCAyMCwgIlIxOiBtb2RlbCByZXN1bHRzIiwgMikKCnByaW50KHByM2IpCmBgYAoKIyMgTVEzOiBNb2RlbCBxdWFsaXR5CgpUaGUgcXVhbGl0eSBvZiB0aGUgbW9kZWwgZXNzZW50aWFsbHkgY29tcGFyZXMgdGhlIGV4cGVjdGVkIHJlc3VsdHMgdnMuIHRoZSBhY3R1YWwgcmVzdWx0cyBvZiB0aGUgbWF0Y2hlcy4KCmBgYHtyfQojIG1vZGVsIHF1YWxpdHkKcHIzYiAlPiUgCiAgZ3JvdXBfYnkobW9kZWxfcXVhbGl0eSkgJT4lIAogIHN1bW1hcmlzZShmcmVxID0gbigpKSAlPiUKICB1bmdyb3VwKCkgJT4lCiAgbXV0YXRlKCJtb2RlbCBxdWFsaXR5ICUiID0gcm91bmQoMTAwKmZyZXEvc3VtKGZyZXEpLCAxKSkKYGBgCgpgYGB7cn0KcHIzYiAlPiUgbXV0YXRlKHBoX3BhID0gcGggLSBwYSkKYGBgCgojIFRvdXJuYW1lbnQgcmVzdWx0cwoKYGBge3J9CiMgbGVhZ3VlX3Jlc3VsdHMKUHJpbnREVChsZWFndWVfcmVzdWx0cygKICBkbSAlPiUgc2VsZWN0KGhvbWVfdGVhbSwgYXdheV90ZWFtLCAKICAgICAgICAgICAgICAgIGhvbWVfc2NvcmUgPSBob21lX2dvYWwsIAogICAgICAgICAgICAgICAgYXdheV9zY29yZSA9IGF3YXlfZ29hbCkpLCAKICA0LCAiVG91cm5hbWVudCByZXN1bHRzIiwgMSkKYGBgCgoKYGBge3J9CnRlYW1zIDwtIGMoIlJlYWwgTWFkcmlkIiwgIkJheWVybiBNdW5pY2giLCAiUGFyaXMgU0ciLAogICAgICAgICAgICJGbHVtaW5lbnNlIiwgIlBhbG1laXJhcyIsICJDaGVsc2VhIiwKICAgICAgICAgICAiQm9yLiBEb3J0bXVuZCIsICJBbC1IaWxhbCIpCgpodF9oZ19hZyA8LSBkbSAlPiUgCiAgZ3JvdXBfYnkoaG9tZV90ZWFtKSAlPiUgCiAgZHJvcF9uYShob21lX2dvYWwpICU+JQogIHN1bW1hcmlzZShuZ19odCA9IG4oKSwgCiAgICAgICAgICAgIGhnX2h0ID0gbWVhbihob21lX2dvYWwsIG5hLnJtID0gVCksIAogICAgICAgICAgICBhZ19odCA9IG1lYW4oYXdheV9nb2FsLCBuYS5ybSA9IFQpKSAlPiUgCiAgdW5ncm91cCgpICU+JQogIGZpbHRlcihob21lX3RlYW0gJWluJSB0ZWFtcykKCmF0X2hnX2FnICA8LSBkbSAlPiUgCiAgZ3JvdXBfYnkoYXdheV90ZWFtKSAlPiUgCiAgZHJvcF9uYShob21lX2dvYWwpICU+JQogIHN1bW1hcmlzZShuZ19hdCA9IG4oKSwKICAgICAgICAgICAgaGdfYXQgPSBtZWFuKGhvbWVfZ29hbCwgbmEucm0gPSBUKSwgCiAgICAgICAgICAgIGFnX2F0ID0gbWVhbihhd2F5X2dvYWwsIG5hLnJtID0gVCkpICAlPiUgCiAgdW5ncm91cCgpICU+JQogIGZpbHRlcihhd2F5X3RlYW0gJWluJSB0ZWFtcykKCmRhIDwtIGNiaW5kKGh0X2hnX2FnLCBhdF9oZ19hZykgJT4lCiAgbXV0YXRlX2lmKGlzLm51bWVyaWMsIHJvdW5kLCAxKSAlPiUKICBtdXRhdGUoc2h0ID0gaGdfaHQgLSBhZ19odCwKICAgICAgICAgc2F0ID0gYWdfYXQgLSBoZ19hdCkKCmRhMSA8LSBmdWxsX2pvaW4ocHIzYiwgZGEsIAogICAgICAgICAgICAgICAgIGJ5ID0gam9pbl9ieShob21lX3RlYW0sIGF3YXlfdGVhbSkpICU+JSAKICBzZWxlY3QoaG9tZV90ZWFtOmF3YXlfdGVhbSwgcGg6d2J5cCwgbmdfaHQ6c2F0KQpgYGAKCg==