Seewhatson commited on
Commit
8de0ef6
·
verified ·
1 Parent(s): 616da7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -181
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
- # 1. Fetch Basic Data
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
- # 2. 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
-
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
- # 3. Preprocessing & Feature Engineering
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
- 'round': gw,
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
- 'round': gw,
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(window_size, min_periods=1).mean().shift(1)
135
  )
136
  team_stats_df['defence_strength'] = team_stats_df.groupby('team_id')['goals_conceded'].transform(
137
- lambda x: x.rolling(window_size, min_periods=1).mean().shift(1)
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
- all_fpl_players_games,
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 Feature
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
- # Player Rolling Stats
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: return '⬆️'
194
- elif short < long: return '⬇️'
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
- # 4. Time-Based Split & Model Training
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
- y_train = train_df[target]
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
- # 5. Generate Predictions for Next Gameweek
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
- 'opponent': f['team_a'],
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
- 'web_name': player['web_name'],
257
- 'team': team_id,
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 get_trend_emoji_latest(row):
292
- short = row[f'total_points_RA5_latest']
293
- long = row['total_points_RA10_latest']
294
  if short > 0 and long > 0:
295
- if short > long: return '⬆️'
296
- elif short < long: return '⬇️'
297
- else: return '➡️'
298
- elif short > 0 or long > 0: return '➡️'
299
- else: return '⚪'
300
 
301
- player_stats_latest['Trend'] = player_stats_latest.apply(get_trend_emoji_latest, axis=1)
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
- prediction_df['Ridge Pred'] = prediction_df['Ridge Pred'].clip(lower=0)
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
- rename_map = {
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 when app starts
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
- def filter_predictions(position=None, team=None, player_name=None):
424
- filtered_df = final_predictions_df.copy()
 
 
 
 
 
 
 
 
 
 
 
 
425
  if position:
426
- filtered_df = filtered_df[filtered_df['position'] == position]
427
- if team:
428
- filtered_df = filtered_df[filtered_df['team_name'] == team]
429
- if player_name and len(player_name) >= 3:
430
- filtered_df = filtered_df[filtered_df['player_name'].str.contains(player_name, case=False, na=False)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
- cols = [
433
- 'element', 'player_name', 'team_name', 'position', 'Next Opponent',
434
- 'Ridge Pred', 'XGB Pred', 'Actual GW Pts', 'Closest Prediction Method',
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"Gameweek {NEXT_GW} Predicted Points") as interface:
454
- gr.Markdown(f"# FPL Gameweek {NEXT_GW} Predicted Points")
455
  gr.Markdown("""
456
- This app predicts Fantasy Premier League player points using machine learning models (Ridge Regression & XGBoost).
457
 
458
- **Features:**
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.Row():
466
- position_filter = gr.Dropdown(choices=[None] + unique_positions, label="Filter by Position")
467
- team_filter = gr.Dropdown(choices=[None] + unique_teams, label="Filter by Team")
468
- player_name_filter = gr.Textbox(label="Filter by Player Name (min 3 letters)")
469
-
470
- output_dataframe = gr.Dataframe(label="Filtered Predictions", interactive=False)
471
-
472
- with gr.Row():
473
- export_filename = gr.Textbox(label="Filename Prefix", value="fpl_predictions")
474
- export_button = gr.Button("Export Predictions to CSV")
475
- export_output = gr.File(label="Download CSV")
476
 
477
- export_button.click(
478
- fn=export_to_csv,
479
- inputs=[output_dataframe, export_filename],
480
- outputs=export_output
481
- )
482
 
483
- gr.Markdown("## Team Performance and Ranks")
484
- gr.Dataframe(
485
- value=team_performance_df[['team_name', 'overall_goals_scored', 'overall_goals_conceded',
486
- 'goals_scored_per_game', 'goals_conceded_per_game',
487
- 'attack_rank', 'defense_rank']].round(2),
488
- label="Team Performance",
489
- interactive=False
490
- )
 
 
 
 
491
 
492
- gr.Markdown("## My Manager Team")
493
- gr.Dataframe(
494
- value=manager_players_df,
495
- label=f"Manager Team for Gameweek {current_gameweek}",
496
- interactive=False
497
- )
498
 
499
- with gr.Row():
500
- mgr_export_filename = gr.Textbox(label="Manager Filename Prefix", value="manager_team")
501
- mgr_export_button = gr.Button("Export Manager Team to CSV")
502
- mgr_export_output = gr.File(label="Download Manager CSV")
 
503
 
504
- manager_df_state = gr.State(value=manager_players_df)
 
 
 
 
 
 
 
 
 
 
 
 
505
 
506
- mgr_export_button.click(
507
- fn=export_to_csv,
508
- inputs=[manager_df_state, mgr_export_filename],
509
- outputs=mgr_export_output
510
- )
511
 
512
- inputs = [position_filter, team_filter, player_name_filter]
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
- interface.launch()
 
 
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)