import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import random
from itertools import combinations
from IPython.display import display, HTML
from nba_api.stats.endpoints import leaguedashteamstats
from nba_api.stats.static import players, teams
from nba_api.stats.endpoints import leaguedashplayerstats
Introduction
In this blog post, I will be exploring the NBA API and combining it with data pulled from my fantasy league. Our league is a dynasty format, where the same players are kept each season and only rookies are drafted. See The Dynamics of Dynasty blog post for a deep dive on the league!
First, let’s start by loading in the fantasy league data to get the fantasy team each player is on. Then this data will be merged with data from the NBA API to pull players stats from last season.
Data Preprocessing
# Load in fantasy data
= pd.read_csv("fantrax_05_18_2024.csv")
fantrax_data
# Keep only 'Player' and 'Status' columns
= fantrax_data[['Player', 'Status', 'FPts', 'FP/G']]
fantrax_data
# Removing free agent players
= fantrax_data[fantrax_data['Status'] != 'FA']
fantrax_data
# Updating Status column name
= fantrax_data.rename(columns = {'Status': 'Fantasy Team'})
fantrax_data
# Create dictionary to redefine team names
= {
status_mapping 'CCC': 'Cream City',
'STARKS': 'Winterfell',
'BBB': 'Bikini Bottom',
'HBC': 'Helsinki',
'Jmarr237': 'Malibu',
'maxpat01': 'Santa Barbara',
'GBRAYERS': 'Scottsdale',
'BIGFOOTS': 'Beaverton',
'$?$': 'Las Vegas',
'SDP': 'San Diego',
'SERP': 'Slytherin',
'Orcas': 'Anacortes'
}
# Apply team name mapping
'Fantasy Team'] = fantrax_data['Fantasy Team'].map(status_mapping) fantrax_data[
Now that the Fantrax data is cleaned, we can join it with each players stats pulled from the NBA API.
# Extract player names
= fantrax_data['Player'].tolist()
player_names
# Get player information from NBA API
= players.get_players()
nba_players
= {player['full_name']: player['id'] for player in nba_players if player['full_name'] in player_names}
player_id_map
'Player_ID'] = fantrax_data['Player'].map(player_id_map)
fantrax_data[
= leaguedashplayerstats.LeagueDashPlayerStats(season='2023-24').get_data_frames()[0]
player_stats
= pd.merge(fantrax_data, player_stats, left_on='Player_ID', right_on='PLAYER_ID', how='left') df
Let’s check out the data to make sure it looks good.
df.columns
Index(['Player', 'Fantasy Team', 'FPts', 'FP/G', 'Player_ID', 'PLAYER_ID',
'PLAYER_NAME', 'NICKNAME', 'TEAM_ID', 'TEAM_ABBREVIATION', 'AGE', 'GP',
'W', 'L', 'W_PCT', 'MIN', 'FGM', 'FGA', 'FG_PCT', 'FG3M', 'FG3A',
'FG3_PCT', 'FTM', 'FTA', 'FT_PCT', 'OREB', 'DREB', 'REB', 'AST', 'TOV',
'STL', 'BLK', 'BLKA', 'PF', 'PFD', 'PTS', 'PLUS_MINUS',
'NBA_FANTASY_PTS', 'DD2', 'TD3', 'WNBA_FANTASY_PTS', 'GP_RANK',
'W_RANK', 'L_RANK', 'W_PCT_RANK', 'MIN_RANK', 'FGM_RANK', 'FGA_RANK',
'FG_PCT_RANK', 'FG3M_RANK', 'FG3A_RANK', 'FG3_PCT_RANK', 'FTM_RANK',
'FTA_RANK', 'FT_PCT_RANK', 'OREB_RANK', 'DREB_RANK', 'REB_RANK',
'AST_RANK', 'TOV_RANK', 'STL_RANK', 'BLK_RANK', 'BLKA_RANK', 'PF_RANK',
'PFD_RANK', 'PTS_RANK', 'PLUS_MINUS_RANK', 'NBA_FANTASY_PTS_RANK',
'DD2_RANK', 'TD3_RANK', 'WNBA_FANTASY_PTS_RANK'],
dtype='object')
df
Player | Fantasy Team | FPts | FP/G | Player_ID | PLAYER_ID | PLAYER_NAME | NICKNAME | TEAM_ID | TEAM_ABBREVIATION | ... | BLK_RANK | BLKA_RANK | PF_RANK | PFD_RANK | PTS_RANK | PLUS_MINUS_RANK | NBA_FANTASY_PTS_RANK | DD2_RANK | TD3_RANK | WNBA_FANTASY_PTS_RANK | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Nikola Jokic | Cream City | 5154 | 65.24 | 203999.0 | 203999.0 | Nikola Jokic | Nikola | 1.610613e+09 | DEN | ... | 32.0 | 541.0 | 542.0 | 6.0 | 5.0 | 1.0 | 1.0 | 2.0 | 2.0 | 2.0 |
1 | Luka Doncic | Winterfell | 4644 | 66.34 | 1629029.0 | 1629029.0 | Luka Doncic | Luka | 1.610613e+09 | DAL | ... | 108.0 | 497.0 | 475.0 | 4.0 | 1.0 | 32.0 | 2.0 | 6.0 | 3.0 | 1.0 |
2 | Giannis Antetokounmpo | Bikini Bottom | 4394 | 60.19 | 203507.0 | 203507.0 | Giannis Antetokounmpo | Giannis | 1.610613e+09 | MIL | ... | 22.0 | 565.0 | 555.0 | 1.0 | 3.0 | 29.0 | 3.0 | 4.0 | 4.0 | 3.0 |
3 | Domantas Sabonis | Bikini Bottom | 4345 | 52.99 | 1627734.0 | 1627734.0 | Domantas Sabonis | Domantas | 1.610613e+09 | SAC | ... | 64.0 | 561.0 | 571.0 | 12.0 | 26.0 | 130.0 | 5.0 | 1.0 | 1.0 | 5.0 |
4 | Anthony Davis | Bikini Bottom | 4338 | 57.08 | 203076.0 | 203076.0 | Anthony Davis | Anthony | 1.610613e+09 | LAL | ... | 4.0 | 559.0 | 524.0 | 7.0 | 12.0 | 85.0 | 4.0 | 3.0 | 10.0 | 4.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
314 | Steven Adams | Scottsdale | 0 | 0.00 | 203500.0 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
315 | Lonzo Ball | Las Vegas | 0 | 0.00 | 1628366.0 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
316 | Kai Jones | Scottsdale | 0 | 0.00 | 1630539.0 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
317 | Christian Koloko | Cream City | 0 | 0.00 | 1631132.0 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
318 | Kevin Porter | Winterfell | 0 | 0.00 | 77876.0 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
319 rows × 71 columns
The data has been merged successfully! Now, let’s perform some data cleaning to filter out players that were not active last season.
= df.dropna() df
While our league is a points league, where fantasy points are the scoring metric, some leagues are set up as a 9 category format where teams try to win each week based on the 9 main statistical categories.
The categories are: - Points - Rebounds - Assists - Steals - Blocks - Field goal percentage - Free throw percentage - Three pointers made - Turnovers
Let’s see where each fantasy teams strengths would be in this format!
= None
pd.options.mode.chained_assignment
# Columns to be used for Z-score calculations
= ['PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3M', 'TOV']
categories
# Calculate per-game stats for each player in each category
for category in categories:
if category not in ['FG_PCT', 'FT_PCT']:
+ '_PG'] = df[category] / df['GP']
df[category else:
+ '_PG'] = df[category] # Percentages are already per-game
df[category
# Calculate Z-scores for each player in each per-game category
for category in categories:
if category not in ['FG_PCT', 'FT_PCT']:
= category + '_PG_Z'
z_score_column = (df[category + '_PG'] - df[category + '_PG'].mean()) / df[category + '_PG'].std()
df[z_score_column] else:
= category + '_Z'
z_score_column = (df[category] - df[category].mean()) / df[category].std()
df[z_score_column]
# Aggregate the Z-scores by team
= df.groupby('Fantasy Team')[[col for col in df.columns if col.endswith('_Z')]].mean()
team_z_scores
# Display the Z-scores table in HTML format
="%.2f", border=0, justify='center'))) display(HTML(team_z_scores.to_html(float_format
PTS_PG_Z | REB_PG_Z | AST_PG_Z | STL_PG_Z | BLK_PG_Z | FG_PCT_Z | FT_PCT_Z | FG3M_PG_Z | TOV_PG_Z | |
---|---|---|---|---|---|---|---|---|---|
Fantasy Team | |||||||||
Anacortes | -0.13 | -0.17 | -0.12 | 0.00 | -0.08 | 0.07 | -0.36 | -0.23 | -0.02 |
Beaverton | 0.09 | -0.36 | -0.03 | -0.04 | -0.41 | -0.33 | 0.26 | 0.49 | 0.07 |
Bikini Bottom | 0.35 | 0.45 | 0.38 | 0.13 | 0.24 | 0.28 | 0.23 | -0.05 | 0.33 |
Cream City | 0.08 | 0.30 | -0.05 | 0.16 | 0.34 | 0.29 | 0.27 | 0.19 | -0.10 |
Helsinki | -0.09 | -0.19 | -0.12 | -0.19 | -0.05 | 0.09 | -0.24 | -0.19 | -0.22 |
Las Vegas | -0.22 | -0.06 | -0.39 | -0.00 | -0.04 | -0.21 | -0.20 | -0.09 | -0.21 |
Malibu | 0.32 | 0.31 | 0.37 | 0.30 | 0.26 | 0.07 | 0.25 | 0.28 | 0.28 |
San Diego | 0.16 | -0.05 | 0.06 | 0.30 | -0.03 | 0.06 | 0.20 | 0.20 | 0.18 |
Santa Barbara | -0.30 | -0.31 | -0.28 | -0.16 | -0.02 | -0.02 | -0.24 | -0.02 | -0.15 |
Scottsdale | -0.24 | 0.01 | -0.02 | -0.20 | 0.02 | -0.13 | -0.57 | -0.38 | -0.25 |
Slytherin | -0.33 | -0.14 | -0.27 | -0.41 | -0.10 | -0.13 | 0.06 | -0.38 | -0.17 |
Winterfell | 0.23 | 0.11 | 0.35 | 0.06 | -0.21 | -0.09 | 0.22 | 0.12 | 0.20 |
This table shows us which teams are strongest in each categories. As we saw in the previous post, BBB (Bikini Bottom Ballers) are by far the strongest team. JMarr237 (Malibu Mamas) also has a deep roster with lots of good players.
Monte Carlo Simulation
When it comes to predicting the outcome of a fantasy basketball season, there are many factors to consider. Each team’s performance can vary from week to week based on the number of games their players have, potential injuries, and the inherent randomness of sports. This is where Monte Carlo simulations come in handy.
Monte Carlo simulations are a powerful tool that allows us to model complex systems and account for randomness. In the context of our fantasy basketball league, we can use these simulations to generate many possible outcomes for the season, taking into account the variability in each team’s performance.
Think of it like a game of tug-of-war. Each team’s Z score represents their baseline strength in a particular category. The higher the Z score, the stronger the team is in that category. However, just like in a real game of tug-of-war, there are external factors that can influence the outcome, such as the condition of the rope, the terrain, or even a momentary lapse in concentration from one of the participants. In our simulation, we’ll be using three levels of randomness: low, medium, and high. These levels represent how much each team’s performance can deviate from their expected Z scores each week. A low level of randomness means that the results will be closer to what we expect based on the Z scores, while a high level of randomness introduces more unpredictability.
By running the simulation 250 times, we can generate a wide range of plausible scenarios. Each simulation will give us a different set of results, and by analyzing these results, we can gain insights into the likelihood of various outcomes.
For example, let’s say Team A has a higher Z score than Team B in the “points” category. In a single simulation, Team B might win that category due to the randomness factor. However, across 250 simulations, we would expect Team A to win the “points” category more often than Team B.
Monte Carlo simulations help us account for the complexities and uncertainties in a fantasy basketball season. By embracing randomness and running many simulations, we can better understand the range of possible outcomes and make more informed decisions when managing our fantasy teams.
= team_z_scores.reset_index()
team_z_scores
# Parameters
= team_z_scores['Fantasy Team'].unique()
teams = {'low': 0.3, 'medium': 0.6, 'high': 0.9}
randomness_levels = 22
num_weeks = 250 num_simulations
# Initialize results df to track wins, losses, and ties
= pd.DataFrame(0, index=teams, columns=['Wins', 'Losses']) results_template
def simulate_season(randomness):
= results_template.copy()
weekly_results
# Create a schedule where each team plays every other team twice
= list(combinations(teams, 2)) * 2
matchups # Randomize the order of matchups
random.shuffle(matchups)
# Distribute matchups across 22 weeks
= [matchups[i:i + len(teams)//2] for i in range(0, len(matchups), len(teams)//2)]
weeks
for week in weeks:
for team1, team2 in week:
= 0
team1_score = 0
team2_score
for category in categories:
= category + '_PG_Z' if category not in ['FG_PCT', 'FT_PCT'] else category + '_Z'
z_score_column = team_z_scores.loc[team_z_scores['Fantasy Team'] == team1, z_score_column].values[0]
team1_z = team_z_scores.loc[team_z_scores['Fantasy Team'] == team2, z_score_column].values[0]
team2_z
+= np.random.normal(0, randomness * abs(team1_z))
team1_z += np.random.normal(0, randomness * abs(team2_z))
team2_z
if category == 'TOV':
if team1_z < team2_z: # Fewer turnovers is better
+= 1
team1_score else:
+= 1
team2_score else:
if team1_z > team2_z: # More is better for other categories
+= 1
team1_score else:
+= 1
team2_score
if team1_score > team2_score:
'Wins'] += 1
weekly_results.at[team1, 'Losses'] += 1
weekly_results.at[team2, elif team1_score < team2_score:
'Losses'] += 1
weekly_results.at[team1, 'Wins'] += 1
weekly_results.at[team2,
return weekly_results
def simulate_multiple_seasons(randomness, num_simulations):
= pd.DataFrame(0, index=teams, columns=['Wins', 'Losses'])
cumulative_results = []
all_simulation_results
for _ in range(num_simulations):
= simulate_season(randomness)
season_results += season_results
cumulative_results 'Wins'].copy())
all_simulation_results.append(season_results[
# Calculate average results
= cumulative_results / num_simulations
average_results return average_results, pd.concat(all_simulation_results, axis=1)
Now that the Monte Carlo simulation functions have been set up, we can run the simulation 250 times.
# Perform the simulations
= {}
final_results = {}
simulation_results
for level, randomness in randomness_levels.items():
= simulate_multiple_seasons(randomness, num_simulations)
avg_results, all_results_df = avg_results
final_results[level] = all_results_df simulation_results[level]
# Combine results ensuring unique column names
= pd.concat(final_results, axis=1, keys=randomness_levels.keys())
results_table = [f'{level.capitalize()} {col}' for level, col in results_table.columns]
results_table.columns
# Sort the table by 'Low Randomness' wins in descending order
= results_table.sort_values(by='Low Wins', ascending=False)
results_table_sorted
="%.2f", border=0, justify='center'))) display(HTML(results_table_sorted.to_html(float_format
Low Wins | Low Losses | Medium Wins | Medium Losses | High Wins | High Losses | |
---|---|---|---|---|---|---|
Malibu | 20.58 | 1.42 | 20.14 | 1.86 | 19.71 | 2.29 |
Cream City | 19.76 | 2.24 | 19.62 | 2.38 | 19.25 | 2.75 |
Bikini Bottom | 19.30 | 2.70 | 19.12 | 2.88 | 18.68 | 3.32 |
San Diego | 15.72 | 6.28 | 16.07 | 5.93 | 15.97 | 6.03 |
Winterfell | 14.64 | 7.36 | 14.84 | 7.16 | 14.82 | 7.18 |
Las Vegas | 8.23 | 13.77 | 6.65 | 15.35 | 6.04 | 15.96 |
Anacortes | 7.68 | 14.32 | 6.80 | 15.20 | 6.54 | 15.46 |
Scottsdale | 7.54 | 14.46 | 7.58 | 14.42 | 7.70 | 14.30 |
Helsinki | 7.39 | 14.61 | 6.80 | 15.20 | 6.80 | 15.20 |
Beaverton | 6.89 | 15.11 | 7.73 | 14.27 | 8.00 | 14.00 |
Santa Barbara | 3.72 | 18.28 | 4.60 | 17.40 | 5.01 | 16.99 |
Slytherin | 0.55 | 21.45 | 2.04 | 19.96 | 3.46 | 18.54 |
Reminder that this is for a 22 week regular season where each team plays each other twice. Under a 9-cat format, the Bikini Bottom Ballers are still the best team in the league. SDP, the San Diego Pilots, would seem to fare much better in a 9-cat format than the current points format.
Plotting Team Outcomes
Now let’s visualize the results by showing the win distribution for each team under each randomness scenario.
for team in teams:
= plt.subplots(1, len(randomness_levels), figsize=(20, 5), sharex=True, sharey=True)
fig, axes f'Number of Wins Distribution for {team} (250 Simulations)', fontsize=20)
fig.suptitle(
for col, (level, results_df) in enumerate(simulation_results.items()):
=10, alpha=0.7, edgecolor='black')
axes[col].hist(results_df.loc[team], binsf'{level.capitalize()} Randomness', fontsize=18)
axes[col].set_title('Number of Wins', fontsize=16)
axes[col].set_xlabel(='both', which='major', labelsize=14)
axes[col].tick_params(axisif col == 0:
'Frequency', fontsize=16)
axes[col].set_ylabel(
=[0, 0, 1, 0.95])
plt.tight_layout(rect plt.show()
Conclusion
In our analysis, we utilized Monte Carlo simulations to project the outcomes of a fantasy basketball season under different levels of randomness. By running 250 simulations for each level of randomness — low, medium, and high — we were able to model the inherent variability in team performances. This approach not only allowed us to see which teams are likely to excel consistently but also highlighted how randomness can impact the overall season. The histograms generated for each team showed the distribution of their wins across simulations, providing a visual representation of their performance stability.
Bonus: Points Analysis
I’ve also created the same workflow, but for points instead of 9-CAT since this the scoring of my fintasy league
= None
pd.options.mode.chained_assignment
# Calculate "percent started" based on player ranking within each fantasy team
'Rank'] = df.groupby('Fantasy Team')['FP/G'].rank(ascending=False)
df['Percent Started'] = df['Rank'].apply(lambda x: 1 if x <= 10 else 0.8 if x <= 15 else 0.4)
df[
# Parameters
= df['Fantasy Team'].unique()
teams = {'low': 0.2, 'medium': 0.4, 'high': 0.6}
randomness_levels = 22
num_weeks = 250
num_simulations
# Initialize results df to track wins and losses
= pd.DataFrame(0, index=teams, columns=['Wins', 'Losses'])
results_template
def simulate_season_fp(randomness):
= results_template.copy()
weekly_results
# Create a schedule where each team plays every other team twice
= list(combinations(teams, 2)) * 2
matchups # Randomize the order of matchups
random.shuffle(matchups)
# Distribute matchups across 22 weeks
= [matchups[i:i + len(teams)//2] for i in range(0, len(matchups), len(teams)//2)]
weeks
for week in weeks:
for team1, team2 in week:
= df[df['Fantasy Team'] == team1]
team1_players = df[df['Fantasy Team'] == team2]
team2_players
= (team1_players['FP/G'] * team1_players['Percent Started']).sum()
team1_fp = (team2_players['FP/G'] * team2_players['Percent Started']).sum()
team2_fp
+= np.random.normal(0, randomness * abs(team1_fp))
team1_fp += np.random.normal(0, randomness * abs(team2_fp))
team2_fp
if team1_fp > team2_fp:
'Wins'] += 1
weekly_results.at[team1, 'Losses'] += 1
weekly_results.at[team2, elif team1_fp < team2_fp:
'Losses'] += 1
weekly_results.at[team1, 'Wins'] += 1
weekly_results.at[team2,
return weekly_results
def simulate_multiple_seasons_fp(randomness, num_simulations):
= pd.DataFrame(0, index=teams, columns=['Wins', 'Losses'])
cumulative_results = []
all_simulation_results
for _ in range(num_simulations):
= simulate_season_fp(randomness)
season_results += season_results
cumulative_results 'Wins'].copy())
all_simulation_results.append(season_results[
# Calculate average results
= cumulative_results / num_simulations
average_results return average_results, pd.concat(all_simulation_results, axis=1)
# Perform the simulations
= {}
final_results_fp = {}
simulation_results_fp
for level, randomness in randomness_levels.items():
= simulate_multiple_seasons_fp(randomness, num_simulations)
avg_results_fp, all_results_df_fp = avg_results_fp
final_results_fp[level] = all_results_df_fp
simulation_results_fp[level]
# Combine results ensuring unique column names
= pd.concat(final_results_fp, axis=1, keys=randomness_levels.keys())
results_table_fp = [' '.join(col).strip() for col in results_table_fp.columns.values]
results_table_fp.columns
# Sort the table by 'low Wins' in descending order
= results_table_fp.sort_values(by='low Wins', ascending=False)
results_table_sorted_fp
="%.2f", border=0, justify='center')))
display(HTML(results_table_sorted_fp.to_html(float_format
# Calculate win percentages (number of wins divided by 22)
= results_table_sorted_fp / num_weeks * 100
win_percentages_fp = [f'{col} Win %' for col in win_percentages_fp.columns]
win_percentages_fp.columns
# Display the win percentage table
="%.2f", border=0, justify='center')))
display(HTML(win_percentages_fp.to_html(float_format
# Plot histograms for the number of wins for each team in each simulation for each level of randomness
for team in teams:
= plt.subplots(1, len(randomness_levels), figsize=(20, 5), sharex=True, sharey=True)
fig, axes f'Number of Wins Distribution for {team} (250 Simulations)', fontsize=20)
fig.suptitle(
for col, (level, results_df_fp) in enumerate(simulation_results_fp.items()):
=10, alpha=0.7, edgecolor='black')
axes[col].hist(results_df_fp.loc[team], binsf'{level.capitalize()} Randomness', fontsize=18)
axes[col].set_title('Number of Wins', fontsize=16)
axes[col].set_xlabel(='both', which='major', labelsize=14)
axes[col].tick_params(axisif col == 0:
'Frequency', fontsize=16)
axes[col].set_ylabel(
=[0, 0, 1, 0.95])
plt.tight_layout(rect plt.show()
low Wins | low Losses | medium Wins | medium Losses | high Wins | high Losses | |
---|---|---|---|---|---|---|
Bikini Bottom | 17.92 | 4.08 | 15.05 | 6.95 | 13.74 | 8.26 |
Malibu | 17.04 | 4.96 | 14.59 | 7.41 | 13.26 | 8.74 |
Winterfell | 14.87 | 7.13 | 13.21 | 8.79 | 12.44 | 9.56 |
Cream City | 14.48 | 7.52 | 12.84 | 9.16 | 12.18 | 9.82 |
San Diego | 12.06 | 9.94 | 11.54 | 10.46 | 11.41 | 10.59 |
Scottsdale | 9.74 | 12.26 | 10.24 | 11.76 | 10.82 | 11.18 |
Beaverton | 9.22 | 12.78 | 10.02 | 11.98 | 10.22 | 11.78 |
Anacortes | 8.55 | 13.45 | 9.67 | 12.33 | 10.03 | 11.97 |
Helsinki | 8.48 | 13.52 | 9.40 | 12.60 | 10.21 | 11.79 |
Slytherin | 6.76 | 15.24 | 8.58 | 13.42 | 9.39 | 12.61 |
Santa Barbara | 6.55 | 15.45 | 8.58 | 13.42 | 9.15 | 12.85 |
Las Vegas | 6.33 | 15.67 | 8.28 | 13.72 | 9.13 | 12.87 |
low Wins Win % | low Losses Win % | medium Wins Win % | medium Losses Win % | high Wins Win % | high Losses Win % | |
---|---|---|---|---|---|---|
Bikini Bottom | 81.47 | 18.53 | 68.40 | 31.60 | 62.47 | 37.53 |
Malibu | 77.44 | 22.56 | 66.31 | 33.69 | 60.29 | 39.71 |
Winterfell | 67.60 | 32.40 | 60.04 | 39.96 | 56.56 | 43.44 |
Cream City | 65.82 | 34.18 | 58.36 | 41.64 | 55.38 | 44.62 |
San Diego | 54.82 | 45.18 | 52.45 | 47.55 | 51.87 | 48.13 |
Scottsdale | 44.29 | 55.71 | 46.53 | 53.47 | 49.18 | 50.82 |
Beaverton | 41.89 | 58.11 | 45.55 | 54.45 | 46.45 | 53.55 |
Anacortes | 38.87 | 61.13 | 43.96 | 56.04 | 45.58 | 54.42 |
Helsinki | 38.55 | 61.45 | 42.75 | 57.25 | 46.42 | 53.58 |
Slytherin | 30.71 | 69.29 | 39.00 | 61.00 | 42.69 | 57.31 |
Santa Barbara | 29.78 | 70.22 | 39.02 | 60.98 | 41.58 | 58.42 |
Las Vegas | 28.76 | 71.24 | 37.64 | 62.36 | 41.51 | 58.49 |