I have a model that predicts the outcome of ATP tennis matches. The quality of predictions varies, and I want to develop a second binary classification model that optimises the decision to bet (or not) based on a number of features about the match. Amongst the second model's features are the probabilities from the first, and archived bookmaker's odds for each match. The size of the bet is determined by the Kelly Criterion. The training data has been classified such that all bets that would have won = 1, all losing bets = 0. I'm using xgboost.
I'm attempting to incorporate the Kelly Criterion into my xgb loss function but without success.
I've had a look at the custom objective example in the xgb demo. From my understanding, for xgb to maximise the expected value of the logarithmic bankroll, my objective function needs to return the first:
$$\frac{\partial}{\partial x}(p\: log(1 + bx)+(1-p)\:log(1-x))=\frac{-(b+1)\:p+b\:x+1}{(x-1)(b\:x+1)}$$
and second order derivatives:
$$\frac{\partial ^2}{\partial x^2}(p\: log(1 + bx)+(1-p)\:log(1-x))=-\frac{b^{2}p}{(bx+1)^{2}}-\frac{1-p}{(1-x)^{2}}$$
where:
- b is the net odds received on the wager ("b to 1"); that is, you could win \$b (on top of getting back your \$1 wagered) for a $1 bet
- p is the probability of winning;
I've also implemented my own cost function that calculates the profit and loss for each bet.
The code I have so far is below.
import pandas as pd
import xgboost as xgb
import numpy as np
import StringIO # ('import io' in python 3.x)
import requests
url_train = 'https://gist.githubusercontent.com/martinstaniforth/162b9691132f7099b4da08fd14defc39/raw/9372c5cac42b545ecde4200503b97f895e24cbfe/train.csv'
url_test = 'https://gist.githubusercontent.com/martinstaniforth/4445b884abea22d4ae238cda869b5e0e/raw/84d3dead9122a644f8d273e04b898920a8e5a811/test.csv'
train_content = requests.get(url_train).content
test_content = requests.get(url_test).content
train_df = pd.read_csv(StringIO.StringIO(train_content.decode('utf-8')), index_col='match_id')
test_df = pd.read_csv(StringIO.StringIO(test_content.decode('utf-8')), index_col='match_id')
train_target_df = train_df.reset_index(drop=True)[['bet_wins']]
train_df = train_df.reset_index(drop=True).drop(['bet_wins'], axis=1)
test_target_df = test_df.reset_index(drop=True)[['bet_wins']]
test_df = test_df.reset_index(drop=True).drop(['bet_wins'], axis=1)
odds_train = train_df['player_odds'].values - 1
probs_train = train_df['win_prob'].values
odds_test = test_df['player_odds'].values - 1
probs_test = test_df['win_prob'].values
dtrain = xgb.DMatrix(train_df.values, train_target_df.values)
dtest = xgb.DMatrix(test_df.values, test_target_df.values)
param = {
'max_depth': 3,
'eta': 0.05,
'silent': 1,
'n_estimators': 50,
'seed': 366}
watchlist = [(dtest, 'eval'), (dtrain, 'train')]
num_round = 200
def kelly_loss(odds_train, probs_train, odds_test, probs_test):
def logregobj(x, dmatrix):
bet_outcome = dmatrix.get_label()
odds = odds_train if len(bet_outcome) == len(odds_train) else odds_test
probs = probs_train if len(bet_outcome) == len(probs_train) else probs_test
y = -((odds + 1) * probs + odds * x + 1) / ((x - 1) * (odds * x + 1))
grad = y - bet_outcome
hess = -(np.power(odds, 2) * probs) / np.power(odds * x + 1, 2) - \
(1 - probs) / np.power(1 - x, 2)
return grad, hess
return logregobj
def kelly_error(odds_train, probs_train, odds_test, probs_test):
def evalerror(preds, dmatrix):
bet_outcome = dmatrix.get_label()
odds = odds_train if len(bet_outcome) == len(odds_train) else odds_test
probs = probs_train if len(bet_outcome) == len(probs_train) else probs_test
kelly_fraction = (probs * (odds + 1) - 1) / odds
def value_bets(f): # ignore any bets with a negative kelly fraction
return 0 if f < 0 else f
kelly_fraction = np.array([value_bets(x) for x in kelly_fraction])
profit = preds * kelly_fraction * odds
loss = (1 - preds) * kelly_fraction
total_profit = float(sum(profit - loss))
return 'error', total_profit
return evalerror
bst = xgb.train(
param,
dtrain,
num_round,
watchlist,
kelly_loss(odds_train, probs_train, odds_test, probs_test),
kelly_error(odds_train, probs_train, odds_test, probs_test))
When I execute the code, xgb does not update predictions. I suspect the logregobj function is incorrect as I haven't fully understood its purpose. Can someone please assist in correctly implementing the Kelly Criterion in a binary classification model? Training data is available in this gist, as referenced in the code.
Thanks in advance.