バスケのターンオーバーとリバウンドは本当に得点につながるのか調べてみた

(日本男子バスケ大躍進おめでとうございます)

# 緒言
バスケでは，シュート成功率以外にも重要視されている数字があります．それがターンオーバーの数とリバウンドの数です．
それぞれについて簡単に説明します．

ターンオーバーとは，シュートミス以外で相手に攻撃権が移ることです．パスをカットされたり，スティールされたりして自チームが攻撃権を失うことです．当然，少なければ少ないほど良いとされています．「シュートミス以外で」というのは，「シュートミスはまぁ仕方ないかぁ」みたいな感じなのか，それとも「シュート成功率っていう別の指標があるし…」ということなのか，たぶんどっちかです．

リバウンドとは，どちらかのチームがシュートに失敗したボールをキャッチすることです．防衛の時に成功すれば相手の攻撃ターンを終わらせることができますし，攻撃中に成功すればたとえシュートをミスしても次のチャンスを得ることができます．ターンオーバーとは逆に，多ければ多いほど良いとされています．

しかし私にはバスケの試合経験がほとんどなく，実感として「リバウンドが多いほど勝ちやすいなぁ！！」と思ったことはありません．「多ければ多いほど良いとされている」といわれても，「理論的にはそう」という話にしか思えないのです．

そこで本記事では，本当にターンオーバーが少ないほど，あるいはリバウンドが多いほど得点につながるのか？ を調べてみました．

なお，ざっくりとした検証なので参考程度にどうぞ．

# 用いた計算方法
カイ二乗検定を用いました．
この検定方法について，簡単に説明します．

下のような感じで，相関が疑われるデータがあるとします(例えばこのデータだと，「ひょっとして一般入学者ほど赤点をとりやすいのでは…？」と予想できます)．

||推薦入学者|一般入学者|
|-|-|-|
|赤点あり|5|10|
|赤点なし|60|50|

この時，ひとまず「入学方式と入学後の成績は関係ない」と仮定します(これを帰無仮説といいます)．
カイ二乗検定では，この仮定が成り立つものとして計算を行い，もし不都合な結果が出れば「やっぱり仮定は成り立たないね」と判断します．数学の背理法みたいな感じです．
もうちょっと具体的に説明します．

例えば，六十回サイコロを振ったとき，それぞれの目が出る回数は，理論値としてはすべて十回ずつでしょう．しかし実際にはちょっとずれます．
ここでも同じことが言えます．「入学方式と入学後の成績は関係ない(と仮定してる)けど，ちょっとぐらいずれることだってあるよねー」って感じです．
そこで，まずは「もし帰無仮説が正しいなら，理論値はこんな感じになるよねー」という値を計算します(これを期待度数と呼びます)． ~~詳しい計算方法はほかのサイトで勉強してください．~~
ちなみにこの例だと期待度数は以下のようになります．

||推薦入学者|一般入学者|
|-|-|-|
|赤点あり|7.8|7.2|
|赤点なし|57.2|52.8|

次に，その「期待度数」と「実測値」を比較します．帰無仮説(入学方式と入学後の成績は関係ないはず！ という仮定)に従うとき，「実測値」は「期待度数」から正規分布的にずれると仮定します．そうなると，「帰無仮説が成り立つなら，○%の確率で実測値は理論値の±●以内の値になる」とか具体的な数値が言えるわけです．
○とか●とかを難しく言った言葉がp値とかカイ二乗値です． ~~詳しくは別のサイトで勉強してください．~~

p値を見れば，「帰無仮説に従うとき，この実測値になる確率はチョメチョメ%だから現実的にはあり得ないな…．じゃあ帰無仮説は正しくない！」とか判断できます．
一般的にはp値が0.05(つまり5%)を下回れば，「現実的にはあり得ない」と判断します．「なんで5%って決まってんの？」とか聞かないでください．「相関係数は0.7超えたら強い相関があるってことにしよう！」という考えと似たようなものです，たぶん．

