Finish-him commited on
Commit
fd69ce5
·
verified ·
1 Parent(s): 602d9a1

Deploy: 5-model ensemble predictor with Gradio API

Browse files
README.md CHANGED
@@ -1,12 +1,45 @@
1
  ---
2
- title: Dota2 Edge Ensemble
3
- emoji: 👀
4
- colorFrom: purple
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.9.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Dota 2 Edge Ensemble
3
+ emoji: 🎮
4
+ colorFrom: red
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
+ # Dota 2 Edge Multi-Model Ensemble Predictor
14
+
15
+ Predict professional Dota 2 match outcomes using a 5-model ensemble
16
+ (XGBoost, LightGBM, CatBoost, LogReg, MLP) with Stacking meta-learner.
17
+
18
+ ## Prediction Moments
19
+
20
+ | Moment | Description | Stacking AUC |
21
+ |--------|-------------|-------------|
22
+ | m0 | Draft (pre-game) | 0.587 |
23
+ | m1 | @10 minutes | 0.735 |
24
+ | m15 | @15 minutes | 0.807 |
25
+ | m2 | @20 minutes | 0.838 |
26
+ | m3 | @30 minutes | 0.963 |
27
+
28
+ ## Dataset
29
+
30
+ Trained on 1,645 professional Dota 2 matches from OpenDota API.
31
+ Dataset available at [Finish-him/dota2-pro-matches](https://huggingface.co/datasets/Finish-him/dota2-pro-matches).
32
+
33
+ ## API Usage
34
+
35
+ ```python
36
+ from gradio_client import Client
37
+
38
+ client = Client("Finish-him/dota2-edge-ensemble")
39
+ result = client.predict(
40
+ moment="m1",
41
+ features_text='{"gold_delta_10": 2500, "xp_delta_10": 1800}',
42
+ api_name="/predict"
43
+ )
44
+ print(result)
45
+ ```
app.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dota 2 Edge — Multi-Model Ensemble Prediction API
3
+ ==================================================
4
+ HuggingFace Space serving XGBoost, LightGBM, CatBoost, LogReg, MLP
5
+ with Stacking meta-learner for 5 prediction moments.
6
+ """
7
+
8
+ import gradio as gr
9
+ import pickle
10
+ import numpy as np
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+
15
+ MODEL_DIR = Path("models")
16
+ MOMENTS = {
17
+ 'm0': 'Draft (pre-game)',
18
+ 'm1': '@10 minutes',
19
+ 'm15': '@15 minutes',
20
+ 'm2': '@20 minutes',
21
+ 'm3': '@30 minutes',
22
+ }
23
+
24
+ # Load all ensemble bundles
25
+ bundles = {}
26
+ for mk in MOMENTS:
27
+ path = MODEL_DIR / f"ensemble_{mk}.pkl"
28
+ if path.exists():
29
+ with open(path, 'rb') as f:
30
+ bundles[mk] = pickle.load(f)
31
+
32
+ # Load ensemble summary
33
+ summary_path = MODEL_DIR / "ensemble_summary.json"
34
+ if summary_path.exists():
35
+ with open(summary_path) as f:
36
+ ensemble_summary = json.load(f)
37
+ else:
38
+ ensemble_summary = {}
39
+
40
+
41
+ def predict(moment: str, features_json: str) -> dict:
42
+ """
43
+ Run ensemble prediction for a given moment.
44
+
45
+ Args:
46
+ moment: One of 'm0', 'm1', 'm15', 'm2', 'm3'
47
+ features_json: JSON object with feature values
48
+
49
+ Returns:
50
+ Dict with individual model probabilities + stacking ensemble
51
+ """
52
+ if moment not in bundles:
53
+ return {"error": f"Moment '{moment}' not loaded. Available: {list(bundles.keys())}"}
54
+
55
+ bundle = bundles[moment]
56
+ feature_cols = bundle['feature_cols']
57
+
58
+ # Parse features
59
+ try:
60
+ if isinstance(features_json, str):
61
+ features = json.loads(features_json)
62
+ else:
63
+ features = features_json
64
+ except json.JSONDecodeError as e:
65
+ return {"error": f"Invalid JSON: {e}"}
66
+
67
+ # Build feature vector
68
+ X = np.array([[features.get(col, 0.0) for col in feature_cols]])
69
+ X_scaled = bundle['scaler'].transform(X)
70
+
71
+ # Individual predictions
72
+ probs = {}
73
+ for name in bundle['base_model_names']:
74
+ model = bundle['models'][name]
75
+ if name in ('logreg', 'mlp'):
76
+ p = model.predict_proba(X_scaled)[:, 1][0]
77
+ else:
78
+ p = model.predict_proba(X)[:, 1][0]
79
+ probs[name] = round(float(p), 4)
80
+
81
+ # Average ensemble
82
+ avg = round(float(np.mean(list(probs.values()))), 4)
83
+
84
+ # Stacking meta-learner
85
+ stack_input = np.array([[probs[n] for n in bundle['base_model_names']]])
86
+ stacking = round(float(bundle['meta_learner'].predict_proba(stack_input)[:, 1][0]), 4)
87
+
88
+ return {
89
+ "moment": moment,
90
+ "moment_name": MOMENTS[moment],
91
+ "n_features_used": len(feature_cols),
92
+ "individual_models": probs,
93
+ "avg_ensemble": avg,
94
+ "stacking_ensemble": stacking,
95
+ "prediction": "Radiant" if stacking > 0.5 else "Dire",
96
+ "confidence": round(abs(stacking - 0.5) * 2, 4),
97
+ }
98
+
99
+
100
+ def predict_all_moments(features_json: str) -> dict:
101
+ """Run prediction for all available moments."""
102
+ results = {}
103
+ for mk in bundles:
104
+ results[mk] = predict(mk, features_json)
105
+ return results
106
+
107
+
108
+ def get_model_info() -> dict:
109
+ """Get info about loaded models."""
110
+ info = {
111
+ "available_moments": {},
112
+ "ensemble_summary": ensemble_summary,
113
+ }
114
+ for mk, bundle in bundles.items():
115
+ info["available_moments"][mk] = {
116
+ "name": MOMENTS[mk],
117
+ "n_features": bundle['n_features'],
118
+ "n_samples_trained": bundle['n_samples'],
119
+ "feature_columns": bundle['feature_cols'],
120
+ "base_models": bundle['base_model_names'],
121
+ "meta_weights": bundle['meta_weights'],
122
+ "results": bundle['results'],
123
+ }
124
+ return info
125
+
126
+
127
+ # ================================================================
128
+ # GRADIO INTERFACE
129
+ # ================================================================
130
+
131
+ def gradio_predict(moment, features_text):
132
+ try:
133
+ result = predict(moment, features_text)
134
+ return json.dumps(result, indent=2)
135
+ except Exception as e:
136
+ return json.dumps({"error": str(e)}, indent=2)
137
+
138
+
139
+ def gradio_info():
140
+ return json.dumps(get_model_info(), indent=2, default=str)
141
+
142
+
143
+ # Example features for @10min
144
+ example_features = {
145
+ "gold_delta_10": 2500,
146
+ "xp_delta_10": 1800,
147
+ "gold_growth_5_10": 450,
148
+ "deny_delta_10": 3,
149
+ "lh_delta_10": 25,
150
+ "kill_delta_10": 2,
151
+ }
152
+
153
+ with gr.Blocks(
154
+ title="Dota 2 Edge — Ensemble Predictor",
155
+ theme=gr.themes.Base(primary_hue="red", secondary_hue="green"),
156
+ ) as demo:
157
+ gr.Markdown("""
158
+ # Dota 2 Edge — Multi-Model Ensemble Predictor
159
+
160
+ Predict professional Dota 2 match outcomes using a 5-model ensemble
161
+ (XGBoost, LightGBM, CatBoost, LogReg, MLP) with Stacking meta-learner.
162
+
163
+ **Moments:** Draft | @10min | @15min | @20min | @30min
164
+
165
+ **Best AUC:** 0.963 (@30min stacking) | 0.807 (@15min) | 0.735 (@10min)
166
+ """)
167
+
168
+ with gr.Tab("Predict"):
169
+ with gr.Row():
170
+ moment_dropdown = gr.Dropdown(
171
+ choices=list(MOMENTS.keys()),
172
+ value="m1",
173
+ label="Prediction Moment",
174
+ )
175
+ features_input = gr.Textbox(
176
+ label="Features (JSON)",
177
+ value=json.dumps(example_features, indent=2),
178
+ lines=10,
179
+ )
180
+ predict_btn = gr.Button("Predict", variant="primary")
181
+ output = gr.Textbox(label="Result", lines=15)
182
+ predict_btn.click(gradio_predict, inputs=[moment_dropdown, features_input], outputs=output)
183
+
184
+ with gr.Tab("Model Info"):
185
+ info_btn = gr.Button("Load Model Info")
186
+ info_output = gr.Textbox(label="Model Details", lines=30)
187
+ info_btn.click(gradio_info, outputs=info_output)
188
+
189
+ with gr.Tab("API"):
190
+ gr.Markdown("""
191
+ ## API Usage
192
+
193
+ This Space exposes a Gradio API. You can call it programmatically:
194
+
195
+ ```python
196
+ from gradio_client import Client
197
+
198
+ client = Client("Finish-him/dota2-edge-ensemble")
199
+ result = client.predict(
200
+ moment="m1",
201
+ features_text='{"gold_delta_10": 2500, "xp_delta_10": 1800}',
202
+ api_name="/predict"
203
+ )
204
+ print(result)
205
+ ```
206
+
207
+ ### Endpoints
208
+ - `/predict` — Single moment prediction
209
+ - `/info` — Model details and feature lists
210
+ """)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ demo.launch()
models/ensemble_m0.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2ec2e47c0051a2d92c6ba649dd616aa15995af0718e0ed4d228f93e94d6167d7
3
+ size 1023369
models/ensemble_m1.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f392e116b51af9f1cd492f20ba2e79ad8cc0507c9dc3f8025d788cb5a031412d
3
+ size 1150686
models/ensemble_m15.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:513e7ecee5cac81774cfc0c5ed5d7390e457326810d6c7c16e373f64db37bc69
3
+ size 1189467
models/ensemble_m2.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9224b17bde7c1dc30346e83e90a5063e1fe7b32eb74b3582dda2f0074c5fc08c
3
+ size 1204821
models/ensemble_m3.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad86eea6863195be81e8da1acab65d2c9d00c687dffeec0e3e1d9797b10532ed
3
+ size 1214363
models/ensemble_summary.json ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "trained_at_brt": "2026-03-24T04:18:04.226896-03:00",
3
+ "moments": {
4
+ "m0": {
5
+ "results": {
6
+ "xgboost": {
7
+ "auc": 0.5726,
8
+ "logloss": 0.7188,
9
+ "brier": 0.2588
10
+ },
11
+ "lightgbm": {
12
+ "auc": 0.5722,
13
+ "logloss": 0.7174,
14
+ "brier": 0.2582
15
+ },
16
+ "catboost": {
17
+ "auc": 0.581,
18
+ "logloss": 0.6801,
19
+ "brier": 0.2436
20
+ },
21
+ "logreg": {
22
+ "auc": 0.5652,
23
+ "logloss": 0.6886,
24
+ "brier": 0.2476
25
+ },
26
+ "mlp": {
27
+ "auc": 0.5615,
28
+ "logloss": 0.6884,
29
+ "brier": 0.2475
30
+ },
31
+ "avg_ensemble": {
32
+ "auc": 0.5847,
33
+ "logloss": 0.6809,
34
+ "brier": 0.244
35
+ },
36
+ "stacking": {
37
+ "auc": 0.5872,
38
+ "logloss": 0.6783,
39
+ "brier": 0.2427
40
+ }
41
+ },
42
+ "meta_weights": {
43
+ "xgboost": 9.4379,
44
+ "lightgbm": 4.8757,
45
+ "catboost": 1.686,
46
+ "logreg": -3.8813,
47
+ "mlp": -1.0309
48
+ }
49
+ },
50
+ "m1": {
51
+ "results": {
52
+ "xgboost": {
53
+ "auc": 0.7153,
54
+ "logloss": 0.6409,
55
+ "brier": 0.2208
56
+ },
57
+ "lightgbm": {
58
+ "auc": 0.7156,
59
+ "logloss": 0.6408,
60
+ "brier": 0.2201
61
+ },
62
+ "catboost": {
63
+ "auc": 0.7243,
64
+ "logloss": 0.6091,
65
+ "brier": 0.211
66
+ },
67
+ "logreg": {
68
+ "auc": 0.7252,
69
+ "logloss": 0.6147,
70
+ "brier": 0.2131
71
+ },
72
+ "mlp": {
73
+ "auc": 0.6925,
74
+ "logloss": 0.6789,
75
+ "brier": 0.2333
76
+ },
77
+ "avg_ensemble": {
78
+ "auc": 0.7314,
79
+ "logloss": 0.6062,
80
+ "brier": 0.2095
81
+ },
82
+ "stacking": {
83
+ "auc": 0.7349,
84
+ "logloss": 0.6025,
85
+ "brier": 0.208
86
+ }
87
+ },
88
+ "meta_weights": {
89
+ "xgboost": 8.4541,
90
+ "lightgbm": 6.2371,
91
+ "catboost": 1.4335,
92
+ "logreg": -3.4229,
93
+ "mlp": -0.2915
94
+ }
95
+ },
96
+ "m15": {
97
+ "results": {
98
+ "xgboost": {
99
+ "auc": 0.7928,
100
+ "logloss": 0.5738,
101
+ "brier": 0.1907
102
+ },
103
+ "lightgbm": {
104
+ "auc": 0.7842,
105
+ "logloss": 0.5818,
106
+ "brier": 0.1946
107
+ },
108
+ "catboost": {
109
+ "auc": 0.8005,
110
+ "logloss": 0.5376,
111
+ "brier": 0.1818
112
+ },
113
+ "logreg": {
114
+ "auc": 0.7925,
115
+ "logloss": 0.5556,
116
+ "brier": 0.1883
117
+ },
118
+ "mlp": {
119
+ "auc": 0.7781,
120
+ "logloss": 0.5628,
121
+ "brier": 0.1917
122
+ },
123
+ "avg_ensemble": {
124
+ "auc": 0.8036,
125
+ "logloss": 0.5352,
126
+ "brier": 0.1808
127
+ },
128
+ "stacking": {
129
+ "auc": 0.8068,
130
+ "logloss": 0.5355,
131
+ "brier": 0.1797
132
+ }
133
+ },
134
+ "meta_weights": {
135
+ "xgboost": 7.6935,
136
+ "lightgbm": 5.5758,
137
+ "catboost": 1.3063,
138
+ "logreg": -2.9682,
139
+ "mlp": 0.4831
140
+ }
141
+ },
142
+ "m2": {
143
+ "results": {
144
+ "xgboost": {
145
+ "auc": 0.8245,
146
+ "logloss": 0.5345,
147
+ "brier": 0.1757
148
+ },
149
+ "lightgbm": {
150
+ "auc": 0.821,
151
+ "logloss": 0.5391,
152
+ "brier": 0.1766
153
+ },
154
+ "catboost": {
155
+ "auc": 0.832,
156
+ "logloss": 0.5013,
157
+ "brier": 0.1669
158
+ },
159
+ "logreg": {
160
+ "auc": 0.8301,
161
+ "logloss": 0.509,
162
+ "brier": 0.1699
163
+ },
164
+ "mlp": {
165
+ "auc": 0.804,
166
+ "logloss": 0.542,
167
+ "brier": 0.182
168
+ },
169
+ "avg_ensemble": {
170
+ "auc": 0.8352,
171
+ "logloss": 0.4976,
172
+ "brier": 0.1658
173
+ },
174
+ "stacking": {
175
+ "auc": 0.8375,
176
+ "logloss": 0.5002,
177
+ "brier": 0.1654
178
+ }
179
+ },
180
+ "meta_weights": {
181
+ "xgboost": 6.8754,
182
+ "lightgbm": 6.2505,
183
+ "catboost": 1.2161,
184
+ "logreg": -2.691,
185
+ "mlp": 0.151
186
+ }
187
+ },
188
+ "m3": {
189
+ "results": {
190
+ "xgboost": {
191
+ "auc": 0.9612,
192
+ "logloss": 0.2771,
193
+ "brier": 0.0795
194
+ },
195
+ "lightgbm": {
196
+ "auc": 0.9617,
197
+ "logloss": 0.2798,
198
+ "brier": 0.0805
199
+ },
200
+ "catboost": {
201
+ "auc": 0.964,
202
+ "logloss": 0.2449,
203
+ "brier": 0.0736
204
+ },
205
+ "logreg": {
206
+ "auc": 0.9564,
207
+ "logloss": 0.2782,
208
+ "brier": 0.0828
209
+ },
210
+ "mlp": {
211
+ "auc": 0.9489,
212
+ "logloss": 0.2883,
213
+ "brier": 0.0891
214
+ },
215
+ "avg_ensemble": {
216
+ "auc": 0.9626,
217
+ "logloss": 0.2477,
218
+ "brier": 0.0746
219
+ },
220
+ "stacking": {
221
+ "auc": 0.9633,
222
+ "logloss": 0.258,
223
+ "brier": 0.0746
224
+ }
225
+ },
226
+ "meta_weights": {
227
+ "xgboost": 4.8879,
228
+ "lightgbm": 4.9469,
229
+ "catboost": 1.7448,
230
+ "logreg": 0.0635,
231
+ "mlp": 0.1361
232
+ }
233
+ }
234
+ },
235
+ "models": [
236
+ "xgboost",
237
+ "lightgbm",
238
+ "catboost",
239
+ "logreg",
240
+ "mlp"
241
+ ],
242
+ "meta_learner": "logistic_regression_stacking",
243
+ "total_time_s": 457.2
244
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ numpy
3
+ scikit-learn
4
+ xgboost
5
+ lightgbm
6
+ catboost