pandasでいろいろplot

概要

ちょっとでっち上げたい数字があったんだけど、普通の回帰だと人間の目から見てイマイチな結果に陥りやすかったので、non-negativeな制約を付けて適当にでっち上げた。

non-negative least squareは名前の通り負の値を取らない最小二乗法。経験的にマイナスにはならない特徴を揃えた際などに使える。

@CretedDate 2015/10/25
@Versions python3.4.3, numpy1.9.2, scipy0.16.0, pandas0.16.2

サンプルデータ

野球を例に、各チームのシーズンの四球、単打、二塁打、三塁打、本塁打の数から、得点を予測するモデルを作成してみる。

あまり多くデータを収集するのは疲れるので、過去3年の12球団のデータ(計36レコード)を利用する。たった36件なので当然ながら足りてない感が出る。

以前、non-negativeでないOLSで同じことをやった時は、三塁打が二塁打より評価が低くなったり、逆に本塁打より評価が高くなったり、データセットによってまったく安定しなかった。これは三塁打の発生数が他の要素と比べて非常に小さい(本塁打の1/5程度しかない)ことが原因と思われる。

三塁打の係数が少しぶれたところで予測精度がそう変わるわけでもないのだけど、「こういった式になります」と人に見せる際には都合がよろしくないので、そのあたりを適当に誤魔化す用途でnon-negativeを利用してみる。

通常のOLS

non-negativeでないnumpyのlinear regression least squareを使ってみる。

import numpy as np
import scipy as sp
import pandas as pd

# 単打、二塁打、三塁打、本塁打、四死球、得点が入ったデータを取り入れる
df = pd.read_csv( 'data.csv' )

# numpyのlinalg.lstsqを使う
np.linalg.lstsq( df[['単打', '二塁打', '三塁打', '本塁打', '四死球']], df['得点'] )[0]

結果はこちら。左から単打、二塁打、三塁打、本塁打、四死球。

0.27946035,  0.29494675,  1.87172755,  1.53306554,  0.10193982

二塁打が単打とそれほど変わらない値になり、三塁打が本塁打より大きくなっている。さすがにデータが少なすぎるので、こうした値になるのも致し方なし。

尚、こんな感じで予測処理を実行できる。

fw = np.linalg.lstsq( df[['単打', '二塁打', '三塁打', '本塁打', '四死球']], df['得点'] )[0] 
df['予測得点'] = ( fw * df[['単打', '二塁打', '三塁打', '本塁打', '四死球']] ).sum(axis=1)
df[['得点', '予測得点']]
     得点        予測得点
0   473  508.218739
1   489  518.561172
2   465  499.986399
3   506  560.574189
4   508  545.079777

以下略

相関係数も見ておく。

df[['得点', '予測得点']].corr()
            得点      予測得点
得点    1.000000  0.897711
予測得点  0.897711  1.000000

だいたい0.9くらい。ちょっと低過ぎるな。

cross validationもしておく。n_folds=5くらいで。

from sklearn import cross_validation
kf = cross_validation.KFold( len(df), n_folds=5 )

for train_index, test_index in kf:
    train, test = df.loc[train_index], df.loc[test_index]
    fw = np.linalg.lstsq( train[['単打', '二塁打', '三塁打', '本塁打', '四死球']], train['得点'] )[0]
    test['予測得点'] = ( fw * test[['単打', '二塁打', '三塁打', '本塁打', '四死球']] ).sum(axis=1)
    print( test[['得点', '予測得点']].corr()['予測得点']['得点'], fw )

出力結果。相関係数, 単打, 二塁打, 三塁打, 本塁打, 四死球の並び。

0.926977400857 [ 0.33873498  0.31380096  1.96133767  1.37387592  0.0298322 ]
0.880858139041 [ 0.31170727  0.14376835  1.94194559  1.5366147   0.09847206]
0.714826705254 [ 0.18722966  0.37678487  2.38578755  1.62955564  0.18962385]
0.825556520029 [ 0.29611549  0.19844715  1.67433077  1.62454346  0.10333003]
0.868592852579 [ 0.23180633  0.525755    1.27241999  1.47280256  0.12227056]

やはり三塁打の係数がもっとも揺れている。あと二塁打もそこそこ揺れている。単打の方が二塁打より価値が上の結果になっているレコードもあったり。

一番下のレコードは一般的に考えられている係数にそこそこ近い。セイバーメトリクスのXRでは、0.5, 0.72, 1.04, 1.44, 0.34くらいになるはずだけど、これはMLBのデータで統一球下のNPBでは数字は落ちる。

