Documentation / Guides
Breakdown dimensions
Seventeen ways to slice the same block.
Every statistics endpoint takes by= and returns the same block split that way.
One shape, seventeen dimensions, and one very easy way to fool yourself.
The dimensions
| Dimension | Splits by | Notes |
|---|---|---|
course | Racecourse | The slice most likely to find something real. |
region | Jurisdiction | Coverage differs sharply; see Coverage. |
run_type | Flat, Hurdle, Chase, NH Flat | Effectively different sports. |
distance | Race distance as published | Use with
course for draw work. |
going | Ground as published | Exact strings, capitalisation included. |
surface | Turf, Dirt, Polytrack… | |
class | Race class | British in practice. |
year, month | Time | Margins move; a nine-year backtest averages several markets. |
draw | Low, middle, high third | Flat only. |
field_size | Runner-count bands | Changes the margin more than anything else. |
layoff | Days since last run | Knowable in advance. |
sp_band | Price band | Where the strongest population effect lives. |
trainer, jockey, owner, sire |
Connections | Not all are valid on every subject. |
A dimension that is fixed for the subject is rejected with invalid_param rather
than silently ignored: by=trainer on a trainer, by=course on a course.
Being told is better than getting one row back and wondering.
min_runs is not optional
It defaults to 1, which is useful for exploring and dangerous for concluding. Set it to 30 for anything you intend to believe, and 100 for anything you intend to act on.
Without it, by=course on a trainer returns sixty rows, most with a handful of
runners, and the top of that list sorted by A/E is a list of the smallest samples.
The trap
This is the important part of the guide.
Test one trainer against sixty courses at the usual threshold and about three will look significant by chance alone. Nothing is wrong with the data or the interval. That is what a 5% error rate means when you run sixty tests.
The angles endpoint makes it worse, because it lets you combine seven filters freely. With a million and a half runner records, you will always find something that looks remarkable. Combining filters until a number pleases you is not analysis, it is searching for noise.
How to use it honestly
- Decide the question before you run it. A hypothesis formed before seeing the data is worth more than a pattern found in it.
- Set min_runs first, not after seeing which rows you like.
- Treat anything found by exploring as a hypothesis, never a finding.
- Test it out of sample. Find it with
date_toa few years back, then rerun on the period after. This is the single most valuable thing this API can do for you and it costs two requests. - Expect most things to disappear. That is the process working. A tool that confirms every idea is worthless.
Out of sample, in two calls
# find it
GET /v1/analysis/angles?run_type=Chase&going=Soft&by=field_size
&min_runs=100&date_to=2023-12-31
# then test it on data you did not look at
GET /v1/analysis/angles?run_type=Chase&going=Soft&by=field_size
&min_runs=100&date_from=2024-01-01
If the interval still excludes 1.00 on the second, you have something. If it does not, you had noise, and you found that out for the price of one request rather than a season.
Rows are not independent
Splitting by course and by distance produces overlapping populations: the same races appear in both. Adding significance across dimensions is not meaningful, and two significant rows that describe the same underlying races are one finding, not two.