2
$\begingroup$

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.

$\endgroup$
1
  • $\begingroup$ I have changed the params to reflect your question on the "binary classification model" part. I believe the default parameter is reg:linear. param = { 'max_depth': 3, 'objective':'binary:logistic', 'eta': 0.1, 'silent': 0, 'n_estimators': 20, 'seed': 366} watchlist = [(dtest, 'eval'), (dtrain, 'train')] num_round = 1000 and changed xgb to bst = xgb.train( param, dtrain, num_round, watchlist, feval=kelly_error(odds_train, probs_train, odds_test, probs_test), maximize=True ) Have you found the possible use of the kelly_loss in your problem? Thanks $\endgroup$
    – aiedu
    May 23, 2018 at 15:31

2 Answers 2

1
$\begingroup$

I have solved this to a certain extent. xgb is able to optimise the problem without altering the objective function. I still don't understand when you should pass in your own function as a parameter but it looks like it isn't necessary.

There were some bugs in my kelly_error function which I have fixed below, including changing the code to optimise for ROI. You will also need to tell xgb to maximise the output (default is to minimise)

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 * bet_outcome * kelly_fraction * odds
        loss = preds * (1 - bet_outcome) * kelly_fraction

        stake = float(sum(preds * kelly_fraction))
        total_profit = float(sum(profit - loss))

        roi = 100 * total_profit / stake
        return 'roi', roi

    return evalerror
$\endgroup$
0
$\begingroup$

When you just change your eval function, XGB will not optimize for this function. XGB will just output the model performance using this function, or XGB will use this function for early stopping and so on.

If you really want to optimize for a specific function, you will need to implement the objective function.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.