論理の流刑地

地獄の底を、爆笑しながら闊歩する

Jリーグをデータから分析する準備

猫背、なかなか直らんすな....

Introduction

サッカーをデータ面から楽しもうという一般peopleのためにFootball Labという神サイトがある。その利用規約には、

なお、個人の方のブログやSNSアカウントでは、引用元を明記していただければFootball LAB内のデータやキャプチャをお使いいただけます。

とあり、個人利用については許容されているので、われわれfootballファンが「データによりfootballを楽しむ」ためのプラットフォームを提供してくれているといえる。


しかし、データを引っこ抜いて分析可能にするのがなかなかに骨なのが、こういうデータ分析に共通する課題なので、
そこの部分をHadley神のつくりたもうたrvest+selectorGadgetを使って、楽にできないかと企てる。

参考URL

  1. rvestのreference
  2. SelectorGadget(chromeの拡張機能)

Scrapingの流れ

使うソフトなりライブラリなりによって細かい技術的プロセスはかわってくるだろうが、だいたいscrapingは以下のような流れによってなされる。

  1. 対象ページへのアクセス
  2. ページ内の対象ノードへのアクセス
  3. 生データの取得
  4. データの整形

んでもって、たいていの場合は複数のページからデータを引き抜きたいので*1
対象ページを走査するためのルールのロジックをかく必要がある。
また、対象ページからどの情報を引き抜いていくか、という点にも人間がロジックを指定しなければならない(ここを省力化するのに役立つのがSelectorGadget)。

つまり

  1. 走査ロジック
  2. データ取得ロジック

の2つが、人間のアタマで考えなくてはならない部分である、といえよう。

欲しいデータの要件の定義

データ要件の定義が大事な理由

とりあえずできるテクノロジーが手元にあるから、すぐにコードを書き始めてしまうのはだいたい悪手であることが多い。
まず着手すべきは、欲しいデータの完成形を定義することである。

もっといえば、データは分析のために作るものだから、分析の目的が定まっていないのであれば、そこからしっかりと言葉で定義しておいたほうがいい。

  • 検証したい仮説
  • 比較したい対象
  • どういった規則性が見出されるとウレシイか

といったことが、自分のアタマの中で(さらに望ましくは言語媒体にoutputされたカタチで)定義されている
ここで、「規則性を見つける」っていうのは、要するに混沌と生起しているグラウンド上の事象に対する漸近模型であるデータの中に
一定のルールを見出すことが、事象=footballの理解につながる、ということである。

Dr.STONEの名言を引用すれば

「科学ではわからないこともある」じゃねえ、
"わからねえことにルールを探す"、そのクッソ地道な努力を科学って呼んでるだけだ……!!

ってことである。

話がそれたが、要するに分析の目的に合致した形でデータを引っこ抜いてこないと意味ない(少なくともムダが多い)よね、って話でした。

データの要件の定義@Football Lab

ということで、実際につくりたいデータの構造をきめていく
具体的には

  • 分析単位(cases)
  • データに含む変数(variables)

を何にするのか、という点を決めていくのである。
ここで、変数としては独立変数/従属変数が何になるのか、ということをイメージできているとよい。

まず従属変数についてだが、わたしが特に関心があるのは、
「どういった場合にグランパスは多くチャンスを生み出しているか」「どういった場合に多くチャンスをつくられているか」という2点につきる。

独立変数については、そこまで決め打ちしたいような仮説があるわけではないが、ぼやっとした言葉でいえば

  • こちらの選手として誰が出ているか
  • 相手のチームがどういった特徴をもつか

といった点は重視したい。

こういった条件から、ひとまず分析単位は「X節のチームY」になる、といえる。
つまり、ID的な変数としては「クラブ」「節」をとることになるだろう。

「こちらの選手として誰がでているか」に関しては、試合ページ(例:名古屋vs神戸)を参照すればよい。
「相手のチームがどういった特徴をもつか」については、

rvestの使用手順を簡単におさらい

rvestはマジで使い方がシンプル*2なので、わざわざ解説するようなこともないのだが、一応おさらいしとく。

基本的な使い方