# 計算と結果
2016年から2023年の間の，Bリーグの試合結果を用いました．
リンクは[参考文献とお借りしたデータ](#参考文献とお借りしたデータ)に記載しています．
また，計算に使用したコードは[全コード](#全コード)参照．
## ターンオーバー
一試合ごとのターンオーバー数と得点が，それぞれ平均を超えているチームとそうでないチームでざっくり四つのグループに分けました．
それぞれを「高ターンオーバー」とか「低得点」とか呼んでいます．
下の表は，ターンオーバーの数と得点がともに平均を超えているのが25チーム，ターンオーバーの数は平均以下で得点が平均を超えてるのが43チーム，…というふうに見ます．

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>高得点</th>
      <th>低得点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>高ターンオーバー</th>
      <td>25</td>
      <td>44</td>
    </tr>
    <tr>
      <th>低ターンオーバー</th>
      <td>43</td>
      <td>26</td>
    </tr>
  </tbody>
</table>

なんとなくこの時点で，「ターンオーバーが少ないほど高得点になりがちだなー」という印象を受けます．
期待度数は以下の通り．

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>高得点</th>
      <th>低得点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>高ターンオーバー</th>
      <td>34.0</td>
      <td>35.0</td>
    </tr>
    <tr>
      <th>低ターンオーバー</th>
      <td>34.0</td>
      <td>35.0</td>
    </tr>
  </tbody>
</table>

p値は0.004でした．

つまり，帰無仮説(ターンオーバーと得点には相関関係がないという仮定)が正しいなら，この観測値が得られる確率は0.4%です．
現実的にあり得ない数値ですので，ターンオーバーと得点の間には相関関係があるってことですね．

## リバウンド
ターンオーバーと同様，一試合ごとのリバウンド数と得点数がそれぞれ平均を超えているチームとそうでないチームでざっくり分けました．

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>高得点</th>
      <th>低得点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>高リバウンド</th>
      <td>38</td>
      <td>29</td>
    </tr>
    <tr>
      <th>低リバウンド</th>
      <td>30</td>
      <td>41</td>
    </tr>
  </tbody>
</table>

リバウンドも定説通り，「多いほど高得点につながりやすいっぽい？」という印象を受けますが，ターンオーバーほど極端ではない印象．
期待度数は以下の通り．

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>高得点</th>
      <th>低得点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>高リバウンド</th>
      <td>33.014493</td>
      <td>33.985507</td>
    </tr>
    <tr>
      <th>低リバウンド</th>
      <td>34.985507</td>
      <td>36.014493</td>
    </tr>
  </tbody>
</table>

p値は0.126でした．「リバウンドの数と得点数の間には相関関係がない」と仮定したとき，この観測値は12.6%の確率で得られます．
5%を超えちゃってますので，「本当は相関関係がないけど，たまたま関係をにおわせるデータが取れちゃった」と判断します．

# 結言
今からこの記事を全否定しますが，「相関関係=因果関係」とは限りません．
ターンオーバーが少ないほど得点が多いからって，「じゃあターンオーバーを減らせば得点が増えるね！」という証明にはなりません(よく分からない人は「アイス 水難事故」とか「リップクリーム 火災」とかでググってください)．
つまり緒言で述べた「調べてみました」はカイ二乗検定では調べられないんですね． ~~とうかカイ二乗検定の得意分野は離散値なのでこの場合は相関係数のほうが合ってる~~

ひとまず，ターンオーバーと得点には強い相関があるけどリバウンドと得点の間にはそんなに強い関係は認められなかったよーという記事でした．

# 参考文献とお借りしたデータ

https://www.bleague.jp/stats/?year=2022&tab=1&target=club-b1&value=AveragePoints&o=desc&e=2&dt=avg

https://ninthcode.net/210806-chi-square-test/

# 全コード
## 例

```py
import pandas as pd
from scipy import stats

df = pd.DataFrame(
    {
        "推薦入学者": [5, 60],
        "一般入学者": [10, 50]
    },
    index=["赤点あり", "赤点なし"]
)

chi2, p, dof, exp = stats.chi2_contingency(df, correction=True)

print("期待度数\n", exp)
print("自由度\n", dof)
print("カイ二乗値\n", chi2)
print("p値\n", p)
```

## ターンオーバー
2020年以前とそのあとでデータ収集方法が変わっているので，それに合わせてデータの扱いを変えています．

```py
import pandas as pd
from scipy import stats

years = [year for year in range(2016, 2023)]

df = pd.DataFrame(
    {
        "PPG": [],
        "TOPG": [],
        "RPG": []
    }
)

for year in years:
    if year >= 2020:
        df_tmp = pd.read_html(f"https://www.bleague.jp/stats/?year={year}&tab=1&target=club-b1&value=AveragePoints&o=desc&e=2&dt=avg")[0]
        df_tmp = pd.DataFrame(
            {
                "PPG": df_tmp["得点・失点"]["PPG"],
                "TOPG": df_tmp["ターンオーバー"]["TOPG"],
                "RPG": df_tmp["リバウンド"]["RPG"]
            }
        )
    else:
        df_tmp = pd.read_html(f"https://www.bleague.jp/stats/?year={year}&tab=1&target=club-b1&value=TotalPoints&o=desc&e=2&dt=tot")[0]
        df_tmp = pd.DataFrame(
            {
                "PPG": df_tmp["得点"]["PTS"] / df_tmp["試合数"]["G"],
                "TOPG": df_tmp["ターンオーバー"]["TO"] / df_tmp["試合数"]["G"],
                "RPG": df_tmp["リバウンド"]["RPG"]
            }
        )
    df = pd.concat([df, df_tmp])

TOPG_median = df["TOPG"].median()
PPG_median = df["PPG"].median()

crossed = pd.DataFrame(
    [
        [
            # 高ターンオーバーかつ高得点
            len(df.query(f"TOPG > {TOPG_median} and PPG > {PPG_median}")),
            # 高ターンオーバーかつ低得点
            len(df.query(f"TOPG > {TOPG_median} and PPG <= {PPG_median}"))
        ],
        [
            # 低ターンオーバーかつ高得点
            len(df.query(f"TOPG <= {TOPG_median} and PPG > {PPG_median}")),
            # 低ターンオーバーかつ低得点
            len(df.query(f"TOPG <= {TOPG_median} and PPG <= {PPG_median}"))
        ]
    ],
    index=["高ターンオーバー", "低ターンオーバー"],
    columns=["高得点", "低得点"]
)

chi2, p, dof, exp = stats.chi2_contingency(crossed, correction=True)

print("期待度数\n", exp)
print("自由度\n", dof)
print("カイ二乗値\n", chi2)
print("p値\n", p)
```

## リバウンド
dfは上のターンオーバーの時と同じもの使ってます．

```py
RPG_median = df["RPG"].median()
PPG_median = df["PPG"].median()

crossed = pd.DataFrame(
    [
        [
            # 高リバウンドかつ高得点
            len(df.query(f"RPG > {RPG_median} and PPG > {PPG_median}")),
            # 高リバウンドかつ低得点
            len(df.query(f"RPG > {RPG_median} and PPG <= {PPG_median}"))
        ],
        [
            # 低リバウンドかつ高得点
            len(df.query(f"RPG <= {RPG_median} and PPG > {PPG_median}")),
            # 低リバウンドかつ低得点
            len(df.query(f"RPG <= {RPG_median} and PPG <= {PPG_median}"))
        ]
    ],
    index=["高リバウンド", "低リバウンド"],
    columns=["高得点", "低得点"]
)

chi2, p, dof, exp = stats.chi2_contingency(crossed, correction=True)

print("期待度数\n", exp)
print("自由度\n", dof)
print("カイ二乗値\n", chi2)
print("p値\n", p)
```