ikeの日記

しがない研究者の雑記。

Bradley-Terryモデルを使ったJリーグデータの分析

先日ツイッタープレミアリーグのデータのBradley-Terryモデルを使った分析を見つけて、自分もやってみたくなった。
こんなことをやっている場合ではないが、同様の分析を2019年シーズンのJリーグのデータを用いてやってみてしまった…ので、ブログにまとめておこうと思う。

Bradley-Terryモデルとは?

Bradley-Terryモデルは、一対比較データから各観測の潜在的な能力を推定するモデルである。サッカーでいえば、各チームの潜在的な強さを試合結果(勝敗データ)から推定するために使われる。

データの収集

以下のRコードを使ってJリーグの公式サイトから2019年シーズンの試合結果を取得する。 ここではリーグ戦のみならずルヴァンカップ及び天皇杯のデータも含めることで、J1からJ3までの全チームの強さを比較できるようにした。

# Load packages
pacman::p_load(BradleyTerry2, dplyr, ggplot2, rvest)

# Scrape match data
dat <- rep(NA, 5)
month <- c("02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12")
for (i in month){ # loop over month
  url <- paste0("https://www.jleague.jp/match/search/?category%5B%5D=j1&category%5B%5D=leaguecup&category%5B%5D=j2&category%5B%5D=j3&category%5B%5D=emperor&year=2019&month%5B%5D=", i)
  web <- read_html(url)
  team_left <- web %>% html_nodes(css = "td[class='clubName leftside']") %>% html_text()
  team_left <- gsub("[[:space:]]", "", team_left)
  team_right <- web %>% html_nodes(css = "td[class='clubName rightside']") %>% html_text()
  team_right <- gsub("[[:space:]]", "", team_right)
  score_left <- web %>% html_nodes(css = "td[class='point leftside']") %>% html_text()
  score_left <- as.numeric(score_left)
  score_right <- web %>% html_nodes(css = "td[class='point rightside']") %>% html_text()
  score_right <- as.numeric(score_right)
  tmp <- cbind(team_left, team_right, score_left, score_right, 
               rep(i, length(team_left)))
  dat <- rbind.data.frame(dat, tmp)
  Sys.sleep(2)
}
dat <- dat[-1,]
# Data cleaning
colnames(dat)[5] <- "month"
dat$score_left <- as.numeric(as.character(dat$score_left))
dat$score_right <- as.numeric(as.character(dat$score_right))
## code match results (ties as 0.5)
dat$team_left_win <- ifelse(dat$score_left > dat$score_right, 1, 
                            ifelse(dat$score_left == dat$score_right, 0.5, 0))
dat$team_right_win <- 1 - dat$team_left_win
## drop cup games with non-league teams
dat <- dat %>% group_by(team_left) %>% mutate(team_left_n = n()) %>% ungroup()
dat <- dat %>% group_by(team_right) %>% mutate(team_right_n = n()) %>% ungroup()
dat <- dat %>% dplyr::filter(team_left_n > 5 & team_right_n > 5)
## convert team names to factor class
dat$team_left <- factor(as.character(dat$team_left))
dat$team_right <- factor(as.character(dat$team_right))

上記により、J1-J3のチーム同士による計1184試合の結果を取得することができる。
データを集めた後、パッケージの仕様に合わせてデータを整理しておく。

モデルの推定

モデルの推定にはRのBradleyTerry2パッケージを使う。最尤法を使っているので、今回のようにそれほど大きくないデータセットであれば推定は瞬時に終わる。

out <- BTm(outcome = cbind(team_left_win, team_right_win), 
           player1 = team_left, player2 = team_right, id = "team",
           formula = ~ team, data = dat, br = TRUE)

簡単!1
ちなみにBradleyTerry2パッケージを使うと潜在スコアのモデリングもできる。
今回の例であれば、勝敗の確率はチームの潜在的な強さのみならず試合会場(ホームかアウェイか)にも左右されうると考えられる。
ただし、天皇杯などどちらがホームかが判別できない試合がデータに含まれているため、ここでは最もシンプルな方法で分析をした。

結果の図示

モデルにより推定された各チームの潜在的な強さをプロットしてみる。

# Summarize estimation results
res <- BTabilities(out)
res <- as.data.frame(res)
res$lower <- res$ability + qnorm(0.025) * res$s.e. # 95% conf. intervals
res$upper <- res$ability + qnorm(0.975) * res$s.e.
res <- res[order(res$ability, decreasing = TRUE),] # sort teams
# Visualize
png("fig_jleague.png", width = 500, height = 1000)
par(mar = c(3, 4, 3, 2), mgp = c(2, 0.5, 0), family = "HiraKakuProN-W3")
plot(NULL, NULL, type = "n", xlim = c(-2.2, 4.0), ylim = c(1, 58),
     xlab = "Ability Estimates", ylab = "", main = "J League (2019 season)",
     axes = FALSE)
axis(1, tck = -0.01, cex.axis = 0.8)
axis(2, at = c(1:58), labels = rownames(res), las = 2, tck = -0.01,
     cex.axis = 0.8)
abline(v = 0, col = 8, lty = 2)
points(res$ability, c(1:58), pch = 20)
segments(res$lower, c(1:58), res$upper, c(1:58))
dev.off()

f:id:ike_og:20200207164417p:plain

出力された図を見てみると、信頼区間が非常に大きいことがわかる。
これは先に引用したツイートでも触れられているが、推定に使われたデータ数が1チームあたり30-40ゲームしかないためであると考えられる。2
ただ、推定誤差は大きいものの、潜在スコアの順序は割と直感にあっているように見える。
各リーグ内の順序は概ねリーグ戦の順位と相関している。また、おそらく多くの人が思っていた通り、柏レイソルはJ1下位チームよりも強く、FC岐阜J3の上位チームよりも弱い、という結果になっている。3


という感じでものすごく簡単に再現することができた。
Bradley-Terryモデルはパッケージを使ってしまえば簡単に推定できる一方、興味深い知見を得られるなぁという感想を持った。
政治学だと一対比較データはあまり見ないが、機会があればぜひ使ってみたいと思う。


  1. パッケージのデフォルトで、セレッソ大阪U23の潜在スコアが0に固定されている。

  2. J3チームの信頼区間が狭くなっている理由はよくわからないが…。

  3. 柏レイソルルヴァンカップ天皇杯などリーグ間をつなぐ試合(bridge observation)でそこまでパフォーマンスがよくなかったが、リーグ戦での勢いをこれらの試合でも見せられていれば、潜在スコアはもっと高く出たのかもしれない。