だいたい、以下のような流れで使うと考えて、さしつかえない。

  1. read_html()を使って、URLにアクセス
  2. html_nodes()にCSSセレクタ等を指定して、必要なノードにアクセス
  3. html_text()やhtml_attr()などを使って、必要なテキストをとってくる
  4. (あるいは手順2-3の代わりにhtml_table()を使って直接tableをdata frameとして読み込む)
  5. 取得した情報を、Rで使いやすいような形式に変換する

実行例

①football labのtopページから、各チームのページのURLをとってくる

library(rvest)
top_p  <- read_html("https://www.football-lab.jp/")
club_nodes <- html_nodes(top_p , css="#teamMenu span")
club_nodes <- club_nodes[c(4:61)]

team_name <- c()
team_alp <- c()

for( i in 1:58){
  team_name <- c(team_name , html_text( html_node(club_nodes[[i]],"span")))
  alp <- html_attr(club_nodes[[i]] , "href")
  alp_n <- nchar(alp)
  team_alp <- c( team_alp , substr( alp ,2 , alp_n-1 ))
}#

jclub_names <- data.frame( JP = team_name, ALPH =team_alp)
head( jclub_names)

#   JP ALPH
# 1   札幌 sapp
# 2   仙台 send
# 3   鹿島 kasm
# 4   浦和 uraw
# 5     柏 kasw
# 6 FC東京 fctk

二行目、html_nodes()におけるCSSセレクタの指定部分においてSelectorGadgetを使ってラクをしている。

② 選手データ(例:チョンソンリョンの2019年)から、出場記録をぶっこぬいてくる

gk_page <- read_html("https://www.football-lab.jp/player/605028/?year=2019")
gk_tbl <- html_table( (html_nodes( gk_page , css="#plLog"))[[1]], fill=TRUE)
gk_tbl <- gk_tbl[-1, ]

head( gk_tbl)

# 節 開催日   相手 スコア   出場時間 Pos. ゴール アシスト    セーブP パスCBP 奪取P
# 2  1   2/23 FC東京    0-0 H       90   GK      0        0 NA    0.09    0.27  0.15
# 3  2    3/1   鹿島    1-1 H       90   GK      0        0 NA    0.00    0.12  0.99
# 4  3   3/10 横浜FM    2-2 A       90   GK      0        0 NA    0.19    0.32  1.39
# 守備P チーム
# 2  0.34 川崎F
# 3  2.19 川崎F
# 4  1.83 川崎F

fill=Tで、中身がない場合にNAで埋められる。

データをとっていく(クラブ編)

本節においては、各ゲームにおける各クラブの主要指標を取得(し、データフレーム形式にまとめる)ためのコードを実装していく。

走査ルール編

まず、必要情報を含むページのURLを適切にとるための関数をつくる

引数として「節」「西暦」「チーム名の略称」をとり、戻り値としてURLを返すような関数を考える。

get_gURL <- function( mday = 1, team ="sapp",year = 2020){
  #---Target URLをつくる---#
  tgt_url <- paste0("https://www.football-lab.jp/", team,"/match/?year=", year )
  t_page <- read_html(tgt_url)
  match_links <- html_nodes( t_page , "table em+a") #emタグに隣接するaタグだけ取得してくる
  print(paste("取得可能な試合数=", length(match_links)))
  tgt_links <- match_links[mday]
  
  ret_links <- sapply(tgt_links , function(mnode){
    link_text <-  html_attr( mnode, "href")
    if(! substr(link_text ,1 , 1) == "h"){
      link_text <- paste0("https://www.football-lab.jp" , link_text) #full URL に変換する
    }
    return( link_text)
  }) #sapply
    return( ret_links)
} #func

# 2019年, 23節, 札幌の試合をとってくる
sapp_url <- get_gURL( mday  = 23  , year = 2019 , team="sapp")
# >sapp_url
# [1] "https://www.football-lab.jp/sapp/report/?year=2019&month=08&date=17"

お目当てのURLが取得できている。

データ取得編①:基本情報編

index(節、クラブ)を指定すれば、必要情報をとってくるような関数をつくる。
例として、20/9/23に行われたG大阪vs名古屋の試合情報ページからデータをとってくるとしよう。

欲しい情報は大きくわけて3つのエリアにわかれて記載されている。
①出場選手に関する情報...ページ上部「メンバー」のエリアに記載
②チャンスビルディングポイントに関する情報...ページ中部「チャンスビルディングポイント」のエリアに記載
③その他スタッツに関する情報...ページ下部「スタッツ」のエリアに記載

