Build a Player Props Analyzer with Python and the SportsGameOdds API
May 13, 2026

If you have spent any time betting player props, you have probably noticed that sportsbooks do not always agree on where to set a line. One book posts LeBron James at 24.5 points, everyone else is at 25.5, and suddenly you have a full point of extra cushion if you know where to look.
The problem is finding those discrepancies manually, across dozens of props and multiple books, before game time. That is exactly what this script automates. By the end of this tutorial you will have a working Python tool that pulls every player prop for any NBA game, compares lines across bookmakers, calculates a consensus, and tells you exactly where the value is sitting.
What You'll Build
You are building a Python command line script that does the heavy lifting for your prop research. Here is what it handles:
- Fetches all player props for any upcoming NBA game in real time
- Compares lines side by side across every available bookmaker
- Calculates the consensus line using mean, median, min, and max
- Flags any sportsbook whose line sits more than a point away from consensus
- Tells you which side to consider and at which book
Perfect for: Sports bettors doing pre-game research, developers building prop tools, or anyone learning to work with live odds data via API.
What You'll Need
Nothing complicated here. Before you start, make sure you have:
- Python 3.8 or newer installed on your machine
- A SportsGameOdds API key, which you can grab for free at sportsgameodds.com/pricing
- The requests library, which we will install together in the next step
- A basic comfort level with Python. You do not need to be an expert
Quick timing note: NBA player props are usually posted 12 to 24 hours before tip-off. If you run the script earlier than that and see no results, that is completely normal. NFL props tend to go up 48 or more hours before kickoff.
Step 1: Get Your Project Set Up
Start by creating a fresh directory, activating a virtual environment, and installing the one package you need:
mkdir props-analyzer && cd props-analyzer
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install requests
Once that is done, set your API key as an environment variable. It is good practice to keep keys out of your code entirely, especially if you plan to share or publish the script:
export SPORTSGAMEODDS_KEY=your_api_key_here
Step 2: Create the Analyzer Script
Create a new file called analyzer.py and paste in the code below. It looks like a lot, but we will walk through exactly what each part does right after. Feel free to read through it first to get a feel for the structure.
# 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 NBA games
Run the script without any extra arguments to see a list of upcoming games you can analyze:
export SPORTSGAMEODDS_KEY=your_api_key_here
python analyzer.py
Drill into a specific game
Once you have an event ID from the list, set it as an environment variable and run again:
export SPORTSGAMEODDS_KEY=your_api_key_here
export EVENT_ID=abc123
python analyzer.py
See the full code examples in our Player Prop Analyzer Docs
How It Works
1. Telling player props apart from team bets
The SportsGameOdds API uses a field called statEntityID to indicate what a bet is actually about. Team level odds like point spreads and game totals use values like all, home, or away. Player props use the player's unique identifier instead, for example LEBRON_JAMES_1_NBA.
The script checks this field on every odd and skips anything that looks like a team level bet, so you only ever see genuine player props in the output.
2. Pulling the line from each bookmaker
Every odds object contains a byBookmaker section. The script loops through it and pulls two things for each available sportsbook: the overUnder line (for example 25.5) and the price on each side (for example -110). If a book does not have the prop available it gets skipped.
3. Building the consensus
With all the lines collected, the script uses Python's built in statistics module to calculate the mean, median, minimum, and maximum across every book. The mean becomes the consensus benchmark that everything else is measured against.
4. Spotting the outliers
Any book whose line sits 1.0 or more points away from the consensus mean gets flagged as an outlier. A full point of difference is meaningful in most prop markets. It is the kind of gap that, over a season of bets, adds up to a real edge.
5. Figuring out which side has value
The logic here is straightforward. If a book's line is lower than consensus, hitting the over there is easier than it would be at other books. If the line is higher, the under becomes the more attractive side. The script surfaces both opportunities clearly so you can decide quickly.
A Real Example: LeBron James Points
Here is what the analysis looks like in practice. Eight books are offering the prop and the consensus lands at 25.5 points. Two books are flagged as outliers, highlighted below:
| Bookmaker | Line | Over | Under | vs Consensus |
|---|---|---|---|---|
| DraftKings | 25.5 | -110 | -110 | — |
| FanDuel | 25.5 | -108 | -112 | — |
| Caesars | 25.5 | -110 | -110 | — |
| BetMGM ★ | 24.5 | -105 | -115 | -1.0 pt (OVER value) |
| Barstool ★ | 26.5 | -110 | -110 | +1.0 pt (UNDER value) |
BetMGM is sitting a full point below everyone else at 24.5. If LeBron scores exactly 25 points tonight, you win the over at BetMGM but lose at every other book. That is the edge the script finds automatically, without you having to open eight different apps and compare manually.
Ways to Take It Further
Track how lines move over time
Each time you run the script, save a snapshot of the odds to a file. Compare snapshots over time and you can see when lines shift sharply before tip-off, which is often a sign of sharp money coming in or a late injury update.
Filter by prop type
On a busy game day you might only care about points props or rebounds. Adding a simple filter argument to the script lets you cut through the noise and focus on what matters to you.
Export everything to a spreadsheet
Python's built in csv module makes it easy to write all your props and bookmaker data to a file you can open in Excel or Google Sheets. Great for sharing with a group or doing deeper analysis over a longer period.
Calculate expected value
The starter calculate_ev() function in the script gives you a foundation to build on. Convert American odds to decimal, factor in the line distance from consensus, and you can start making decisions based on expected value rather than gut feel.
Things That Might Go Wrong
The script says no player props were found
This is almost always a timing issue rather than a code problem. NBA props typically go live 12 to 24 hours before tip-off, and NFL props appear 48 or more hours out. Run the script again when you are closer to game time and you should see them appear.
Some players are missing from the output
Bookmakers only post props for starters and key bench players, usually somewhere between 10 and 15 players per NBA game. Spot players and anyone listed as a DNP will not have lines posted.
Player names are showing up as raw IDs
Make sure you are calling format_player_name() wherever you print player names in the output. If you use the raw statEntityID value directly you will see identifiers like LEBRON_JAMES_1_NBA instead of a readable name.
Getting a 401 error from the API
This almost always means the environment variable is not set in your current shell session. Double check that you have run the export command in the same terminal window you are using to run the script.
What to Build Next
Once this analyzer is running smoothly, it pairs really well with a few other tools in the SportsGameOdds documentation:
- Live Odds Tracker: monitor prop line movement in real time as game time gets closer
- Arbitrage Calculator: check whether the spread between books creates a risk free opportunity
- Parlay Builder: combine your best value props and calculate the combined odds
Get your free API key at sportsgameodds.com/pricing and you can have this running in under 10 minutes.
The full API documentation lives at sportsgameodds.com/docs if you want to explore what else is possible.