Sitemap

Mastering Euroleague Fantasy with Data-Driven Decisions

Leverage web scraping, advanced metrics, and Monte Carlo simulations to dominate Euroleague Fantasy

--

Press enter or click to view image in full size
DALL-E Generated

Euroleague Fantasy is more than just a game — it’s a puzzle that combines strategy, knowledge of basketball, and a bit of luck. Whether you’re a seasoned player or a curious newcomer, making the right decisions about your team can be a daunting task. How do you decide which players to pick? How can you predict their performance? What tools can help you rise above your competition?

In this article, we’ll take a deep dive into the data-driven approach to mastering Euroleague Fantasy. By combining the power of web scraping to gather raw player data, advanced statistics to uncover hidden insights, and Monte Carlo simulations to model different scenarios, you can make informed decisions that maximize your chances of success.

Here’s what we’ll cover:

  1. Scraping Euroleague Data: Learn how to extract the stats you need from available sources.
  2. Analyzing Advanced Metrics: Discover the key numbers that can help you evaluate players’ performance more effectively.
  3. Optimizing Your Team with Monte Carlo Simulations: Use statistical modeling to explore the best lineup under different conditions.

If you’re ready to take your Euroleague Fantasy game to the next level, let’s get started. Data isn’t just a tool — it’s your competitive edge.

Scraping Euroleague Data

To make data-driven decisions in Euroleague Fantasy, we first need reliable data. Fortunately, the web is full of useful resources for player statistics. In this section, we’ll scrape the necessary data using Python, and calculate metrics that will help us evaluate players effectively.

We’ll focus on gathering the following for each player:

  • Credits: The cost to include the player in your team.
  • Average Fantasy Score: The player’s mean fantasy performance.
  • EWMA Fantasy Score: Exponentially Weighted Moving Average, giving more weight to recent performances.
  • Standard Deviation of Fantasy Score: A measure of the player’s consistency.
  • Above Score: Average Fantasy Score minus Credits, indicating value.
  • Percentage Difference: Ratio of above score to credits

Currently, the Euroleague Fantasy is at the end of Week 12. We will get the data from the Euroleague Stats page, the Overall and the Weekly Data. The Weekly data will be used to derive the Standard Deviation and the EWMA Score.

Press enter or click to view image in full size
Data from Week 12
Press enter or click to view image in full size
Overall Data

Here’s how you can do it:

Step 1: Scrape the Data

Get the Overall Data

import pandas as pd
import numpy as np
import requests

# Get the Overall Data

# Define the query parameters as a dictionary
params = {
"season_id": 17,
"mode": "dunkest",
"stats_type": "avg", # tot
"weeks[]": [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
"rounds[]": [1, 2, 3],
"teams[]": [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 48, 60],
"positions[]": [1, 2, 3],
"player_search": "",
"min_cr": 4,
"max_cr": 35,
"sort_by": "pdk",
"sort_order": "desc",
"iframe": "yes"
}

# Define the headers for the request
headers = {
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "el-GR,el;q=0.9,en;q=0.8,es;q=0.7,de;q=0.6,it;q=0.5",
"user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Mobile Safari/537.36",
"x-requested-with": "XMLHttpRequest"
}

# Make the GET request
response = requests.get(url, params=params, headers=headers)

# Check the response status
if response.status_code == 200:
print("Request was successful!")
data = response.json() # Parse the JSON response
#print(data) # Print or handle the data as needed
else:
print(f"Request failed with status code {response.status_code}")


df=pd.DataFrame(data)
Press enter or click to view image in full size

Get the Weekly Data

def weekly_stats(week):
url = "https://www.dunkest.com/api/stats/table"

# Define the query parameters as a dictionary
params = {
"season_id": 17,
"mode": "dunkest",
"stats_type": "avg",
"weeks[]": week,
"rounds[]": [1, 2, 3],
"teams[]": [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 48, 60],
"positions[]": [1, 2, 3],
"player_search": "",
"min_cr": 4,
"max_cr": 35,
"sort_by": "pdk",
"sort_order": "desc",
"iframe": "yes"
}

# Define the headers for the request
headers = {
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "el-GR,el;q=0.9,en;q=0.8,es;q=0.7,de;q=0.6,it;q=0.5",
"user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Mobile Safari/537.36",
"x-requested-with": "XMLHttpRequest"
}

# Make the GET request
response = requests.get(url, params=params, headers=headers)

# Check the response status
if response.status_code == 200:
# print("Request was successful!")
data = response.json() # Parse the JSON response
# print(data) # Print or handle the data as needed
else:
print(f"Request failed with status code {response.status_code}")

df=pd.DataFrame(data)

return df


weekly_df = pd.DataFrame()

# add the weeks here
weeks = [1,2,3,4,5,6,7,8,9,10,11,12]
for w in weeks:
tmp = weekly_stats(w)
tmp['week'] = w
weekly_df = pd.concat([weekly_df,tmp])

# change the data type to float
weekly_df['pdk'] = weekly_df['pdk'].astype(float)
weekly_df['cr'] = weekly_df['cr'].astype(float)

# Order the weekly df by user id and week
weekly_df.sort_values(['id', 'week'], inplace = True)
Press enter or click to view image in full size

Step 2: Calculate Metrics

Once the data is scraped, use pandas to compute the advanced statistics such as “Standard Deviation”, “EWMA”, “Above Pirce”, and “Pct Above Price”. The Weekly data will be used for the Standard Deviation and the EWMA. Once we get these statistics for each player, we will merge them with the Overall Data.

# StDev
player_std = weekly_df.groupby(['id'], as_index = False)['pdk'].std()
player_std.columns = ['id', 'stdev']

# EWMA
# Calculate EWMA for each player
weekly_df['ewma_score'] = weekly_df.groupby('id')['pdk'].transform(lambda x: x.ewm(alpha=0.15, adjust=True).mean())

# Get the latest EWMA score for each player
latest_ewma = weekly_df.groupby('id').tail(1)[['id', 'ewma_score']].reset_index(drop=True)

# Merge the stdev
df = pd.merge(left = df, right = player_std, how = "left", left_on = 'id', right_on = 'id')

# Merge the EWMA
df = pd.merge(left = df, right = latest_ewma, how = "left", left_on = 'id', right_on = 'id')


df['pdk'] = df['pdk'].astype(float)
df['cr'] = df['cr'].astype(float)
df['gp'] = df['gp'].astype(float)
df['above_price'] = df['pdk'] - df['cr']
df['pct_above_price'] = df['above_price']/df['cr']

# Optional: Remove the inactive or injured players and those with less than 5 games

# remove the injured players
df = df.loc[~df['last_name'].isin(['Vezenkov', 'Bolomboy', 'Bolmaro', 'Giffey', 'Koumadje',
'Hermannsson', 'Rivero', 'Musa', 'Wilbekin', 'Larkin', 'Jaiteh',
'Blatt', 'Lee', 'Bean'])]

df = df.loc[df['gp']>=5]


# Optional: Keep only the players whose score, avg or ewma is above their price or their is equal to 4 which is the lowest

df = pd.concat([df.loc[df['above_price']>=1],
df.loc[df['ewma_score']>=df['cr']],
df.loc[df['cr']==4],
])
df.drop_duplicates(inplace=True)

Other key metrics for examining different scenarios are the quantiles in scoring. We can easily extract the quantiles since we have the average value (‘pdk’ or ‘ewma_score’) and the standard deviation. For example, if we want to be conservative, we can build our team based on the q35 and if we want to play risky, we can build our team based on the q65. Let’s see how to extract these values.

from scipy.stats import norm

# Calculate the 65th and 35th percentile for the pdk and ewma
df['q65'] = df.apply(lambda row: norm.ppf(0.65, loc=row['pdk'], scale=row['stdev']), axis=1)
df['q35'] = df.apply(lambda row: norm.ppf(0.35, loc=row['pdk'], scale=row['stdev']), axis=1)
df['ewma_q65'] = df.apply(lambda row: norm.ppf(0.65, loc=row['ewma_score'], scale=row['stdev']), axis=1)
df['ewma_q35'] = df.apply(lambda row: norm.ppf(0.35, loc=row['ewma_score'], scale=row['stdev']), axis=1)

Now that we have this data, we can easily query them by extracting valuable information. For example:

Q: Which are the Top 10 players with the highest “above-price” metric?

df[['last_name','cr', 'pdk', 'ewma_score', 'stdev', 'above_price', 'pct_above_price']]\
.sort_values('above_price', ascending = False).head(10)
Press enter or click to view image in full size

So, these are the players with the higher difference between their fantasy score (“pdk”) minus their credits (“cr”). For instance, Maledon costs 15 and brings on average 22.8, i.e. the “above_price” is 7.8.

Q: Which are the Top 10 players with the highest “above-price percentage” metric?

df[['last_name','cr', 'pdk', 'ewma_score', 'stdev', 'above_price', 'pct_above_price']]\
.sort_values('pct_above_price', ascending = False).head(10)
Press enter or click to view image in full size

These are the player with the higher relative increase in their fantasy score compared to their credits.

Q: Which are the Top 10 players with the lowest “standard deviation” metric?

Let’s consider those with more than 10 “pdk” score. This metric tells us which players have less volatility in scoring.

df.query('pdk>10')[['last_name','cr', 'pdk', 'ewma_score', 'stdev', 'above_price', 'pct_above_price']]\
.sort_values('stdev', ascending = True).head(10)
Press enter or click to view image in full size

Q: Which are the Top 10 players with the highest “Sharpe ratio”?

In finance, there is a metric called shape ratio. Let’s consider each player as a stock and let’s apply the Sharpe ratio which can be defined as the “above prive” over “standard deviation”.

df['sharpe_ratio'] = df['above_price']/df['stdev']

df[['last_name','cr', 'pdk', 'ewma_score', 'stdev', 'above_price', 'pct_above_price', 'sharpe_ratio']]\
.sort_values('sharpe_ratio', ascending = False).head(10)
Press enter or click to view image in full size

Important Note

For these metrics we considered the “pdk” which is the average fantasy points. You could instead work with the “ewma_score” that gives more weight to the most recent observations.

Analyzing Advanced Features

Once we’ve scraped the raw data and calculated core metrics, the next step is to derive actionable insights. In this section, we’ll explore how to use the calculated statistics to estimate probabilities, such as the likelihood of a player scoring more than a certain percentage of their price. This analysis helps identify players with a high chance of delivering value relative to their cost.

Step 1: Understanding the Problem

We want to calculate the probability that a player’s fantasy score exceeds a certain percentage of their price (credits). For example, if a player costs 10 credits, and we are interested in the probability of them scoring more than 12 points (120% of their price), we can use the player’s average score and standard deviation to estimate this.

Assuming that fantasy scores are approximately normally distributed (a reasonable assumption for this kind of data), we can calculate probabilities using the cumulative distribution function (CDF) or Monte Carlo simulation.

Let’s say that we want to get the Top 10 players with the highest probability to score more than 20% of their price.

from scipy.stats import norm
import numpy as np

# Step 1: Define the probability function
def calculate_probability(avg_score, std_dev, target_score):
if std_dev == 0: # Avoid division by zero
return 1.0 if avg_score >= target_score else 0.0
# Use the cumulative distribution function
return 1 - norm.cdf(target_score, loc=avg_score, scale=std_dev)

# Step 2: Add the target threshold as a percentage of price
target_percentage = 1.2 # Example: 120% of price
df['target_score'] = df['cr'] * target_percentage

# Step 3: Calculate the probability for each player
df['prob'] = df.apply(
lambda x: calculate_probability(x['pdk'], x['stdev'], x['target_score']), axis=1
)

# Step 4: Display results
df[['last_name', 'cr', 'pdk', 'target_score', 'prob']].sort_values('prob', ascending = False).head(10)

Let’s do the same excerice but this time by taking into consideration the ewma_score. For exhibition purposes, we will work with Monte Carlo simulation this time.

ids = []
probs = []

# pct that you want to score above
extra = 0.2
for id in df.id.unique():
prob = np.mean(np.random.normal(df.loc[df['id'] == id]['ewma_score'],
df.loc[df['id'] == id]['stdev'],
size=1000000)>(df.loc[df['id'] == id]['cr'].values[0] * (1 + extra)))
ids.append(id)
probs.append(prob)

prob_df = pd.DataFrame({'id': ids, 'prob' : probs})
pd.merge(df, prob_df)[['last_name', 'gain_prob', 'cr', 'pdk', 'ewma_score','stdev']].sort_values('prob', ascending = False).head(10)
Press enter or click to view image in full size

Step 3: Interpreting the Results

After running the script, the probcolumn shows the likelihood of each player scoring above their target. You can use this information in several ways:

  • High Probability Players: Players with a high chance of exceeding their price threshold are likely undervalued.
  • Low Probability Players: These players might be overpriced or risky picks.
  • Threshold Adjustments: Adjusting the target_percentage allows you to explore different scenarios, such as conservative or aggressive strategies.

This analysis provides a data-driven approach to identifying players likely to exceed their cost in fantasy points. Adjusting the target percentage can help refine your strategy further.

Building the Optimal Team: Maximizing Expected Points

Now that we’ve analyzed player statistics and derived valuable probabilities, it’s time to put it all together. In this section, we’ll use optimization techniques to build a fantasy squad with the highest expected score, given a limited budget.

We will:

  1. Use the expected points based on the metrics we calculated earlier.
  2. Apply Euroleague Fantasy rules, including captain bonuses and reduced bench player scores.
  3. Implement a constraint-based optimization approach to ensure the team adheres to budget and lineup rules

This is a complex problem to be solved using linear programming equations. We will approach it using Monte Carlo Simulation by running 10M simulations.

Remember that we do not consider the Head Coaches since this requires a different approach. On the contrary, we assume that we have 100 credits available for the players. The expected score will be based on the average score. Later, we will do it using the ewma score

def generate_team(df, my_credits= 100, kpi = 'pdk'):
"""
Generate a valid team with 2 centers, 4 forwards, and 4 guards
"""
team = (
pd.concat([
df[df['position'] == 'C'].sample(2),
df[df['position'] == 'F'].sample(4),
df[df['position'] == 'G'].sample(4)
])
)

team['ranking'] = team[kpi].rank(ascending=False, method = 'first')
team['points'] = team[kpi]
team.loc[team['ranking'] == 1, 'points'] = team.loc[team['ranking'] == 1, kpi]*1.5
team.loc[team['ranking'] >= 7, 'points'] = team.loc[team['ranking'] >= 7, kpi]*0.5

check_validity = len(team.loc[team['ranking'] < 7, 'position'].unique())
check_credits = team['cr'].sum()


if check_validity == 3 and check_credits <= my_credits:
return team[['id', 'last_name', 'position', 'cr', 'pdk', 'ewma_score', 'stdev', 'ranking', 'points']]
else:
return None


def monte_carlo_team_simulation(df, num_simulations=10000):
"""
Run Monte Carlo simulation to find the highest-scoring team

Parameters:
df (pandas.DataFrame): Input dataframe with player information
num_simulations (int): Number of team simulations to run

Returns:
dict: Best team and its total score
"""
best_team = None
best_score = float('-inf')

for _ in range(num_simulations):
current_team = generate_team(df, my_credits= 100, kpi='pdk')
if current_team is not None:
current_score = current_team['points'].sum()

if current_score > best_score:
best_score = current_score
best_team = current_team

return {
'best_team': best_team,
'best_score': best_score,
'risk': best_team.stdev.sum()
}


simulation_output = monte_carlo_team_simulation(df, num_simulations=10000000)


simulation_output['best_team'].to_csv('best_team.csv', index=False)

The best team according to our assumptions and the Monte Carlo simulation is the following with an expected score of 133.3 points without the head coach:

Press enter or click to view image in full size
Best Team of Week 12 with 100 Credits based on pdk

Let’s see what would be the best team based on the ewma score instead of the pdk.

Press enter or click to view image in full size
Best Team of Week 12 with 100 Credits based on ewma score

Adjusting the Team with Weekly Trades

In Euroleague Fantasy, one of the most strategic aspects is the ability to trade a limited number of players each week. To accommodate this, we can adjust the optimization script to ensure that the new team differs from the current team by no more than 3 or 4 players (depending on your choice to trade the head coach or not).

Limitations and Room for Improvement

While the proposed data-driven approach provides a solid foundation for building and optimizing your Euroleague Fantasy team, it’s important to acknowledge its limitations. Fantasy basketball is inherently unpredictable, and there are several real-world factors that this method does not fully capture.

Key Limitations

  1. Injuries and Role Changes
    The model does not account for injuries or the subsequent impact on team dynamics. When a key player is injured, their teammates often see increased opportunities to contribute. For example, Vezenkov might take on a larger role when Peters is unavailable. Incorporating injury reports and lineup changes would make the model more responsive to such scenarios.
  2. Opponent Strength and Matchups
    The model assumes that player performance is consistent across all games, ignoring the impact of opposing teams. In reality, players tend to perform better against weaker defenses and struggle against tougher opponents. Factoring in opponent strength — such as pace of play, defensive rating, and rebound rates — could refine the expected points calculation.
  3. T1 and T2 Round Dynamics
    Euroleague Fantasy includes T1 (Round 1) and T2 (Round 2) mechanics, where users can make adjustments mid-week. This is especially relevant for selecting captains. If your chosen captain underperforms in T1, you can pick another player from your team who hasn’t yet played in T2. The current model assumes a static lineup, missing the strategic advantage offered by this rule.

Opportunities for Improvement

Integrating Contextual Data

  • Use injury reports
  • Incorporate opponent statistics (e.g., defensive efficiency, average points allowed) into the expected points formula.

Scenario-Based Optimization

  • Develop a two-stage optimization model for T1 and T2 rounds, allowing users to pre-plan captain changes based on T1 outcomes.
  • Simulate multiple scenarios, such as varying performances for key players, to test lineup robustness (volatility, distribution of scoring).

Incorporating Advanced Metrics

  • Include metrics like usage rate, true shooting percentage, and player efficiency rating to better evaluate player potential.
  • Model the variance in player performance more precisely using historical distributions rather than assuming normality.

Closing Thoughts

Fantasy sports is as much an art as it is a science. While this approach provides a data-driven framework to help you make informed decisions, real-world variables and your own intuition remain critical. By addressing these limitations and incorporating contextual factors, the model can become even more robust, helping you stay ahead in the competitive world of Euroleague Fantasy.

Ready to dive deeper into customization or refine your strategy further? The possibilities are endless.

Your Feedback Matters!

Thank you for taking the time to explore this guide to optimizing your Euroleague Fantasy team. I hope you found it insightful and helpful for building a winning strategy. Fantasy sports thrive on collaboration and shared ideas, so I’d love to hear your thoughts!

  • Did you find this approach useful?
  • What other strategies or features would you like to see covered?

If this article helped you, please take a moment to:
👍 Clap to show your appreciation.
💬 Comment with your feedback, questions, or suggestions for improvement.
📢 Share this article with other fantasy enthusiasts to help grow our community.

Your engagement motivates me to keep improving and exploring new ways to bring actionable insights to Euroleague Fantasy fans like you. Together, let’s take fantasy basketball to the next level!

--

--

George Pipis
George Pipis

Written by George Pipis

Sr. Director, Data Scientist @ Persado | Co-founder of the Data Science blog: https://predictivehacks.com/