Up-Down Votes
In data analysis applied to horse racing (Equus Analytics), we frequently draw inspiration and fresh insights from other disciplines. Today, we are focusing on an adaptation of Evan Miller's Bayesian method for handling up-down vote scenarios in online content ranking. To that end, we have two primary goals:
- Work with minimal data, and
- Identify convex bets (live longshots).
It is always intriguing to determine what insights we can infer when working with minimal data (e.g. horse lifetime record):
Horse | Runs | Win | Place | Show |
---|---|---|---|---|
Zulu | 23 | 7 | 5 | 2 |
This situation frequently arises when trading foreign (e.g. international) racing circuits (e.g. Hong Kong, Japan), which have exceptional racing industries but for which we do not have detailed past performance records for most horses.
Bayesian Foundation
At its core, the Bayesian average rating system provides a statistically valid way to rank items based on positive and negative feedback, accounting for uncertainties due to limited data. In the context of up-down votes, it is straightforward: items receive up-votes (successes) and down-votes (failures), and we wish to rank them to balance their average rating with our confidence in that rating.
Translating this to horse racing, we consider:
- Events Placed: Number of times a horse has finished "in the money" (e.g., first, second, or third).
- Events Unplaced: Number of times a horse has raced but did not finish "in the money".
- Time Since Placed/Unplaced: Time elapsed since the horse's last placed or unplaced finish, allowing us to weigh recent performances more heavily than older ones.
Adaptation
The Bayesian model requires a prior belief and adjusts this belief based on new evidence. Here's how we adapt the model:
- Prior Beliefs (Pseudo-Events): Assign each horse a baseline level of performance. This prevents horses with very few races from being unfairly ranked at the extremes due to insufficient data.
- Updating with New Data: Each horse's actual race outcomes are added to the prior beliefs, giving us the total "events placed" and "events unplaced".
- Exponential Decay of Events: To ensure that recent performances have more impact, we apply an exponential decay to the events based on the time since they occurred. This approach mirrors how specific online platforms weigh newer votes more heavily than older ones.
- Computing the Bayesian Rating: We calculate the Bayesian average rating (the "sorting criterion") using the beta distribution, which balances the observed data and the uncertainty inherent in limited or decayed data.
Code Walkthrough
Below is an excerpt of the Python implementation. For brevity, we'll focus on the key components.
import numpy as np
import pandas as pd
from scipy.special import betaincinv
import time
def initialize_ratings(state):
new_state = state.copy()
ratings_df = pd.DataFrame(new_state['new_ratings'])
ratings_df.rename(columns={'name': 'entry_id'}, inplace=True)
# Add prior beliefs (pseudo-events)
ratings_df['events_placed'] += new_state['pseudo_events_placed']
ratings_df['events_unplaced'] += new_state['pseudo_events_unplaced']
# Convert time since last events to absolute times
current_time = time.time()
ratings_df['last_placed_time'] = current_time - ratings_df['time_since_placed']
ratings_df['last_unplaced_time'] = current_time - ratings_df['time_since_unplaced']
new_state['ratings'] = ratings_df
return new_state
In 'initialize_ratings', we set up our DataFrame with the horse entries, adjust for prior beliefs, and calculate the timestamps.
def decay_events(state):
new_state = state.copy()
current_time = time.time()
half_life = new_state['half_life']
ratings_df = new_state['ratings']
# Calculate decay factors
ratings_df['decay_factor_placed'] = 2 ** (-(current_time - ratings_df['last_placed_time']) / half_life)
ratings_df['decay_factor_unplaced'] = 2 ** (-(current_time - ratings_df['last_unplaced_time']) / half_life)
# Apply decay
ratings_df['events_placed'] *= ratings_df['decay_factor_placed']
ratings_df['events_unplaced'] *= ratings_df['decay_factor_unplaced']
new_state['ratings'] = ratings_df
return new_state
The 'decay_events' function applies exponential decay to the events. While we lack individual timestamps for all events, using the time since the last events (i.e. placed and unplaced) provides a pragmatic approximation under current data limitations.
def construct_sorting_criterion(state):
new_state = state.copy()
loss_multiple = new_state['loss_multiple']
new_state['ratings']['sorting_criterion'] = new_state['ratings'].apply(
lambda row: betaincinv(
row['events_placed'] + 1,
row['events_unplaced'] + 1,
1 / (1 + loss_multiple)
), axis=1
)
return new_state
Here, 'construct_sorting_criterion' computes the Bayesian rating using the inverse incomplete beta function, factoring in our desired level of caution via the 'loss_multiple'.
Worked Example
Let us consider a race with several horses and their performance data:
initial_state = {
'pseudo_events_placed': 5.0, # Prior belief of 5 placed events
'pseudo_events_unplaced': 5.0, # Prior belief of 5 unplaced events
'half_life': 3600 * 24 * 7, # One week in seconds
'new_ratings': [
{'name': '1. Alpha', 'events_placed': 13, 'events_unplaced': 36, 'time_since_placed': 5, 'time_since_unplaced': 24},
{'name': '2. Bravo', 'events_placed': 6, 'events_unplaced': 22, 'time_since_placed': 432, 'time_since_unplaced': 14},
# Additional horse data...
],
'loss_multiple': 5
}
After running the code, we obtain the following rankings:
Entry | Placed | Unplaced | Ratings | S/P | F/P |
---|---|---|---|---|---|
5. Echo | 12.00 | 16.00 | 0.35 | 20/1 | 3/11 |
8. Hotel | 8.00 | 11.00 | 0.32 | 5/1 | 1/11 |
11. Kilo | 5.00 | 8.00 | 0.28 | ||
3. Charlie | 11.00 | 22.00 | 0.27 | 9/2 | 2/11 |
1. Alpha | 18.00 | 41.00 | 0.25 | 11/8 | 4/11 |
... | ... | ... | ... | ... | ... |
- Ratings: The Bayesian rating computed for each horse.
- S/P (Starting Price): The odds offered at the start of the race.
- F/P (Finishing Position): The horse's finishing position in the race.
Results
Notably, four of the top five horses in our rankings secured the first four positions in the race (as indicated in the 'F/P' column). Interestingly, Echo, with a starting price of 20/1, was ranked highest by our model and finished third. This suggests that the Bayesian approach might help identify "live longshots" horses with higher odds that have a reasonable chance of performing well.
Limitations
An astute observer might point out that applying decay to the total event count based solely on the time since the last event is not entirely accurate. Ideally, we would decay each event individually based on when it occurred. However, lacking detailed timestamps, our current method provides a reasonable approximation, especially if:
- Event Timing is Similar Across Entries: If most horses have events spaced similarly over time, the relative decay applied will be consistent.
- Recent Performance is Indicative: If a horse's most recent performance strongly indicates its current form, weighting events based on the last event may be defensible.
While this is not a perfect solution, the model's success in our worked example suggests it holds practical value.
Power of Bayesian Analysis
This adaptation of the Bayesian average rating system demonstrates that we can extract meaningful insights even from minimal data with some mathematical ingenuity and a pragmatic approach to data limitations. The model doesn't guarantee winners but offers a statistically sound method to rank horses beyond surface-level metrics.
By highlighting horses like Echo, the model can point out potential value bets with favourable odds that may have slipped under the radar of the casual punter.
Moving Forward
For those interested in refining this approach:
- Gather More Data: To improve the decay function, collect timestamps for individual events.
- Experiment with Parameters: Adjust the 'half_life' and 'loss_multiple' to see how sensitive the model is to these parameters.
- Re-Define Priors: Adjust 'pseudo_events_placed' and 'pseudo_events_unplaced' to see how sensitive the model is to these priors.
Conclusion
While we must remain mindful of the model's limitations, this Bayesian approach provides a solid starting point for horse racing analysis with minimal data. It blends statistical rigour with practical application.
As with all forms of betting and analysis, there are no certainties; there are only probabilities. This model helps us navigate those probabilities with more confidence, perhaps shining a light on those "live longshots".
Enjoy!
Note: The first draft of this post was generated by an LLM from our Python code listing and Evan Miller's original article.