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
mkdir props-analyzer && cd props-analyzer
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install requestsStep 2: Create analyzer.py
# 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:
export SPORTSGAMEODDS_KEY=your_api_key_here
python analyzer.pyAnalyze specific game:
export SPORTSGAMEODDS_KEY=your_api_key_here
export EVENT_ID=abc123
python analyzer.pyExpected 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:
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_entity2. Process Bookmaker Odds
Each odd contains prices from multiple bookmakers in the byBookmaker object:
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
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
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
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:
| Bookmaker | Line | Over | Under |
|---|---|---|---|
| DraftKings | 25.5 | -110 | -110 |
| FanDuel | 25.5 | -108 | -112 |
| BetMGM | 24.5 | -105 | -115 |
| Caesars | 25.5 | -110 | -110 |
| Barstool | 26.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
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 movementsFilter by Prop Type
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 propsExport to Spreadsheet
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
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 percentageTroubleshooting
“No player props found”
Causes:
- Props not posted yet (too early before game)
- Event has no props available
- 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:
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_idNext Steps
Combine with Other Examples
- Live Odds Tracker - Monitor prop line movement
- Arbitrage Calculator - Find prop arbitrage
Learn More
- Understanding oddID - Decode prop identifiers
- Best Practices - Optimize queries
