Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import pandas as pd
|
| 2 |
import numpy as np
|
| 3 |
import requests
|
|
@@ -7,6 +12,7 @@ import gradio as gr
|
|
| 7 |
from sklearn.linear_model import Ridge
|
| 8 |
from sklearn.preprocessing import StandardScaler
|
| 9 |
from xgboost import XGBRegressor
|
|
|
|
| 10 |
import warnings
|
| 11 |
|
| 12 |
warnings.filterwarnings('ignore')
|
|
@@ -21,7 +27,6 @@ class CachedFPL_API:
|
|
| 21 |
os.makedirs(cache_dir)
|
| 22 |
|
| 23 |
def fetch_data(self, endpoint):
|
| 24 |
-
# Create a safe filename from the endpoint
|
| 25 |
filename = endpoint.replace("/", "_").replace("?", "_").replace("&", "_") + ".json"
|
| 26 |
filepath = os.path.join(self.cache_dir, filename)
|
| 27 |
|
|
@@ -29,7 +34,6 @@ class CachedFPL_API:
|
|
| 29 |
with open(filepath, 'r', encoding='utf-8') as f:
|
| 30 |
return json.load(f)
|
| 31 |
|
| 32 |
-
# If not cached, fetch from API
|
| 33 |
url = f"{self.base_url}{endpoint}"
|
| 34 |
print(f"Fetching data from API: {url}")
|
| 35 |
try:
|
|
@@ -63,31 +67,26 @@ class CachedFPL_API:
|
|
| 63 |
|
| 64 |
def load_and_process_data():
|
| 65 |
"""Load and process FPL data, train models, and generate predictions"""
|
| 66 |
-
|
| 67 |
-
# Initialize API
|
| 68 |
FPL = CachedFPL_API()
|
| 69 |
|
| 70 |
-
#
|
| 71 |
bootstrap_data = FPL.get_bootstrap_static()
|
| 72 |
events = bootstrap_data['events']
|
| 73 |
teams = bootstrap_data['teams']
|
| 74 |
elements = bootstrap_data['elements']
|
| 75 |
|
| 76 |
-
# Determine Current Gameweek
|
| 77 |
current_gameweek = next((event['id'] for event in events if event['is_current']), None)
|
| 78 |
if not current_gameweek:
|
| 79 |
current_gameweek = next((event['id'] for event in events if event['is_next']), 1) - 1
|
| 80 |
|
| 81 |
-
# Create Players DataFrame
|
| 82 |
all_players = pd.DataFrame(elements)
|
| 83 |
all_players['full_name'] = all_players['first_name'] + " " + all_players['second_name']
|
| 84 |
|
| 85 |
-
#
|
| 86 |
player_histories = []
|
| 87 |
for index, player in all_players.iterrows():
|
| 88 |
player_id = player['id']
|
| 89 |
history = FPL.get_player_summary(player_id)
|
| 90 |
-
|
| 91 |
if history and 'history' in history:
|
| 92 |
df_history = pd.DataFrame(history['history'])
|
| 93 |
df_history['element'] = player_id
|
|
@@ -95,7 +94,7 @@ def load_and_process_data():
|
|
| 95 |
|
| 96 |
all_fpl_players_games = pd.concat(player_histories, ignore_index=True)
|
| 97 |
|
| 98 |
-
#
|
| 99 |
cols_to_numeric = ['influence', 'creativity', 'threat', 'ict_index', 'expected_goals',
|
| 100 |
'expected_assists', 'expected_goal_involvements', 'expected_goals_conceded']
|
| 101 |
for col in cols_to_numeric:
|
|
@@ -112,29 +111,24 @@ def load_and_process_data():
|
|
| 112 |
for fixture in fixtures:
|
| 113 |
if fixture['finished']:
|
| 114 |
team_stats_data.append({
|
| 115 |
-
'team_id': fixture['team_h'],
|
| 116 |
-
'
|
| 117 |
-
'goals_scored': fixture['team_h_score'],
|
| 118 |
-
'goals_conceded': fixture['team_a_score'],
|
| 119 |
'was_home': True
|
| 120 |
})
|
| 121 |
team_stats_data.append({
|
| 122 |
-
'team_id': fixture['team_a'],
|
| 123 |
-
'
|
| 124 |
-
'goals_scored': fixture['team_a_score'],
|
| 125 |
-
'goals_conceded': fixture['team_h_score'],
|
| 126 |
'was_home': False
|
| 127 |
})
|
| 128 |
|
| 129 |
team_stats_df = pd.DataFrame(team_stats_data)
|
| 130 |
-
window_size = 5
|
| 131 |
team_stats_df = team_stats_df.sort_values(by=['team_id', 'round'])
|
| 132 |
|
| 133 |
team_stats_df['attack_strength'] = team_stats_df.groupby('team_id')['goals_scored'].transform(
|
| 134 |
-
lambda x: x.rolling(
|
| 135 |
)
|
| 136 |
team_stats_df['defence_strength'] = team_stats_df.groupby('team_id')['goals_conceded'].transform(
|
| 137 |
-
lambda x: x.rolling(
|
| 138 |
)
|
| 139 |
team_stats_df.fillna(0, inplace=True)
|
| 140 |
|
|
@@ -142,23 +136,15 @@ def load_and_process_data():
|
|
| 142 |
columns={'attack_strength': 'opponent_attack_strength', 'defence_strength': 'opponent_defence_strength'}
|
| 143 |
)
|
| 144 |
|
| 145 |
-
final_df = pd.merge(
|
| 146 |
-
|
| 147 |
-
opponent_strength,
|
| 148 |
-
left_on=['opponent_team', 'round'],
|
| 149 |
-
right_on=['team_id', 'round'],
|
| 150 |
-
how='left'
|
| 151 |
-
)
|
| 152 |
final_df.drop('team_id', axis=1, inplace=True)
|
| 153 |
|
| 154 |
-
# Add Difficulty
|
| 155 |
all_fixtures = FPL.get_fixtures()
|
| 156 |
fixture_difficulty_map = {}
|
| 157 |
for f in all_fixtures:
|
| 158 |
-
fixture_difficulty_map[f['id']] = {
|
| 159 |
-
'h': f['team_h_difficulty'],
|
| 160 |
-
'a': f['team_a_difficulty']
|
| 161 |
-
}
|
| 162 |
|
| 163 |
def get_difficulty(row):
|
| 164 |
fid = row['fixture']
|
|
@@ -168,7 +154,7 @@ def load_and_process_data():
|
|
| 168 |
|
| 169 |
final_df['difficulty'] = final_df.apply(get_difficulty, axis=1)
|
| 170 |
|
| 171 |
-
#
|
| 172 |
stats_to_roll = ['minutes', 'goals_scored', 'assists', 'bps', 'influence', 'creativity',
|
| 173 |
'threat', 'expected_goals', 'expected_assists', 'expected_goals_conceded', 'total_points']
|
| 174 |
final_df = final_df.sort_values(by=['element', 'round'])
|
|
@@ -181,20 +167,15 @@ def load_and_process_data():
|
|
| 181 |
final_df['total_points_SD5'] = final_df.groupby('element')['total_points'].transform(
|
| 182 |
lambda x: x.rolling(window=5, min_periods=1).std().shift(1)
|
| 183 |
)
|
| 184 |
-
|
| 185 |
final_df['total_points_RA10'] = final_df.groupby('element')['total_points'].transform(
|
| 186 |
lambda x: x.rolling(window=10, min_periods=1).mean().shift(1)
|
| 187 |
)
|
| 188 |
|
| 189 |
def get_trend_emoji(row):
|
| 190 |
-
short = row['total_points_RA5']
|
| 191 |
-
long = row['total_points_RA10']
|
| 192 |
if short > 0 and long > 0:
|
| 193 |
-
if short > long
|
| 194 |
-
|
| 195 |
-
else: return '➡️'
|
| 196 |
-
elif short > 0 or long > 0: return '➡️'
|
| 197 |
-
else: return '⚪'
|
| 198 |
|
| 199 |
final_df['Trend'] = final_df.apply(get_trend_emoji, axis=1)
|
| 200 |
final_df.fillna(0, inplace=True)
|
|
@@ -204,7 +185,7 @@ def load_and_process_data():
|
|
| 204 |
'opponent_defence_strength', 'total_points_SD5'] + [f'{stat}_RA5' for stat in stats_to_roll]
|
| 205 |
target = 'total_points'
|
| 206 |
|
| 207 |
-
#
|
| 208 |
final_df = final_df.sort_values(by='round')
|
| 209 |
unique_rounds = final_df['round'].unique()
|
| 210 |
split_idx = int(len(unique_rounds) * 0.8)
|
|
@@ -213,14 +194,11 @@ def load_and_process_data():
|
|
| 213 |
train_df = final_df[final_df['round'] <= split_round]
|
| 214 |
test_df = final_df[final_df['round'] > split_round]
|
| 215 |
|
| 216 |
-
X_train = train_df[features]
|
| 217 |
-
|
| 218 |
-
X_test = test_df[features]
|
| 219 |
-
y_test = test_df[target]
|
| 220 |
|
| 221 |
scaler = StandardScaler()
|
| 222 |
X_train_scaled = scaler.fit_transform(X_train)
|
| 223 |
-
X_test_scaled = scaler.transform(X_test)
|
| 224 |
|
| 225 |
ridge_model = Ridge()
|
| 226 |
ridge_model.fit(X_train_scaled, y_train)
|
|
@@ -228,7 +206,7 @@ def load_and_process_data():
|
|
| 228 |
xgb_model = XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42)
|
| 229 |
xgb_model.fit(X_train_scaled, y_train)
|
| 230 |
|
| 231 |
-
#
|
| 232 |
NEXT_GW = current_gameweek + 1
|
| 233 |
next_gw_fixtures = FPL.get_gameweek_fixtures(NEXT_GW)
|
| 234 |
prediction_rows = []
|
|
@@ -236,29 +214,17 @@ def load_and_process_data():
|
|
| 236 |
if next_gw_fixtures:
|
| 237 |
team_fixture_map = {}
|
| 238 |
for f in next_gw_fixtures:
|
| 239 |
-
team_fixture_map[f['team_h']] = {
|
| 240 |
-
|
| 241 |
-
'was_home': True,
|
| 242 |
-
'difficulty': f['team_h_difficulty']
|
| 243 |
-
}
|
| 244 |
-
team_fixture_map[f['team_a']] = {
|
| 245 |
-
'opponent': f['team_h'],
|
| 246 |
-
'was_home': False,
|
| 247 |
-
'difficulty': f['team_a_difficulty']
|
| 248 |
-
}
|
| 249 |
|
| 250 |
for idx, player in all_players.iterrows():
|
| 251 |
team_id = player['team']
|
| 252 |
if team_id in team_fixture_map:
|
| 253 |
fix_info = team_fixture_map[team_id]
|
| 254 |
prediction_rows.append({
|
| 255 |
-
'element': player['id'],
|
| 256 |
-
'
|
| 257 |
-
'
|
| 258 |
-
'value': player['now_cost'],
|
| 259 |
-
'was_home': fix_info['was_home'],
|
| 260 |
-
'opponent_team': fix_info['opponent'],
|
| 261 |
-
'difficulty': fix_info['difficulty']
|
| 262 |
})
|
| 263 |
|
| 264 |
prediction_df = pd.DataFrame(prediction_rows)
|
|
@@ -274,7 +240,6 @@ def load_and_process_data():
|
|
| 274 |
|
| 275 |
# Calculate latest rolling stats
|
| 276 |
player_stats_latest = final_df.sort_values(by=['element', 'round']).copy()
|
| 277 |
-
|
| 278 |
for stat in stats_to_roll:
|
| 279 |
player_stats_latest[f'{stat}_RA5_latest'] = player_stats_latest.groupby('element')[stat].transform(
|
| 280 |
lambda x: x.rolling(window=5, min_periods=1).mean()
|
|
@@ -283,22 +248,17 @@ def load_and_process_data():
|
|
| 283 |
player_stats_latest['total_points_SD5_latest'] = player_stats_latest.groupby('element')['total_points'].transform(
|
| 284 |
lambda x: x.rolling(window=5, min_periods=1).std()
|
| 285 |
)
|
| 286 |
-
|
| 287 |
player_stats_latest['total_points_RA10_latest'] = player_stats_latest.groupby('element')['total_points'].transform(
|
| 288 |
lambda x: x.rolling(window=10, min_periods=1).mean()
|
| 289 |
)
|
| 290 |
|
| 291 |
-
def
|
| 292 |
-
short = row[
|
| 293 |
-
long = row['total_points_RA10_latest']
|
| 294 |
if short > 0 and long > 0:
|
| 295 |
-
if short > long
|
| 296 |
-
|
| 297 |
-
else: return '➡️'
|
| 298 |
-
elif short > 0 or long > 0: return '➡️'
|
| 299 |
-
else: return '⚪'
|
| 300 |
|
| 301 |
-
player_stats_latest['Trend'] = player_stats_latest.apply(
|
| 302 |
player_stats_latest = player_stats_latest.groupby('element').tail(1)
|
| 303 |
|
| 304 |
cols_to_merge = ['element', 'total_points_SD5_latest', 'Trend'] + [f'{stat}_RA5_latest' for stat in stats_to_roll]
|
|
@@ -314,19 +274,16 @@ def load_and_process_data():
|
|
| 314 |
X_pred = prediction_df[features]
|
| 315 |
X_pred_scaled = scaler.transform(X_pred)
|
| 316 |
|
| 317 |
-
prediction_df['Ridge Pred'] = ridge_model.predict(X_pred_scaled)
|
| 318 |
-
prediction_df['XGB Pred'] = xgb_model.predict(X_pred_scaled)
|
| 319 |
|
| 320 |
-
|
| 321 |
-
prediction_df['XGB Pred'] = prediction_df['XGB Pred'].clip(lower=0)
|
| 322 |
-
|
| 323 |
-
# Prepare final predictions dataframe
|
| 324 |
final_predictions_df = prediction_df.copy()
|
| 325 |
final_predictions_df['player_name'] = final_predictions_df['web_name']
|
| 326 |
|
| 327 |
element_type_map = all_players.set_index('id')['element_type'].to_dict()
|
| 328 |
-
final_predictions_df['element_type'] = final_predictions_df['element'].map(element_type_map)
|
| 329 |
position_map = {1: 'Goalkeeper', 2: 'Defender', 3: 'Midfielder', 4: 'Forward'}
|
|
|
|
| 330 |
final_predictions_df['position'] = final_predictions_df['element_type'].map(position_map)
|
| 331 |
|
| 332 |
team_name_mapping = {team['id']: team['name'] for team in teams}
|
|
@@ -342,28 +299,11 @@ def load_and_process_data():
|
|
| 342 |
|
| 343 |
final_predictions_df = pd.merge(final_predictions_df, cols_from_players, on='element', how='left')
|
| 344 |
|
| 345 |
-
|
| 346 |
'total_points_RA5': 'Pts Mean (5GW)',
|
| 347 |
'total_points_SD5': 'Pts Std Dev (5GW)',
|
| 348 |
'opponent_team': 'Next Opponent'
|
| 349 |
-
}
|
| 350 |
-
final_predictions_df.rename(columns=rename_map, inplace=True)
|
| 351 |
-
|
| 352 |
-
final_predictions_df['Closest Prediction Method'] = 'Pending'
|
| 353 |
-
final_predictions_df['Actual GW Pts'] = 0
|
| 354 |
-
|
| 355 |
-
cols_to_show = [
|
| 356 |
-
'element', 'player_name', 'team_name', 'position', 'Next Opponent',
|
| 357 |
-
'Ridge Pred', 'XGB Pred', 'Actual GW Pts', 'Closest Prediction Method',
|
| 358 |
-
'Total Pts', 'Last GW Pts', 'Pts Mean (5GW)', 'Pts Std Dev (5GW)', 'Trend',
|
| 359 |
-
'difficulty', 'was_home', 'value'
|
| 360 |
-
]
|
| 361 |
-
|
| 362 |
-
for col in cols_to_show:
|
| 363 |
-
if col not in final_predictions_df.columns:
|
| 364 |
-
final_predictions_df[col] = 0
|
| 365 |
-
|
| 366 |
-
final_predictions_df = final_predictions_df[cols_to_show]
|
| 367 |
|
| 368 |
numericals = ['Ridge Pred', 'XGB Pred', 'Pts Mean (5GW)', 'Pts Std Dev (5GW)']
|
| 369 |
for col in numericals:
|
|
@@ -402,8 +342,7 @@ def load_and_process_data():
|
|
| 402 |
manager_players_df = pd.merge(
|
| 403 |
manager_players_df,
|
| 404 |
final_predictions_df[['element', 'XGB Pred', 'difficulty', 'Next Opponent']],
|
| 405 |
-
on='element',
|
| 406 |
-
how='left'
|
| 407 |
)
|
| 408 |
else:
|
| 409 |
manager_players_df = pd.DataFrame(columns=['player_name', 'team_name', 'position', 'XGB Pred'])
|
|
@@ -414,106 +353,200 @@ def load_and_process_data():
|
|
| 414 |
return final_predictions_df, team_performance_df, manager_players_df, current_gameweek, NEXT_GW
|
| 415 |
|
| 416 |
|
| 417 |
-
# Load data
|
| 418 |
print("Loading FPL data and training models...")
|
| 419 |
final_predictions_df, team_performance_df, manager_players_df, current_gameweek, NEXT_GW = load_and_process_data()
|
| 420 |
print("Data loaded successfully!")
|
| 421 |
|
| 422 |
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
if position:
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
-
cols = [
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
'Total Pts', 'Last GW Pts', 'Pts Mean (5GW)', 'Pts Std Dev (5GW)', 'Trend',
|
| 436 |
-
'difficulty', 'was_home', 'value'
|
| 437 |
-
]
|
| 438 |
-
return filtered_df[cols]
|
| 439 |
|
|
|
|
| 440 |
|
| 441 |
-
def export_to_csv(df, filename_prefix="fpl_predictions"):
|
| 442 |
-
if not filename_prefix:
|
| 443 |
-
filename_prefix = "fpl_predictions"
|
| 444 |
-
filename = f"{filename_prefix}.csv"
|
| 445 |
-
df.to_csv(filename, index=False)
|
| 446 |
-
return filename
|
| 447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
-
unique_positions = final_predictions_df['position'].unique().tolist()
|
| 450 |
-
unique_teams = final_predictions_df['team_name'].unique().tolist()
|
| 451 |
|
| 452 |
# Build Gradio Interface
|
| 453 |
-
with gr.Blocks(title=f"
|
| 454 |
-
gr.Markdown(f"# FPL Gameweek {NEXT_GW}
|
| 455 |
gr.Markdown("""
|
| 456 |
-
This app
|
| 457 |
|
| 458 |
-
|
| 459 |
-
- Predictions based on rolling averages, team strength, fixture difficulty, and more
|
| 460 |
-
- Trend indicators showing player form (⬆️ improving, ⬇️ declining, ➡️ stable)
|
| 461 |
-
- Team performance analysis
|
| 462 |
-
- Export predictions to CSV
|
| 463 |
""")
|
| 464 |
|
| 465 |
-
with gr.
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
export_output = gr.File(label="Download CSV")
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
|
| 483 |
-
gr.
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
|
| 492 |
-
gr.
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
interactive=False
|
| 497 |
-
)
|
| 498 |
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
|
|
|
| 503 |
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
outputs=mgr_export_output
|
| 510 |
-
)
|
| 511 |
|
| 512 |
-
|
| 513 |
-
for inp in inputs:
|
| 514 |
-
inp.change(fn=filter_predictions, inputs=inputs, outputs=output_dataframe)
|
| 515 |
|
| 516 |
-
interface.load(fn=filter_predictions, inputs=inputs, outputs=output_dataframe)
|
| 517 |
|
| 518 |
if __name__ == "__main__":
|
| 519 |
-
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FPL Point Predictor with Gradio MCP Support
|
| 3 |
+
Exposes FPL predictions as MCP tools for Claude Desktop
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
import pandas as pd
|
| 7 |
import numpy as np
|
| 8 |
import requests
|
|
|
|
| 12 |
from sklearn.linear_model import Ridge
|
| 13 |
from sklearn.preprocessing import StandardScaler
|
| 14 |
from xgboost import XGBRegressor
|
| 15 |
+
from typing import Optional, List
|
| 16 |
import warnings
|
| 17 |
|
| 18 |
warnings.filterwarnings('ignore')
|
|
|
|
| 27 |
os.makedirs(cache_dir)
|
| 28 |
|
| 29 |
def fetch_data(self, endpoint):
|
|
|
|
| 30 |
filename = endpoint.replace("/", "_").replace("?", "_").replace("&", "_") + ".json"
|
| 31 |
filepath = os.path.join(self.cache_dir, filename)
|
| 32 |
|
|
|
|
| 34 |
with open(filepath, 'r', encoding='utf-8') as f:
|
| 35 |
return json.load(f)
|
| 36 |
|
|
|
|
| 37 |
url = f"{self.base_url}{endpoint}"
|
| 38 |
print(f"Fetching data from API: {url}")
|
| 39 |
try:
|
|
|
|
| 67 |
|
| 68 |
def load_and_process_data():
|
| 69 |
"""Load and process FPL data, train models, and generate predictions"""
|
|
|
|
|
|
|
| 70 |
FPL = CachedFPL_API()
|
| 71 |
|
| 72 |
+
# Fetch Basic Data
|
| 73 |
bootstrap_data = FPL.get_bootstrap_static()
|
| 74 |
events = bootstrap_data['events']
|
| 75 |
teams = bootstrap_data['teams']
|
| 76 |
elements = bootstrap_data['elements']
|
| 77 |
|
|
|
|
| 78 |
current_gameweek = next((event['id'] for event in events if event['is_current']), None)
|
| 79 |
if not current_gameweek:
|
| 80 |
current_gameweek = next((event['id'] for event in events if event['is_next']), 1) - 1
|
| 81 |
|
|
|
|
| 82 |
all_players = pd.DataFrame(elements)
|
| 83 |
all_players['full_name'] = all_players['first_name'] + " " + all_players['second_name']
|
| 84 |
|
| 85 |
+
# Fetch Player History
|
| 86 |
player_histories = []
|
| 87 |
for index, player in all_players.iterrows():
|
| 88 |
player_id = player['id']
|
| 89 |
history = FPL.get_player_summary(player_id)
|
|
|
|
| 90 |
if history and 'history' in history:
|
| 91 |
df_history = pd.DataFrame(history['history'])
|
| 92 |
df_history['element'] = player_id
|
|
|
|
| 94 |
|
| 95 |
all_fpl_players_games = pd.concat(player_histories, ignore_index=True)
|
| 96 |
|
| 97 |
+
# Preprocessing
|
| 98 |
cols_to_numeric = ['influence', 'creativity', 'threat', 'ict_index', 'expected_goals',
|
| 99 |
'expected_assists', 'expected_goal_involvements', 'expected_goals_conceded']
|
| 100 |
for col in cols_to_numeric:
|
|
|
|
| 111 |
for fixture in fixtures:
|
| 112 |
if fixture['finished']:
|
| 113 |
team_stats_data.append({
|
| 114 |
+
'team_id': fixture['team_h'], 'round': gw,
|
| 115 |
+
'goals_scored': fixture['team_h_score'], 'goals_conceded': fixture['team_a_score'],
|
|
|
|
|
|
|
| 116 |
'was_home': True
|
| 117 |
})
|
| 118 |
team_stats_data.append({
|
| 119 |
+
'team_id': fixture['team_a'], 'round': gw,
|
| 120 |
+
'goals_scored': fixture['team_a_score'], 'goals_conceded': fixture['team_h_score'],
|
|
|
|
|
|
|
| 121 |
'was_home': False
|
| 122 |
})
|
| 123 |
|
| 124 |
team_stats_df = pd.DataFrame(team_stats_data)
|
|
|
|
| 125 |
team_stats_df = team_stats_df.sort_values(by=['team_id', 'round'])
|
| 126 |
|
| 127 |
team_stats_df['attack_strength'] = team_stats_df.groupby('team_id')['goals_scored'].transform(
|
| 128 |
+
lambda x: x.rolling(5, min_periods=1).mean().shift(1)
|
| 129 |
)
|
| 130 |
team_stats_df['defence_strength'] = team_stats_df.groupby('team_id')['goals_conceded'].transform(
|
| 131 |
+
lambda x: x.rolling(5, min_periods=1).mean().shift(1)
|
| 132 |
)
|
| 133 |
team_stats_df.fillna(0, inplace=True)
|
| 134 |
|
|
|
|
| 136 |
columns={'attack_strength': 'opponent_attack_strength', 'defence_strength': 'opponent_defence_strength'}
|
| 137 |
)
|
| 138 |
|
| 139 |
+
final_df = pd.merge(all_fpl_players_games, opponent_strength,
|
| 140 |
+
left_on=['opponent_team', 'round'], right_on=['team_id', 'round'], how='left')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
final_df.drop('team_id', axis=1, inplace=True)
|
| 142 |
|
| 143 |
+
# Add Difficulty
|
| 144 |
all_fixtures = FPL.get_fixtures()
|
| 145 |
fixture_difficulty_map = {}
|
| 146 |
for f in all_fixtures:
|
| 147 |
+
fixture_difficulty_map[f['id']] = {'h': f['team_h_difficulty'], 'a': f['team_a_difficulty']}
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
def get_difficulty(row):
|
| 150 |
fid = row['fixture']
|
|
|
|
| 154 |
|
| 155 |
final_df['difficulty'] = final_df.apply(get_difficulty, axis=1)
|
| 156 |
|
| 157 |
+
# Rolling Stats
|
| 158 |
stats_to_roll = ['minutes', 'goals_scored', 'assists', 'bps', 'influence', 'creativity',
|
| 159 |
'threat', 'expected_goals', 'expected_assists', 'expected_goals_conceded', 'total_points']
|
| 160 |
final_df = final_df.sort_values(by=['element', 'round'])
|
|
|
|
| 167 |
final_df['total_points_SD5'] = final_df.groupby('element')['total_points'].transform(
|
| 168 |
lambda x: x.rolling(window=5, min_periods=1).std().shift(1)
|
| 169 |
)
|
|
|
|
| 170 |
final_df['total_points_RA10'] = final_df.groupby('element')['total_points'].transform(
|
| 171 |
lambda x: x.rolling(window=10, min_periods=1).mean().shift(1)
|
| 172 |
)
|
| 173 |
|
| 174 |
def get_trend_emoji(row):
|
| 175 |
+
short, long = row['total_points_RA5'], row['total_points_RA10']
|
|
|
|
| 176 |
if short > 0 and long > 0:
|
| 177 |
+
return '⬆️' if short > long else ('⬇️' if short < long else '➡️')
|
| 178 |
+
return '➡️' if short > 0 or long > 0 else '⚪'
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
final_df['Trend'] = final_df.apply(get_trend_emoji, axis=1)
|
| 181 |
final_df.fillna(0, inplace=True)
|
|
|
|
| 185 |
'opponent_defence_strength', 'total_points_SD5'] + [f'{stat}_RA5' for stat in stats_to_roll]
|
| 186 |
target = 'total_points'
|
| 187 |
|
| 188 |
+
# Train Models
|
| 189 |
final_df = final_df.sort_values(by='round')
|
| 190 |
unique_rounds = final_df['round'].unique()
|
| 191 |
split_idx = int(len(unique_rounds) * 0.8)
|
|
|
|
| 194 |
train_df = final_df[final_df['round'] <= split_round]
|
| 195 |
test_df = final_df[final_df['round'] > split_round]
|
| 196 |
|
| 197 |
+
X_train, y_train = train_df[features], train_df[target]
|
| 198 |
+
X_test, y_test = test_df[features], test_df[target]
|
|
|
|
|
|
|
| 199 |
|
| 200 |
scaler = StandardScaler()
|
| 201 |
X_train_scaled = scaler.fit_transform(X_train)
|
|
|
|
| 202 |
|
| 203 |
ridge_model = Ridge()
|
| 204 |
ridge_model.fit(X_train_scaled, y_train)
|
|
|
|
| 206 |
xgb_model = XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42)
|
| 207 |
xgb_model.fit(X_train_scaled, y_train)
|
| 208 |
|
| 209 |
+
# Generate Predictions
|
| 210 |
NEXT_GW = current_gameweek + 1
|
| 211 |
next_gw_fixtures = FPL.get_gameweek_fixtures(NEXT_GW)
|
| 212 |
prediction_rows = []
|
|
|
|
| 214 |
if next_gw_fixtures:
|
| 215 |
team_fixture_map = {}
|
| 216 |
for f in next_gw_fixtures:
|
| 217 |
+
team_fixture_map[f['team_h']] = {'opponent': f['team_a'], 'was_home': True, 'difficulty': f['team_h_difficulty']}
|
| 218 |
+
team_fixture_map[f['team_a']] = {'opponent': f['team_h'], 'was_home': False, 'difficulty': f['team_a_difficulty']}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
for idx, player in all_players.iterrows():
|
| 221 |
team_id = player['team']
|
| 222 |
if team_id in team_fixture_map:
|
| 223 |
fix_info = team_fixture_map[team_id]
|
| 224 |
prediction_rows.append({
|
| 225 |
+
'element': player['id'], 'web_name': player['web_name'], 'team': team_id,
|
| 226 |
+
'value': player['now_cost'], 'was_home': fix_info['was_home'],
|
| 227 |
+
'opponent_team': fix_info['opponent'], 'difficulty': fix_info['difficulty']
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
})
|
| 229 |
|
| 230 |
prediction_df = pd.DataFrame(prediction_rows)
|
|
|
|
| 240 |
|
| 241 |
# Calculate latest rolling stats
|
| 242 |
player_stats_latest = final_df.sort_values(by=['element', 'round']).copy()
|
|
|
|
| 243 |
for stat in stats_to_roll:
|
| 244 |
player_stats_latest[f'{stat}_RA5_latest'] = player_stats_latest.groupby('element')[stat].transform(
|
| 245 |
lambda x: x.rolling(window=5, min_periods=1).mean()
|
|
|
|
| 248 |
player_stats_latest['total_points_SD5_latest'] = player_stats_latest.groupby('element')['total_points'].transform(
|
| 249 |
lambda x: x.rolling(window=5, min_periods=1).std()
|
| 250 |
)
|
|
|
|
| 251 |
player_stats_latest['total_points_RA10_latest'] = player_stats_latest.groupby('element')['total_points'].transform(
|
| 252 |
lambda x: x.rolling(window=10, min_periods=1).mean()
|
| 253 |
)
|
| 254 |
|
| 255 |
+
def get_trend_latest(row):
|
| 256 |
+
short, long = row['total_points_RA5_latest'], row['total_points_RA10_latest']
|
|
|
|
| 257 |
if short > 0 and long > 0:
|
| 258 |
+
return '⬆️' if short > long else ('⬇️' if short < long else '➡️')
|
| 259 |
+
return '➡️' if short > 0 or long > 0 else '⚪'
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
+
player_stats_latest['Trend'] = player_stats_latest.apply(get_trend_latest, axis=1)
|
| 262 |
player_stats_latest = player_stats_latest.groupby('element').tail(1)
|
| 263 |
|
| 264 |
cols_to_merge = ['element', 'total_points_SD5_latest', 'Trend'] + [f'{stat}_RA5_latest' for stat in stats_to_roll]
|
|
|
|
| 274 |
X_pred = prediction_df[features]
|
| 275 |
X_pred_scaled = scaler.transform(X_pred)
|
| 276 |
|
| 277 |
+
prediction_df['Ridge Pred'] = ridge_model.predict(X_pred_scaled).clip(min=0)
|
| 278 |
+
prediction_df['XGB Pred'] = xgb_model.predict(X_pred_scaled).clip(min=0)
|
| 279 |
|
| 280 |
+
# Prepare final dataframe
|
|
|
|
|
|
|
|
|
|
| 281 |
final_predictions_df = prediction_df.copy()
|
| 282 |
final_predictions_df['player_name'] = final_predictions_df['web_name']
|
| 283 |
|
| 284 |
element_type_map = all_players.set_index('id')['element_type'].to_dict()
|
|
|
|
| 285 |
position_map = {1: 'Goalkeeper', 2: 'Defender', 3: 'Midfielder', 4: 'Forward'}
|
| 286 |
+
final_predictions_df['element_type'] = final_predictions_df['element'].map(element_type_map)
|
| 287 |
final_predictions_df['position'] = final_predictions_df['element_type'].map(position_map)
|
| 288 |
|
| 289 |
team_name_mapping = {team['id']: team['name'] for team in teams}
|
|
|
|
| 299 |
|
| 300 |
final_predictions_df = pd.merge(final_predictions_df, cols_from_players, on='element', how='left')
|
| 301 |
|
| 302 |
+
final_predictions_df.rename(columns={
|
| 303 |
'total_points_RA5': 'Pts Mean (5GW)',
|
| 304 |
'total_points_SD5': 'Pts Std Dev (5GW)',
|
| 305 |
'opponent_team': 'Next Opponent'
|
| 306 |
+
}, inplace=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
numericals = ['Ridge Pred', 'XGB Pred', 'Pts Mean (5GW)', 'Pts Std Dev (5GW)']
|
| 309 |
for col in numericals:
|
|
|
|
| 342 |
manager_players_df = pd.merge(
|
| 343 |
manager_players_df,
|
| 344 |
final_predictions_df[['element', 'XGB Pred', 'difficulty', 'Next Opponent']],
|
| 345 |
+
on='element', how='left'
|
|
|
|
| 346 |
)
|
| 347 |
else:
|
| 348 |
manager_players_df = pd.DataFrame(columns=['player_name', 'team_name', 'position', 'XGB Pred'])
|
|
|
|
| 353 |
return final_predictions_df, team_performance_df, manager_players_df, current_gameweek, NEXT_GW
|
| 354 |
|
| 355 |
|
| 356 |
+
# Load data
|
| 357 |
print("Loading FPL data and training models...")
|
| 358 |
final_predictions_df, team_performance_df, manager_players_df, current_gameweek, NEXT_GW = load_and_process_data()
|
| 359 |
print("Data loaded successfully!")
|
| 360 |
|
| 361 |
|
| 362 |
+
# MCP Tool Functions (exposed to Claude)
|
| 363 |
+
def get_top_predictions(top_n: int = 10, position: Optional[str] = None) -> pd.DataFrame:
|
| 364 |
+
"""
|
| 365 |
+
Get top predicted players for the upcoming gameweek.
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
top_n (int): Number of top players to return (default: 10)
|
| 369 |
+
position (str): Optional filter by position: Goalkeeper, Defender, Midfielder, or Forward
|
| 370 |
+
|
| 371 |
+
Returns:
|
| 372 |
+
pd.DataFrame: Top predicted players with their stats
|
| 373 |
+
"""
|
| 374 |
+
df = final_predictions_df.copy()
|
| 375 |
+
|
| 376 |
if position:
|
| 377 |
+
df = df[df['position'] == position]
|
| 378 |
+
|
| 379 |
+
df = df.sort_values('XGB Pred', ascending=False).head(top_n)
|
| 380 |
+
|
| 381 |
+
cols = ['player_name', 'team_name', 'position', 'value', 'XGB Pred',
|
| 382 |
+
'Pts Mean (5GW)', 'Trend', 'difficulty']
|
| 383 |
+
|
| 384 |
+
return df[cols]
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def compare_players(player1_name: str, player2_name: str, player3_name: Optional[str] = None) -> pd.DataFrame:
|
| 388 |
+
"""
|
| 389 |
+
Compare multiple players side-by-side with their statistics.
|
| 390 |
+
|
| 391 |
+
Args:
|
| 392 |
+
player1_name (str): First player name to compare
|
| 393 |
+
player2_name (str): Second player name to compare
|
| 394 |
+
player3_name (str): Optional third player name to compare
|
| 395 |
+
|
| 396 |
+
Returns:
|
| 397 |
+
pd.DataFrame: Comparison of players with their predictions and stats
|
| 398 |
+
"""
|
| 399 |
+
df = final_predictions_df.copy()
|
| 400 |
+
|
| 401 |
+
player_names = [player1_name, player2_name]
|
| 402 |
+
if player3_name:
|
| 403 |
+
player_names.append(player3_name)
|
| 404 |
+
|
| 405 |
+
matched_players = []
|
| 406 |
+
for name in player_names:
|
| 407 |
+
matches = df[df['player_name'].str.contains(name, case=False, na=False)]
|
| 408 |
+
if not matches.empty:
|
| 409 |
+
matched_players.append(matches.iloc[0])
|
| 410 |
+
|
| 411 |
+
if not matched_players:
|
| 412 |
+
return pd.DataFrame({"error": ["No players found"]})
|
| 413 |
+
|
| 414 |
+
comparison_df = pd.DataFrame(matched_players)
|
| 415 |
|
| 416 |
+
cols = ['player_name', 'team_name', 'position', 'value', 'XGB Pred',
|
| 417 |
+
'Ridge Pred', 'Total Pts', 'Last GW Pts', 'Pts Mean (5GW)',
|
| 418 |
+
'Pts Std Dev (5GW)', 'Trend', 'difficulty']
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
+
return comparison_df[cols]
|
| 421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
+
def get_team_stats(team_name: Optional[str] = None) -> pd.DataFrame:
|
| 424 |
+
"""
|
| 425 |
+
Get team performance statistics including attack and defense rankings.
|
| 426 |
+
|
| 427 |
+
Args:
|
| 428 |
+
team_name (str): Optional team name to filter (e.g., 'Arsenal', 'Liverpool')
|
| 429 |
+
|
| 430 |
+
Returns:
|
| 431 |
+
pd.DataFrame: Team performance statistics
|
| 432 |
+
"""
|
| 433 |
+
df = team_performance_df.copy()
|
| 434 |
+
|
| 435 |
+
if team_name:
|
| 436 |
+
df = df[df['team_name'].str.contains(team_name, case=False, na=False)]
|
| 437 |
+
|
| 438 |
+
df = df.sort_values('attack_rank')
|
| 439 |
+
|
| 440 |
+
cols = ['team_name', 'goals_scored_per_game', 'goals_conceded_per_game',
|
| 441 |
+
'attack_rank', 'defense_rank']
|
| 442 |
+
|
| 443 |
+
return df[cols]
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def get_differentials(max_price: float = 10.0, min_predicted: float = 4.0, top_n: int = 10) -> pd.DataFrame:
|
| 447 |
+
"""
|
| 448 |
+
Find best differential players (high predicted points at low price).
|
| 449 |
+
|
| 450 |
+
Args:
|
| 451 |
+
max_price (float): Maximum player price in millions (default: 10.0)
|
| 452 |
+
min_predicted (float): Minimum predicted points (default: 4.0)
|
| 453 |
+
top_n (int): Number of results to return (default: 10)
|
| 454 |
+
|
| 455 |
+
Returns:
|
| 456 |
+
pd.DataFrame: Best differential players
|
| 457 |
+
"""
|
| 458 |
+
df = final_predictions_df.copy()
|
| 459 |
+
|
| 460 |
+
df = df[(df['value'] <= max_price) & (df['XGB Pred'] >= min_predicted)]
|
| 461 |
+
df = df.sort_values('XGB Pred', ascending=False).head(top_n)
|
| 462 |
+
|
| 463 |
+
cols = ['player_name', 'team_name', 'position', 'value', 'XGB Pred',
|
| 464 |
+
'Pts Mean (5GW)', 'Trend', 'difficulty']
|
| 465 |
+
|
| 466 |
+
return df[cols]
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def get_manager_team_predictions() -> pd.DataFrame:
|
| 470 |
+
"""
|
| 471 |
+
Get your current FPL manager team with predictions for next gameweek.
|
| 472 |
+
|
| 473 |
+
Returns:
|
| 474 |
+
pd.DataFrame: Manager team with predictions
|
| 475 |
+
"""
|
| 476 |
+
return manager_players_df
|
| 477 |
|
|
|
|
|
|
|
| 478 |
|
| 479 |
# Build Gradio Interface
|
| 480 |
+
with gr.Blocks(title=f"FPL GW {NEXT_GW} Predictor (MCP)") as demo:
|
| 481 |
+
gr.Markdown(f"# FPL Gameweek {NEXT_GW} Point Predictor")
|
| 482 |
gr.Markdown("""
|
| 483 |
+
**MCP-Enabled**: This app exposes prediction tools for Claude Desktop!
|
| 484 |
|
| 485 |
+
Use the functions below or connect via Claude Desktop to query FPL data.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
""")
|
| 487 |
|
| 488 |
+
with gr.Tab("Top Predictions"):
|
| 489 |
+
with gr.Row():
|
| 490 |
+
top_n_input = gr.Slider(5, 50, value=10, step=5, label="Number of Players")
|
| 491 |
+
position_input = gr.Dropdown(
|
| 492 |
+
choices=["", "Goalkeeper", "Defender", "Midfielder", "Forward"],
|
| 493 |
+
label="Position Filter",
|
| 494 |
+
value=""
|
| 495 |
+
)
|
| 496 |
+
top_pred_btn = gr.Button("Get Top Predictions")
|
| 497 |
+
top_pred_output = gr.Dataframe()
|
|
|
|
| 498 |
|
| 499 |
+
top_pred_btn.click(
|
| 500 |
+
fn=lambda n, p: get_top_predictions(n, p if p else None),
|
| 501 |
+
inputs=[top_n_input, position_input],
|
| 502 |
+
outputs=top_pred_output
|
| 503 |
+
)
|
| 504 |
|
| 505 |
+
with gr.Tab("Compare Players"):
|
| 506 |
+
player1 = gr.Textbox(label="Player 1", value="Salah")
|
| 507 |
+
player2 = gr.Textbox(label="Player 2", value="Haaland")
|
| 508 |
+
player3 = gr.Textbox(label="Player 3 (Optional)", value="")
|
| 509 |
+
compare_btn = gr.Button("Compare Players")
|
| 510 |
+
compare_output = gr.Dataframe()
|
| 511 |
+
|
| 512 |
+
compare_btn.click(
|
| 513 |
+
fn=lambda p1, p2, p3: compare_players(p1, p2, p3 if p3 else None),
|
| 514 |
+
inputs=[player1, player2, player3],
|
| 515 |
+
outputs=compare_output
|
| 516 |
+
)
|
| 517 |
|
| 518 |
+
with gr.Tab("Team Stats"):
|
| 519 |
+
team_input = gr.Textbox(label="Team Name (optional)", value="")
|
| 520 |
+
team_btn = gr.Button("Get Team Stats")
|
| 521 |
+
team_output = gr.Dataframe()
|
|
|
|
|
|
|
| 522 |
|
| 523 |
+
team_btn.click(
|
| 524 |
+
fn=lambda t: get_team_stats(t if t else None),
|
| 525 |
+
inputs=team_input,
|
| 526 |
+
outputs=team_output
|
| 527 |
+
)
|
| 528 |
|
| 529 |
+
with gr.Tab("Differentials"):
|
| 530 |
+
with gr.Row():
|
| 531 |
+
max_price = gr.Slider(4.0, 15.0, value=8.0, step=0.5, label="Max Price (£m)")
|
| 532 |
+
min_pred = gr.Slider(0.0, 10.0, value=4.0, step=0.5, label="Min Predicted Points")
|
| 533 |
+
diff_top_n = gr.Slider(5, 30, value=10, step=5, label="Results")
|
| 534 |
+
diff_btn = gr.Button("Find Differentials")
|
| 535 |
+
diff_output = gr.Dataframe()
|
| 536 |
+
|
| 537 |
+
diff_btn.click(
|
| 538 |
+
fn=get_differentials,
|
| 539 |
+
inputs=[max_price, min_pred, diff_top_n],
|
| 540 |
+
outputs=diff_output
|
| 541 |
+
)
|
| 542 |
|
| 543 |
+
with gr.Tab("My Team"):
|
| 544 |
+
mgr_btn = gr.Button("Load My Team")
|
| 545 |
+
mgr_output = gr.Dataframe()
|
|
|
|
|
|
|
| 546 |
|
| 547 |
+
mgr_btn.click(fn=get_manager_team_predictions, outputs=mgr_output)
|
|
|
|
|
|
|
| 548 |
|
|
|
|
| 549 |
|
| 550 |
if __name__ == "__main__":
|
| 551 |
+
# Enable MCP server - this exposes all functions as tools for Claude!
|
| 552 |
+
demo.launch(mcp_server=True)
|