{ "cells": [ { "cell_type": "markdown", "id": "3faa9d74", "metadata": {}, "source": [ "# ๐ŸŽฒ Betting odds with `sportsdataverse-py`\n", "\n", "Welcome! In a few lines of Python you're about to pull **live betting odds**\n", "from a whole market of sportsbooks โ€” moneylines, spreads, totals, player\n", "props, scores, even point-in-time history. `sportsdataverse.odds` wraps\n", "[The Odds API](https://the-odds-api.com) v4 and hands you back tidy **polars**\n", "DataFrames that are ready to model. ๐Ÿš€\n", "\n", "If you've used the R package [oddsapiR](https://oddsapir.sportsdataverse.org),\n", "the `toa_*` names will feel right at home. Let's dive in!" ] }, { "cell_type": "markdown", "id": "ed7715ad", "metadata": {}, "source": [ "## ๐Ÿงฐ The toolbox\n", "\n", "Every function returns a tidy **polars** `DataFrame` by default โ€” pass\n", "`return_as_pandas=True` for pandas, or `return_parsed=False` for the raw JSON.\n", "Here's the whole kit (click any name for the full reference):\n", "\n", "| Function | What it gives you | Quota |\n", "|---|---|---|\n", "| [`toa_sports`](../odds/reference/additional.md#toa_sports) | Every in-season sport/league key (the `sport=` value) | ๐Ÿ†“ free |\n", "| [`toa_sports_odds`](../odds/reference/additional.md#toa_sports_odds) | **Current odds** for a sport โ€” one row per outcome | ๐Ÿ’ณ paid |\n", "| [`toa_event_odds`](../odds/reference/additional.md#toa_event_odds) | Odds for a **single game**, including player props | ๐Ÿ’ณ paid |\n", "| [`toa_event_markets`](../odds/reference/additional.md#toa_event_markets) | Which markets a game has on offer | ๐Ÿ†“ free |\n", "| [`toa_sports_scores`](../odds/reference/additional.md#toa_sports_scores) | Live + recently-completed **scores** | ๐Ÿ†“ free |\n", "| [`toa_sports_events`](../odds/reference/additional.md#toa_sports_events) | Upcoming + live **event list** (grab `event_id`s here) | ๐Ÿ†“ free |\n", "| [`toa_sports_participants`](../odds/reference/additional.md#toa_sports_participants) | Teams / participants for a sport | ๐Ÿ†“ free |\n", "| [`toa_sports_odds_history`](../odds/reference/additional.md#toa_sports_odds_history) | **Historical** odds snapshot (paid plans) | ๐Ÿ’ณ paid |\n", "| [`toa_sports_events_history`](../odds/reference/additional.md#toa_sports_events_history) | Historical event snapshot | ๐Ÿ’ณ paid |\n", "| [`toa_event_odds_history`](../odds/reference/additional.md#toa_event_odds_history) | Historical single-game odds | ๐Ÿ’ณ paid |\n", "| [`toa_usage`](../odds/reference/additional.md#toa_usage) | Your remaining quota (reads cached headers) | ๐Ÿ†“ free |" ] }, { "cell_type": "markdown", "id": "591a97bc", "metadata": {}, "source": [ "## ๐Ÿ”‘ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "The Odds API needs a key โ€” grab a free one at\n", "[the-odds-api.com](https://the-odds-api.com/#get-access). Set it once as the\n", "`ODDS_API_KEY` environment variable (the same name `oddsapiR` uses) or pass\n", "`api_key=` to any call. The live cells below run only when a key is present,\n", "so this page is happy either way. ๐Ÿ˜Š" ] }, { "cell_type": "code", "execution_count": null, "id": "6d9843e2", "metadata": {}, "outputs": [], "source": [ "import os\n", "import polars as pl\n", "import sportsdataverse.odds as odds\n", "\n", "HAS_KEY = bool(os.environ.get(\"ODDS_API_KEY\"))\n", "print(\"ODDS_API_KEY set:\", HAS_KEY, \"โ€” live cells will\" + (\"\" if HAS_KEY else \" NOT\") + \" run\")" ] }, { "cell_type": "markdown", "id": "95ae4526", "metadata": {}, "source": [ "## ๐Ÿ—‚๏ธ What's on the board?\n", "\n", "Start with [`toa_sports`](../odds/reference/additional.md#toa_sports) โ€” it lists every sport/league key,\n", "and it's **free** (doesn't touch your quota). The `key` column is what you\n", "pass as `sport=` everywhere else." ] }, { "cell_type": "code", "execution_count": null, "id": "aaf33df5", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " sports = odds.toa_sports(all_sports=True)\n", " out = sports.select([c for c in [\"key\", \"group\", \"title\", \"active\"] if c in sports.columns]).head(12)\n", "else:\n", " out = \"set ODDS_API_KEY to run: odds.toa_sports(all_sports=True)\"\n", "out" ] }, { "cell_type": "markdown", "id": "48cc8642", "metadata": {}, "source": [ "## ๐Ÿ’ฐ The main event: live odds\n", "\n", "[`toa_sports_odds`](../odds/reference/additional.md#toa_sports_odds) is the workhorse. It returns **long\n", "format** โ€” one row per *event ร— bookmaker ร— market ร— outcome* โ€” which is\n", "exactly the shape you want for filtering and modelling. Knobs:\n", "\n", "- `regions` โ€” bookmaker regions: `us`, `us2`, `uk`, `eu`, `au` (comma-separate to mix).\n", "- `markets` โ€” `h2h` (moneyline), `spreads`, `totals`, `outrights`, โ€ฆ (comma-separated).\n", "- `odds_format` โ€” `american` or `decimal`.\n", "- `bookmakers` โ€” pin specific books (takes precedence over `regions`)." ] }, { "cell_type": "code", "execution_count": null, "id": "be784b2d", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " board = odds.toa_sports_odds(sport=\"americanfootball_nfl\", regions=\"us\", markets=\"h2h,spreads\")\n", " keep = [\"home_team\", \"away_team\", \"bookmaker_key\", \"market_key\", \"outcome_name\", \"outcome_point\", \"outcome_price\"]\n", " out = board.select([c for c in keep if c in board.columns]).head(10)\n", "else:\n", " board = None\n", " out = \"set ODDS_API_KEY to run: odds.toa_sports_odds(sport='americanfootball_nfl', regions='us')\"\n", "out" ] }, { "cell_type": "markdown", "id": "9c12c1ae", "metadata": {}, "source": [ "## ๐Ÿณ Cookbook: common odds tasks\n", "\n", "Because everything is one tidy long frame, the fun stuff is just a few polars\n", "expressions away. Twelve recipes you'll reach for constantly โ€” every live\n", "cell is key-guarded, so the page renders fine with or without a key." ] }, { "cell_type": "markdown", "id": "465e0be0", "metadata": {}, "source": [ "### Recipe 1 โ€” Best available moneyline (line shopping ๐Ÿ›’)\n", "\n", "For each team, find the **highest** moneyline price across every book โ€” and\n", "which book is offering it. Sort by price descending, group, take the top." ] }, { "cell_type": "code", "execution_count": null, "id": "5d0b11a5", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY and board is not None:\n", " h2h = board.filter(pl.col(\"market_key\") == \"h2h\")\n", " best = (\n", " h2h.sort(\"outcome_price\", descending=True)\n", " .group_by([\"home_team\", \"away_team\", \"outcome_name\"], maintain_order=True)\n", " .agg(pl.first(\"outcome_price\").alias(\"best_price\"), pl.first(\"bookmaker_key\").alias(\"best_book\"))\n", " )\n", " out = best.head(10)\n", "else:\n", " out = \"needs ODDS_API_KEY\"\n", "out" ] }, { "cell_type": "markdown", "id": "48e4db65", "metadata": {}, "source": [ "### Recipe 2 โ€” Spreads & totals for a slate ๐Ÿ“‹\n", "\n", "Ask for `markets=\"spreads,totals\"` and the `outcome_point` column carries the\n", "line (the spread number / the over-under total)." ] }, { "cell_type": "code", "execution_count": null, "id": "dbc9a5cb", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " st = odds.toa_sports_odds(sport=\"americanfootball_nfl\", regions=\"us\", markets=\"spreads,totals\")\n", " out = (\n", " st.filter(pl.col(\"bookmaker_key\") == st[\"bookmaker_key\"][0])\n", " .select([\"home_team\", \"away_team\", \"market_key\", \"outcome_name\", \"outcome_point\", \"outcome_price\"])\n", " .head(10)\n", " if st.height else \"no spreads/totals on the board right now\"\n", " )\n", "else:\n", " out = \"needs ODDS_API_KEY\"\n", "out" ] }, { "cell_type": "markdown", "id": "53319952", "metadata": {}, "source": [ "### Recipe 3 โ€” Just one book ๐ŸŽฏ\n", "\n", "Pin a single sportsbook with `bookmakers=`. Great for tracking *your* book's\n", "line without paying for a whole region." ] }, { "cell_type": "code", "execution_count": null, "id": "6cc8a030", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " dk = odds.toa_sports_odds(sport=\"americanfootball_nfl\", bookmakers=\"draftkings\", markets=\"h2h\")\n", " out = dk.select([\"home_team\", \"away_team\", \"outcome_name\", \"outcome_price\"]).head() if dk.height else \"no lines yet\"\n", "else:\n", " out = \"needs ODDS_API_KEY\"\n", "out" ] }, { "cell_type": "markdown", "id": "01b31c29", "metadata": {}, "source": [ "### Recipe 4 โ€” Implied probability & the hold ๐Ÿงฎ\n", "\n", "American moneyline prices convert to **implied win probability** with a tiny\n", "formula. Add up both sides and the excess over 100% is the book's *hold* (the\n", "vig). Pure polars math on the frame you already pulled โ€” no extra API call." ] }, { "cell_type": "code", "execution_count": null, "id": "e3794088", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY and board is not None:\n", " h2h = board.filter(pl.col(\"market_key\") == \"h2h\")\n", " devig = (\n", " h2h.with_columns(\n", " pl.when(pl.col(\"outcome_price\") < 0)\n", " .then(-pl.col(\"outcome_price\") / (-pl.col(\"outcome_price\") + 100))\n", " .otherwise(100 / (pl.col(\"outcome_price\") + 100))\n", " .alias(\"implied_prob\")\n", " )\n", " .group_by([\"home_team\", \"away_team\", \"bookmaker_key\"], maintain_order=True)\n", " .agg(pl.sum(\"implied_prob\").alias(\"market_total\"))\n", " .with_columns(((pl.col(\"market_total\") - 1) * 100).round(2).alias(\"hold_pct\"))\n", " .sort(\"hold_pct\")\n", " )\n", " out = devig.head(10)\n", "else:\n", " out = \"needs ODDS_API_KEY\"\n", "out" ] }, { "cell_type": "markdown", "id": "931743d5", "metadata": {}, "source": [ "### Recipe 5 โ€” Find the biggest favorite on the board ๐Ÿป\n", "\n", "Sort the moneyline outcomes by price ascending โ€” the most negative number is\n", "the heaviest chalk on the slate. A classic \"find the X\" one-liner." ] }, { "cell_type": "code", "execution_count": null, "id": "e8821c26", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY and board is not None:\n", " faves = (\n", " board.filter(pl.col(\"market_key\") == \"h2h\")\n", " .sort(\"outcome_price\")\n", " .select([\"home_team\", \"away_team\", \"outcome_name\", \"outcome_price\", \"bookmaker_key\"])\n", " .head(5)\n", " )\n", " out = faves\n", "else:\n", " out = \"needs ODDS_API_KEY\"\n", "out" ] }, { "cell_type": "markdown", "id": "700ec4e6", "metadata": {}, "source": [ "### Recipe 6 โ€” Consensus over/under per game ๐Ÿ“Š\n", "\n", "Books disagree by a half-point here and there. Take the **median** total\n", "across every book to get a stable market consensus for each matchup." ] }, { "cell_type": "code", "execution_count": null, "id": "d601111e", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " tot = odds.toa_sports_odds(sport=\"americanfootball_nfl\", regions=\"us\", markets=\"totals\")\n", " if tot.height:\n", " consensus = (\n", " tot.filter(pl.col(\"outcome_name\") == \"Over\")\n", " .group_by([\"home_team\", \"away_team\"], maintain_order=True)\n", " .agg(\n", " pl.median(\"outcome_point\").alias(\"consensus_total\"),\n", " pl.col(\"bookmaker_key\").n_unique().alias(\"n_books\"),\n", " )\n", " .sort(\"consensus_total\", descending=True)\n", " )\n", " out = consensus.head(10)\n", " else:\n", " out = \"no totals on the board right now\"\n", "else:\n", " out = \"needs ODDS_API_KEY\"\n", "out" ] }, { "cell_type": "markdown", "id": "f0eb8625", "metadata": {}, "source": [ "### Recipe 7 โ€” Just today's slate โฐ\n", "\n", "Narrow the pull to a time window with `commence_time_from` / `commence_time_to`\n", "(ISO-8601, UTC). Here: only games kicking off in the next 24 hours." ] }, { "cell_type": "code", "execution_count": null, "id": "ed2e7d51", "metadata": {}, "outputs": [], "source": [ "from datetime import datetime, timedelta, timezone\n", "\n", "if HAS_KEY:\n", " now = datetime.now(timezone.utc)\n", " fmt = \"%Y-%m-%dT%H:%M:%SZ\"\n", " today = odds.toa_sports_odds(\n", " sport=\"americanfootball_nfl\",\n", " regions=\"us\",\n", " markets=\"h2h\",\n", " commence_time_from=now.strftime(fmt),\n", " commence_time_to=(now + timedelta(hours=24)).strftime(fmt),\n", " )\n", " out = (\n", " today.select([\"commence_time\", \"home_team\", \"away_team\"]).unique(maintain_order=True).head(10)\n", " if today.height else \"nothing kicks off in the next 24h\"\n", " )\n", "else:\n", " out = \"set ODDS_API_KEY to run the commence-time filter recipe\"\n", "out" ] }, { "cell_type": "markdown", "id": "4390a342", "metadata": {}, "source": [ "### Recipe 8 โ€” Player props for one game ๐ŸŽฏ\n", "\n", "Event-level markets (player props!) live on [`toa_event_odds`](../odds/reference/additional.md#toa_event_odds).\n", "Grab an `event_id` from [`toa_sports_events`](../odds/reference/additional.md#toa_sports_events), then ask\n", "for a prop market like `player_pass_tds` or `player_anytime_td`." ] }, { "cell_type": "code", "execution_count": null, "id": "8d7ea073", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " events = odds.toa_sports_events(sport=\"americanfootball_nfl\", return_parsed=False)\n", " if events:\n", " eid = events[0][\"id\"]\n", " props = odds.toa_event_odds(sport=\"americanfootball_nfl\", event_id=eid, markets=\"player_pass_tds\")\n", " out = props.select([c for c in [\"outcome_name\", \"outcome_description\", \"outcome_point\", \"outcome_price\"]\n", " if c in props.columns]).head()\n", " else:\n", " out = \"no upcoming NFL events right now\"\n", "else:\n", " out = \"set ODDS_API_KEY to run the player-props recipe\"\n", "out" ] }, { "cell_type": "markdown", "id": "0c064903", "metadata": {}, "source": [ "### Recipe 9 โ€” Which markets does a game offer? ๐Ÿ—ƒ๏ธ\n", "\n", "Not sure which props are even available? [`toa_event_markets`](../odds/reference/additional.md#toa_event_markets)\n", "lists every market on offer per book โ€” and it's **free**. Count them up to\n", "see which sportsbook posts the deepest menu." ] }, { "cell_type": "code", "execution_count": null, "id": "66fe8113", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " events = odds.toa_sports_events(sport=\"americanfootball_nfl\", return_parsed=False)\n", " if events:\n", " eid = events[0][\"id\"]\n", " mk = odds.toa_event_markets(sport=\"americanfootball_nfl\", event_id=eid)\n", " out = (\n", " mk.group_by(\"bookmaker_key\").agg(pl.col(\"market_key\").n_unique().alias(\"n_markets\"))\n", " .sort(\"n_markets\", descending=True).head(10)\n", " if mk.height else \"no markets posted for this event yet\"\n", " )\n", " else:\n", " out = \"no upcoming NFL events right now\"\n", "else:\n", " out = \"set ODDS_API_KEY to run the event-markets recipe\"\n", "out" ] }, { "cell_type": "markdown", "id": "6cace678", "metadata": {}, "source": [ "### Recipe 10 โ€” Recent finals & margin of victory ๐Ÿ\n", "\n", "[`toa_sports_scores`](../odds/reference/additional.md#toa_sports_scores) returns live + recently\n", "completed games (`days_from=1..3`, **free**). Keep the completed ones and\n", "show the final scoreline โ€” handy for grading bets after the fact." ] }, { "cell_type": "code", "execution_count": null, "id": "fd7350bb", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " sc = odds.toa_sports_scores(sport=\"americanfootball_nfl\", days_from=3)\n", " keep = [c for c in [\"completed\", \"home_team\", \"away_team\", \"scores\", \"last_update\"] if c in sc.columns]\n", " if sc.height and \"completed\" in sc.columns:\n", " out = sc.filter(pl.col(\"completed\")).select(keep).head(10)\n", " else:\n", " out = sc.select(keep).head(10) if sc.height else \"no recent scores right now\"\n", "else:\n", " out = \"set ODDS_API_KEY to run: odds.toa_sports_scores(sport='americanfootball_nfl', days_from=3)\"\n", "out" ] }, { "cell_type": "markdown", "id": "fe6b1338", "metadata": {}, "source": [ "### Recipe 11 โ€” Tour several leagues at once ๐Ÿ”\n", "\n", "The `sport=` key is the only thing that changes between leagues, so one loop\n", "counts the upcoming events across a handful of them. `toa_sports_events` is\n", "**free**, so this sweep costs you nothing." ] }, { "cell_type": "code", "execution_count": null, "id": "1abfd12d", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " keys = [\"americanfootball_nfl\", \"basketball_nba\", \"icehockey_nhl\", \"baseball_mlb\"]\n", " rows = []\n", " for k in keys:\n", " evs = odds.toa_sports_events(sport=k, return_parsed=False)\n", " rows.append({\"sport\": k, \"upcoming_events\": len(evs) if isinstance(evs, list) else 0})\n", " out = pl.DataFrame(rows).sort(\"upcoming_events\", descending=True)\n", "else:\n", " out = \"set ODDS_API_KEY to tour leagues with odds.toa_sports_events(sport=...)\"\n", "out" ] }, { "cell_type": "markdown", "id": "ad77d5b4", "metadata": {}, "source": [ "### Recipe 12 โ€” Who's in the league? (participants ๐Ÿ‘ฅ)\n", "\n", "[`toa_sports_participants`](../odds/reference/additional.md#toa_sports_participants) lists every team /\n", "participant for a sport โ€” the lookup table you join odds against by name.\n", "Also **free**." ] }, { "cell_type": "code", "execution_count": null, "id": "30871f49", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " parts = odds.toa_sports_participants(sport=\"americanfootball_nfl\")\n", " keep = [c for c in [\"full_name\", \"id\", \"abbreviation\"] if c in parts.columns]\n", " out = parts.select(keep if keep else parts.columns).head(10) if parts.height else \"no participants listed\"\n", "else:\n", " out = \"set ODDS_API_KEY to run: odds.toa_sports_participants(sport='americanfootball_nfl')\"\n", "out" ] }, { "cell_type": "markdown", "id": "4074bd00", "metadata": {}, "source": [ "## โ›ฝ Mind your quota\n", "\n", "Paid calls cost credits (every 10 bookmakers ร— market โ‰ˆ 1 credit). After any\n", "call, [`toa_usage`](../odds/reference/additional.md#toa_usage) reads the most recent\n", "`x-requests-remaining` / `x-requests-used` headers **without spending a\n", "request** โ€” handy to drop at the end of a script." ] }, { "cell_type": "code", "execution_count": null, "id": "36d377ce", "metadata": {}, "outputs": [], "source": [ "odds.toa_usage() if HAS_KEY else \"set ODDS_API_KEY to track quota with odds.toa_usage()\"" ] }, { "cell_type": "markdown", "id": "9be208ba", "metadata": {}, "source": [ "## โณ Time travel: historical odds\n", "\n", "On a paid plan you can pull point-in-time snapshots โ€” perfect for *closing\n", "line value* studies. Pass a `date=` ISO-8601 timestamp; the snapshot is\n", "unwrapped to the same long format and every row is stamped with the snapshot\n", "time.\n", "\n", "| Function | Snapshot ofโ€ฆ |\n", "|---|---|\n", "| [`toa_sports_odds_history`](../odds/reference/additional.md#toa_sports_odds_history) | a whole sport's odds at `date` |\n", "| [`toa_sports_events_history`](../odds/reference/additional.md#toa_sports_events_history) | the events at `date` |\n", "| [`toa_event_odds_history`](../odds/reference/additional.md#toa_event_odds_history) | one game's odds at `date` |\n", "\n", "```python\n", "odds.toa_sports_odds_history(sport=\"americanfootball_nfl\", date=\"2023-11-29T22:45:00Z\")\n", "```" ] }, { "cell_type": "markdown", "id": "9e6ac402", "metadata": {}, "source": [ "## ๐ŸŽ‰ Where to next\n", "\n", "- Pass `return_as_pandas=True` for a pandas frame, or `return_parsed=False` for raw JSON.\n", "- Full reference: the **Betting โ†’ Odds** section in the sidebar.\n", "- R user? The same surface lives in [oddsapiR](https://oddsapir.sportsdataverse.org).\n", "\n", "Happy modelling โ€” may your closing line value be ever positive! ๐Ÿ“ˆ" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }