Tennis, football and basketball results as clean JSON, without a sports data contract

Tennis, football and basketball results as clean JSON, without a sports data contract

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 '') Enter fullscreen mode Exit fullscreen mode 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" } Enter fullscreen mode Exit fullscreen mode 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.

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.