dersblog

Dünya Kupası 2014, Veri Analizi

Daha önceki maç verisine bakarak 2014 Dünya Kupası maçlarını tahmin edebilen istatistik (yapay öğrenim) teknikleri Google mühendisleri tarafından paylaşıldı, kullanılan teknik lojistik regresyon. Verinin şekillendirilmesi, veriden özellik (feature) yaratmak işin püf noktalarından - veri hangi detay seviyesinde (maç seviyesinde mi takım seviyesinde mi) ve hangi kolonlar üzerinden modele dahil edilecek? Görülüyor ki nihai regresyon her maç için iki takımı yanyana koyuyor (A takımı öğeleri belli kolonlar B öğeleri belli kolonlar) ve 1,0 etiketini tahmine uğraşıyor. Öğelerin önemli bir özelliği o ana kadar her iki takımın oynadığı önceki N maçın özeti olmaları. Yani A takımı son 3 maçta (N=3) maçta dakikada 5 pas atmışsa passes öğesi 5 olacaktır, B takımı dakikada 10 atmışsa op_passes 10 olacaktır. Böylece lojistik regresyona 5'e karşı 10 pas ağırlığı olan bir veri satırı hakkında irdeleme yapma imkanı veriyoruz; ve bilinen etikete göre LR gerekli ağırlıkları hesaplayarak sonuca erişiyor.

Projede kullanılan 4 Python dosyası var:

match_stats: Maç istatistiklerini yükleyen kodlar.

features: Ham istatistik verileri özelliklere (features) döndürüyor, ki bu özellikler yapay öğrenim modeline girilebilsin. Bu özellikler önceki K maçın verilerini özetleme amaçlı yaratıldılar, ki bu özelliklere dayanarak bir sonraki maçı tahmnin edebilelim.

world_cup: Veriyi temizlemek ve modeli kurmak için kullanılan yardımcı kodlar.

power: Birbiriyle belli sayıda maç yapmış takımların bir ``güç sıralamasını'' hesaplamak.

Özellik inşası

Sonraki maç tahmini için önceki K maçın özet istatistiklerine bakıyoruz, K'nın ne olduğu history_size ile tanımlı.

import world_cup
import features
import match_stats
import pandas as pd

history_size = 3

game_summaries = features.get_game_summaries()
data = features.get_features(history_size)

Bu özellikler, dediğimiz gibi, önceki K maçın özeti. Bu özetlerin çoğu bir ortalamadır, ayrıca bu ortalamaların çoğu dakika bazlı çünkü maç zamanını aşan maçları da hesaba katmak için.. Eğer maç başına yapılan pas değeri alınsaydı, o zaman vakti aşan bir maçta o değer normalden çok daha fazla olacaktı, bu modeli bozardı.

Modelde kullanılacak özellikler:

is_home: Takım evinde mi, deplasmanda mı oynuyor. Futbolda bu değişkenin çok önemli olduğunu biliyoruz.

avg_points: Önceki K maçta kazanılan ortalama puan (galibiyet için 3, eşitlik için 1, kayıp için 0).

avg_goals: Önceki K maçta atılan averaj gol.

op_average_goals: Rakip tarafından son K maçta atılan averaj gol.

pass_70/80: Hücum sahasının 30%-20%'sinde dakika başına verilen başarılı pas.

op_pass70/80: Hücum sahasının 30%-20%'sinde rakip tarafından verilmiş dakika bazında başarılı paslar.

expected_goals: Son K maçtaki gol beklentisi, ki bu beklenti atılan şut ve ve şutun kaleden uzaklığı baz alınarak hesaplanan bir sayı.

passes: Dakika başına atılan paslar.

bad_passes: Dakika bazında verilen ama başarılı olmayan paslar.

pass_ratio: Başarılı pasların oranı.

corners: Dakika bazında atılan kornerler.

fouls: Yapılan faul sayısı (dk bazlı)

cards: Kırmızı ya da sarı alınan kart ceza sayısı (maç başına).

shots: Dakika bazında atılan şut.

op_*: Rakipler hakkındaki bazı tarihi istatistikler. Dikkat, bu `rakip''opteamnamede gösterilen rakip değil, genel olarak bu takımın rakiplerinin ona karşı nasıl oynadığını göstermeye çalışan bir istatistik. Meselaop_corners` bu takımın rakiplerinin dakika başına kaç korner kazandığını gösteriyor.

*_op_ratio: Takimin istatistiklerinin rakiplerine olan orani [?]

Ozellik olmayan kolonlar

matchid: Maçın id'si

teamid: Takımın id'si

op_teamid: Rakip takımın özgün id'si

team_name: Takımın ismi

op_team_name: Rakip takımın ismi

timestamp: Maç ne zaman oynandı

competitionid: Genel müsabakayı gösteren kod (dünya kupası, vs).

Hedef kolonlar:

Alttaki kolonlar tahmin edilmeye uğraşılabilecek olan kolonlar. Eğer bilinen veri üzerinde tahmin yapmak istiyorsak, bu kolonları tahmin öncesi dışarı atmalıyız, bunu unutmayalım. Birkaç hedef kolon var ama, biz sadece kazanılan puanı tahmin etmeye uğraşacağız, belki diğer modeller diğer kolonları tahmin etmeye uğraşırlar, mesela atılan gol sayısı gibi.

points: Maçın puan sonucu.

goals: teamid deki takımın attığı gol sayısı.

op_goals: op_teamid ile gösterilen takımın attığı gol sayısı.

club_data = data[data['competitionid'] <> 4]
# Show the features latest game in competition id 4, which is the world cup.
print data[data['competitionid'] == 4].iloc[0]
matchid                                  731828
teamid                                      366
op_teamid                                   632
competitionid                                 4
seasonid                                   2013
is_home                                       0
team_name                           Netherlands
op_team_name                          Argentina
timestamp            2014-07-09 21:00:00.000000
goals                                         0
op_goals                                      0
points                                        1
avg_points                              2.33333
avg_goals                               1.33333
op_avg_goals                           0.333333
pass_70                                0.472036
pass_80                                0.150698
op_pass_70                              0.26478
op_pass_80                             0.078501
expected_goals                          1.44437
op_expected_goals                      0.411425
passes                                  3.83486
bad_passes                              1.01362
pass_ratio                             0.765595
corners                               0.0709912
fouls                                  0.126237
cards                                         1
shots                                  0.155226
op_passes                               3.38986
op_bad_passes                           1.02455
op_corners                            0.0346796
op_fouls                               0.157066
op_cards                                2.66667
op_shots                              0.0924966
goals_op_ratio                          1.33333
shots_op_ratio                          1.70227
pass_op_ratio                           1.02543
Name: 0, dtype: object

Maç bazında atılan goller ve maçın sonucunu eksenlere alarak bir tablo yaratalım (crosstab).

import pandas as pd
print pd.crosstab(
    club_data['goals'], 
    club_data.replace(
        {'points': {
            0: 'lose', 1: 'tie', 3: 'win'}})['points'])
points  lose  tie  win
goals                 
0        768  279    0
1        508  416  334
2        134  218  531
3         23   42  325
4          2    6  158
5          0    2   67
6          0    0   13
7          0    0    6
8          0    0    1

5'den fazla gol atmak tabii ki kazanmayı garantiliyor, hiç atmamak 75\% ihtimalle kaybedilecek demektir (bazen de beraberlik olur tabii!). Not: Fakat tabloda 4 gol sonrası kazanımlar direk artmıyor, niye? Çünkü bu maçlar uzatma sonrası atılan penaltılardan geliyor, her iki takımda bu sırada çok gol atıyor, ve biri mutlaka kaybediyor [1].

Modeli eğitmek

Veri tabanımızdaki klüp verisini kullanarak (yani hiç dünya kupası verisi kullanmadan) eğiteceğiz. Bu kod world_cup.py içinde. Sonuç bir lojistik regresyon modeli olacak, ve sonra test verisi üzerinde tahmin yapacağız. Regresyonun Rsquared değerini göstereceğiz, ki bu eğitim verisi üzerinden gösterilebilir. Rsquared modelin veriye ne kadar uyduğunu gösteren bir rakamdır, ne kadar yüksekse o kadar iyidir.

