Sports data is one of those areas where the gap between "public information" and "affordable to obtain" is
absurd. Scores and results are facts, printed in every newspaper. Getting them as structured data usually means
either a five-figure contract with a rights holder, or writing your own scraper and maintaining it forever.
If you are building a model, a scoreboard or a fantasy tool rather than a betting exchange, there is a middle
path. This post is about the details that separate usable sports data from a spreadsheet you cannot trust.
The status field is where most sports scrapers get it wrong
Every match has a state, and naive scrapers reduce it to three: upcoming, live, finished. Real fixtures are
messier. A tennis match can be retired mid-set. A football match can be abandoned at half time, replayed, awarded
to one side, or decided on penalties. Each of those needs a different treatment in any model you build.
I learned this the hard way. My tennis scraper inferred status codes from observed data, and the mapping looked
convincing: code 5 seemed to be a walkover, codes 17 and 18 seemed to be postponed and cancelled, because that
was consistent with the handful of matches I had checked.
Then I found the authoritative list. Codes 17 and 18 are FIRST_SET and SECOND_SET. They mean the match is
being played right now. My scraper was reporting live matches as cancelled. On one day's data, seven matches
were mislabelled, including one sitting at 6-4, 0-0 with a set already on the board.
The lesson generalises beyond this one feed: when you infer an enum from samples, you are fitting to whatever
happened to be in your sample. A match in its first set and a cancelled match both look like "no result yet" if
you squint. Find the real mapping, or at minimum, add an assertion that catches the contradiction. Mine now
refuses to label anything as postponed or cancelled if there are games on the board.
Scores are not a single number
The second thing that bites you is assuming a score is two integers.
- Tennis needs set scores and tiebreak points. A set that ends 7-6 is meaningless without the 7-4 in the tiebreak, and a match at 6-6 in the first set has a live tiebreak count that looks exactly like a games score if you map it carelessly.
- Basketball needs quarters and any overtime periods. The useful assertion here is that they must add up: 20 + 27 + 21 + 21 = 89 for the home team, or your mapping is wrong. That single check is what proved my field mapping rather than my confidence in it.
- Football needs halves, extra time and shootouts, and there is a trap. A match that finished 0-0 and was won 4-3 on penalties is commonly displayed as 1-0, because the display score absorbs the shootout point. If you derive the first-half score by subtracting the second half from the total, you will be wrong by exactly one goal on every shootout in your dataset. Better to leave that field null than to publish a confident wrong number.
Getting it
I published one Actor per sport, because a tennis-specific tool is more useful than a generic one and easier to
find:
- Tennis: ATP, WTA, ITF and Challenger, with set and tiebreak scores, match statistics split by set, and full point-by-point sequences.
- Football: every league, with goals and assists, cards and the offence behind them, substitutions, and statistics including expected goals.
- Basketball: quarter and overtime scores plus the full statistics sheet.
All three cost $0.001 per match, so a full day of world football, roughly 1,700 matches, is under two dollars.
from apify_client import ApifyClient
client = ApifyClient("YOUR_TOKEN")
run = client.actor("clearfetch/flashscore-football-scraper").call(
run_input={"days": ["0"], "countries": ["England"], "status": ["finished"]}
)
for m in client.dataset(run["defaultDatasetId"]).iterate_items():
ht = m["score"]["firstHalf"]
print(f'{m["home"]["name"]} {m["score"]["home"]}-{m["score"]["away"]} {m["away"]["name"]}'
f' (HT {ht["home"]}-{ht["away"]})' if ht else '')
A real row, from a Premier League match:
{
"status": "finished",
"stageDetail": "FINISHED",
"home": { "name": "Newcastle" },
"away": { "name": "Bournemouth" },
"score": { "home": 2, "away": 2, "firstHalf": { "home": 1, "away": 2 } },
"winner": "draw"
}
Ask for the details of that match and you get the 19 incidents behind the scoreline: who scored, who assisted,
which card was for what offence, and the substitutions, plus 120 statistic rows including expected goals.
Two things worth knowing
Draws exist. Obvious for football, but a surprising number of scrapers model winner as home or away and
quietly drop the third case. Mine returns draw, and 47 of the 1,759 matches on the day I tested were draws.
Everything unmapped is still there. Each row carries a raw object with every original field from the
source. If you need something I did not map, you do not have to wait for me to add it.
Scores and results are facts, and reading a public page is not the same as redistributing a rights-holder's
feed. What you do with the data afterwards, particularly if you republish it, is your responsibility to check.
This article was originally published by DEV Community and written by Clear Fetch.
Read original article on DEV Community