# Player Props Analyzer Example - Python
URL: https://sockodds.com/docs/examples/player-props-analyzer/

# Player Props Analyzer Example - Python [#player-props-analyzer-example---python]

Analyze player prop bets by fetching props for an event and comparing lines across every Australian bookmaker.

## What you'll build [#what-youll-build]

A script that:

- Fetches all player props for an AFL event
- Compares lines across bookmakers
- Shows consensus lines
- Identifies outlier books
- Highlights potential value bets

**Perfect for:** Finding value in player props, comparing bookmaker offerings, prop research

## Prerequisites [#prerequisites]

- Python 3.8+
- SockOdds API key ([free](https://sockodds.com/signup/))
- Basic Python

## Complete code [#complete-code]

### Step 1: Setup project [#step-1-setup-project]

```bash
mkdir props-analyzer && cd props-analyzer
python -m venv venv && source venv/bin/activate
pip install requests
```

### Step 2: Create analyzer.py [#step-2-create-analyzerpy]

```python
# analyzer.py
import os, sys, statistics, requests

API_KEY = os.environ["SOCKODDS_KEY"]
BASE = "https://api.sockodds.com/v2"
LEAGUE = sys.argv[1] if len(sys.argv) > 1 else "AFL"
TEAM_SIDES = {"home", "away", "all"}

def fetch_event(league):
    r = requests.get(f"{BASE}/events", params={"leagueID": league, "oddsAvailable": "true", "limit": 1}, headers={"x-api-key": API_KEY}); r.raise_for_status()
    data = r.json()["data"]
    return data[0] if data else None

def is_player_prop(odd):
    return odd["statEntityID"] not in TEAM_SIDES          # the one check that identifies a prop

def analyze(event):
    players = event.get("players", {}); rows = []
    for odd in event["odds"].values():
        if not is_player_prop(odd) or odd["betTypeID"] != "ou" or odd["sideID"] != "over": continue
        lines = {b: float(q["overUnder"]) for b, q in odd["byBookmaker"].items() if q.get("available") and q.get("overUnder")}
        prices = {b: q["decimal"] for b, q in odd["byBookmaker"].items() if q.get("available") and q.get("decimal")}
        if len(lines) < 2: continue
        consensus = statistics.median(lines.values())
        name = players.get(odd["statEntityID"], {}).get("name", odd["statEntityID"])
        for book, line in lines.items():
            if line != consensus:
                rows.append((abs(line - consensus), name, odd["statID"], book, line, consensus, prices.get(book)))
    return sorted(rows, reverse=True)

if __name__ == "__main__":
    e = fetch_event(LEAGUE)
    if not e: sys.exit(f"no {LEAGUE} events with odds")
    print(f"{e['teams']['away']['names']['medium']} @ {e['teams']['home']['names']['medium']}  ({e['eventID']})")
    props = [o for o in e["odds"].values() if is_player_prop(o)]
    print(f"{len(props)} player-prop odds across {len({o['statID'] for o in props})} stats\n")
    print(f"{'player':22} {'stat':14} {'book':14} {'line':>6} {'consensus':>10} {'price':>6}")
    for diff, name, stat, book, line, cons, price in analyze(e)[:25]:
        flag = "  <- value?" if diff >= 1 else ""
        print(f"{name:22} {stat:14} {book:14} {line:6.1f} {cons:10.1f} {price or 0:6.2f}{flag}")
```

### Step 3: Run it [#step-3-run-it]

```bash
SOCKODDS_KEY=so_live_… python analyzer.py AFL
```

## Expected output [#expected-output]

```
Hawthorn @ Fremantle  (afl_2026-09-03_fremantle_vs_hawthorn)
236 player-prop odds across 4 stats

player                 stat           book             line  consensus  price
Caleb Serong           disposals      pointsbet        28.5       29.5   1.95  <- value?
Jai Newcombe           disposals      tab              26.5       27.5   1.91  <- value?
```

## How it works [#how-it-works]

### 1. Identify player props [#1-identify-player-props]

Any market whose `statEntityID` is not home/away/all.

### 2. Process bookmaker odds [#2-process-bookmaker-odds]

Each book has its own `overUnder` and `decimal`; skip unavailable ones.

### 3. Calculate consensus [#3-calculate-consensus]

The median line across books.

### 4. Find outliers [#4-find-outliers]

Books whose line differs from the consensus, sorted by the size of the gap.

### 5. Identify value [#5-identify-value]

A lower over line at the same price is worth a look — then check `fairOdds` on Base.

## Enhancements [#enhancements]

### Track props over time [#track-props-over-time]

```python
# store (eventID, oddID, book, line, price, lastUpdatedAt) each run and diff
```

### Filter by prop type [#filter-by-prop-type]

```python
props = [o for o in props if o["statID"] == "disposals"]
```

### Export to spreadsheet [#export-to-spreadsheet]

```python
import csv  # write rows with csv.writer
```

### Add EV calculations [#add-ev-calculations]

```python
# on Base: edge = 1/decimal(fairOdds) - 1/price  (see the +EV use case)
```

## Troubleshooting [#troubleshooting]

### "No player props found" [#no-player-props-found]

Props are posted a day or three out; try a league in season or `includeFinished` for a recent event.

### Props missing for some players [#props-missing-for-some-players]

Not every book prices every player.

### Player names showing as IDs [#player-names-showing-as-ids]

The `players` map only carries players the source resolved; fall back to the id.

## Next steps [#next-steps]

### Combine with other examples [#combine-with-other-examples]

- [Live odds tracker](https://sockodds.com/docs/examples/live-odds-tracker/)
- [Arbitrage calculator](https://sockodds.com/docs/examples/arbitrage-calculator/)
- [Odds comparison dashboard](https://sockodds.com/docs/examples/odds-comparison-dashboard/)
- [Parlay builder](https://sockodds.com/docs/examples/parlay-builder/)

### Learn more [#learn-more]

- [The oddID grammar](https://sockodds.com/docs/data-types/odds/)
- [Best practices](https://sockodds.com/docs/info/best-practices/)
- [API reference](https://sockodds.com/docs/reference/)

> Need help?[FAQ](https://sockodds.com/docs/faq/) · [Email](mailto:api@sockodds.com) · [Contact](https://sockodds.com/contact-us/)