non-negative版

今回の特徴はnon-negativeにするまでもなくプラスに働いているので、このまま利用しても結果は変わらない。

そこで、単打以上(単打+二塁打+三塁打+本塁打)、二塁打以上(二塁打+三塁打+本塁打)、三塁打以上(三塁打、本塁打)のように集計してみる。

df['単打以上'] = df['単打'] + df['二塁打'] + df['三塁打'] + df['本塁打']
df['二塁打以上'] = df['二塁打'] + df['三塁打'] + df['本塁打']
df['三塁打以上'] = df['三塁打'] + df['本塁打']
df['本塁打以上'] = df['本塁打']

これで係数を求めるとこんな感じ。

np.linalg.lstsq( df[['単打以上', '二塁打以上', '三塁打以上', '本塁打以上', '四死球']], df['得点'] )[0]
[ 0.27946035,  0.0154864 ,  1.5767808 , -0.33866201,  0.10193982]

三塁打で跳ね上がって、本塁打で逆に下がっている。

これをnon-negative(sp.optimize.nnlsを利用)にすればせめて三塁打=本塁打になって、価値が下がるということはなくなるはず。

sp.optimize.nnls( df[['単打以上', '二塁打以上', '三塁打以上', '本塁打以上', '四死球']], df['得点'] )[0]
[ 0.27836955,  0.0442818 ,  1.22343202,  0.        ,  0.10319832]
df['予測得点'] = ( sp.optimize.nnls( df[['単打以上', '二塁打以上', '三塁打以上', '本塁打以上', '四死球']], df['得点'] )[0] * df[['単打以上', '二塁打以上', '三塁打以上', '本塁打以上', '四死球']] ).sum(axis=1)
df[['得点', '予測得点']].corr()

結果、相関係数は0.896699とほぼ変わらない値が取れた。

cross validationもしておく。

kf = cross_validation.KFold( len(df), n_folds=5 )

for train_index, test_index in kf:
    train, test = df.loc[train_index], df.loc[test_index]
    fw = sp.optimize.nnls( train[['単打以上', '二塁打以上', '三塁打以上', '本塁打以上', '四死球']], train['得点'] )[0]
    test['予測得点'] = ( fw * test[['単打以上', '二塁打以上', '三塁打以上', '本塁打以上', '四死球']] ).sum(axis=1)
    print( test[['得点', '予測得点']].corr()['予測得点']['得点'], fw )
0.947991450085 [ 0.33786527  0.03061814  1.01604353  0.          0.03081697]
0.884863211364 [ 0.28476671  0.          1.25718189  0.          0.10435996]
0.786175468811 [ 0.19956266  0.17247152  1.29887519  0.          0.18925236]
0.831315758099 [ 0.28283751  0.          1.31461116  0.          0.10093315]
0.868592852579 [ 0.23180633  0.29394867  0.74666499  0.20038257  0.12227056]

単打と二塁打の価値が一緒になったり、三塁打と本塁打の価値が一緒になったりと課題は多いものの、逆転することはなくなった。corrも若干改善しているけどまあオマケ程度の話。人が見て「えっ?」と思わないことが今回の目的なので。

これの平均を使って係数をでっち上げると、こうなる。(arrは上述の結果をappendした配列)

df2 = pd.DataFrame( arr, columns=['単打', '二塁打', '三塁打', '本塁打', '四死球'] )
df2['本塁打'] = df2['単打'] + df2['二塁打'] + df2['三塁打'] + df2['本塁打']
df2['三塁打'] = df2['単打'] + df2['二塁打'] + df2['三塁打']
df2['二塁打'] = df2['単打'] + df2['二塁打']
df2.mean()
単打     0.267368
二塁打    0.366775
三塁打    1.493451
本塁打    1.533527
四死球    0.109527

二塁打、三塁打、本塁打のバランスが今ひとつ良くないけど、最初の結果よりはまあ人には見せやすいだろう。データを増やせばもう少しマシな値になるはず。

出来上がった式で結果確認。

df['予測得点'] = df['単打'] * 0.267368 + df['二塁打'] * 0.366775 + df['三塁打'] * 1.493451 + df['本塁打'] * 1.533527 + df['四死球'] * 0.109527
df[['予測得点', '得点']].corr()
          予測得点        得点
予測得点  1.000000  0.896853
得点    0.896853  1.000000

まあ、こんなもんでしょう。これででっち上げスキルが1つ上昇した。