Britzzy commited on
Commit
a8addfb
·
verified ·
1 Parent(s): 1a1d0e5

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +32 -61
  2. fairvalue_xgboost.json +0 -0
app.py CHANGED
@@ -18,16 +18,15 @@ def load_data_and_model():
18
  return None, None
19
  df = pd.read_csv(df_path)
20
 
21
- # Standardize ALL possible market value names to match model
22
- # The common ones are market_value_in_eur, market_value_in_eur_x, market_value_in_eur_y
23
  mv_rename_map = {}
24
  for col in df.columns:
25
- if 'market_value' in col.lower() and 'eur' in col.lower():
26
  mv_rename_map[col] = 'market_value_in_eur'
27
  if mv_rename_map:
28
  df.rename(columns=mv_rename_map, inplace=True)
29
 
30
- # Deduplicate early
31
  df = df.loc[:, ~df.columns.duplicated()].copy()
32
 
33
  # Load model
@@ -38,9 +37,13 @@ def load_data_and_model():
38
  path = "FairValue_xgboost.json"
39
 
40
  if os.path.exists(path):
 
 
 
 
41
  model.load_model(path)
42
  else:
43
- return df, f"MODEL_FILE_NOT_FOUND (Checked: {os.getcwd()})"
44
  except Exception as e:
45
  return df, f"LOAD_ERROR: {str(e)}"
46
 
@@ -50,10 +53,9 @@ df, model = load_data_and_model()
50
 
51
  if isinstance(model, str):
52
  st.error(f"⚠️ {model}")
53
- st.info("Ensure the model file is uploaded to the root of your Space.")
54
  st.stop()
55
  if df is None:
56
- st.error("⚠️ Data file not found at 'data/processed/app_features.csv'")
57
  st.stop()
58
 
59
  # ======= Sidebar =======
@@ -68,15 +70,10 @@ if input_mode == "Select Existing Player":
68
  selected_name = st.sidebar.selectbox("Target Database Player", player_list)
69
  player_data = df[df[name_col] == selected_name].iloc[0:1].copy()
70
  else:
71
- st.sidebar.error("Player names column not found.")
72
  st.stop()
73
-
74
- st.sidebar.markdown("---")
75
- st.sidebar.markdown("**Simulation Parameters**")
76
 
77
  contract_default = max(0.5, min(6.0, float(player_data['Contract_Years_Left'].iloc[0])))
78
  age_default = max(16, min(40, int(player_data['Age'].iloc[0])))
79
-
80
  injury_col = next((c for c in ['Injury_Days_Total_24m', 'Injury_Days'] if c in player_data.columns), None)
81
  injury_default = 0
82
  if injury_col:
@@ -87,43 +84,33 @@ if input_mode == "Select Existing Player":
87
  injuries = st.sidebar.number_input("Injury missed days (24m)", 0, 500, injury_default)
88
 
89
  else:
90
- st.sidebar.markdown("---")
91
- st.sidebar.markdown("**Custom Player Attributes**")
92
- # MEDIAN fallback to ensure all 20+ columns are filled
93
  player_data = df.median(numeric_only=True).to_frame().T
94
-
95
  contract_years = st.sidebar.slider("Contract Years Remaining", 0.5, 6.0, 2.0, 0.5)
96
  age = st.sidebar.slider("Age", 16, 40, 24)
97
  injuries = st.sidebar.number_input("Injury missed days (24m)", 0, 500, 10)
98
 
99
- # Try to find the market value column to let user edit it
100
- mv_col = 'market_value_in_eur'
101
- if mv_col in player_data.columns:
102
  m_val = st.sidebar.number_input("Current Market Value Estimation (£m)", 1.0, 200.0, 20.0)
103
- player_data[mv_col] = (m_val * 1_000_000) / 0.85
104
 
105
  asking_price = st.sidebar.number_input("Selling Club Asking Price (£m)", 1.0, 300.0, 45.0)
106
 
107
- # Apply overrides
 
108
  player_data['Contract_Years_Left'] = contract_years
109
  player_data['Age'] = age
110
- inj_col_to_set = next((c for c in ['Injury_Days_Total_24m', 'Injury_Days'] if c in player_data.columns), 'Injury_Days_Total_24m')
111
- player_data[inj_col_to_set] = injuries
112
 
113
- # Prepare inference payload
114
- expected_cols = model.feature_names_in_
115
  X_infer = player_data.reindex(columns=expected_cols, fill_value=0)
116
 
117
  if st.button("Calculate Prediction", type="primary"):
118
- # Debug: Check if the market value is zero
119
- if X_infer['market_value_in_eur'].iloc[0] == 0:
120
- st.warning("⚠️ Market Value feature is 0.0! The model will underestimate. Checking input alignment...")
121
- st.write("Columns available in player_data:", player_data.columns.tolist())
122
-
123
- log_pv = model.predict(X_infer)[0]
124
  baseline_pv = np.expm1(log_pv)
125
 
