Skip to content

Build a Player Props Analyzer

Analyze player prop bets by fetching props for a specific game and comparing lines across bookmakers.

What You’ll Build

A Python script that:

  • Fetches all player props for an NBA game
  • 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 bet research

Prerequisites

  • Python 3.8+
  • SportsGameOdds API key (Get one free)
  • Basic Python knowledge

Complete Code

Step 1: Setup Project

bash
mkdir props-analyzer && cd props-analyzer
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install requests

Step 2: Create analyzer.py

python
# analyzer.py
import requests
import os
from collections import defaultdict
from statistics import mean, median

API_KEY = os.environ.get('SPORTSGAMEODDS_KEY')
API_BASE = 'https://api.sportsgameodds.com/v2'

def fetch_game_props(event_id):
    """Fetch all props for a specific game"""
    try:
        response = requests.get(
            f'{API_BASE}/events',
            params={'eventIDs': event_id},
            headers={'x-api-key': API_KEY}
        )
        response.raise_for_status()

        data = response.json()
        if not data['data']:
            print(f'Event {event_id} not found')
            return None

        return data['data'][0]
    except requests.exceptions.RequestException as e:
        print(f'Error fetching event: {e}')
        return None

def find_upcoming_nba_games():
    """Find upcoming NBA games to analyze"""
    try:
        response = requests.get(
            f'{API_BASE}/events',
            params={
                'leagueID': 'NBA',
                'finalized': 'false',
                'oddsAvailable': 'true',
                'limit': 10
            },
            headers={'x-api-key': API_KEY}
        )
        response.raise_for_status()
        return response.json()['data']
    except requests.exceptions.RequestException as e:
        print(f'Error fetching games: {e}')
        return []

def extract_player_props(odds):
    """
    Extract and organize player props from odds data.

    Player props are identified by statEntityID NOT being 'all', 'home', or 'away'.
    The player identifier is stored in statEntityID (e.g., 'LEBRON_JAMES_1_NBA').
    """
    props_by_player = defaultdict(lambda: defaultdict(list))

    for odd_id, odd in odds.items():
        stat_entity = odd.get('statEntityID', 'all')

        # Skip team-level odds (not player props)
        if stat_entity in ['all', 'home', 'away']:
            continue

        # This is a player prop
        player = stat_entity
        stat_id = odd.get('statID', 'unknown')
        bet_type = odd['betTypeID']
        side_id = odd['sideID']

        # Build a readable prop type from statID and betTypeID
        prop_type = f"{stat_id}_{bet_type}"

        # Process each bookmaker's odds
        for bookmaker_id, bookmaker_data in (odd.get('byBookmaker') or {}).items():
            if not bookmaker_data.get('available', True):
                continue

            props_by_player[player][prop_type].append({
                'bookmaker': bookmaker_id,
                'line': bookmaker_data.get('overUnder'),
                'over_price': bookmaker_data.get('odds') if side_id == 'over' else None,
                'under_price': bookmaker_data.get('odds') if side_id == 'under' else None,
                'side': side_id,
                'lastUpdatedAt': bookmaker_data.get('lastUpdatedAt')
            })

    return props_by_player

def format_player_name(player_id):
    """Convert player ID like 'LEBRON_JAMES_1_NBA' to 'LeBron James'"""
    if not player_id:
        return 'Unknown'

    # Remove suffix like '_1_NBA'
    parts = player_id.split('_')
    if len(parts) >= 2:
        # Take all parts except the last 2 (number and league)
        name_parts = parts[:-2] if len(parts) > 2 else parts
        # Capitalize each word
        return ' '.join(word.capitalize() for word in name_parts)
    return player_id

def calculate_consensus(props):
    """Calculate consensus line from all bookmakers"""
    lines = [float(p['line']) for p in props if p['line'] is not None]

    if not lines:
        return None

    return {
        'mean': round(mean(lines), 1),
        'median': median(lines),
        'min': min(lines),
        'max': max(lines),
        'books': len(lines)
    }

def find_outliers(props, consensus):
    """Find bookmakers with outlier lines"""
    if not consensus:
        return []

    mean_line = consensus['mean']
    outliers = []

    for prop in props:
        if prop['line'] is None:
            continue

        line_value = float(prop['line'])
        diff = abs(line_value - mean_line)

        # Outlier if differs by 1+ from consensus
        if diff >= 1.0:
            outliers.append({
                'bookmaker': prop['bookmaker'],
                'line': line_value,
                'diff': diff,
                'direction': 'higher' if line_value > mean_line else 'lower'
            })

    return sorted(outliers, key=lambda x: x['diff'], reverse=True)

