APIHorseRacing

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

DimensionSplits byNotes
courseRacecourseThe slice most likely to find something real.
regionJurisdictionCoverage differs sharply; see Coverage.
run_typeFlat, Hurdle, Chase, NH FlatEffectively different sports.
distanceRace distance as publishedUse with course for draw work.
goingGround as publishedExact strings, capitalisation included.
surfaceTurf, Dirt, Polytrack…
classRace classBritish in practice.
year, monthTimeMargins move; a nine-year backtest averages several markets.
drawLow, middle, high thirdFlat only.
field_sizeRunner-count bandsChanges the margin more than anything else.
layoffDays since last runKnowable in advance.
sp_bandPrice bandWhere the strongest population effect lives.
trainer, jockey, owner, sire ConnectionsNot 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

  1. Decide the question before you run it. A hypothesis formed before seeing the data is worth more than a pattern found in it.
  2. Set min_runs first, not after seeing which rows you like.
  3. Treat anything found by exploring as a hypothesis, never a finding.
  4. Test it out of sample. Find it with date_to a 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.
  5. 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.