126
- # Hard Cap logic
127
  conservative_bound = baseline_pv * 0.85
128
  risk_discount_pct = 0.0
129
  if contract_years < 1.5: risk_discount_pct += 0.20
@@ -142,37 +129,25 @@ if st.button("Calculate Prediction", type="primary"):
142
  fig = go.Figure(go.Indicator(
143
  mode="gauge+number+delta",
144
  value=asking_price,
145
- delta={'reference': hard_cap_m, 'increasing': {'color': "red"}, 'decreasing': {'color': "green"}},
146
- title={'text': "Asking Price vs Calculated Hard Cap (£m)"},
147
- gauge={
148
- 'axis': {'range': [0, max(asking_price, hard_cap_m) * 1.5]},
149
- 'steps': [
150
- {'range': [0, hard_cap_m], 'color': "rgba(0, 255, 0, 0.3)"},
151
- {'range': [hard_cap_m, hard_cap_m*1.2], 'color': "rgba(255, 255, 0, 0.3)"},
152
- ],
153
- 'threshold': {'line': {'color': "white", 'width': 4}, 'thickness': 0.75, 'value': hard_cap_m}
154
- }
155
  ))
156
  st.plotly_chart(fig, use_container_width=True)
157
 
158
  with col2:
159
  st.subheader("Metrics Breakdown")
160
  st.metric("Predicted Market Value (ML Baseline)", f"£{baseline_pv_m:.1f}m")
161
- if risk_discount_pct > 0:
162
- st.metric("Risk Penalty Applied", f"-£{discount_val/1_000_000:.1f}m", f"-{risk_discount_pct*100:.0f}% Risk penalty!")
163
- else:
164
- st.metric("Risk Penalty Applied", f"£0m", "0% Risk")
165
  st.metric("Recommended Hard Cap", f"£{hard_cap_m:.1f}m")
166
-
167
  if asking_price > hard_cap_m:
168
- st.error(f"Winner's Curse Risk: The asking price is £{asking_price - hard_cap_m:.1f}m OVER the Cap.")
169
- else:
170
- st.success("Asking price is within acceptable margins.")
 
 
 
 
171
 
172
- # SHAP / Diagnostics
173
- with st.expander("Show Model Input Diagnostics"):
174
- st.write("X_infer Payload:", X_infer)
175
-
176
  st.subheader("Algorithmic Interpretability (SHAP)")
177
  try:
178
  explainer = shap.TreeExplainer(model)
@@ -180,13 +155,9 @@ if st.button("Calculate Prediction", type="primary"):
180
  shap_df = pd.DataFrame({'Feature': X_infer.columns, 'Impact': shap_values.values[0]}).sort_values(by='Impact', ascending=False)
181
  st.dataframe(shap_df.head(10), use_container_width=True)
182
  except:
183
- st.info("Global Feature Importances shown as fallback.")
184
- imp_df = pd.DataFrame({'Feature': expected_cols, 'Weight': model.feature_importances_}).sort_values(by='Weight', ascending=False)
185
- st.dataframe(imp_df.head(10), use_container_width=True)
186
 
187
  st.markdown("---")
188
  st.subheader("Model Performance Assessment")
189
- st.markdown(f"**Training Set Size:** `{len(df):,}` verified transfers")
190
- st.markdown(f"**Features Engined:** `{len(expected_cols)}` columns")
191
- st.markdown("**Validation MAE (Mean Absolute Error):** `~£23,980,000` *(Statistical Range)*")
192
- st.caption("Training occurred utilizing a 5-Folds RandomizedSearchCV across 40 hyperparameter iterations.")
 
18
  return None, None
19
  df = pd.read_csv(df_path)
20
 
21
+ # Standardize ALL possible market value names
 
22
  mv_rename_map = {}
23
  for col in df.columns:
24
+ if 'market_value' in col.lower():
25
  mv_rename_map[col] = 'market_value_in_eur'
26
  if mv_rename_map:
27
  df.rename(columns=mv_rename_map, inplace=True)
28
 
29
+ # Deduplicate
30
  df = df.loc[:, ~df.columns.duplicated()].copy()
31
 
32
  # Load model
 
37
  path = "FairValue_xgboost.json"
38
 
39
  if os.path.exists(path):
40
+ # Check file size to catch corrupted uploads
41
+ filesize = os.path.getsize(path)
42
+ if filesize < 100:
43
+ return df, f"CORRUPTED_MODEL (Size: {filesize} bytes. Is too small!)"
44
  model.load_model(path)
45
  else:
46
+ return df, f"MODEL_FILE_NOT_FOUND (Checked root for {path})"
47
  except Exception as e:
48
  return df, f"LOAD_ERROR: {str(e)}"
49
 
 
53
 
54
  if isinstance(model, str):
55
  st.error(f"⚠️ {model}")
 
56
  st.stop()
57
  if df is None:
58
+ st.error("⚠️ Data file not found.")
59
  st.stop()
60
 
61
  # ======= Sidebar =======
 