import world_cup
reload(world_cup)
import match_stats
pd.set_option('display.width', 80)

# Don't train on games that ended in a draw, since they have less signal.
train = club_data.loc[club_data['points'] <> 1] 
# train = club_data

(model, test) = world_cup.train_model(
     train, match_stats.get_non_feature_columns())
print "Rsquared: %0.03g" % model.prsquared
Rsquared: 0.149

Önemli özellikleri seçmek

Lojistik regresyon modelimiz regülarizasyon kullanıyor; bu demektir ki daha çetrefil modeller cezalandırılıyor. Bu cezalandırmanın yan etkisi olarak biz hangi özelliklerin daha önemli olduğunu görebiliyoruz, çünkü daha önemsiz olan özellikler modelden atılıyorlar (katsayıları sıfıra iniyor).

Bu bağlamda özellikleri üçe ayırabiliriz:

Pozitif özellikler: Bu özellikler mevcut ise takımın kazanma şansı yükseliyor.

Negative özellikler: Tam tersi

Atılan değerler: Önemli olmayan özellikler, ki bu özellikler modele dahil edilirse aşırı uygunluk (overfitting) durumu ortaya çıkar.

def print_params(model, limit=None):    
    params = model.params.copy()
    params.sort(ascending=False)
    del params['intercept']

    if not limit:
        limit = len(params)

    print("Pozitif ozellikler")
    params.sort(ascending=False)
    print np.exp(params[[param > 0.001 for param in params]]).sub(1)[:limit]

    print("\nAtilan ozellikler")
    print params[[param  == 0.0 for param in params]][:limit]

    print("\nNegatif ozellikler")
    params.sort(ascending=True)
    print np.exp(params[[param < -0.001 for param in params]]).sub(1)[:limit]

print_params(model, 10)
Pozitif ozellikler
is_home           0.848337
pass_70           0.254729
expected_goals    0.169235
opp_op_corners    0.159163
op_passes         0.120319
opp_op_pass_80    0.095970
avg_goals         0.092000
opp_bad_passes    0.075657
opp_cards         0.068903
fouls             0.062809
dtype: float64

Atilan ozellikler
op_pass_70            0
opp_op_cards          0
op_bad_passes         0
opp_op_bad_passes     0
opp_op_fouls          0
corners               0
pass_ratio            0
opp_corners           0
op_fouls              0
opp_goals_op_ratio    0
dtype: float64

Negatif ozellikler
opp_pass_70          -0.203015
opp_expected_goals   -0.144740
op_corners           -0.137309
opp_op_passes        -0.107397
op_pass_80           -0.087566
opp_avg_goals        -0.084249
bad_passes           -0.070335
cards                -0.064461
opp_fouls            -0.059097
opp_passes           -0.049240
dtype: float64

Klüp verisi üzerinde tahmin

predicted: Takımın kazanma şansı (tahmin).

points: Gerçekten ne oldu.

reload(world_cup)
results = world_cup.predict_model(model, test, match_stats.get_non_feature_columns())

predictions = world_cup.extract_predictions(results.copy(), results['predicted'])

print 'Dogru tahminler:'
print predictions[(predictions['predicted'] > 50) & (predictions['points'] == 3)][:5]
Dogru tahminler:
             team_name         op_team_name  predicted            expected  \
8     Portland Timbers       Real Salt Lake  52.418756    Portland Timbers   
42      Rayo Vallecano           Granada CF  60.862465      Rayo Vallecano   
49  Atltico de Madrid               Getafe  64.383541  Atltico de Madrid   
57     Colorado Rapids  Vancouver Whitecaps  51.836366     Colorado Rapids   
58         Real Madrid        Real Sociedad  64.100904         Real Madrid   

                winner  points  
8     Portland Timbers       3  
42      Rayo Vallecano       3  
49  Atltico de Madrid       3  
57     Colorado Rapids       3  
58         Real Madrid       3  
print 'Yanlis tahminler:'
print predictions[(predictions['predicted'] > 50) & (predictions['points'] < 3)][:5]
Yanlis tahminler:
                 team_name         op_team_name  predicted  \
1      Seattle Sounders FC  Vancouver Whitecaps  51.544963   
2   New England Revolution       Real Salt Lake  63.950714   
3       Philadelphia Union            FC Dallas  54.213693   
14  New England Revolution      Montreal Impact  52.762065   
20      New York Red Bulls           Toronto FC  55.533969   

                  expected               winner  points  
1      Seattle Sounders FC  Vancouver Whitecaps       0  
2   New England Revolution       Real Salt Lake       0  
3       Philadelphia Union            FC Dallas       0  
14  New England Revolution      Montreal Impact       0  
20      New York Red Bulls           Toronto FC       0  

Tahminlerimizi kontrol etmek

Kontrol için mesela hesabımızın rasgele tahminden ne kadar iyi olduğunu hesaplayabiliriz (lift) ya da AUC hesabı yapıp ROC eğrisini hesaplarız. AUC herhalde en iyisi, bu hesap çok ilginçtir, 0.5 (kafadan atmak) ve 1.0 arasındadır (mükemmel tahmin), ve bu hesap dengesiz veri setlerine karşı dayanıklıdır. Mesela 0/1 etiketi tahmininde test setinde diyelim ki yüzde 90 oranında olsa ve modelimiz sürekli 1 tahmin etse, basit bir ölçüm bize modelimizin yüzde 90 başarılı olduğunu söylerdi. AUC böyle durumlara karşı dayanıklıdır, bize 0.5 sonucunu verir.

baseline = (sum([yval == 3 for yval in club_data['points']]) 
            * 1.0 / len(club_data))
y = [yval == 3 for yval in test['points']]
world_cup.validate(3, y, results['predicted'], baseline, 
                   compute_auc=True)
plt.savefig('stat_worldcup_01.png')
(3) Lift: 1.42 Auc: 0.738

Modelde eksik olan bir şey var; sonraki maçı önceki birkaç maçın özetinden tahmin etmeye uğraşıyoruz ama belki bazı takımlar önceki K maçta çok zorlu rakiplerle uğraşmıştır, bazıları çok kolay rakiplerle uğraşmıştır. Bu durumda önceki maçların istatistiği bize tüm hikayeyi anlatmayacaktır.

Bu problemi çözmek için ayrı bir regresyon daha işletebiliriz. Bu regresyon bir güç sıralaması (power ranking) hesaplayabilir, bu hesap FIFA/CocaCola'nın enternasyonel takımlar için yaptığı güç sıralama hesabına benzer. ABD'de beyzbol ve Amerikan futbolu için de benzer bir hesap yapılıyor.

Güç sıralaması hesabını yaptıktan sonra -tek bir sayı, bazı takımlar için daha yüksek bazı takımlar için daha alçak, ki onun üzerinden sıralama yapılabilsin- onu bir özellik olarak lojistik regresyon modeline dahil edebiliriz. Güç sıralaması esas olarak şu tür irdelemelerin modelimize dahilini mümkün kılar; A takımı B'yi yendiyse, B C'yi yendiyse, A büyük ihtimalle C'yi yener. Bu tür bilgi niye önemli? Çünkü elimizde yapılabilecek tüm maçların kombinasyonu yok, maç verisi seyrek (sparse). Ama eldeki birkaç maçtan bir güç sıralaması hesaplayabilirsek, bu bize takımlar arasında, daha önce maç oynamamış olsalar bile, otomatik olarak bir ek irdeleme yapabilmemizi sağlayacaktır.

Sıralama hesabı yapıldıktan sonra bazı kontrolleri hızla, çıplak gözle yapabiliriz, mesela sonuca bakarız, eğer Wiggan (zayıf bir takım) 1.0 değeri almış, Chelsea (güçlü bir takım) 0.0 değeri almış ise bir şeyler yanlış demektir.