これらの情報を取得してくるには、html_text()やhtml_table()をつかう。

上記情報を具体的に取得してくるコードはあえて書かないが、
たとえば、J1の順位表をJリーグ公式から取得してくるには、以下のようなコードをかけばよい。

#URL読み込み
j1std_page <- read_html("https://www.jleague.jp/standings/j1/")
#CSSセレクタによる要素の取得
j1std_node <-  (html_nodes(j1std_page ,css = "table.scoreTable01.J1table"))[[1]]
j1std_tbl <- html_table( j1std_node , fill=T , header=T)

head( j1std_tbl,3)
# 順位                         クラブ名 勝点 試合数 勝 分 負 得点 失点 得失点
# 1 NA    1 川崎フロンターレ川崎フロンターレ   53     20 17  2  1   59   18     41
# 2 NA    2         セレッソ大阪セレッソ大阪   42     20 13  3  4   30   20     10
# 3 NA    3                 FC東京FC東京   38     21 11  5  5   35   27      8
# 直近5試合
# 1        NA
# 2        NA
# 3        NA

この要領で、必要情報を取得していく。

上記のテーブル群をさらっていくことで、ひとまず20/9/27時点で取得可能な情報をさらうことができた。

データ取得編②:AGI/KAGIなどを取りに行く

Football Labの魅力として、多くの独自指標がある*3
そのなかのAGI/KAGI(参考:AGI/KGIについての解説)は、サマリーページのほうにしかない。

したがってAGI/KAGIのデータをとりにいくためには、わざわざサマリーページを訪問していくしかない。
たとえば、大分トリニータの、2020年についてのAGI/KAGIをサマリページからとってくるには、以下のような関数をかくといい。

#[1], url : summary pageのURL
smr_tbl_get <- function(url){
    smr_page <- read_html( url)
    tbl_node <- html_table((html_nodes( smr_page, css="table.statsTbl10"))[[1]], fill=T)[-1,]
    colnames(tbl_node) <- c("MatchDay", "Date", "Youbi","Opponent", "Score", "HorA", "Stadium", "Audience","Tenki", "AGI","KAGI", "Chance_Rate","Shot","Shot_Rate","Possesion","Offense_CBP","Pass_CBP","Steal_CBP","Deffense_CBP", "Scorer","Manager")
    
    ###----AGI/KAGIをnumeric化する---###
    tbl_node$AGI <- as.numeric(tbl_node$AGI)
    tbl_node$KAGI <- as.numeric(tbl_node$KAGI)
    
    ###---得点/失点の情報取得----###
    score_l <-  lapply( tbl_node$Score , function(scr){
      scr_c  <- (strsplit( scr , "-"))[[1]]
      if( length(scr_c) == 0 ){
        return( c( NA , NA ))
      }else{
        return( as.numeric(scr_c)) #
      } #if
    }) #lapply

    score_mat <-  do.call( what = rbind, args = score_l)
    score_df <- as.data.frame( score_mat) #for
    colnames( score_df) <- c("Goal_For", "Goal_Against")
    
    ####---必要な部分だけ選んで、戻り値する---####
    stat_df <-  cbind( dplyr::select(  tbl_node , MatchDay , Opponent , HorA , AGI, KAGI) , score_df) 
    return( stat_df)
} #get_url

#実際にはこうやって使う
oita_smr <- smr_tbl_get( "https://www.football-lab.jp/oita/match/")
head( oita_smr, 4)
# MatchDay Opponent HorA AGI KAGI Goal_For Goal_Against
# 2        1   C大阪    A  52   58        0            1
# 3        2     鳥栖    H  36   58        2            0
# 4        3     広島    A  40   46        2            1
# 5        4     神戸    H  33   60        1            1

Conclusion

だいたいの分析用の基盤はできたので満足。
次の課題は、①差分取得のProgram②前後半など時間帯をわけたデータの整備、かな。


野猿 / Fish Fight!

Enjoy!!

*1:対象が1ページだったらコピペのほうが早いよね

*2:referenceをみても、そもそも関数の数自体がめちゃ少ない

*3:おそらく機械学習統計学にまつわる諸知識を動員して、つくったんだろうけど