70
  selected_name = st.sidebar.selectbox("Target Database Player", player_list)
71
  player_data = df[df[name_col] == selected_name].iloc[0:1].copy()
72
  else:
 
73
  st.stop()
 
 
 
74
 
75
  contract_default = max(0.5, min(6.0, float(player_data['Contract_Years_Left'].iloc[0])))
76
  age_default = max(16, min(40, int(player_data['Age'].iloc[0])))
 
77
  injury_col = next((c for c in ['Injury_Days_Total_24m', 'Injury_Days'] if c in player_data.columns), None)
78
  injury_default = 0
79
  if injury_col:
 
84
  injuries = st.sidebar.number_input("Injury missed days (24m)", 0, 500, injury_default)
85
 
86
  else:
 
 
 
87
  player_data = df.median(numeric_only=True).to_frame().T
 
88
  contract_years = st.sidebar.slider("Contract Years Remaining", 0.5, 6.0, 2.0, 0.5)
89
  age = st.sidebar.slider("Age", 16, 40, 24)
90
  injuries = st.sidebar.number_input("Injury missed days (24m)", 0, 500, 10)
91
 
92
+ if 'market_value_in_eur' in player_data.columns:
 
 
93
  m_val = st.sidebar.number_input("Current Market Value Estimation (£m)", 1.0, 200.0, 20.0)
94
+ player_data['market_value_in_eur'] = (m_val * 1_000_000) / 0.85
95
 
96
  asking_price = st.sidebar.number_input("Selling Club Asking Price (£m)", 1.0, 300.0, 45.0)
97
 
98
+ # Build X_infer
99
+ expected_cols = model.feature_names_in_
100
  player_data['Contract_Years_Left'] = contract_years
101
  player_data['Age'] = age
102
+ if 'Injury_Days_Total_24m' in player_data.columns:
103
+ player_data['Injury_Days_Total_24m'] = injuries
104
 
 
 
105
  X_infer = player_data.reindex(columns=expected_cols, fill_value=0)
106
 
107
  if st.button("Calculate Prediction", type="primary"):
108
+ # Debugging Visibility
109
+ raw_preds = model.predict(X_infer)
110
+ log_pv = raw_preds[0]
 
 
 
111
  baseline_pv = np.expm1(log_pv)
112
 
113
+ # UI logic
114
  conservative_bound = baseline_pv * 0.85
115
  risk_discount_pct = 0.0
116
  if contract_years < 1.5: risk_discount_pct += 0.20
 
129
  fig = go.Figure(go.Indicator(
130
  mode="gauge+number+delta",
131
  value=asking_price,
132
+ delta={'reference': hard_cap_m, 'increasing': {'color': "red"}},
133
+ title={'text': "Asking Price vs Hard Cap (£m)"},
134
+ gauge={'axis': {'range': [0, max(asking_price, 100)]}, 'threshold': {'line': {'color': "white", 'width': 4}, 'value': hard_cap_m}}
 
 
 
 
 
 
 
135
  ))
136
  st.plotly_chart(fig, use_container_width=True)
137
 
138
  with col2:
139
  st.subheader("Metrics Breakdown")
140
  st.metric("Predicted Market Value (ML Baseline)", f"£{baseline_pv_m:.1f}m")
 
 
 
 
141
  st.metric("Recommended Hard Cap", f"£{hard_cap_m:.1f}m")
 
142
  if asking_price > hard_cap_m:
143
+ st.error(f"Winner's Curse Risk: £{asking_price - hard_cap_m:.1f}m Overpay.")
144
+
145
+ with st.expander("🔬 Technical Diagnostics (FOR DEBUGGING)"):
146
+ st.write("Raw Log-Prediction from Model:", log_pv)
147
+ st.write("Input Feature Vector (X_infer):", X_infer)
148
+ if log_pv == 0 and X_infer['market_value_in_eur'].iloc[0] > 0:
149
+ st.error("CRITICAL: The model is receiving data but returning exactly 0.0. This means the uploaded model file is likely empty or corrupted.")
150
 
 
 
 
 
151
  st.subheader("Algorithmic Interpretability (SHAP)")
152
  try:
153
  explainer = shap.TreeExplainer(model)
 
155
  shap_df = pd.DataFrame({'Feature': X_infer.columns, 'Impact': shap_values.values[0]}).sort_values(by='Impact', ascending=False)
156
  st.dataframe(shap_df.head(10), use_container_width=True)
157
  except:
158
+ st.warning("SHAP Error. Model file/version mismatch.")
 
 
159
 
160
  st.markdown("---")
161
  st.subheader("Model Performance Assessment")
162
+ st.markdown(f"**Training Set Size:** `{len(df):,}` transfers | **Features:** `{len(expected_cols)}` cols")
163
+ st.markdown("**Validation MAE:** `~£23,980,000` | **Engine:** XGBoost + SHAP")
 
 
fairvalue_xgboost.json CHANGED
The diff for this file is too large to render. See raw diff