Expected Wins Analysis in Fantasy Basketball Using the ESPN API

ESPN Fantasy
statistical analysis
Author

Jonathan Kierce

Published

January 21, 2026

My friends and I participate each year in a fantasy ESPN league. Invariably, there are always a few that complain of how unlucky they were in scheduling, and that there were teams that were worse that made finals/finished higher/didn’t come last instead.

There is (some) merit to these complaints. Because matchups are head-to-head, your record depends partly on who you played, not just how many points you scored. Each week 2 teams are matched and the team with the higher score wins. Therefore it is possible to have the 2nd highest score of the week, and lose to the highest scorer. Similarly, it is possible to have the 2nd lowest score of the week and still win because you are playing the lowest scorer that week.

While the Points For and Points Against metrics are often pointed to as a way of measuring how ‘unlucky’ you have been with the schedule, this doesn’t exactly reflect how good your team is. For example, if you were really good for the first half of the year, but then had a bunch of injuries, you may have won more games than someone who has the same amount of Points For but was just consistently bad.

Expected Wins

This led me to develop an Expected Wins metric. The Expected Wins considers the schedule to be random, and for each round awards the fraction of wins you would have had against each opponent in the league.

For example, in a 10-team league, there are 9 possible opponents. If you would have beaten 3 of those opponents, you are awarded 3/9 or ~.33 expected wins. If you would have beaten all teams because you had the highest score of the week, you are awarded 1.0 expected wins, while the lowest gets 0 expected wins.

NoteA note on league types

This is written for points leagues; for categories you’d compute ‘expected category wins’ similarly, but ties are more frequent and need explicit handling.

Pulling your League

I pull league data using espn_api (Wendt 2025).

You start by pulling your league. To do this, you need the league_id. In order to do this, go to the ESPN fantasy website on a browser, go the league you want to analyse, and then check the URL. It should be something like https://fantasy.espn.com/basketball/team?leagueId=xxx, where xxx is your league ID. Also note that for this to work, the league needs to be publicly viewable. If it is private, you also need a SWID and ESPN_S2 value. I am using a random league (not mine) with a public ID for this post.

#!pip install espn_api # install espn_api package if needed
from espn_api.basketball import League

league = League(league_id=727593540, year=2025)
num_teams = len(league.teams)
print(f"There are {num_teams} teams in this league")
There are 12 teams in this league

We now want to iterate through each team / round and collect results into a NumPy array. I print the number of games for each team to ensure they are the same.

#!pip install espn-api  # install if needed
from espn_api.basketball import League
import numpy as np

league = League(league_id=727593540, year=2025)

teams = league.teams
num_teams = len(teams)
print(f"There are {num_teams} teams in this league")

# Use the minimum completed games across teams
num_weeks = min(t.wins + t.losses + t.ties for t in teams)

results = np.zeros((num_teams, num_weeks, 2), dtype=float)

for i, team in enumerate(teams):
    played = team.wins + team.losses + team.ties
    print(f"{team} has played {played} games.")
    for w in range(num_weeks):
        matchup = team.schedule[w]
        if team == matchup.home_team:
            team_score = matchup.home_final_score
            opp_score = matchup.away_final_score
        else:
            team_score = matchup.away_final_score
            opp_score = matchup.home_final_score

        results[i, w, 0] = team_score
        results[i, w, 1] = opp_score
There are 12 teams in this league
Team(Pete's Bucket Boys) has played 20 games.
Team(Boston Celtics) has played 20 games.
Team(Rachid's Rowdy Team) has played 20 games.
Team(Perfect ur Rob) has played 20 games.
Team(All Jams! no Peanut Butter! ) has played 20 games.
Team(Gustavo's Great Team) has played 20 games.
Team(BadrTheBest) has played 20 games.
Team(SAN ANTONIO SPURS) has played 20 games.
Team(Leon Rose On The Phone) has played 20 games.
Team(Msnsnsnsnn's Magnificent Team) has played 20 games.
Team(Trash League) has played 20 games.
Team(EA sports) has played 20 games.

Now we have the results, we can analyse using the Expected Wins metric I described earlier.

expected_wins = np.zeros(num_teams, dtype=float)

# Tie-safe expected wins:
# expected wins that week = (#strictly lower + 0.5*#equal) / (n-1)
for w in range(num_weeks):
    week_scores = results[:, w, 0]
    for i in range(num_teams):
        s = week_scores[i]
        lower = np.sum(week_scores < s)
        equal = np.sum(week_scores == s) - 1  # exclude self
        expected_wins[i] += (lower + 0.5 * equal) / (num_teams - 1)

table = [["Team", "Expected Wins", "Actual Wins", "Difference (Actual - Expected)"]]
for i, team in enumerate(league.teams):
    team_name = str(team).replace("Team(", "").replace(")", "")
    actual_wins = team.wins
    diff = actual_wins - expected_wins[i]
    table.append([team_name, round(expected_wins[i], 2), actual_wins, round(diff, 2)])

sorted_table = sorted(table[1:], key=lambda x: x[1], reverse=True)
sorted_table.insert(0, table[0])

#print(tb.tabulate(sorted_table, headers="firstrow"))
Table 1: Expected wins vs actual wins
Team Expected Wins Actual Wins Difference (Actual - Expected)
Leon Rose On The Phone 16.82 19 2.18
All Jams! no Peanut Butter! 16.45 16 -0.45
EA sports 15.27 14 -1.27
BadrTheBest 12.64 13 0.36
Rachid’s Rowdy Team 11.27 12 0.73
Perfect ur Rob 11 8 -3
Msnsnsnsnn’s Magnificent Team 10.82 12 1.18
Gustavo’s Great Team 10.73 13 2.27
Pete’s Bucket Boys 8.18 7 -1.18
Boston Celtics 5 4 -1
SAN ANTONIO SPURS 1.73 2 0.27
Trash League 0.09 0 -0.09

And there you have it. Those with a positive difference had more expected wins than actual wins- they were ‘lucky’ in their scheduling, and vice versa with the negative differences- they were ‘unlucky’.

I hope this provides some help in settling the never-ending complaints over who has been hard done by!

References

Wendt, Christian. 2025. “Espn-Api: ESPN Fantasy API (Football, Basketball).” https://github.com/cwendt94/espn-api.