def analyze_player_props(event):
    """Analyze all player props for an event"""
    away_name = event['teams']['away']['names']['long']
    home_name = event['teams']['home']['names']['long']
    matchup = f"{away_name} @ {home_name}"

    print(f'\n{matchup}')
    start_time = event.get('status', {}).get('startsAt', 'TBD')
    print(f"Start: {start_time}")
    print('=' * 80)

    props_by_player = extract_player_props(event.get('odds', {}))

    if not props_by_player:
        print('No player props found for this game')
        print('\nNote: Player props are typically available 12-24 hours before game time.')
        return

    print(f"\nFound props for {len(props_by_player)} players\n")

    for player, prop_types in sorted(props_by_player.items()):
        display_name = format_player_name(player)
        print(f'\n{display_name}')
        print('-' * 80)

        for prop_type, props in sorted(prop_types.items()):
            # Combine over/under entries by bookmaker
            combined_props = {}

            for prop in props:
                bookmaker = prop['bookmaker']
                if bookmaker not in combined_props:
                    combined_props[bookmaker] = {'bookmaker': bookmaker, 'line': prop['line']}

                if prop['over_price']:
                    combined_props[bookmaker]['over'] = prop['over_price']
                if prop['under_price']:
                    combined_props[bookmaker]['under'] = prop['under_price']

            props_list = list(combined_props.values())

            if not props_list:
                continue

            # Calculate consensus
            consensus = calculate_consensus(props_list)

            # Find outliers
            outliers = find_outliers(props_list, consensus)

            # Format prop name for display
            prop_name = prop_type.replace('_', ' ').title()

            print(f'\n  {prop_name}')

            if consensus:
                print(f"  Consensus: {consensus['mean']} (from {consensus['books']} books)")
                print(f"  Range: {consensus['min']} - {consensus['max']}")

            print(f'\n  {"Bookmaker":<15} {"Line":<8} {"Over":<10} {"Under":<10} {"Notes"}')
            print(f'  {"-" * 70}')

            for prop in sorted(props_list, key=lambda x: float(x['line']) if x['line'] else 0):
                line = f"{prop['line']}" if prop['line'] else 'N/A'
                over = f"{prop.get('over')}" if prop.get('over') else '-'
                under = f"{prop.get('under')}" if prop.get('under') else '-'

                # Check if outlier
                outlier = next((o for o in outliers if o['bookmaker'] == prop['bookmaker']), None)

                notes = ''
                if outlier:
                    notes = f"{outlier['diff']:.1f}pts {outlier['direction']}"

                print(f"  {prop['bookmaker']:<15} {line:<8} {over:<10} {under:<10} {notes}")

            # Highlight best value
            if outliers:
                print(f'\n  Value Opportunities:')
                for outlier in outliers[:2]:  # Top 2
                    if outlier['direction'] == 'lower':
                        print(f"     - {outlier['bookmaker']}: Line {outlier['diff']:.1f}pts lower (consider OVER)")
                    else:
                        print(f"     - {outlier['bookmaker']}: Line {outlier['diff']:.1f}pts higher (consider UNDER)")

def main():
    print('SportsGameOdds Player Props Analyzer\n')

    # Option 1: Analyze specific event
    event_id = os.environ.get('EVENT_ID')
    if event_id:
        event = fetch_game_props(event_id)
        if event:
            analyze_player_props(event)
        return

    # Option 2: Show upcoming games
    print('Upcoming NBA Games:\n')

    games = find_upcoming_nba_games()

    if not games:
        print('No upcoming NBA games found')
        print('Tip: Check during NBA season (October-June)')
        return

    for i, game in enumerate(games, 1):
        away_name = game['teams']['away']['names']['long']
        home_name = game['teams']['home']['names']['long']
        matchup = f"{away_name} @ {home_name}"
        start_time = game.get('status', {}).get('startsAt', 'TBD')

        # Count player props (statEntityID not in team-level values)
        prop_count = sum(
            1 for odd in game.get('odds', {}).values()
            if odd.get('statEntityID') not in ['all', 'home', 'away', None]
        )

        print(f"{i}. {matchup}")
        print(f"   Event ID: {game['eventID']}")
        print(f"   Start: {start_time}")
        print(f"   Player Props Available: {prop_count}")
        print()

    print('\nTo analyze a specific game:')
    print('export EVENT_ID=<event_id>')
    print('python analyzer.py')