Tabii buna rağmen bazı takımlara hala uygun sıralama veremeyebiliriz, mesela A,B'yi, B,C'yi yeniyor, sonra veriye göre, C A'yı yeniyor. Bu şekilde sıralayamadığımız durumda takıma 0.5 verip tam ortaya koyacağız.

Ayrıca enternasyonel takımların sıralaması çok gürültülü veri olduğu ve (klüp verisinden bile daha) seyrek olduğu için onu yüzdeliklere (quartiles) ayırarak göstereceğiz, yani sıralamalar 0, .33, .66, or 1.0 olarak gözükecekler.

Fakat hesap işi bitince, ve bu sıralamayı nihai lojistik modele dahil edince başarı oranımızın zıplama yaptığını göreceğiz.

import power
reload(power)
reload(world_cup)
def points_to_sgn(p):
  if p > 0.1: return 1.0
  elif p < -0.1: return -1.0
  else: return 0.0
power_cols = [
  ('points', points_to_sgn, 'points'),
]

power_data = power.add_power(club_data, game_summaries, power_cols)
power_train = power_data.loc[power_data['points'] <> 1] 

# power_train = power_data
(power_model, power_test) = world_cup.train_model(
    power_train, match_stats.get_non_feature_columns())
print "\nRsquared: %0.03g, Power Coef %0.03g" % (
    power_model.prsquared, 
    math.exp(power_model.params['power_points']))

power_results = world_cup.predict_model(power_model, power_test, 
    match_stats.get_non_feature_columns())
power_y = [yval == 3 for yval in power_test['points']]
world_cup.validate(3, power_y, power_results['predicted'], baseline, 
                   compute_auc=True, quiet=False)

print_params(power_model, 8)

plt.plot([0, 1], [0, 1], '--', color=(0.6, 0.6, 0.6), label='Luck')
# Add the old model to the graph
world_cup.validate('old', y, results['predicted'], baseline, 
                   compute_auc=True, quiet=True)
plt.legend(loc="lower right")
plt.savefig('world_cup_02.png')
New season 2014
New season 2013
New season 2013
New season 2012
New season 2012
New season 2011

