evanskim113 commited on
Commit
8fc5081
·
verified ·
1 Parent(s): 0939fb7

Upload 10 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ proto_core_with_proba_0904_0717.xlsx filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,20 +1,13 @@
 
1
  ---
2
- title: Mustrategy V2
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
  pinned: false
11
- short_description: Football match outcome predictor using odds and ML models
12
- license: mit
13
  ---
14
 
15
- # Welcome to Streamlit!
16
-
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
-
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
1
+
2
  ---
3
+ title: Similar Match Filter App
4
+ emoji: ⚽️
5
+ colorFrom: green
6
+ colorTo: blue
7
+ sdk: streamlit
8
+ sdk_version: "1.31.1"
9
+ app_file: app.py
 
10
  pinned: false
 
 
11
  ---
12
 
13
+ 유사 배당 구조를 기반으로 정배 방향, 무 위치, 배당대 등을 필터링하여 유사 경기를 분석하고 분포 및 예측 결과를 제공합니다.
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,808 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.isotonic import IsotonicRegression
3
+ from sklearn.preprocessing import StandardScaler # ← NEW
4
+ from sklearn.metrics.pairwise import euclidean_distances
5
+
6
+ from decimal import Decimal, getcontext
7
+ getcontext().prec = 12
8
+
9
+ def decimal_places(x):
10
+ """소수 자릿수 정확 계산(부동소수 오차 방지)"""
11
+ s = f"{float(x):.6f}".rstrip("0").rstrip(".")
12
+ return 0 if "." not in s else len(s.split(".")[1])
13
+
14
+ # ===== (NEW) 무/핸무 순위 마스크 =====
15
+ def _rank12_app26(value, trio):
16
+ vals = [float(x) for x in trio]
17
+ uniq = sorted(set(vals), reverse=True)
18
+ v = float(value)
19
+ if len(uniq) == 0:
20
+ return 0
21
+ if v == uniq[0]:
22
+ return 1
23
+ if len(uniq) > 1 and v == uniq[1]:
24
+ return 2
25
+ return 0
26
+
27
+ class AverageProbaEstimator:
28
+ def __init__(self, models):
29
+ self.models = models
30
+ def fit(self, X, y=None):
31
+ for model in self.models:
32
+ model.fit(X, y)
33
+ return self
34
+ def predict_proba(self, X):
35
+ probas = [model.predict_proba(X) for model in self.models]
36
+ return np.mean(probas, axis=0)
37
+ def predict(self, X):
38
+ return np.argmax(self.predict_proba(X), axis=1)
39
+
40
+ class SoftVotingIsotonicWrapper:
41
+ def __init__(self, models, class_idx=1):
42
+ self.avg_model = AverageProbaEstimator(models)
43
+ self.iso = IsotonicRegression(out_of_bounds="clip")
44
+ self.class_idx = class_idx
45
+ def fit(self, X, y):
46
+ self.avg_model.fit(X, y)
47
+ probs = self.avg_model.predict_proba(X)
48
+ self.iso.fit(probs[:, self.class_idx], y)
49
+ return self
50
+ def predict_proba(self, X):
51
+ probs = self.avg_model.predict_proba(X)
52
+ calibrated = self.iso.predict(probs[:, self.class_idx])
53
+ result = np.zeros_like(probs)
54
+ result[:, self.class_idx] = calibrated
55
+ other = (1 - calibrated) / (probs.shape[1] - 1)
56
+ for i in range(probs.shape[1]):
57
+ if i != self.class_idx:
58
+ result[:, i] = other
59
+ return result
60
+ def predict(self, X):
61
+ return np.argmax(self.predict_proba(X), axis=1)
62
+
63
+
64
+ # ✅ 여기에 기존 import문 추가
65
+ import streamlit as st
66
+ import pandas as pd
67
+ import numpy as np
68
+ import joblib
69
+ from xgboost import XGBClassifier
70
+
71
+ expected_cols = [
72
+ 'norm_win', 'norm_draw', 'norm_lose', 'mean_odds', 'std_odds', 'cv_odds',
73
+ 'p_win', 'p_draw', 'p_lose', 'overround', 'entropy', 'spread', 'spread_draw',
74
+ 'odds_ratio_wd', 'odds_ratio_wl', 'odds_ratio_dl',
75
+ 'draw_prob_ratio', 'draw_ratio', 'draw_prob_gap',
76
+ 'fav_gap', 'fav_draw_gap', 'fav_diff', 'draw_gap_mean',
77
+ 'rank_win', 'rank_draw', 'rank_lose',
78
+ 'p_win_norm', 'p_draw_norm', 'p_lose_norm',
79
+ 'ev_win', 'ev_draw', 'ev_lose',
80
+ 'draw_vs_avg', 'draw_vs_max',
81
+ 'cv_spread', 'cv_draw_gap',
82
+ 'draw_margin','fav_ratio','draw_skew','log_spread','draw_entropy_component','dominance_score'
83
+ ]
84
+
85
+ def generate_features_from_input(win, draw, lose):
86
+ import numpy as np
87
+ import pandas as pd
88
+
89
+ d = {}
90
+ d['norm_win'] = win / (win + draw + lose)
91
+ d['norm_draw'] = draw / (win + draw + lose)
92
+ d['norm_lose'] = lose / (win + draw + lose)
93
+ d['mean_odds'] = np.mean([win, draw, lose])
94
+ d['std_odds'] = np.std([win, draw, lose])
95
+ d['cv_odds'] = d['std_odds'] / d['mean_odds']
96
+ d['p_win'] = 1 / win
97
+ d['p_draw'] = 1 / draw
98
+ d['p_lose'] = 1 / lose
99
+ p_total = d['p_win'] + d['p_draw'] + d['p_lose']
100
+ d['p_win_norm'] = d['p_win'] / p_total
101
+ d['p_draw_norm'] = d['p_draw'] / p_total
102
+ d['p_lose_norm'] = d['p_lose'] / p_total
103
+ d['overround'] = p_total
104
+ d['entropy'] = -sum([d[k] * np.log(d[k]) for k in ['p_win_norm', 'p_draw_norm', 'p_lose_norm']])
105
+ d['spread'] = max(win, draw, lose) - min(win, draw, lose)
106
+ d['spread_draw'] = abs(draw - (win + lose)/2)
107
+ d['odds_ratio_wd'] = win / draw
108
+ d['odds_ratio_wl'] = win / lose
109
+ d['odds_ratio_dl'] = draw / lose
110
+ d['draw_prob_ratio'] = d['p_draw'] / max(d['p_win'], d['p_lose'])
111
+ d['draw_ratio'] = draw / min(win, lose)
112
+ d['draw_prob_gap'] = abs(d['p_draw'] - (d['p_win'] + d['p_lose']) / 2)
113
+ d['fav_gap'] = abs(win - lose)
114
+ d['fav_draw_gap'] = abs(draw - min(win, lose))
115
+ d['fav_diff'] = abs(win - lose)
116
+ d['draw_gap_mean'] = abs(draw - d['mean_odds'])
117
+ d['rank_win'] = sorted([win, draw, lose]).index(win) + 1
118
+ d['rank_draw'] = sorted([win, draw, lose]).index(draw) + 1
119
+ d['rank_lose'] = sorted([win, draw, lose]).index(lose) + 1
120
+ d['ev_win'] = win * d['p_win_norm']
121
+ d['ev_draw'] = draw * d['p_draw_norm']
122
+ d['ev_lose'] = lose * d['p_lose_norm']
123
+ d['draw_vs_avg'] = draw / d['mean_odds']
124
+ d['draw_vs_max'] = draw / max(win, draw, lose)
125
+ d['cv_spread'] = d['spread'] / d['mean_odds']
126
+ d['cv_draw_gap'] = d['fav_draw_gap'] / d['mean_odds']
127
+ d['draw_margin'] = abs(draw - (win + lose)/2)
128
+ d['fav_ratio'] = min(win, lose) / max(win, lose)
129
+ d['draw_skew'] = (draw - win) - (lose - draw)
130
+ d['log_spread'] = np.log(max(win, draw, lose)) - np.log(min(win, draw, lose))
131
+ d['draw_entropy_component'] = -d['p_draw_norm'] * np.log(d['p_draw_norm'])
132
+ d['dominance_score'] = max(d['p_win_norm'], d['p_lose_norm']) - d['p_draw_norm']
133
+ return pd.DataFrame([d])[expected_cols]
134
+
135
+ expected_cols_handicap = [
136
+ 'log_win', 'log_draw', 'log_lose',
137
+ 'log_hwin', 'log_hdraw', 'log_hlose',
138
+ 'pn_win', 'pn_draw', 'pn_lose',
139
+ 'pn_hwin', 'pn_hdraw', 'pn_hlose',
140
+ 'spread_base', 'spread_hand',
141
+ 'mean_odds_h', 'std_odds_h', 'cv_odds_h', 'entropy_h',
142
+ 'ratio_draw_win_h', 'ratio_draw_lose_h',
143
+ 'log_ratio_base_hand', 'gap_hdraw_base_draw',
144
+ 'overround_base', 'overround_hand',
145
+ 'ev_hwin', 'ev_hdraw', 'ev_hlose',
146
+ 'rank_win', 'rank_draw', 'rank_lose'
147
+ ]
148
+
149
+ def generate_similarity_features_for_input(win, draw, lose, hwin, hdraw, hlose):
150
+ p_base = to_probs_from_odds(win, draw, lose) # [pW, pD, pL]
151
+ p_hand = to_probs_from_handicap(hwin, hdraw, hlose) # [pHW, pHD, pHL]
152
+
153
+ # 기존 피처 계산
154
+ spread = max(win, draw, lose) - min(win, draw, lose)
155
+ draw_prob_ratio = (1.0/draw) / max(1.0/win, 1.0/lose)
156
+ entropy_base = entropy_of_probs(p_base)
157
+ entropy_h = entropy_of_probs(p_hand)
158
+ entropy_delta = entropy_h - entropy_base
159
+ overround = (1.0/win + 1.0/draw + 1.0/lose)
160
+ overround_h = (1.0/hwin + 1.0/hdraw + 1.0/hlose)
161
+ overround_diff = overround_h - overround
162
+ prob_entropy_ratio = entropy_h / max(entropy_base, 1e-12)
163
+
164
+ fav_gap = abs(win - lose)
165
+ mean_odds = (win + draw + lose) / 3.0
166
+ cv_spread = spread / mean_odds
167
+ cv_draw_gap = abs(draw - min(win, lose)) / mean_odds
168
+ draw_prob_gap = abs(p_base[1] - (p_base[0] + p_base[2]) / 2.0)
169
+ delta_base_handicap_prob = abs(p_base[0] - p_hand[0]) + abs(p_base[1] - p_hand[1]) + abs(p_base[2] - p_hand[2])
170
+
171
+ p_one_goal = p_hand[1]
172
+ p_large_margin = p_hand[0]
173
+ p_upset = min(p_base[0], p_base[2])
174
+ fav_dog_win_ratio = max(p_base[0], p_base[2]) / max(p_upset, 1e-12)
175
+ herfindahl_base = herfindahl(p_base)
176
+ draw_dev = p_base[1] - (p_base[0] + p_base[2]) / 2.0
177
+
178
+ # 추가: dominance_score
179
+ sorted_probs = sorted(p_base)
180
+ dominance_score = sorted_probs[-1] - sorted_probs[-2]
181
+
182
+ shin_bias = 0.0
183
+ p_draw_shin_delta = 0.0
184
+ if USE_SHIN:
185
+ p_shin, shin_bias = shin_adjusted_probs((win, draw, lose))
186
+ p_draw_shin_delta = p_shin[1] - p_base[1]
187
+
188
+ feats = {
189
+ "spread": spread,
190
+ "draw_prob_ratio": draw_prob_ratio,
191
+ "entropy": entropy_base,
192
+ "overround": overround,
193
+ "fav_gap": fav_gap,
194
+ "cv_spread": cv_spread,
195
+ "cv_draw_gap": cv_draw_gap,
196
+ "draw_prob_gap": draw_prob_gap,
197
+ "p_one_goal": p_one_goal,
198
+ "p_large_margin": p_large_margin,
199
+ "p_upset": p_upset,
200
+ "fav_dog_win_ratio": fav_dog_win_ratio,
201
+ "entropy_h": entropy_h,
202
+ "entropy_delta": entropy_delta,
203
+ "herfindahl_base": herfindahl_base,
204
+ "draw_dev": draw_dev,
205
+ "dominance_score": dominance_score,
206
+ "delta_base_handicap_prob": delta_base_handicap_prob,
207
+ "overround_diff": overround_diff,
208
+ "prob_entropy_ratio": prob_entropy_ratio
209
+ }
210
+ if USE_SHIN:
211
+ feats["shin_bias"] = shin_bias
212
+ feats["p_draw_shin_delta"] = p_draw_shin_delta
213
+
214
+ return feats
215
+
216
+
217
+ # ===== Similarity Features Config =====
218
+ USE_SHIN = False # 처음엔 False로 시작. 느려도 괜찮으면 True로 바꿔 사용.
219
+
220
+ # 유사 경기 거리 전용 피처 셋 (기존 8개 + 신규들)
221
+ KEY_FEATS = [
222
+ # 기존 8개
223
+ "spread", "draw_prob_ratio", "entropy", "overround",
224
+ "fav_gap", "cv_spread", "cv_draw_gap", "draw_prob_gap",
225
+ "delta_base_handicap_prob", "overround_diff", "dominance_score", "prob_entropy_ratio",
226
+
227
+ # 신규 (10개 제안 중 앱에서 바로 계산 가능한 것들)
228
+ "p_one_goal", # 핸디 무 확률 (한 골차 승부)
229
+ "p_large_margin", # 핸디 승 확률 (여유 승)
230
+ "p_upset", # 언더독 승 확률
231
+ "fav_dog_win_ratio", # 강팀 승확률 / 약팀 승확률
232
+ "entropy_h", # 핸디 엔트로피
233
+ "entropy_delta", # (핸디 - 기본) 엔트로피 차이
234
+ "herfindahl_base", # 1X2 확률 집중도(제곱합)
235
+ "draw_dev", # 무확률의 편차 (vs 비무 평균)
236
+ # Shin 관련 (옵션)
237
+ *([ "shin_bias", "p_draw_shin_delta" ] if USE_SHIN else [])
238
+ ]
239
+
240
+ def generate_handicap_features_from_input(win, draw, lose,
241
+ hwin, hdraw, hlose):
242
+ import numpy as np, pandas as pd
243
+
244
+ # --- 로그 배당 ---
245
+ log_win, log_draw, log_lose = np.log([win, draw, lose])
246
+ log_hwin, log_hdraw, log_hlose = np.log([hwin, hdraw, hlose])
247
+
248
+ # --- 역배당 확률 & 정규화 ---
249
+ p_base = 1 / np.array([win, draw, lose])
250
+ pn_win, pn_draw, pn_lose = p_base / p_base.sum()
251
+
252
+ p_hand = 1 / np.array([hwin, hdraw, hlose])
253
+ pn_hwin, pn_hdraw, pn_hlose = p_hand / p_hand.sum()
254
+
255
+ # --- 스프레드·통계 ---
256
+ spread_base = win - lose
257
+ spread_hand = hwin - hlose
258
+ mean_h = np.mean([hwin, hdraw, hlose])
259
+ std_h = np.std([hwin, hdraw, hlose])
260
+ cv_h = std_h / mean_h
261
+ entropy_h = -np.sum([pn_hwin, pn_hdraw, pn_hlose] *
262
+ np.log([pn_hwin, pn_hdraw, pn_hlose]))
263
+
264
+ # --- 비율·차이 ---
265
+ ratio_draw_win_h = hdraw / hwin
266
+ ratio_draw_lose_h = hdraw / hlose
267
+ log_ratio_base_hand = np.log((hdraw + 1e-6) / (draw + 1e-6))
268
+ gap_hdraw_base_draw = hdraw - draw
269
+ overround_base = (1/win + 1/draw + 1/lose) - 1
270
+ overround_hand = (1/hwin + 1/hdraw + 1/hlose) - 1
271
+
272
+ # --- EV ---
273
+ ev_hwin = hwin * pn_hwin
274
+ ev_hdraw = hdraw * pn_hdraw
275
+ ev_hlose = hlose * pn_hlose
276
+
277
+ # --- 순위 ---
278
+ rank_win, rank_draw, rank_lose = pd.Series([win, draw, lose]).rank().tolist()
279
+
280
+ feat = {
281
+ # 6 로그
282
+ 'log_win':log_win, 'log_draw':log_draw, 'log_lose':log_lose,
283
+ 'log_hwin':log_hwin,'log_hdraw':log_hdraw,'log_hlose':log_hlose,
284
+ # 6 확률
285
+ 'pn_win':pn_win,'pn_draw':pn_draw,'pn_lose':pn_lose,
286
+ 'pn_hwin':pn_hwin,'pn_hdraw':pn_hdraw,'pn_hlose':pn_hlose,
287
+ # 6 스프레드·통계
288
+ 'spread_base':spread_base,'spread_hand':spread_hand,
289
+ 'mean_odds_h':mean_h,'std_odds_h':std_h,'cv_odds_h':cv_h,'entropy_h':entropy_h,
290
+ # 6 비율·차이
291
+ 'ratio_draw_win_h':ratio_draw_win_h,'ratio_draw_lose_h':ratio_draw_lose_h,
292
+ 'log_ratio_base_hand':log_ratio_base_hand,'gap_hdraw_base_draw':gap_hdraw_base_draw,
293
+ 'overround_base':overround_base,'overround_hand':overround_hand,
294
+ # 6 EV·순위
295
+ 'ev_hwin':ev_hwin,'ev_hdraw':ev_hdraw,'ev_hlose':ev_hlose,
296
+ 'rank_win':rank_win,'rank_draw':rank_draw,'rank_lose':rank_lose
297
+ }
298
+
299
+ return pd.DataFrame([feat])[expected_cols_handicap]
300
+
301
+ def dec_group(x: float) -> int:
302
+ """
303
+ 2 → 소수 둘째 자리까지 있음
304
+ 1 → 소수 첫째 자리만 있거나 정수
305
+ """
306
+ s = str(x)
307
+ if "." not in s:
308
+ return 1
309
+ return 2 if len(s.split(".")[1]) == 2 else 1
310
+
311
+ def first_decimal_floor(x: float) -> float:
312
+ x = float(x)
313
+ return np.floor(x * 10) / 10.0
314
+
315
+ def decimal_class_by_value(x: float) -> int:
316
+ x = float(x)
317
+ if abs(x*10 - round(x*10)) < 1e-8:
318
+ return 1 # 소수 1자리(또는 정수)
319
+ elif abs(x*100 - round(x*100)) < 1e-6:
320
+ return 2 # 소수 2자리
321
+ else:
322
+ return 2 # 안전하게 2로 취급(표현상 3자리라도 배당은 보통 2자리)
323
+
324
+ def to_probs_from_odds(win, draw, lose):
325
+ p = np.array([1.0/win, 1.0/draw, 1.0/lose], dtype=float)
326
+ return p / p.sum()
327
+
328
+ def to_probs_from_handicap(hwin, hdraw, hlose):
329
+ p = np.array([1.0/hwin, 1.0/hdraw, 1.0/hlose], dtype=float)
330
+ return p / p.sum()
331
+
332
+ def entropy_of_probs(p):
333
+ p = np.clip(p, 1e-12, 1.0)
334
+ return -np.sum(p * np.log(p))
335
+
336
+ def herfindahl(p):
337
+ return np.sum(np.square(p))
338
+
339
+ def shin_adjusted_probs(odds, max_iter=40, tol=1e-9):
340
+ # 간단화한 Shin 추정. USE_SHIN=True일 때만 호출 권장.
341
+ w, d, l = odds
342
+ q = np.array([1.0/w, 1.0/d, 1.0/l], dtype=float)
343
+ lo, hi = 0.0, 0.66
344
+ for _ in range(max_iter):
345
+ z = (lo + hi) / 2.0
346
+ denom = (1 - z)
347
+ p = (np.sqrt(z*z + 4*denom*q) - z) / (2*denom)
348
+ s = p.sum()
349
+ if abs(s - 1.0) < tol:
350
+ break
351
+ if s > 1.0:
352
+ lo = z
353
+ else:
354
+ hi = z
355
+ shin_bias = z
356
+ p = p / p.sum()
357
+ return p, shin_bias
358
+
359
+ def ensure_similarity_features_df(df):
360
+ # base probs
361
+ pW = 1.0 / df["승"].to_numpy()
362
+ pD = 1.0 / df["무"].to_numpy()
363
+ pL = 1.0 / df["패"].to_numpy()
364
+ pt = pW + pD + pL
365
+ pWn, pDn, pLn = pW/pt, pD/pt, pL/pt
366
+
367
+ # hand probs
368
+ pHW = 1.0 / df["핸디 승"].to_numpy()
369
+ pHD = 1.0 / df["핸디 무"].to_numpy()
370
+ pHL = 1.0 / df["핸디 패"].to_numpy()
371
+ pht = pHW + pHD + pHL
372
+ pHWn, pHDn, pHLn = pHW/pht, pHD/pht, pHL/pht
373
+
374
+ mean_odds = (df["승"] + df["무"] + df["패"]) / 3.0
375
+ trio = np.stack([df["승"], df["무"], df["패"]], axis=1)
376
+ spread = trio.max(axis=1) - trio.min(axis=1)
377
+
378
+ # 기존 8개
379
+ df["spread"] = spread
380
+ df["draw_prob_ratio"] = (pD / np.maximum(pW, pL))
381
+
382
+ # 🔒 엔트로피(기본)
383
+ P_base = np.stack([pWn, pDn, pLn], axis=1)
384
+ df["entropy"] = entropy_of_probs(P_base)
385
+
386
+ df["overround"] = pt
387
+ df["fav_gap"] = np.abs(df["승"] - df["패"])
388
+ df["cv_spread"] = spread / mean_odds
389
+ df["cv_draw_gap"] = np.abs(df["무"] - np.minimum(df["승"], df["패"])) / mean_odds
390
+ df["draw_prob_gap"] = np.abs(pDn - (pWn + pLn)/2.0)
391
+
392
+ # 신규 유사도용 피처
393
+ df["p_one_goal"] = pHDn
394
+ df["p_large_margin"] = pHWn
395
+ df["p_upset"] = np.minimum(pWn, pLn)
396
+ df["fav_dog_win_ratio"] = np.maximum(pWn, pLn) / np.maximum(df["p_upset"], 1e-12)
397
+
398
+ # 🔒 엔트로피(핸디)
399
+ P_hand = np.stack([pHWn, pHDn, pHLn], axis=1)
400
+ df["entropy_h"] = entropy_of_probs(P_hand)
401
+
402
+ df["entropy_delta"] = df["entropy_h"] - df["entropy"]
403
+ df["herfindahl_base"] = pWn*pWn + pDn*pDn + pLn*pLn
404
+ df["draw_dev"] = pDn - (pWn + pLn)/2.0
405
+
406
+ # ▶ NEW: 확률 기반 피처 4종
407
+ df["delta_base_handicap_prob"] = (
408
+ np.abs(pWn - pHWn) + np.abs(pDn - pHDn) + np.abs(pLn - pHLn)
409
+ )
410
+ df["overround_diff"] = (pW + pD + pL) - (pHW + pHD + pHL)
411
+
412
+ # ✅ dominance_score 고정
413
+ sorted_probs = np.sort(np.stack([pWn, pDn, pLn], axis=1), axis=1)
414
+ df["dominance_score"] = sorted_probs[:, -1] - sorted_probs[:, -2]
415
+
416
+ df["prob_entropy_ratio"] = df["entropy_h"] / np.maximum(df["entropy"], 1e-6)
417
+
418
+ # ✅ Shin 피처 추가 (선택)
419
+ if USE_SHIN:
420
+ shin_biases = []
421
+ p_draw_shin_deltas = []
422
+ for w, d, l in zip(df["승"], df["무"], df["패"]):
423
+ p_shin, shin_bias = shin_adjusted_probs((w, d, l))
424
+ p_base = to_probs_from_odds(w, d, l)
425
+ shin_biases.append(shin_bias)
426
+ p_draw_shin_deltas.append(p_shin[1] - p_base[1])
427
+ df["shin_bias"] = shin_biases
428
+ df["p_draw_shin_delta"] = p_draw_shin_deltas
429
+
430
+ return df
431
+
432
+ def entropy_of_probs(p):
433
+ """
434
+ p: shape (3,) or (n,3) 확률 벡터(정규화된 값)
435
+ log(0) 방지를 위해 아주 작은 값으로 클리핑 후 엔트로피 계산
436
+ """
437
+ p = np.clip(p, 1e-12, 1.0)
438
+ # 벡터/행렬 모두 처리
439
+ if p.ndim == 1:
440
+ return -np.sum(p * np.log(p))
441
+ else:
442
+ return -np.sum(p * np.log(p), axis=1)
443
+
444
+
445
+ from catboost import CatBoostClassifier
446
+
447
+ @st.cache_resource
448
+ def load_softmax_model():
449
+ return joblib.load("xgb_model_wdl_softmax.pkl")
450
+
451
+ softmax_model = load_softmax_model()
452
+
453
+ # 2) 핸디캡 3‑클래스 새 모델 + 인코더
454
+ import xgboost as xgb
455
+ import joblib
456
+
457
+ @st.cache_resource
458
+ def load_handicap_model():
459
+ # Colab에서 joblib.dump(model_hand, ...) 로 저장한 pkl
460
+ return joblib.load("xgb_model_handicap_30f_fast.pkl")
461
+
462
+ @st.cache_resource
463
+ def load_handicap_encoder():
464
+ """라벨 인코더(pkl) 로드 ─ classes_: ['핸디 승','핸디 무','핸디 패']"""
465
+ return joblib.load("label_encoder_handicap.pkl")
466
+
467
+ handicap_model = load_handicap_model()
468
+ handicap_encoder = load_handicap_encoder()
469
+
470
+
471
+ @st.cache_data
472
+ def load_match_data():
473
+ return pd.read_parquet("proto_core_with_proba_0904_0717.parquet")
474
+
475
+ @st.cache_resource
476
+ def load_keyfeat_scaler(df_for_fit=None):
477
+ from sklearn.preprocessing import StandardScaler
478
+ try:
479
+ sc = joblib.load("keyfeat_scaler.pkl")
480
+ # KEY_FEATS 개수가 바뀌면 자동 재적합
481
+ if hasattr(sc, "n_features_in_") and sc.n_features_in_ != len(KEY_FEATS):
482
+ raise ValueError("KEY_FEATS dimension changed")
483
+ return sc
484
+ except Exception:
485
+ sc = StandardScaler()
486
+ if df_for_fit is None:
487
+ df_for_fit = load_match_data()
488
+ # 유사도용 피처 생성 보장
489
+ df_for_fit = ensure_similarity_features_df(df_for_fit)
490
+ sc.fit(df_for_fit[KEY_FEATS].values)
491
+ # (선택) 다음 실행 속도 향상을 위해 저장
492
+ try:
493
+ joblib.dump(sc, "keyfeat_scaler.pkl")
494
+ except Exception:
495
+ pass
496
+ return sc
497
+
498
+ @st.cache_data
499
+ def get_scaled_matrix(df):
500
+ # 유사도용 피처 생성 보장
501
+ df = ensure_similarity_features_df(df)
502
+ sc = load_keyfeat_scaler(df_for_fit=df)
503
+ return sc, sc.transform(df[KEY_FEATS].values)
504
+ # ================================================
505
+
506
+ def extract_booster(model):
507
+ """
508
+ XGBClassifier 혹은 CalibratedClassifierCV 어디서든
509
+ fit 완료된 booster 를 안전하게 꺼낸다.
510
+ 순서: ① 이미 get_booster() 있으면 → ② calibrated_classifiers_ →
511
+ ③ estimator (prefit일 때) → 오류.
512
+ """
513
+ # ① 순수 XGBClassifier
514
+ if hasattr(model, "get_booster"):
515
+ return model.get_booster()
516
+
517
+ # ② CalibratedClassifierCV(cv>1) ─ fit 된 clone 보유
518
+ if hasattr(model, "calibrated_classifiers_") and model.calibrated_classifiers_:
519
+ est = model.calibrated_classifiers_[0].estimator
520
+ if hasattr(est, "get_booster"):
521
+ return est.get_booster()
522
+
523
+ # ③ CalibratedClassifierCV(cv='prefit')
524
+ if hasattr(model, "estimator") and hasattr(model.estimator, "get_booster"):
525
+ return model.estimator.get_booster()
526
+
527
+ raise AttributeError("Booster를 찾을 수 없습니다 (extract_booster)")
528
+
529
+ # === 예측 함수 ===
530
+ def predict_all(inputs):
531
+ win, draw, lose = inputs["승"], inputs["무"], inputs["패"]
532
+ hwin, hdraw, hlose = inputs["핸디 승"], inputs["핸디 무"], inputs["핸디 패"]
533
+
534
+ # 1. 기본 승무패
535
+ df_input_base = generate_features_from_input(win, draw, lose)
536
+ df_input_base = df_input_base[expected_cols]
537
+ probs_base = softmax_model.predict_proba(df_input_base)[0]
538
+ labels_base = ["승", "무", "패"]
539
+ pred_idx_base = np.argmax(probs_base)
540
+ fav_dir_base = labels_base[pred_idx_base]
541
+ fav_odds_base = [win, draw, lose][pred_idx_base]
542
+ prob_hit_base = probs_base[pred_idx_base]
543
+ ev_base = fav_odds_base * prob_hit_base
544
+
545
+ # 2. 핸디캡 softmax
546
+ # ------------------------------------------------------------------
547
+ # ① 30개 피처 생성 → 컬럼 순서 고정
548
+ # 1) 30 개 피처 DataFrame 1행 생성 ← 이 줄이 다시 필요
549
+ features_h = generate_handicap_features_from_input(win, draw, lose, hwin, hdraw, hlose)
550
+
551
+ # 2) 컬럼 순서 고정
552
+ df_input_handicap = features_h[expected_cols_handicap]
553
+
554
+ # ⬅︎ 새 줄
555
+ probs_handicap = handicap_model.predict_proba(df_input_handicap)[0]
556
+
557
+ # ③ 라벨은 인코더 순서 사용
558
+ labels_handicap = handicap_encoder.classes_ # ★
559
+ # ------------------------------------------------------------------
560
+
561
+ # 라벨‑배당 딕셔너리
562
+ odds_dict = {"핸디 승": hwin, "핸디 무": hdraw, "핸디 패": hlose}
563
+
564
+ # 확률 딕셔너리
565
+ prob_dict = dict(zip(labels_handicap, probs_handicap))
566
+
567
+ # 최고 확률 라벨과 EV
568
+ fav_dir_handicap = max(prob_dict, key=prob_dict.get)
569
+ prob_hit_handicap = prob_dict[fav_dir_handicap]
570
+ fav_odds_handicap = odds_dict[fav_dir_handicap]
571
+ ev_handicap = fav_odds_handicap * prob_hit_handicap
572
+
573
+ return {
574
+ "정배 Softmax": {
575
+ "예측": fav_dir_base,
576
+ "확률": round(prob_hit_base, 4),
577
+ "배당": fav_odds_base,
578
+ "EV": round(ev_base, 4)
579
+ },
580
+ "핸디 Softmax": {
581
+ "예측": fav_dir_handicap,
582
+ "확률": round(prob_hit_handicap, 4),
583
+ "배당": fav_odds_handicap,
584
+ "EV": round(ev_handicap, 4)
585
+ },
586
+ "wdl_probs": probs_base.tolist(),
587
+ "handicap_probs": probs_handicap.tolist()
588
+ }
589
+
590
+
591
+ # === UI ===
592
+ st.title("🎯 통합 예측기 + 유사 경기")
593
+ user_input = st.text_input("배당 입력 (승 무 패 핸디승 핸디무 핸디패):", "1.85/3.2/4.1/2.9/3.2/1.55")
594
+
595
+ if user_input:
596
+ try:
597
+ odds = list(map(float, user_input.replace("/", " ").split()))
598
+ if len(odds) != 6:
599
+ st.error("❌ 정확히 6개의 숫자를 입력해주세요.")
600
+ else:
601
+ inputs = {"승": odds[0], "무": odds[1], "패": odds[2], "핸디 승": odds[3], "핸디 무": odds[4], "핸디 패": odds[5]}
602
+ result = predict_all(inputs)
603
+
604
+ 입력_무_class = decimal_class_by_value(odds[1])
605
+ 입력_핸무_class = decimal_class_by_value(odds[4])
606
+
607
+
608
+ # 🔹 예측 결과 출력
609
+ st.subheader("✅ 예측 결과")
610
+ st.markdown("**🔹 기본 승/무/패 Softmax 확률**")
611
+ labels_base = ["승", "무", "패"]
612
+ for label, prob in zip(labels_base, result.get("wdl_probs", [0, 0, 0])):
613
+ st.write(f" - {label}: {prob * 100:.2f}%")
614
+
615
+ pred = result["정배 Softmax"]
616
+ st.markdown("**🔹 정배 방향 예측 결과**")
617
+ st.write(f" - 예측: **{pred['예측']}**")
618
+ st.write(f" - 확률: **{pred['확률'] * 100:.2f}%**")
619
+ st.write(f" - EV: **{pred['EV']:.3f}**")
620
+
621
+ st.markdown("**🔹 핸디캡 승/무/패 Softmax 확률**")
622
+
623
+ # ⬇️ 1) 하드코드 → 인코더 순서로 변경
624
+ labels_handicap = handicap_encoder.classes_ # 모델·인코더 순서
625
+ probs_handicap = result["handicap_probs"] # 동일 길이 확률 배열
626
+
627
+ # 👇 ① 라벨‑확률 매핑 딕셔너리로 만든 뒤
628
+ prob_map = dict(zip(labels_handicap, probs_handicap))
629
+
630
+ for lbl in ["핸디 승", "핸디 무", "핸디 패"]:
631
+ st.write(f" - {lbl}: {prob_map[lbl] * 100:.2f}%")
632
+
633
+ pred_h = result["핸디 Softmax"]
634
+ st.markdown("**🔹 핸디 정배 방향 예측 결과**")
635
+ st.write(f" - 예측: **{pred_h['예측']}**")
636
+ st.write(f" - 확률: **{pred_h['확률'] * 100:.2f}%**")
637
+ st.write(f" - EV: **{pred_h['EV']:.3f}**")
638
+
639
+ # ================= 유사 경기 (특징 거리 기반 k-NN) =================
640
+ df_all = load_match_data().copy()
641
+ df_all = ensure_similarity_features_df(df_all)
642
+
643
+ # (안전) 숫자형 강제 변환
644
+ for c in ["승","무","패","핸디 승","핸디 무","핸디 패"]:
645
+ df_all[c] = pd.to_numeric(df_all[c], errors="coerce")
646
+
647
+ # 입력 요약
648
+ base_odds = np.array([inputs["승"], inputs["무"], inputs["패"]], dtype=float)
649
+ hand_odds = np.array([inputs["핸디 승"], inputs["핸디 무"], inputs["핸디 패"]], dtype=float)
650
+ base_dir_in = ["승","무","패"][np.argmin(base_odds)]
651
+ hand_dir_in = ["핸디 승","핸디 무","핸디 패"][np.argmin(hand_odds)]
652
+ 정배당, 핸디정배당 = base_odds.min(), hand_odds.min()
653
+ # (NEW) app26 규칙과 같은 무/핸무 순위 라벨
654
+ draw_rank_in = 1 if base_odds[1] == base_odds.max() else 2
655
+ hdraw_rank_in = 1 if hand_odds[1] == hand_odds.max() else 2
656
+
657
+ # ===== (NEW) 무/핸무 순위 라벨 =====
658
+ # base_odds = np.array([inputs["승"], inputs["무"], inputs["패"]], dtype=float)
659
+ # hand_odds = np.array([inputs["핸디 승"], inputs["핸디 무"], inputs["핸디 패"]], dtype=float)
660
+ draw_rank_in = _rank12_app26(base_odds[1], base_odds) # 무의 rank(1 or 2)
661
+ hdraw_rank_in = _rank12_app26(hand_odds[1], hand_odds) # 핸무의 rank(1 or 2)
662
+
663
+ # ===== (NEW) 6개 배당 정수부 =====
664
+ base_ints = {
665
+ "승": int(inputs["승"]),
666
+ "무": int(inputs["무"]),
667
+ "패": int(inputs["패"]),
668
+ "핸디 승": int(inputs["핸디 승"]),
669
+ "핸디 무": int(inputs["핸디 무"]),
670
+ "핸디 패": int(inputs["핸디 패"]),
671
+ }
672
+
673
+ # 1) 특징 스케일/쿼리
674
+ scaler, M_scaled = get_scaled_matrix(df_all)
675
+ qf = generate_similarity_features_for_input(inputs["승"], inputs["무"], inputs["패"],
676
+ inputs["핸디 승"], inputs["핸디 무"], inputs["핸디 패"])
677
+ q_vec = np.array([qf[k] for k in KEY_FEATS], dtype=float).reshape(1, -1)
678
+ q_scaled = scaler.transform(q_vec)[0]
679
+
680
+ # 2) 거리(유클리드) + k 후보 + 거리 상한
681
+ df_all["feat_dist"] = euclidean_distances(M_scaled, q_scaled[None]).ravel()
682
+ N = len(df_all)
683
+ k = min(100, N)
684
+ if k < N:
685
+ top_idx = np.argpartition(df_all["feat_dist"].values, k)[:k]
686
+ mask_knn = pd.Series(False, index=df_all.index); mask_knn.iloc[top_idx] = True
687
+ else:
688
+ mask_knn = pd.Series(True, index=df_all.index)
689
+ # 상위 1.5% 거리만 허용(이상치 컷)
690
+ dist_cut = df_all["feat_dist"].quantile(0.015) if N > 1000 else df_all["feat_dist"].quantile(0.05)
691
+ mask_knn = mask_knn & (df_all["feat_dist"] <= dist_cut)
692
+
693
+ # 3) 1자리 버킷(반올림) 일치
694
+ row_base_min = df_all[["승","무","패"]].min(axis=1)
695
+ row_hand_min = df_all[["핸디 승","핸디 무","핸디 패"]].min(axis=1)
696
+
697
+ # 버킷 경계(내림) 계산
698
+ b_lo = first_decimal_floor(정배당); b_hi = b_lo + 0.1
699
+ h_lo = first_decimal_floor(핸디정배당); h_hi = h_lo + 0.1
700
+
701
+ # “같은 1.x대”를 확실히: [버킷 하한, 하한+0.1) 구간 매칭
702
+ mask_1d = (row_base_min >= b_lo) & (row_base_min < b_hi) & \
703
+ (row_hand_min >= h_lo) & (row_hand_min < h_hi)
704
+
705
+ # 4) 무/핸무 자릿수 정확 일치
706
+ mu_dp_in = decimal_places(inputs["무"])
707
+ hmu_dp_in = decimal_places(inputs["핸디 무"])
708
+ mask_decimals_strict = (df_all["무"].apply(decimal_places) == mu_dp_in) & \
709
+ (df_all["핸디 무"].apply(decimal_places) == hmu_dp_in)
710
+
711
+
712
+ # 6) 방향 일치(+동률 제거)
713
+ df_base_dir = df_all[["승","무","패"]].idxmin(axis=1).str.replace(" ", "", regex=False)
714
+ df_hand_dir = df_all[["핸디 승","핸디 무","핸디 패"]].idxmin(axis=1).str.replace(" ", "", regex=False)
715
+ mask_dir = (df_base_dir == base_dir_in.replace(" ","")) & (df_hand_dir == hand_dir_in.replace(" ",""))
716
+ # 최솟값-2위값 차이가 너무 작은 동률/근접 케이스 제외
717
+ base_sorted = np.sort(df_all[["승","무","패"]].values, axis=1)
718
+ hand_sorted = np.sort(df_all[["핸디 승","핸디 무","핸디 패"]].values, axis=1)
719
+ GAP_TOL = 0.01
720
+ # (옵션) app(26)과 결과 맞추려면 동률 제외를 끕니다.
721
+ # mask_dir = mask_dir & ((base_sorted[:,1]-base_sorted[:,0] >= GAP_TOL) &
722
+ # (hand_sorted[:,1]-hand_sorted[:,0] >= GAP_TOL))
723
+
724
+ # ===== (NEW) 무/핸무 순위 마스크 =====
725
+
726
+ def _row_rank_mask(row):
727
+ try:
728
+ r_base = _rank12_app26(row["무"], [row["승"], row["무"], row["패"]])
729
+ r_hand = _rank12_app26(row["핸디 무"], [row["핸디 승"], row["핸디 무"], row["핸디 패"]])
730
+ return (r_base == draw_rank_in) and (r_hand == hdraw_rank_in)
731
+ except Exception:
732
+ return False
733
+
734
+ mask_rank = df_all.apply(_row_rank_mask, axis=1)
735
+
736
+ # ===== (NEW) 6개 배당 정수부 일치 마스크 =====
737
+ mask_intparts = (
738
+ (df_all["승"].fillna(0).astype(float).astype(int) == base_ints["승"]) &
739
+ (df_all["무"].fillna(0).astype(float).astype(int) == base_ints["무"]) &
740
+ (df_all["패"].fillna(0).astype(float).astype(int) == base_ints["패"]) &
741
+ (df_all["핸디 승"].fillna(0).astype(float).astype(int) == base_ints["핸디 승"]) &
742
+ (df_all["핸디 무"].fillna(0).astype(float).astype(int) == base_ints["핸디 무"]) &
743
+ (df_all["핸디 패"].fillna(0).astype(float).astype(int) == base_ints["핸디 패"])
744
+ ) # ← 여기 닫는 괄호 추가!
745
+
746
+ # 8) 최종 마스크
747
+ final_mask = (
748
+ # mask_knn &
749
+ mask_1d &
750
+ mask_decimals_strict &
751
+ mask_dir &
752
+ mask_rank & # (NEW)
753
+ mask_intparts # (NEW)
754
+ )
755
+
756
+ df_sim = (df_all.loc[final_mask]
757
+ .copy()
758
+ .sort_values("feat_dist")
759
+ .reset_index(drop=True))
760
+
761
+
762
+ # 9) 사후 검증(안전망): 조건 위반행이 남아있으면 마지막으로 드롭
763
+ def _dir_ok(row):
764
+ return (row[["승","무","패"]].idxmin().replace(" ","") == base_dir_in.replace(" ","")) and \
765
+ (row[["핸디 승","핸디 무","핸디 패"]].idxmin().replace(" ","") == hand_dir_in.replace(" ",""))
766
+
767
+ if not df_sim.empty:
768
+ q_row = {
769
+ "mu_dp": decimal_places(inputs["무"]),
770
+ "hmu_dp": decimal_places(inputs["핸디 무"]),
771
+ }
772
+ s_base_min = df_sim[["승","무","패"]].min(axis=1)
773
+ s_hand_min = df_sim[["핸디 승","핸디 무","핸디 패"]].min(axis=1)
774
+
775
+ b_lo = first_decimal_floor(정배당); b_hi = b_lo + 0.1
776
+ h_lo = first_decimal_floor(핸디정배당); h_hi = h_lo + 0.1
777
+
778
+ ok_post = (s_base_min >= b_lo) & (s_base_min < b_hi)
779
+ ok_post &= (s_hand_min >= h_lo) & (s_hand_min < h_hi)
780
+ ok_post &= (df_sim["무"].apply(decimal_places) == q_row["mu_dp"])
781
+ ok_post &= (df_sim["핸디 무"].apply(decimal_places) == q_row["hmu_dp"])
782
+ ok_post &= df_sim.apply(_dir_ok, axis=1)
783
+ df_sim = df_sim.loc[ok_post].reset_index(drop=True)
784
+
785
+
786
+
787
+
788
+
789
+ if "일자" in df_sim.columns:
790
+ df_sim["일자"] = pd.to_datetime(df_sim["일자"], errors="coerce").dt.strftime("%Y-%m-%d")
791
+
792
+ st.subheader(f"✅ 유사 경기 목록 ({len(df_sim)})")
793
+ cols = ["일자","리그","홈팀","원정팀","승","무","패",
794
+ "핸디 승","핸디 무","핸디 패",
795
+ "feat_dist","softmax_dist",
796
+ "결과","핸디결과"]
797
+ cols = [c for c in cols if c in df_sim.columns]
798
+ st.dataframe(df_sim[cols])
799
+
800
+ st.subheader("📊 결과 분포")
801
+ if "결과" in df_sim.columns:
802
+ st.write(df_sim["결과"].value_counts())
803
+ if "핸디결과" in df_sim.columns:
804
+ st.write(df_sim["핸디결과"].value_counts())
805
+
806
+ except Exception as e:
807
+ st.error("❌ 예측 또는 유사 경기 분석 중 오류 발생")
808
+ st.exception(e)
keyfeat_scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bf5c6199e51213f6d7259b8226c96f7defa59bc9ab911a8cb77650e921f008fe
3
+ size 807
label_encoder_handicap.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b6386fff08287a6486fc798dd63c5aae1a4edb31e43cb6e839f51787673f6b4
3
+ size 375
meta.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "train_time": "0904_0717",
3
+ "model_base_path": "/content/models_full_0904_0717/xgb_model_wdl_softmax.pkl",
4
+ "model_hand_path": "/content/models_full_0904_0717/xgb_model_handicap_30f_fast.pkl",
5
+ "encoder_hand_path": "/content/models_full_0904_0717/label_encoder_handicap.pkl",
6
+ "scaler_keyfeat_path": "/content/models_full_0904_0717/keyfeat_scaler.pkl",
7
+ "features_base": [
8
+ "norm_win",
9
+ "norm_draw",
10
+ "norm_lose",
11
+ "mean_odds",
12
+ "std_odds",
13
+ "cv_odds",
14
+ "p_win",
15
+ "p_draw",
16
+ "p_lose",
17
+ "overround",
18
+ "entropy",
19
+ "spread",
20
+ "spread_draw",
21
+ "odds_ratio_wd",
22
+ "odds_ratio_wl",
23
+ "odds_ratio_dl",
24
+ "draw_prob_ratio",
25
+ "draw_ratio",
26
+ "draw_prob_gap",
27
+ "fav_gap",
28
+ "fav_draw_gap",
29
+ "fav_diff",
30
+ "draw_gap_mean",
31
+ "rank_win",
32
+ "rank_draw",
33
+ "rank_lose",
34
+ "p_win_norm",
35
+ "p_draw_norm",
36
+ "p_lose_norm",
37
+ "ev_win",
38
+ "ev_draw",
39
+ "ev_lose",
40
+ "draw_vs_avg",
41
+ "draw_vs_max",
42
+ "cv_spread",
43
+ "cv_draw_gap",
44
+ "draw_margin",
45
+ "fav_ratio",
46
+ "draw_skew",
47
+ "log_spread",
48
+ "draw_entropy_component",
49
+ "dominance_score"
50
+ ],
51
+ "features_hand": [
52
+ "log_win",
53
+ "log_draw",
54
+ "log_lose",
55
+ "log_hwin",
56
+ "log_hdraw",
57
+ "log_hlose",
58
+ "pn_win",
59
+ "pn_draw",
60
+ "pn_lose",
61
+ "pn_hwin",
62
+ "pn_hdraw",
63
+ "pn_hlose",
64
+ "spread_base",
65
+ "spread_hand",
66
+ "mean_odds_h",
67
+ "std_odds_h",
68
+ "cv_odds_h",
69
+ "entropy_h",
70
+ "ratio_draw_win_h",
71
+ "ratio_draw_lose_h",
72
+ "log_ratio_base_hand",
73
+ "gap_hdraw_base_draw",
74
+ "overround_base",
75
+ "overround_hand",
76
+ "ev_hwin",
77
+ "ev_hdraw",
78
+ "ev_hlose",
79
+ "rank_win",
80
+ "rank_draw",
81
+ "rank_lose"
82
+ ],
83
+ "key_feats": [
84
+ "spread",
85
+ "draw_prob_ratio",
86
+ "entropy",
87
+ "overround",
88
+ "fav_gap",
89
+ "cv_spread",
90
+ "cv_draw_gap",
91
+ "draw_prob_gap"
92
+ ],
93
+ "label_map_base": {
94
+ "승": 0,
95
+ "무": 1,
96
+ "패": 2
97
+ },
98
+ "label_order_hand": [
99
+ "핸디 승",
100
+ "핸디 무",
101
+ "핸디 패"
102
+ ]
103
+ }
proto_core_with_proba_0904_0717.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5da433f90a65df133ad81516a373f80c36369e20acfcf8e3e0023d891f12b4a
3
+ size 4519147
proto_core_with_proba_0904_0717.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87e4767b8d37c4a0e215b01da08594086ff00193b8a6e9b15a7419625d18ebc9
3
+ size 16645363
requirements.txt CHANGED
@@ -1,3 +1,7 @@
1
- altair
 
 
 
2
  pandas
 
3
  streamlit
 
1
+ catboost
2
+ scikit-learn==1.3.2 # 훈련 당시 버전과 일치시키는 것이 가장 안전
3
+ xgboost
4
+ numpy
5
  pandas
6
+ openpyxl
7
  streamlit
xgb_model_handicap_30f_fast.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7601022371da860b9e6ee2c3664dabd01ec672953611304728460708bc7bc854
3
+ size 4470563
xgb_model_wdl_softmax.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc0fd1afe41cef97914b2c426d570912e8b6dfa423bf311ac63751e0a0ef21f9
3
+ size 4369923