if __name__ == '__main__':
    main()

Step 3: Run It

List upcoming games:

bash
export SPORTSGAMEODDS_KEY=your_api_key_here
python analyzer.py

Analyze specific game:

bash
export SPORTSGAMEODDS_KEY=your_api_key_here
export EVENT_ID=abc123
python analyzer.py

Expected Output

SportsGameOdds Player Props Analyzer

Boston Celtics @ Los Angeles Lakers
Start: 2024-11-14T19:00:00Z
================================================================================

Found props for 15 players

LeBron James
--------------------------------------------------------------------------------

  Points Ou

  Consensus: 25.5 (from 8 books)
  Range: 24.5 - 26.5

  Bookmaker        Line     Over       Under      Notes
  ----------------------------------------------------------------------
  betmgm           24.5     -105       -115       1.0pts lower
  draftkings       25.5     -110       -110
  fanduel          25.5     -108       -112
  caesars          25.5     -110       -110
  pointsbet        26.0     -105       -115
  barstool         26.5     -110       -110       1.0pts higher

  Value Opportunities:
     - betmgm: Line 1.0pts lower (consider OVER)
     - barstool: Line 1.0pts higher (consider UNDER)

  Rebounds Ou

  Consensus: 7.5 (from 7 books)
  Range: 7.5 - 8.5

  Bookmaker        Line     Over       Under      Notes
  ----------------------------------------------------------------------
  draftkings       7.5      -110       -110
  fanduel          7.5      -115       -105
  betmgm           7.5      -110       -110
  caesars          8.5      -120       +100       1.0pts higher

  Value Opportunities:
     - caesars: Line 1.0pts higher (consider UNDER)

  Assists Ou

  Consensus: 6.5 (from 6 books)
  Range: 6.5 - 6.5

  Bookmaker        Line     Over       Under      Notes
  ----------------------------------------------------------------------
  draftkings       6.5      -120       +100
  fanduel          6.5      -110       -110
  betmgm           6.5      -105       -115

Anthony Davis
--------------------------------------------------------------------------------

  Points Ou

  Consensus: 23.5 (from 7 books)
  Range: 22.5 - 24.5
  ...

How It Works

1. Identify Player Props

Player props are identified by their statEntityID. Team-level odds have values like 'all', 'home', or 'away', while player props have player identifiers:

python
stat_entity = odd.get('statEntityID', 'all')

# Skip team-level odds
if stat_entity in ['all', 'home', 'away']:
    continue

# This is a player prop - statEntityID is the player (e.g., 'LEBRON_JAMES_1_NBA')
player = stat_entity

2. Process Bookmaker Odds

Each odd contains prices from multiple bookmakers in the byBookmaker object:

python
for bookmaker_id, bookmaker_data in odd.get('byBookmaker', {}).items():
    if not bookmaker_data.get('available', True):
        continue

    line = bookmaker_data.get('overUnder')  # e.g., "25.5"
    price = bookmaker_data.get('odds')       # e.g., "-110"

3. Calculate Consensus

python
lines = [float(p['line']) for p in props if p['line'] is not None]

consensus = {
    'mean': round(mean(lines), 1),      # Average line
    'median': median(lines),             # Middle value
    'min': min(lines),                   # Lowest line
    'max': max(lines)                    # Highest line
}

Example:

  • DraftKings: 25.5
  • FanDuel: 25.5
  • BetMGM: 24.5
  • Caesars: 26.5

Consensus: 25.5 (mean), Range: 24.5 - 26.5

4. Find Outliers

python
diff = abs(float(prop['line']) - mean_line)

if diff >= 1.0:  # 1+ points from consensus
    outliers.append({
        'bookmaker': prop['bookmaker'],
        'diff': diff,
        'direction': 'higher' if prop['line'] > mean_line else 'lower'
    })

Why outliers matter:

  • Lower line = easier to hit OVER
  • Higher line = easier to hit UNDER
  • Potential value if one book is off-market

5. Identify Value