['Blackburn Rovers: 0.000', 'Real Betis: 0.000', 'D.C. United: 0.000',
'Celta de Vigo: 0.004', 'Deportivo de La Coru\xc3\xb1a: 0.009',
'Wolverhampton Wanderers: 0.021', 'Reading: 0.022', 'Real Zaragoza: 0.026',
'Real Valladolid: 0.044', 'Granada CF: 0.062', 'Queens Park Rangers:
0.073', 'Mallorca: 0.089', 'Aston Villa: 0.092', 'Bolton Wanderers: 0.102',
'Osasuna: 0.109', 'Espanyol: 0.112', 'Wigan Athletic: 0.124', 'Sunderland:
0.130', 'Rayo Vallecano: 0.138', 'Almer\xc3\xada: 0.145', 'Levante: 0.148',
'Elche: 0.154', 'Getafe: 0.170', 'Swansea City: 0.192', 'Southampton:
0.197', 'Norwich City: 0.206', 'Toronto FC: 0.211', 'Chivas USA: 0.218',
'West Ham United: 0.220', 'West Bromwich Albion: 0.224', 'Villarreal:
0.231', 'Stoke City: 0.255', 'Fulham: 0.274', 'Valencia: 0.296', 'Valencia
CF: 0.296', 'M\xc3\xa1laga: 0.305', 'Newcastle United: 0.342', 'Sevilla:
0.365', 'Columbus Crew: 0.366', 'Athletic Club: 0.386', 'Liverpool: 0.397',
'Everton: 0.417', 'Philadelphia Union: 0.466', 'Montreal Impact: 0.470',
'Chelsea: 0.530', 'Real Sociedad: 0.535', 'Tottenham Hotspur: 0.551',
'Arsenal: 0.592', 'Houston Dynamo: 0.593', 'FC Dallas: 0.612', 'Chicago
Fire: 0.612', 'Vancouver Whitecaps: 0.615', 'San Jose Earthquakes: 0.632',
'New England Revolution: 0.634', 'Atl\xc3\xa9tico de Madrid: 0.672',
'Colorado Rapids: 0.743', 'Barcelona: 0.759', 'Seattle Sounders FC: 0.781',
'New York Red Bulls: 0.814', 'Sporting Kansas City: 0.854', 'LA Galaxy:
0.882', 'Real Salt Lake: 0.922', 'Manchester City: 0.928', 'Real Madrid:
1.000', 'Manchester United: 1.000', 'Portland Timbers: 1.000'] 

Rsquared: 0.22, Power Coef 2.18
(3) Lift: 1.56 Auc: 0.791
    Base: 0.374 Acc: 0.708 P(1|t): 0.778 P(0|f): 0.667
    Fp/Fn/Tp/Tn p/n/c: 99/248/347/496 595/595/1190
Pozitif ozellikler
power_points      1.177169
is_home           0.787110
opp_op_corners    0.170848
expected_goals    0.058597
opp_cards         0.045538
pass_70           0.036267
avg_goals         0.035456
opp_avg_points    0.033857
dtype: float64

Atilan ozellikler
passes                0
op_pass_80            0
op_expected_goals     0
opp_shots_op_ratio    0
bad_passes            0
pass_ratio            0
opp_pass_op_ratio     0
shots                 0
dtype: float64

Negatif ozellikler
opp_power_points     -0.540688
op_corners           -0.145918
opp_expected_goals   -0.055353
cards                -0.043555
opp_pass_70          -0.034997
opp_avg_goals        -0.034242
avg_points           -0.032748
opp_fouls            -0.022867
dtype: float64
(old) Lift: 1.42 Auc: 0.738

Şimdi dünya kupasını tahmin edelim!

Aynen klüp verisinde yaptığımız gibi dünya kupası için de benzer istatistikleri hesaplayabiliriz. Bu durumda elimizde hedefler olmayacak, yani kimin kazandığını bilemeyeceğiz (aslında bazı dünya kupası maçlarının sonucunu biliyoruz, ama tahminlerimizi hiçbir maçı bilmiyormuş gibi yapalım). Ve tekrar vurgulayalım: klüp verisiyle eğittiğimiz modeli kullanarak dünya kupasını tahmin edeceğiz. Yani model ve tahmin tamamen farklı takımlar üzerinden yapılacak!

features.get_wc_features() bize tüm dünya kupası maçları için gereken özellikleri yaratıp döndürecektir.

import world_cup
import features
reload(match_stats)
reload(features)
reload(world_cup)
wc_data = world_cup.prepare_data(features.get_wc_features(history_size))
wc_labeled = world_cup.prepare_data(features.get_features(history_size))
wc_labeled = wc_labeled[wc_labeled['competitionid'] == 4]
wc_power_train = game_summaries[game_summaries['competitionid'] == 4].copy()

Ev sahası avantajı

Klüp verisi ile dünya kupası verisi arasındaki bazı farklardan biri budur: dünya kupasında bir maçta ev sahibi olmak ya da deplasmanda olmak ne demektir? Resmi olarak tek ev sahibi tüm 2014 kupasına ev sahipliği yapan Brezilya'dır, o zaman sadece Brezilya mı oynadığı maçlarda ev sahibi olabilir? Bu pek akla yatkın gelmiyor.

Belki diğer Latin Amerika takımlarını da ev sahibi olarak görebiliriz.. Adam aynı kıtadan, belki o kıtaya alışık, iklimi vs ona normal geliyor.. ? Olabilir mi? Diğer bazı modeller is_home u sadece Brezilya'ya vermiş, sonra aynı kitadaki diğer takımlara da 'azıcık' ev sahipliği vermişler, çünkü istatistiklere göre bu takımlar kendi kıtalarında daha iyi performans gösteriyorlarmış, vs.

Biz daha değişik bir model kullanacağız, bu model belki biraz sübjektif.. Biz is_home öğesine 0.0 ila 1.0 arasında bir değer atayacağız, ve bu değerin büyüklüğü o takımın taraftarlarının hem sayı, hem de destek enerjisi üzerinden ölçülecek. Bunu yapmamızın sebebi ilk turlarda görüldüğü üzere, taraftarının daha iyi desteklediği takımların diğerlerine göre daha iyi performans göstermesi. Mesela Şili'nin taraftarı takımını müthiş destekledi, İspanya taraftarı oralı bile olmadı, Şili-İspanya maçını Şili 2-0 kazandı. Bunun gibi pek çok maç gözlemledik, çoğunda güney Amerika takımları vardı, ama çok taraftar gönderen takımlar da vardı, mesela Meksika. Ya da ABD vardı, çok taraftarı vardı ama sessizdiler, onlar daha düşük skorlar aldılar.

import pandas as pd
wc_home = pd.read_csv('wc_home.csv')

def add_home_override(df, home_map):
  for ii in xrange(len(df)):
    team = df.iloc[ii]['teamid']
    if team in home_map:
        df['is_home'].iloc[ii] = home_map[team]
    else:
        # If we don't know, assume not at home.
        df['is_home'].iloc[ii] = 0.0

home_override = {}
for ii in xrange(len(wc_home)):
    row = wc_home.iloc[ii]
    home_override[row['teamid']] = row['is_home']

# Add home team overrides.
add_home_override(wc_data, home_override)    

Dünya Kupası Güç Sıralaması

Bu hesabın dünya kupası verisi üzerinde yapılması lazım, çünkü güç sıralaması o takımların arasındaki maçlara dayanılarak yapılan bir hesap. Bu maçlar ise, dünya kupası takımları bağlamında, oldukça seyrek çünkü bazı takımlar bazı takımlarla neredeyse onyıldır oynamamış. Çoğu Avrupa takımı mesela güney Amerika takımıyla oynamamış, Asyalı takımlarla daha bile az oynamış. Klüp bazında kullandığımız aynı numarayı burada da kullanabiliriz, ama başarısızlığa hazır olmak lazım!

Hesap altta

# Guc verisiyle egitirken, kupa birden fazla maca yayildigi icin 
# is_home'u 0.5'e set et. Yoksa 2010'daki kupa maclarina baktigimizda
# Guney Afrika yerine Brezilya'nin hala ev sahibi olduugnu zannedebilirdik.

wc_power_train['is_home'] = 0.5
wc_power_data = power.add_power(wc_data, wc_power_train, power_cols)

wc_results = world_cup.predict_model(power_model, wc_power_data, 
    match_stats.get_non_feature_columns())
New season 2013
New season 2009
New season 6
['Australia: 0.000', 'Serbia: 0.016', 'USA: 0.017', 'Cameroon: 0.035',
'Iran: 0.081', 'Croatia: 0.180', 'Nigeria: 0.204', "C\xc3\xb4te d'Ivoire:
0.244", 'Costa Rica: 0.254', 'Algeria: 0.267', 'Paraguay: 0.277',
'Honduras: 0.279', 'Slovakia: 0.281', 'Greece: 0.284', 'Switzerland:
0.291', 'Ecuador: 0.342', 'Uruguay: 0.367', 'Sweden: 0.386', 'Japan:
0.406', 'Mexico: 0.409', 'Chile: 0.413', 'Colombia: 0.438', 'England:
0.460', 'Belgium: 0.467', 'Ukraine: 0.470', 'Portugal: 0.487', 'Ghana:
0.519', 'South Korea: 0.532', 'France: 0.648', 'Spain: 0.736', 'Argentina:
0.793', 'Italy: 0.798', 'Brazil: 0.898', 'Netherlands: 0.918', 'Germany:
1.000'] 

Güç sırası da ayrı bir lojistik regresyon aslında, power.py içinde biz bu regresyona giren matris ve etiketleri hesap yapılmadan önce çekip çıkarttık, ve bir dosyaya kaydettirdik. Bakarsak,

games = pd.read_csv('/tmp/games.csv')
outcomes = pd.read_csv('/tmp/outcomes.csv')

Herhangi bir satıra göz atalım,

print 'mac', games[100:101]
print 'sonuc', outcomes[100:101]
mac      1041  1042  114  1161  118  119  1215  1216  1219  1221  1223  1224  \
100     0     0    0     0    0    0     0     0     0     0     0     0   

     1264  1266  1794  1801  1804  357  359  360  361  364  365  366  367  \
100     0     0     0     0     0    0    0    0    0    0    0    0    0   

     368     369     494  497  507  510  511  517  522  535  536  537  575  \
100    0 -1.5625  1.5625    0    0    0    0    0    0    0    0    0    0   

     596  614  632  659  830  831  832  835  837  838  847  
100    0    0    0    0    0    0    0    0    0    0    0  
sonuc      0.0
100    0

Yani güç sıralaması lojistik regresyonuna girdi olan matrisin her satırı ayrı bir maç, her kolonu ise ayrı bir takım. Maç yapan iki takımın değerleri olacak, diğerleri sıfır olacak. Üstteki satır mesela, 369. takım ve rakipte 494. takım için,

raw_games = pd.read_csv('results-20140714-124014.csv')
tmp = raw_games[(raw_games['teamid'] == 369) & (raw_games['op_teamid'] == 494)]
tmp = tmp[['teamid','team_name','op_team_name','is_home','points']]
print tmp
      teamid team_name op_team_name  is_home  points
4231     369   Denmark     Cameroon        0       3

Danimarka Kamerun maçına aitmiş. Bu maçta Danimarka kazandı, ev sahibi Kamerun. Şimdi burada birkaç önemli takla atılıyor, Google veri bilimcileri lojistik regresyonda, girdi olarak, deplasman takımına her maç başında otomatik olarak eksi bir değer veriyorlar, ev sahibine artı değer veriyorlar. Etiket ise 'ev sahibi kazandı mı?' sorusunun cevabı.

Ev sahibi olup kazanmak daha kolay, regresyon bağlamında artı değere sahip olursanız, az bir katsayı modeli uydurmaya yeterli olabilir, pozitife hemen yaklaşırız. Diğer yandan deplasman takımı ne kadar iyi oynarsa, onun büyüyen katsayısı eksi değerini o kadar arttırır, ve ev sahibinin artışını (onun öğesi çarpı katsayısı yani) eksilterek kaybetme durumuna yaklaştırır.

Kötü oynayan deplasman takımının eksi değeri eksi katsayı ile çarpılır, ve daha büyük bir artı sayıyı oluşturur, ev sahibinin kazanması durumunu güçlendirir.

Katsayıları doğal olarak bir takımın ne kadar iyi olduğunu gösterecektir.

Tabii regresyona pek çok satır verilecek, Kamerun birden fazla satırda ortaya çıkabilecek (ama hep aynı kolonda, işin püf noktası burada), bazen artı değerli olarak (ev sahibi) bazen eksi değerli olarak (deplasman).

İtiraf etmek gerekir ki veri bilimi bağlamında üstteki teknik, model, düşünce tarzı dahiyane bir yaklaşım, alanın ruhunu göstermesi bakımından eğitici. Veri temsilinden tutun, regresyonun kodlanmasında çeyrekliklere ayırmak, az veri olduğu için yaklaşıksallık (convergence) olmayabilir diye değişik parametrelerle regresyonu birkaç kez işletmek, bunu yaklaşıksallık olana kadar yapmak, tam anlamıyla bir ders niteliği taşıyor.

Tahmin

Nihayet hazırlandığımız ana geldik. Şimdi dünya kupası maçlarını tahmin edelim. Birkaç kolon göstereceğiz:

predicted: Yüzde kaç ihtimalle (ismi ilk gelen) takımın kazanacağı

points: Gerçekten ne olduğu. Oynanmayan maç NaN. Dikkat, penaltı atışlarına giden maçlar eşitlik olarak gösterilecek.

Ama bir dakika! Bu sonuçlar daha önce gösterdiğiniz [Google tahminleri kastediliyor] tahminlerinden değişik! Bunun sebepleri şunlar: Bazı hataları tamir ettik, yani kod değişti. İlk model mesela uzayan maçlar yüzünden kabaran istatistiklerin durumunu hesaba almıyordu.

İkinci sebep, model baştan planlı (deterministik) değil, eğitim verisi için verinin belli bir kısmını rasgele olarak seçiyoruz, bu sebeple sonuçlar bir hesaptan diğerine değişik çıkabiliyor (ki bazen sonuçlar çok değişik olabiliyor). Not: Aslında bu kod değiştirilerek rasgelelik içinden tamamen çıkartılabilir (ev ödeviniz!).

16'ıncı turu tahmin ederken mesela önceki 3 maçı, çeyrek finaller için önceki 4, yarıfınaller için 5, ve finaller için önceki 6 maçı kullandık [biz bu dokümanda önceki 3 maçı kullandık, history_size parametresiyle oynayarak değişik sonuçlar kontrol edilebilir].

pd.set_option('display.max_rows', 5000)
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1000)

wc_with_points = wc_power_data.copy()
wc_with_points.index = pd.Index(
    zip(wc_with_points['matchid'], wc_with_points['teamid']))
wc_labeled.index = pd.Index(
    zip(wc_labeled['matchid'], wc_labeled['teamid']))
wc_with_points['points'] = wc_labeled['points']

wc_pred = world_cup.extract_predictions(wc_with_points, 
                                        wc_results['predicted'])

# Reverse our predictions to show the most recent first.
wc_pred.reindex(index=wc_pred.index[::-1])
# Show our predictions for the games that have already happenned.
print wc_pred
        team_name   op_team_name  predicted       expected         winner  points
0       Argentina        Germany  46.070814        Germany             NA     NaN
1     Netherlands         Brazil  42.833863         Brazil             NA     NaN
2     Netherlands      Argentina  48.641542      Argentina           draw       1
3         Germany         Brazil  44.011593         Brazil        Germany       3
4      Costa Rica    Netherlands  14.442625    Netherlands           draw       1
5         Belgium      Argentina  18.596031      Argentina      Argentina       0
6        Colombia         Brazil  23.890421         Brazil         Brazil       0
7         Germany         France  75.116349        Germany        Germany       3
8             USA        Belgium  32.400646        Belgium        Belgium       0
9     Switzerland      Argentina  19.272768      Argentina      Argentina       0
10        Algeria        Germany   5.926496        Germany        Germany       0
11        Nigeria         France   8.694729         France         France       0
12         Greece     Costa Rica  40.448104     Costa Rica           draw       1
13         Mexico    Netherlands  20.402491    Netherlands    Netherlands       0
14        Uruguay       Colombia  46.480264       Colombia       Colombia       0
15          Chile         Brazil  26.574916         Brazil           draw       1
16        Germany            USA  91.980986        Germany        Germany       3
17          Ghana       Portugal  49.051707       Portugal       Portugal       0
18    Switzerland       Honduras  60.223070    Switzerland    Switzerland       3
19         France        Ecuador  84.538857         France           draw       1
20      Argentina        Nigeria  88.491450      Argentina      Argentina       3
21  Côte d'Ivoire         Greece  61.074502  Côte d'Ivoire         Greece       0
22        Uruguay          Italy  32.685428          Italy        Uruguay       3
23        England     Costa Rica  63.457326        England           draw       1
24         Brazil       Cameroon  94.788074         Brazil         Brazil       3
25         Mexico        Croatia  78.020214         Mexico         Mexico       3
26          Spain      Australia  90.521542          Spain          Spain       3
27          Chile    Netherlands  28.342133    Netherlands    Netherlands       0
28       Portugal            USA  65.457259       Portugal           draw       1
29        Algeria    South Korea  17.376285    South Korea        Algeria       3
30          Ghana        Germany  14.588539        Germany           draw       1
31           Iran      Argentina   5.193843      Argentina      Argentina       0
32        Ecuador       Honduras  53.848926        Ecuador        Ecuador       3
33         France    Switzerland  78.659381         France         France       3
34     Costa Rica          Italy  24.836756          Italy     Costa Rica       3
35         Greece          Japan  44.355013          Japan           draw       1
36        England        Uruguay  61.012694        England        Uruguay       0
37        Croatia       Cameroon  40.212875       Cameroon        Croatia       3
38          Chile          Spain  42.624474          Spain          Chile       3
39    Netherlands      Australia  93.535889    Netherlands    Netherlands       3
40         Mexico         Brazil  20.372064         Brazil           draw       1
41            USA          Ghana  39.500993          Ghana            USA       3
42        Nigeria           Iran  53.813244        Nigeria           draw       1
43       Portugal        Germany  15.337884        Germany        Germany       0
44       Honduras         France  22.953848         France         France       0
45        Ecuador    Switzerland  59.987076        Ecuador    Switzerland       0
46          Japan  Côte d'Ivoire  51.528885          Japan  Côte d'Ivoire       0
47          Italy        England  68.767968          Italy          Italy       3
48     Costa Rica        Uruguay  45.347946        Uruguay     Costa Rica       3
49      Australia          Chile  19.487987          Chile          Chile       0
50    Netherlands          Spain  60.493928    Netherlands    Netherlands       3
51       Cameroon         Mexico  30.018950         Mexico         Mexico       0
52        Croatia         Brazil   6.268704         Brazil         Brazil       0
53          Spain    Netherlands  35.602227    Netherlands          Spain       3
54        Germany        Uruguay  76.467450        Germany        Germany       3
55          Spain        Germany  29.438134        Germany          Spain       3
56    Netherlands        Uruguay  71.342186    Netherlands    Netherlands       3
57          Spain       Paraguay  83.007655          Spain          Spain       3
58        Germany      Argentina  42.635127      Argentina        Germany       3
59          Ghana        Uruguay  41.784682        Uruguay           draw       1
60         Brazil    Netherlands  60.821972         Brazil    Netherlands       0
61       Portugal          Spain  23.464891          Spain          Spain       0
62          Japan       Paraguay  61.278000          Japan           draw       1
63          Chile         Brazil  24.459600         Brazil         Brazil       0
64       Slovakia    Netherlands  12.082967    Netherlands    Netherlands       0
65         Mexico      Argentina  17.626748      Argentina      Argentina       0
66        England        Germany  20.763176        Germany        Germany       0
67          Ghana            USA  71.310871          Ghana          Ghana       3
68    South Korea        Uruguay  45.148588        Uruguay        Uruguay       0
69         Brazil       Portugal  81.610878         Brazil           draw       1
70        Germany          Ghana  81.621494        Germany        Germany       3
71         Serbia      Australia  38.204905      Australia      Australia       0
72  Côte d'Ivoire         Brazil  10.186423         Brazil         Brazil       0
73      Australia          Ghana  23.702414          Ghana           draw       1
74          Japan    Netherlands  10.773998    Netherlands    Netherlands       0
75         Serbia        Germany   4.731113        Germany         Serbia       3
76         Mexico         France  42.801515         France         Mexico       3
77    South Korea      Argentina  15.255040      Argentina      Argentina       0
78    Switzerland          Spain  18.747704          Spain    Switzerland       3
79       Portugal  Côte d'Ivoire  65.031075       Portugal           draw       1
80       Paraguay          Italy  12.288896          Italy           draw       1
81      Australia        Germany   7.395354        Germany        Germany       0
82          Ghana         Serbia  83.682899          Ghana          Ghana       3
83            USA        England  34.763699        England           draw       1
84         France          Italy  28.651132          Italy           draw       1
85       Portugal        Germany  14.833907        Germany        Germany       0
86         France       Portugal  72.141913         France         France       3
87          Italy        Germany  33.364112        Germany          Italy       3
88         France         Brazil  22.742882         Brazil         France       3
89       Portugal        England  49.550454        England           draw       1
90        Ukraine          Italy  28.378865          Italy          Italy       0
91      Argentina        Germany  46.801014        Germany           draw       1
92         France          Spain  47.126654          Spain         France       3
93          Ghana         Brazil   9.144470         Brazil         Brazil       0
94        Ukraine    Switzerland  62.637340        Ukraine           draw       1
95      Australia          Italy   8.365416          Italy          Italy       0
96    Netherlands       Portugal  70.231295    Netherlands       Portugal       0
97        Ecuador        England  34.379086        England        England       0
98         Mexico      Argentina  29.233199      Argentina      Argentina       0
99         Sweden        Germany  10.914079        Germany        Germany       0

Kodlar

world_cup.py

"""
    Futbol maclarinin sonucunu lojistik regresyon kullanarak tahmin eder.
"""

import random
import math

import numpy as np
random.seed(987654321)
np.random.seed(987654321)
import pandas as pd
import pylab as pl
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
import statsmodels.api as sm


def _drop_unbalanced_matches(data):
    """
    Mac sirasinda her iki takim hakkinda elimide veri olmadigi icin,
    tarihi veride bir macta oynayan iki takimin ikisininde hakkinda veri
    yoksa o iki takimi egitim verisinden at. Bu durum eger bir takim hakkinda
    10 mactan daha az veri var ise ortaya cikabilir.
    """
    keep = []
    index = 0
    data = data.dropna()
    while index < len(data) - 1:
        skipped = False
        for col in data:
            if isinstance(col, float) and math.isnan(col):
                keep.append(False)
                index += 1
                skipped = True

        if skipped:
            pass
        elif data.iloc[index]['matchid'] == data.iloc[index+1]['matchid']:
            keep.append(True)
            keep.append(True)
            index += 2
        else:
            keep.append(False)
            index += 1
    while len(keep) < len(data):
        keep.append(False)
    results = data[keep]
    if len(results) % 2 != 0:
        raise Exception('Unexpected results')
    return results


def _swap_pairwise(col):
    """ 0 ile 1, 2 ile 2, vs.. seklinda satir degis tokusu yap """
    col = pd.np.array(col)
    for index in xrange(0, len(col), 2):
        val = col[index]
        col[index] = col[index + 1]
        col[index+1] = val
    return col


def _splice(data):
    """ Bir maci temsil eden iki satiri tek bir satir olacak sekilde birlestir """
    data = data.copy()
    opp = data.copy()
    opp_cols = ['opp_%s' % (col,) for col in opp.columns]
    opp.columns = opp_cols
    opp = opp.apply(_swap_pairwise)
    del opp['opp_is_home']

    return data.join(opp)


def split(data, test_proportion=0.4):
    """
    Bir dataframe'i egitim set'i ve test set'i olarak ikiyi ayirir.
    Dikkatli olmak lazim cunku dataframe icinde bir macin satirlari ardi
    ardina gelmeli, bu sebeple bir macin tum verileri ya tamamen
    egitim ya da tamamen test set'inde olmali.
    """

    train_vec = []
    if len(data) % 2 != 0:
        raise Exception('Unexpected data length')
    while len(train_vec) < len(data):
        rnd = random.random()
        train_vec.append(rnd > test_proportion) 
        train_vec.append(rnd > test_proportion)

    test_vec = [not val for val in train_vec]
    train = data[train_vec]
    test = data[test_vec]
    if len(train) % 2 != 0:
        raise Exception('Unexpected train length')
    if len(test) % 2 != 0:
        raise Exception('Unexpected test length')
    return (train, test)


def _extract_target(data, target_col):
    """
    Egitimde hedef olarak kullanilan kolonu dataframe'den cikart.
    Geriye verilen dataframe eksi o kolonu dondur.
    """
    target = data[target_col]
    train_df = data.copy()
    del train_df[target_col]
    return target, train_df


def _check_eq(value): 
    """
    Geriye oyle bir fonksiyon dondur ki gecilen tamsayi degerine (value) uyup
    uymadigini kontrol etsin. Dikkat, geriye gecilen degeri kontrol eden
    _fonksiyon_ donduruyoruz, o degeri burada kontrol etmiyoruz.
    """
    return lambda (x): int(x) == int(value)


L1_ALPHA = 16.0
def build_model_logistic(target, data, acc=0.00000001, alpha=L1_ALPHA):
    """
    Bir lojistik regresyon modelini egitir. target parametresi
    hedef, yani etiket. target vektorunun satir sayisi data icindeki
    egitim verisinin satir sayisi kadar olmali.
    """
    data = data.copy()
    data['intercept'] = 1.0
    target = np.array(target)
    if np.any(target==-1): target[target==-1] = 0
    logit = sm.Logit(target, data, disp=False)
    return logit.fit_regularized(maxiter=1024, alpha=alpha, acc=acc, disp=False)


def validate(label, target, predictions, baseline=0.5, compute_auc=False,
             quiet=True):
    """
    Ikisel tahminleri kontrol et, karisiklik matrisi (confusion matrix) ve
    AUC hesaplar.

    Verili bir tahmin vektoru ve gercek degerlere bakarak tahminde ne kadar
    basarili oldugumuzu hesaplar.

    Argumanlar:

    label: kontrol ettigimiz seyin etiketi
    target: gercek degerlerin vektoru
    predictions: tahmin edilen degerleri - bu bir olasilik vektoru
       olabilir, ki bu durumda onu siralayip (sort) en emin olunan
       degerleri aliriz, emin olmak bir esik degerine gore hesaplanir.
       Tabii tahmin 1 ya da 0 ise direk dogru ya da yanlis sonucuna
       varabiliriz.
    compute_auc: Eger dogru ise tahminler icin bir AUC hesaplar.
       Bu arguman dogru ise tahminlerin de bir olasilik vektoru
       olmasi gerekir. 
    """

    if len(target) != len(predictions):
        raise Exception('Length mismatch %d vs %d' % (len(target), 
                                                      len(predictions)))
    if baseline > 1.0:
        # Baseline number is expected count, not proportion. Get the proportion.
        baseline = baseline * 1.0 / len(target)

    zipped = sorted(zip(target, predictions), key=lambda tup: -tup[1])
    expect = len(target) * baseline

    (true_pos, true_neg, false_pos, false_neg) = (0, 0, 0, 0)
    for index in xrange(len(target)):
        (yval, prob) = zipped[index]
        if float(prob) == 0.0:
            predicted = False
        elif float(prob) == 1.0:
            predicted = True
        else:
            predicted = index < expect
        if predicted:
            if yval:
                true_pos += 1
            else:
                false_pos += 1 
        else:
            if yval:
                false_neg += 1
            else:
                true_neg += 1
    pos = true_pos + false_neg
    neg = true_neg + false_pos
    # P(1 | predicted(1)) and P(0 | predicted(f))
    pred_t = true_pos + false_pos
    pred_f = true_neg + false_neg
    prob1_t = true_pos * 1.0 / pred_t if pred_t > 0.0 else -1.0
    prob0_f = true_neg * 1.0 / pred_f if pred_f > 0.0 else -1.0

    # Lift = P(1 | t) / P(1)
    prob_1 = pos * 1.0 / (pos + neg)
    lift = prob1_t / prob_1 if prob_1 > 0 else 0.0

    accuracy = (true_pos + true_neg) * 1.0 / len(target)

    if compute_auc:
        y_bool =  [True if yval else False for (yval, _) in zipped]
        x_vec = [xval for (_, xval) in zipped]
        auc_value = roc_auc_score(y_bool, x_vec)
        fpr, tpr, _ = roc_curve(y_bool, x_vec)
        pl.plot(fpr, tpr, lw=1.5,
            label='ROC %s (area = %0.2f)' % (label, auc_value))
        pl.xlabel('False Positive Rate', fontsize=18)
        pl.ylabel('True Positive Rate', fontsize=18)
        pl.title('ROC curve', fontsize=18)
        auc_value = '%0.03g' % auc_value
    else:
        auc_value = 'NA'

    print '(%s) Lift: %0.03g Auc: %s' % (label, lift, auc_value)
    if not quiet:
        print '    Base: %0.03g Acc: %0.03g P(1|t): %0.03g P(0|f): %0.03g' % (
            baseline, accuracy, prob1_t, prob0_f)
        print '    Fp/Fn/Tp/Tn p/n/c: %d/%d/%d/%d %d/%d/%d' % (
            false_pos, false_neg, true_pos, true_neg, pos, neg, len(target))


def _coerce_types(vals):
    """ Bir liste icindeki tum degerlerin float (reel sayi) oldugunu kontrol et """
    return [1.0 * val for val in vals]


def _coerce(data):
    """
    Dataframe icindeki tum degerleri float'a cevir ve degerleri
    standardize et
    """
    return _standardize(data.apply(_coerce_types))


def _standardize_col(col):
    """ Tek bir kolonu standardize et (ortalamayi cikar, standart sapma
        ile bol
    """
    std = np.std(col)
    mean = np.mean(col)
    if abs(std) > 0.001:
        return col.apply(lambda val: (val - mean)/std)
    else:
        return col


def _standardize(data):
    """ Tum dataframe'i standardize et. Tum kolonlar sayisal olmali """
    return data.apply(_standardize_col)


def _clone_and_drop(data, drop_cols):
    """ Icinde belli bazi kolonlarin atildigi bir Dataframe'in kopyasini dondur """
    clone = data.copy()
    for col in drop_cols:
        if col in clone.columns:
            del clone[col]
    return clone


def _normalize(vec):
    """ Listeyi normalize et ki toplami 1 olsun """
    total = float(sum(vec))
    return [val / total for val in vec]


def _games(data):
    """ Tek sayili satirlari at. Bu fonksiyon faydali cunku bazen
        ardi ardina ayni mac hakkinda iki satira ihtiyacimiz olmuyor
        bir tanesi yeterli.
    """
    return data[[idx % 2 == 0 for idx in xrange(len(data))]] 


def _team_test_prob(target):
    """ A takiminin B takimini, ve B takiminin A takimini yenme olasiliklarini
        hesapliyoruz. Her iki yondeki olasiliklari kullanarak genel
        bir olasilik hesabi yap.
    """
    results = []
    for idx in range(len(target)/2):
        game0 = float(target.iloc[idx*2])
        game1 = float(target.iloc[idx*2+1])
        results.append(game0/(game0+game1))
    return results


def extract_predictions(data, predictions):
    """
         Uyum verileri ve tahminleri iceren Dataframe'leri birlestir.
         Geriye dondurulen DF icinde takim isimleri, tahminler, ve
         (eger mevcut ise) puan olarak sonuc. 
    """
    probs = _team_test_prob(predictions)
    teams0 = []
    teams1 = []
    points = []
    for game in xrange(len(data)/2):
        if data['matchid'].iloc[game*2] != data['matchid'].iloc[game*2+1]:
            raise Exception('Unexpeted match id %d vs %d', (
                               data['matchid'].iloc[game * 2],
                               data['matchid'].iloc[game * 2 + 1]))
        team0 = data['team_name'].iloc[game * 2]
        team1 = data['op_team_name'].iloc[game * 2]
        if 'points' in data.columns: 
            points.append(data['points'].iloc[game * 2])
        teams0.append(team0)
        teams1.append(team1)
    results =  pd.DataFrame(
        {'team_name': pd.Series(teams0), 
         'op_team_name': pd.Series(teams1),
         'predicted': pd.Series(probs).mul(100)},
         columns = ['team_name', 'op_team_name', 'predicted'])

    expected_winner = []
    for game in xrange(len(results)):
        row = results.iloc[game]
        col = 'team_name' if row['predicted'] >= 50 else 'op_team_name' 
        expected_winner.append(row[col])

    results['expected'] = pd.Series(expected_winner)

    if len(points) > 0:
        winners = []
        for game in xrange(len(results)):
            row = results.iloc[game]
            point = points[game]
            if point > 1.1:
                winners.append(row['team_name'])
            elif point < 0.9:
                winners.append(row['op_team_name'])
            elif point > -0.1:
                winners.append('draw')
            else:
                winners.append('NA')
        results['winner'] = pd.Series(winners)
        results['points'] = pd.Series(points)
    return results


def _check_data(data):
    """ Dataframe'i gez ve her seyin iyi durumda olup olmadigini kontrol et """
    i = 0
    if len(data) % 2 != 0:
        raise Exception('Unexpeted length')
    matches = data['matchid']
    teams = data['teamid']
    op_teams = data['op_teamid']
    while i < len(data) - 1:
        if matches.iloc[i] != matches.iloc[i + 1]:
            raise Exception('Match mismatch: %s vs %s ' % (
                            matches.iloc[i], matches.iloc[i + 1]))
        if teams.iloc[i] != op_teams.iloc[i + 1]:
            raise Exception('Team mismatch: match %s team %s vs %s' % (
                            matches.iloc[i], teams.iloc[i], 
                            op_teams.iloc[i + 1]))
        if teams.iloc[i + 1] != op_teams.iloc[i]:
            raise Exception('Team mismatch: match %s team %s vs %s' % (
                            matches.iloc[i], teams.iloc[i + 1], 
                            op_teams.iloc[i]))
        i += 2


def prepare_data(data):
    """ birbiri ile uyan ama iki takim hakkinda veri olmadigi durumda o satirlari at"""
    data = data.copy()
    data = _drop_unbalanced_matches(data)
    _check_data(data)
    return data


def train_model(data, ignore_cols):
    """
    Veri uzerinde bir lojistik regresyon modeli egitir. ignore_cols icinde
    gecilen kolonlar dikkate alinmaz. 
    """
    # Validate the data
    data = prepare_data(data)
    target_col = 'points'
    (train, test) = split(data)
    train.to_csv('/tmp/out3.csv')
    (y_train, x_train) = _extract_target(train, target_col)
    x_train2 = _splice(_coerce(_clone_and_drop(x_train, ignore_cols)))

    y_train2 = [int(yval) == 3 for yval in y_train]
    model = build_model_logistic(y_train2, x_train2, alpha=8.0)
    return (model, test)


def predict_model(model, test, ignore_cols):
    """
    Bir takimin kazanip kazanmayacaginin tahmin eden basit bir algoritma
    """

    x_test = _splice(_coerce(_clone_and_drop(test, ignore_cols)))
    x_test['intercept'] = 1.0
    predicted =  model.predict(x_test)
    result = test.copy()
    result['predicted'] = predicted
    return result

power.py

"""
Futbol takimlarin gecmiste oynadiklari maclara gore bir guc indisi atayarak
derecelendirir.
"""

import numpy as np
from numpy.linalg import LinAlgError
import pandas as pd

import world_cup

def _build_team_matrix(data, target_col):
    """
    Verili bir mac dataframe'ine gore bir seyrek guc matrisi yaratir.
    Girdi verisinde her mac icin arka arkaya iki tane satir kaydi olmasini
    bekliyoruz. Ilk satir deplasman takimi hakkinda bilgi tasiyacak,
    ikinci satir konuk takim hakkinda. Hesap yapilan matriste takimlar kolonlarda
    maclar ise satirlarda olacak. Her mac icin deplasman takiminin
    kolonunda pozitif bir deger olacak. Konuk takim icin negatif bir deger
    olacak. Futbolde deplasman avantaji cok onemli oldugu icin
    bu deplasmanda oynayan takimdan biraz avantaj cikartiyoruz. Tabii
    burada dikatli olmak ta lazim, cunku dunya kupasi icin is_home (deplasman mi?)
    kolonu evet/hayir formatinda degil (0.0 ile 1.0 arasinda reel degerler).
    Guc matrisindeki en son kolon bir puan matrisi, bu kolona
    deplasman takimi ile konuk takimin hedef kolonlari arasindaki fark
    yazilmis.
    """
    teams = {}
    nrows = len(data) / 2
    for teamid in data['teamid']:
        teams[str(teamid)] = pd.Series(np.zeros(nrows))

    result = pd.Series(np.empty(nrows))
    teams[target_col] = result

    current_season = None
    current_discount = 2.0

    for game in xrange(nrows):
        home = data.iloc[game * 2]
        away = data.iloc[game * 2 + 1]
        if home['seasonid'] != current_season:
            # Discount older seasons.
            current_season = home['seasonid']
            current_discount *= 0.6
            print "New season %s" % (current_season,)

        home_id = str(home['teamid'])
        away_id = str(away['teamid'])
        points = home[target_col] - away[target_col]

        # Discount home team's performance.
        teams[home_id][game] = (1.0 + home['is_home'] * .25) / current_discount
        teams[away_id][game] = (-1.0 - away['is_home'] * .25) / current_discount
        result[game] = points

    return pd.DataFrame(teams)


def _build_power(games, outcomes, coerce_fn, acc=0.0001, alpha=1.0, snap=True):
    """ Birbiri ile alakali bir kume mac uzerinden bir guc modeli
        hazirlar (tum bu maclar ayni turnuvadan, musabakadan olmalidir mesela).
        Bu maclar ve onlarin sonucunu alarak bu fonksiyon bir lojistik
        regresyon modeli kurar, ve bu model tum takimlarin birbirine
        olan izafi bir siralamasini hesaplar. Geriye takim id'si ve
        o takimin 0 ve 1 arasindaki bir guc indisini dondurur. Eger
        snap degiskeni True ise, indisler ceyreklere bolunur. Bu faydali
        cunku siralama tahmini oldukca kabaca yapilan bir tahmin ve
        tek bir numaradan elde edilecek bir tur "asiri spesifiklik" 
        bizi yaniltabilirdi. 
    """
    outcomes = pd.Series([coerce_fn(val) for val in outcomes])
    games.to_csv('/tmp/games.csv',index=None)
    outcomes.to_csv('/tmp/outcomes.csv',index=None)
    model = world_cup.build_model_logistic(outcomes, games, 
        acc=acc, alpha=alpha)

    #print model.summary()
    params = np.exp(model.params)
    del params['intercept']
    params = params[params != 1.0]
    max_param = params.max()
    min_param = params.min()
    param_range = max_param - min_param
    if len(params) == 0 or param_range < 0.0001:
        return None

    params = params.sub(min_param)
    params = params.div(param_range)
    qqs = np.percentile(params, [20, 40, 60, 80])
    def _snap(val): 
        """ Snaps a value to a quartile. """
        for idx in xrange(len(qqs)):
            if (qqs[idx] > val):
                return idx * 0.25
        return 1.0

    if snap:
        # Snap power data to rough quartiles.
        return params.apply(_snap).to_dict()
    else:
        return params.to_dict()


def _get_power_map(competition, competition_data, col, coerce_fn):
    """
       Verili bir musabakadaki maclar ve sonuclarini iceren hedef kolonlarini
       kullanarak tum bu takimlarin guc siralamasini hesapla. Uyum biraz
       gevsek olacaktir, evet, bu sebeple uydurma islemini farkli
       regularizasyon ve alpha parametreleri ile birkac kez denememiz
       gerekebilir, ki boylece uydurmanin yakinsamasini (converge)
       elde edebilmis oluruz. Geriye takim id ve guc siralamsini dondurur.
    """
    acc = 0.000001
    alpha = 0.5
    while True:
        if alpha < 0.1:
            print "Skipping power ranking for competition %s column %s" % (
                competition, col)
            return {}
        try:
            games = _build_team_matrix(competition_data, col)
            outcomes = games[col]
            del games[col]
            competition_power = _build_power(games, outcomes, coerce_fn, acc,
                                             alpha, snap=False)
            if not competition_power:
                alpha /= 2
                print 'Reducing alpha for %s to %f due lack of range' % (
                    competition, alpha)
            else:
                return competition_power
        except LinAlgError, err:
            alpha /= 2  
            print 'Reducing alpha for %s to %f due to error %s' % (
                competition, alpha, err)


def add_power(data, power_train_data, cols):
    """
        Dataframe'e bazi guc kolonlari ekliyor. Egitim power_train_data
        verisini musabakalara boler (cunku bu parcalar birbirinden farkli
        guc istatistigine sahip olacaktir; mesela EPL takimlari MLS takimlari
        ile normal lig maclarinda oynamazlar), hangi takimin daha fazla ya da
        az guclu olup olmadigini bu maclar bazinda anlamak faydali olmazdi.

        cols icinde bir kolon ismi olacak, ki bu kolon tahmin etmek
        icin kullanilacak, bir fonksiyon ki onceden bahsedilen kolondaki
        farki irdelemekle gorevli, ve sonucun yazilacagi hedef kolon.

        Geriye bir dataframe dondurur, bu dataframe 'data' parametresiyle
        ayni boyutlardadir, sadece ekl olarak guc istatistigi eklemis olacaktir.
    """   
    data = data.copy()
    competitions = data['competitionid'].unique()
    for (col, coerce_fn, final_name) in cols:
        power = {}
        for competition in competitions:
            competition_data = power_train_data[
                power_train_data['competitionid'] == competition]
            power.update(
                _get_power_map(competition, competition_data, col, coerce_fn))

        names = {}
        power_col = pd.Series(np.zeros(len(data)), data.index)
        for index in xrange(len(data)):
            teamid = str(data.iloc[index]['teamid'])
            names[data.iloc[index]['team_name']] = power.get(teamid, 0.5)
            power_col.iloc[index] = power.get(teamid, 0.5)
        print ['%s: %0.03f' % (x[0], x[1])
               for x in sorted(names.items(), key=(lambda x: x[1]))]
        data['power_%s' % (final_name)] = power_col
    return data

features.py

"""
    Mac hakkindaki ham veriyi tahmin icin kullanabilecegimiz
    ozelliklere donusturur. Tarihi verideki birkac maci birlestirip
    birlesik / ozetsel bazi hesaplar uret ki boylece sonraki maci
    tahmin edebilelim.
"""

import pandas as pd

import match_stats

def get_wc_features(history_size):
    return pd.read_csv('results-20140714-123022.csv',sep=',')

def get_features(history_size):
    return pd.read_csv('results-20140714-123519.csv',sep=',')

def get_game_summaries():
    return pd.read_csv('results-20140714-124014.csv',sep=',')

match_stats.py

def get_non_feature_columns():
    """ 
    Ozellikler dataframe'inin kolon isimlerini dondur. Bu dataframe
    tahmin icin kullanilmayan bir dataframe; kolonlari metaveri
    aslinda (yani veri hakkinda veri), mesela takim ismi gibi, ya da
    regresyon icin kaynak degil hedef veri iceren kolonlar. Hedef kolonunu
    kullanmak istemiyoruz cunku bir macin verisini kullanarak ayni maci tahmin
    ettigimiz bir durumda olmak istemeyiz.
    """
    return ['teamid', 'op_teamid', 'matchid', 'competitionid', 'seasonid',
            'goals', 'op_goals', 'points', 'timestamp', 'team_name', 
            'op_team_name']

def get_feature_columns(all_cols):
    """
    Tahminde kullanilmasi gereken tum kolonlari geri dondurur, mesela
    mesela dataframe'de olan ama features.get_non_feature_column() icinde
    olmayan tum ogeler (kolonlar)
    """
    return [col for col in all_cols if col not in get_non_feature_columns()]

Kaynaklar

[1] Google Cloud Platform Blog, Google Cloud Platform goes 8 for 8 in World Cup predictions, http://googlecloudplatform.blogspot.de/2014/07/google-cloud-platform-goes-8-for-8-in-soccer-predictions.html

[2] Google Cloud Platform, Sample iPython notebook with soccer predictions, https://github.com/GoogleCloudPlatform/ipython-soccer-predictions

[3] Google, Predicting the World Cup with the Google Cloud Platform, http://nbviewer.ipython.org/github/GoogleCloudPlatform/ipython-soccer-predictions/blob/master/predict/wc-final.ipynb


Yukarı