python
if outlier['direction'] == 'lower':
    print(f"Consider OVER (easier to beat {outlier['line']})")
else:
    print(f"Consider UNDER (easier to stay under {outlier['line']})")

Real-World Example

LeBron James Points:

BookmakerLineOverUnder
DraftKings25.5-110-110
FanDuel25.5-108-112
BetMGM24.5-105-115
Caesars25.5-110-110
Barstool26.5-110-110

Consensus: 25.5 points

Analysis:

  • BetMGM outlier: 24.5 (1 point lower)
    • Value: OVER 24.5 @ -105
    • Why: Easier to hit than market consensus of 25.5
    • If LeBron scores 25 points, you win at BetMGM but lose everywhere else

Enhancements

Track Props Over Time

python
import json
from datetime import datetime

def save_props_snapshot(event, props):
    """Save props for historical tracking"""
    snapshot = {
        'timestamp': datetime.now().isoformat(),
        'event_id': event['eventID'],
        'props': props
    }

    with open(f"snapshots/{event['eventID']}.jsonl", 'a') as f:
        f.write(json.dumps(snapshot) + '\n')

def detect_line_movement(event_id):
    """Compare current props to historical snapshots"""
    snapshots = []

    try:
        with open(f"snapshots/{event_id}.jsonl", 'r') as f:
            for line in f:
                snapshots.append(json.loads(line))
    except FileNotFoundError:
        return []

    movements = []

    for i in range(1, len(snapshots)):
        prev = snapshots[i-1]
        curr = snapshots[i]

        # Compare lines
        # ... (implementation)

    return movements

Filter by Prop Type

python
def main():
    prop_filter = input('Filter by prop type (points/rebounds/assists/all): ')

    # In analyze function
    for prop_type, props in sorted(prop_types.items()):
        if prop_filter != 'all' and prop_filter not in prop_type.lower():
            continue  # Skip non-matching props

Export to Spreadsheet

python
import csv

def export_props_to_csv(props_by_player, filename='props.csv'):
    rows = []

    for player, prop_types in props_by_player.items():
        for prop_type, props in prop_types.items():
            for prop in props:
                rows.append({
                    'Player': format_player_name(player),
                    'Prop': prop_type,
                    'Bookmaker': prop['bookmaker'],
                    'Line': prop['line'],
                    'Over': prop.get('over_price'),
                    'Under': prop.get('under_price')
                })

    with open(filename, 'w', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)

    print(f'Exported to {filename}')

Add EV Calculations

python
def american_to_decimal(american):
    """Convert American odds to decimal"""
    odds = int(american)
    if odds > 0:
        return (odds / 100) + 1
    return (100 / abs(odds)) + 1

def calculate_ev(line, consensus_line, price, hit_rate=0.5):
    """
    Calculate expected value

    Assumes hit_rate based on distance from consensus
    """
    # Adjust hit rate based on line difference
    diff = float(line) - consensus_line

    if diff < 0:  # Lower line (easier to hit over)
        adjusted_hit_rate = hit_rate + (abs(diff) * 0.05)
    else:
        adjusted_hit_rate = hit_rate - (diff * 0.05)

    adjusted_hit_rate = max(0.3, min(0.7, adjusted_hit_rate))

    # Calculate EV
    decimal_odds = american_to_decimal(price)
    ev = (adjusted_hit_rate * (decimal_odds - 1)) - (1 - adjusted_hit_rate)

    return ev * 100  # As percentage

Troubleshooting

“No player props found”

Causes:

  1. Props not posted yet (too early before game)
  2. Event has no props available
  3. Wrong event ID

Solution: Check when props are typically posted:

  • NBA: 12-24 hours before tip-off
  • NFL: 48+ hours before kickoff

Props missing for some players

Cause: Bookmakers don’t offer props for all players (usually starters + key bench players only).

Expected: Props for 10-15 players per NBA game.

Player names showing as IDs

Cause: The API returns player IDs like LEBRON_JAMES_1_NBA.

Solution: Use the format_player_name function to convert:

python
def format_player_name(player_id):
    parts = player_id.split('_')
    if len(parts) >= 2:
        name_parts = parts[:-2] if len(parts) > 2 else parts
        return ' '.join(word.capitalize() for word in name_parts)
    return player_id

Next Steps

Combine with Other Examples

Learn More