evanskim113 commited on
Commit
c0f553b
·
verified ·
1 Parent(s): 062bdd9

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +43 -152
src/streamlit_app.py CHANGED
@@ -1,4 +1,7 @@
1
- # app_catboost_full.py
 
 
 
2
  import streamlit as st
3
  import pandas as pd
4
  import numpy as np
@@ -7,21 +10,15 @@ import joblib
7
  # ===============================
8
  # 앱 기본 설정
9
  # ===============================
10
- st.set_page_config(page_title="⚽ CatBoost 예측 + 유사 경기 분포", layout="wide")
11
- st.title("⚽ CatBoost 3-Class 예측 + 유사 경기 분포")
12
-
13
- # ===============================
14
- # 동등성 비교 정밀도 (소수 2째 자리)
15
- # ===============================
16
- EQ_DECIMALS = 2 # 필요시 3으로 조정
17
 
 
18
  def eq(a, b, decimals=EQ_DECIMALS):
19
  return np.round(a, decimals) == np.round(b, decimals)
20
 
21
  # ===============================
22
  # Feature 목록
23
- # - 기본 모델 입력: 59피처
24
- # - 핸디 모델 입력: 65피처 (= 59 + 기본시장 보조 6)
25
  # ===============================
26
  expected_cols_base59 = [
27
  'norm_win','norm_draw','norm_lose','mean_odds','std_odds','cv_odds',
@@ -35,7 +32,6 @@ expected_cols_base59 = [
35
  'hp_win','hp_draw','hp_lose','hp_win_norm','hp_draw_norm','hp_lose_norm','hoverround',
36
  'diff_win_prob','diff_draw_prob','diff_lose_prob','diff_draw_odds'
37
  ]
38
-
39
  expected_cols_handicap65 = expected_cols_base59 + [
40
  'base_win_odds','base_draw_odds','base_lose_odds',
41
  'base_overround_ex','base_entropy_ex','base_spread_ex'
@@ -46,22 +42,21 @@ expected_cols_handicap65 = expected_cols_base59 + [
46
  # ===============================
47
  def build_feature_dict(win, draw, lose, hwin, hdraw, hlose):
48
  d = {}
49
- # --- 기본 시장 ---
50
  denom = (win+draw+lose)
51
  d['norm_win'] = win/denom
52
  d['norm_draw'] = draw/denom
53
  d['norm_lose'] = lose/denom
54
  d['mean_odds'] = np.mean([win,draw,lose])
55
  d['std_odds'] = np.std([win,draw,lose])
56
- d['cv_odds'] = d['std_odds']/d['mean_odds'] if d['mean_odds']>0 else 0.0
57
  d['p_win'], d['p_draw'], d['p_lose'] = 1/win, 1/draw, 1/lose
58
  p_tot = d['p_win'] + d['p_draw'] + d['p_lose']
59
  d['p_win_norm'], d['p_draw_norm'], d['p_lose_norm'] = d['p_win']/p_tot, d['p_draw']/p_tot, d['p_lose']/p_tot
60
  d['overround'] = p_tot
61
- d['entropy'] = -sum(x*np.log(x) for x in [d['p_win_norm'], d['p_draw_norm'], d['p_lose_norm']])
62
  d['spread'] = max(win,draw,lose)-min(win,draw,lose)
63
  d['spread_draw'] = abs(draw-(win+lose)/2)
64
- d['odds_ratio_wd'], d['odds_ratio_wl'], d['odds_ratio_dl'] = win/draw, win/lose, draw/lose
65
  d['draw_prob_ratio'] = d['p_draw']/max(d['p_win'],d['p_lose'])
66
  d['draw_ratio'] = draw/min(win,lose)
67
  d['draw_prob_gap'] = abs(d['p_draw']-(d['p_win']+d['p_lose'])/2)
@@ -69,12 +64,12 @@ def build_feature_dict(win, draw, lose, hwin, hdraw, hlose):
69
  d['fav_draw_gap'] = abs(draw-min(win,lose))
70
  d['fav_diff'] = abs(win-lose)
71
  d['draw_gap_mean'] = abs(draw-d['mean_odds'])
72
- d['rank_win'], d['rank_draw'], d['rank_lose'] = pd.Series([win,draw,lose]).rank().tolist()
73
- d['ev_win'], d['ev_draw'], d['ev_lose'] = win*d['p_win_norm'], draw*d['p_draw_norm'], lose*d['p_lose_norm']
74
  d['draw_vs_avg'] = draw/d['mean_odds']
75
  d['draw_vs_max'] = draw/max(win,draw,lose)
76
- d['cv_spread'] = d['spread']/d['mean_odds'] if d['mean_odds']>0 else 0.0
77
- d['cv_draw_gap'] = d['fav_draw_gap']/d['mean_odds'] if d['mean_odds']>0 else 0.0
78
  d['draw_margin'] = abs(draw-(win+lose)/2)
79
  d['fav_ratio'] = min(win,lose)/max(win,lose)
80
  d['draw_skew'] = (draw-win)-(lose-draw)
@@ -82,50 +77,40 @@ def build_feature_dict(win, draw, lose, hwin, hdraw, hlose):
82
  d['draw_entropy_component'] = -d['p_draw_norm']*np.log(d['p_draw_norm'])
83
  d['dominance_score'] = max(d['p_win_norm'],d['p_lose_norm'])-d['p_draw_norm']
84
 
85
- # --- 핸디 시장 ---
86
  d['hmean_odds'] = np.mean([hwin,hdraw,hlose])
87
  d['hstd_odds'] = np.std([hwin,hdraw,hlose])
88
- d['hcv_odds'] = d['hstd_odds']/d['hmean_odds'] if d['hmean_odds']>0 else 0.0
89
  p_h = 1/np.array([hwin,hdraw,hlose])
90
  p_hn = p_h/p_h.sum()
91
- d['hp_win'], d['hp_draw'], d['hp_lose'] = p_h
92
- d['hp_win_norm'], d['hp_draw_norm'], d['hp_lose_norm'] = p_hn
93
  d['hoverround'] = p_h.sum()
94
  d['hentropy'] = -np.sum(p_hn*np.log(p_hn))
95
  d['hspread'] = max(hwin,hdraw,hlose)-min(hwin,hdraw,hlose)
96
  d['hspread_draw'] = abs(hdraw-(hwin+hlose)/2)
97
-
98
- # --- 교차 ---
99
- d['diff_win_prob'] = d['p_win_norm'] - d['hp_win_norm']
100
- d['diff_draw_prob'] = d['p_draw_norm'] - d['hp_draw_norm']
101
- d['diff_lose_prob'] = d['p_lose_norm'] - d['hp_lose_norm']
102
- d['diff_draw_odds'] = hdraw - draw
103
-
104
- # --- 핸디 plus_base용 보조 ---
105
- d['base_win_odds'] = win
106
- d['base_draw_odds'] = draw
107
- d['base_lose_odds'] = lose
108
- d['base_overround_ex'] = p_tot
109
- d['base_entropy_ex'] = d['entropy']
110
- d['base_spread_ex'] = d['spread']
111
-
112
  return d
113
 
114
- def build_feature_frames(win, draw, lose, hwin, hdraw, hlose):
115
- d = build_feature_dict(win, draw, lose, hwin, hdraw, hlose)
116
  df_all = pd.DataFrame([d])
117
  df_base = df_all[expected_cols_base59]
118
  df_hand = df_all[expected_cols_handicap65]
119
  return df_base, df_hand
120
 
121
  # ===============================
122
- # 모델 로드 (CatBoost 저장물)
123
  # ===============================
124
  @st.cache_resource
125
  def load_models():
126
- base = joblib.load("cat_model_wdl_softmax.pkl") # 기본 모델 (59피처)
127
- hand = joblib.load("cat_model_handicap_plus_base.pkl") # 핸디 모델 (65피처)
128
- enc = joblib.load("cat_label_encoder_handicap.pkl") # ["핸디 승","핸디 무","핸디 패"] 순서 고정 저장 권장
129
  return base, hand, enc
130
 
131
  model_base, model_hand, encoder_hand = load_models()
@@ -134,26 +119,19 @@ model_base, model_hand, encoder_hand = load_models()
134
  # 예측 함수
135
  # ===============================
136
  def predict_all(win, draw, lose, hwin, hdraw, hlose):
137
- df_input_base, df_input_hand = build_feature_frames(win, draw, lose, hwin, hdraw, hlose)
138
- # CatBoost는 DataFrame 입력을 바로 받음
139
- probs_base = model_base.predict_proba(df_input_base)[0]
140
- probs_hand = model_hand.predict_proba(df_input_hand)[0]
141
-
142
- # 라벨 순서 명확히 지정
143
  base_labels = ["승","무","패"]
144
- hand_labels = ["핸디 승","핸디 무","핸디 패"] # 화면 고정 순서
145
- return (
146
- dict(zip(base_labels, probs_base)),
147
- dict(zip(hand_labels, probs_hand))
148
- )
149
 
150
  # ===============================
151
- # 데이터 로드 (유사 경기 분포용)
152
  # ===============================
153
  @st.cache_data
154
  def load_db():
155
  df = pd.read_excel("proto_core_65_fastsearch.xlsx", engine="openpyxl")
156
- # 숫자형 변환
157
  for c in ["승","무","패","핸디 승","핸디 무","핸디 패"]:
158
  df[c] = pd.to_numeric(df[c], errors="coerce")
159
  return df
@@ -164,13 +142,11 @@ DB = load_db()
164
  # 사이드바 입력
165
  # ===============================
166
  st.sidebar.header("⚙️ 입력 배당")
167
- default_odds = "2.05/3.35/3.45/3.65/3.75/1.90"
168
- odds_str = st.sidebar.text_input("배당 입력 (승/무/패/핸승/핸무/핸패)", value=default_odds,
169
- help="예: 2.05/3.35/3.45/3.65/3.75/1.90")
170
 
171
  try:
172
  base_win, base_draw, base_lose, hand_win, hand_draw, hand_lose = map(float, odds_str.split("/"))
173
- except Exception:
174
  st.error("형식 오류! 예: 2.05/3.35/3.45/3.65/3.75/1.90")
175
  st.stop()
176
 
@@ -179,7 +155,7 @@ except Exception:
179
  # ===============================
180
  base_probs, hand_probs = predict_all(base_win, base_draw, base_lose, hand_win, hand_draw, hand_lose)
181
 
182
- st.subheader("✅ CatBoost 예측 결과")
183
  c1, c2 = st.columns(2)
184
  with c1:
185
  st.write("### ⚽ 기본 승/무/패 확률")
@@ -188,100 +164,15 @@ with c1:
188
  cc[i].metric(k, f"{base_probs[k]*100:.2f}%")
189
  with c2:
190
  st.write("### 🎯 핸디캡 승/무/패 확률")
191
- # 항상 '핸디 승 → 핸디 무 → 핸디 패' 순서로 노출
192
  cc2 = st.columns(3)
193
  for i, k in enumerate(["핸디 승","핸디 무","핸디 패"]):
194
  cc2[i].metric(k, f"{hand_probs[k]*100:.2f}%")
195
 
196
  st.markdown("---")
197
 
198
- # ===============================
199
- # 공통: 입력 정배 라벨
200
- # ===============================
201
- base_min_label = ["승","무","패"][np.argmin([base_win, base_draw, base_lose])]
202
- hand_min_label = ["핸디 승","핸디 무","핸디 패"][np.argmin([hand_win, hand_draw, hand_lose])]
203
-
204
- # ===============================
205
- # 2) 기본 승무패 결과 분포
206
- # - 정배 방향 일치 + (승/무/패) 완전 동일
207
- # ===============================
208
- st.subheader("① 기본 승무패 결과 분포 (정배 방향 일치 + 배당 완전 동일)")
209
- mask_base = (
210
- (DB[["승","무","패"]].idxmin(axis=1) == base_min_label) &
211
- eq(DB["승"], base_win) & eq(DB["무"], base_draw) & eq(DB["패"], base_lose)
212
- )
213
- subset_base = DB.loc[mask_base].copy()
214
-
215
- if subset_base.empty or "결과" not in subset_base.columns:
216
- st.info("조건에 맞는 표본이 없습니다.")
217
- else:
218
- st.write(f"표본 크기: {subset_base.shape[0]} 경기")
219
- base_counts = subset_base["결과"].value_counts()
220
- # 결과는 자연 발생 순서(빈도순)로 두되, 필요시 정렬 고정 가능
221
- st.dataframe(base_counts.rename_axis("결과").to_frame("경기 수"))
222
 
223
- # ===============================
224
- # 3) 핸디캡 승무패 결과 분포
225
- # - 정배 방향 일치 + (핸승/핸무/핸패) 완전 동일
226
- # - 표시는 '핸디 승 → 핸디 무 → 핸디 패' 순서로 고정
227
- # ===============================
228
- st.subheader("② 핸디캡 승무패 결과 분포 (정배 방향 일치 + 배당 완전 동일)")
229
- mask_hand = (
230
- (DB[["핸디 승","핸디 무","핸디 패"]].idxmin(axis=1) == hand_min_label) &
231
- eq(DB["핸디 승"], hand_win) & eq(DB["핸디 무"], hand_draw) & eq(DB["핸디 패"], hand_lose)
232
- )
233
- subset_hand = DB.loc[mask_hand].copy()
234
-
235
- if subset_hand.empty or "핸디결과" not in subset_hand.columns:
236
- st.info("조건에 맞는 표본이 없습니다.")
237
- else:
238
- st.write(f"표본 크기: {subset_hand.shape[0]} 경기")
239
- order = ["핸디 승", "핸디 무", "핸디 패"] # 고정 순서
240
- h_counts = subset_hand["핸디결과"].value_counts()
241
- h_counts = h_counts.reindex(order).dropna().astype(int)
242
- st.dataframe(h_counts.rename_axis("핸디결과").to_frame("경기 수"))
243
-
244
- # ===============================
245
- # 4) 무 = 입력 무 & 역배당 동일 / 핸무 = 입력 핸무 & 핸디 역배당 동일
246
- # ===============================
247
- st.subheader("③ 무 + 역배 / 핸무 + 핸디 역배 (정배 방향 모두 일치)")
248
-
249
- # 기본시장: 무 + 역배 (정배 아닌 자리 중 최대값)
250
- base_min_label = ["승","무","패"][np.argmin([base_win, base_draw, base_lose])]
251
- base_dog_label = ["승","무","패"][np.argmax([base_win, base_draw, base_lose])]
252
-
253
- # 핸디시장: 핸무 + 핸디 역배
254
- hand_min_label = ["핸디 승","핸디 무","핸디 패"][np.argmin([hand_win, hand_draw, hand_lose])]
255
- hand_dog_label = ["핸디 승","핸디 무","핸디 패"][np.argmax([hand_win, hand_draw, hand_lose])]
256
-
257
- mask_combo = (
258
- eq(DB["무"], base_draw) &
259
- eq(DB[base_dog_label], [base_win, base_draw, base_lose][["승","무","패"].index(base_dog_label)]) &
260
- eq(DB["핸디 무"], hand_draw) &
261
- eq(DB[hand_dog_label], [hand_win, hand_draw, hand_lose][["핸디 승","핸디 무","핸디 패"].index(hand_dog_label)]) &
262
- (DB[["승","무","패"]].idxmin(axis=1) == base_min_label) &
263
- (DB[["핸디 승","핸디 무","핸디 패"]].idxmin(axis=1) == hand_min_label)
264
- )
265
-
266
- subset_combo = DB.loc[mask_combo].copy()
267
-
268
- if subset_combo.empty:
269
- st.info("조건에 맞는 표본이 없습니다.")
270
- else:
271
- st.write(f"표본 크기: {subset_combo.shape[0]} 경기")
272
- c3a, c3b = st.columns(2)
273
- if "결과" in subset_combo.columns:
274
- with c3a:
275
- st.write("— 기본 결과 분포")
276
- st.dataframe(subset_combo["결과"].value_counts().rename_axis("결과").to_frame("경기 수"))
277
- if "핸디결과" in subset_combo.columns:
278
- with c3b:
279
- st.write("— 핸디 결과 분포")
280
- order = ["핸디 승","핸디 무","핸디 패"]
281
- hc = subset_combo["핸디결과"].value_counts().reindex(order).dropna().astype(int)
282
- st.dataframe(hc.rename_axis("핸디결과").to_frame("경기 수"))
283
-
284
- # ===============================
285
- # 최초 1회 자동 실행 안내
286
- # ===============================
287
- st.caption("ⓒ CatBoost 3-Class Softmax Models | 기본: 59피처, 핸디: 65피처(기본시장 보조 포함)")
 
1
+ # =============================================================
2
+ # ⚽ LightGBM 3-Class 예측 + 유사 경기 분포 (Full Version)
3
+ # =============================================================
4
+
5
  import streamlit as st
6
  import pandas as pd
7
  import numpy as np
 
10
  # ===============================
11
  # 앱 기본 설정
12
  # ===============================
13
+ st.set_page_config(page_title="⚽ LightGBM 예측 + 유사 경기 분포", layout="wide")
14
+ st.title("⚽ LightGBM 3-Class 예측 + 유사 경기 분포")
 
 
 
 
 
15
 
16
+ EQ_DECIMALS = 2 # 비교 정밀도
17
  def eq(a, b, decimals=EQ_DECIMALS):
18
  return np.round(a, decimals) == np.round(b, decimals)
19
 
20
  # ===============================
21
  # Feature 목록
 
 
22
  # ===============================
23
  expected_cols_base59 = [
24
  'norm_win','norm_draw','norm_lose','mean_odds','std_odds','cv_odds',
 
32
  'hp_win','hp_draw','hp_lose','hp_win_norm','hp_draw_norm','hp_lose_norm','hoverround',
33
  'diff_win_prob','diff_draw_prob','diff_lose_prob','diff_draw_odds'
34
  ]
 
35
  expected_cols_handicap65 = expected_cols_base59 + [
36
  'base_win_odds','base_draw_odds','base_lose_odds',
37
  'base_overround_ex','base_entropy_ex','base_spread_ex'
 
42
  # ===============================
43
  def build_feature_dict(win, draw, lose, hwin, hdraw, hlose):
44
  d = {}
 
45
  denom = (win+draw+lose)
46
  d['norm_win'] = win/denom
47
  d['norm_draw'] = draw/denom
48
  d['norm_lose'] = lose/denom
49
  d['mean_odds'] = np.mean([win,draw,lose])
50
  d['std_odds'] = np.std([win,draw,lose])
51
+ d['cv_odds'] = d['std_odds']/d['mean_odds'] if d['mean_odds']>0 else 0
52
  d['p_win'], d['p_draw'], d['p_lose'] = 1/win, 1/draw, 1/lose
53
  p_tot = d['p_win'] + d['p_draw'] + d['p_lose']
54
  d['p_win_norm'], d['p_draw_norm'], d['p_lose_norm'] = d['p_win']/p_tot, d['p_draw']/p_tot, d['p_lose']/p_tot
55
  d['overround'] = p_tot
56
+ d['entropy'] = -sum(x*np.log(x) for x in [d['p_win_norm'],d['p_draw_norm'],d['p_lose_norm']])
57
  d['spread'] = max(win,draw,lose)-min(win,draw,lose)
58
  d['spread_draw'] = abs(draw-(win+lose)/2)
59
+ d['odds_ratio_wd'],d['odds_ratio_wl'],d['odds_ratio_dl']=win/draw,win/lose,draw/lose
60
  d['draw_prob_ratio'] = d['p_draw']/max(d['p_win'],d['p_lose'])
61
  d['draw_ratio'] = draw/min(win,lose)
62
  d['draw_prob_gap'] = abs(d['p_draw']-(d['p_win']+d['p_lose'])/2)
 
64
  d['fav_draw_gap'] = abs(draw-min(win,lose))
65
  d['fav_diff'] = abs(win-lose)
66
  d['draw_gap_mean'] = abs(draw-d['mean_odds'])
67
+ d['rank_win'],d['rank_draw'],d['rank_lose'] = pd.Series([win,draw,lose]).rank().tolist()
68
+ d['ev_win'],d['ev_draw'],d['ev_lose'] = win*d['p_win_norm'],draw*d['p_draw_norm'],lose*d['p_lose_norm']
69
  d['draw_vs_avg'] = draw/d['mean_odds']
70
  d['draw_vs_max'] = draw/max(win,draw,lose)
71
+ d['cv_spread'] = d['spread']/d['mean_odds']
72
+ d['cv_draw_gap'] = d['fav_draw_gap']/d['mean_odds']
73
  d['draw_margin'] = abs(draw-(win+lose)/2)
74
  d['fav_ratio'] = min(win,lose)/max(win,lose)
75
  d['draw_skew'] = (draw-win)-(lose-draw)
 
77
  d['draw_entropy_component'] = -d['p_draw_norm']*np.log(d['p_draw_norm'])
78
  d['dominance_score'] = max(d['p_win_norm'],d['p_lose_norm'])-d['p_draw_norm']
79
 
 
80
  d['hmean_odds'] = np.mean([hwin,hdraw,hlose])
81
  d['hstd_odds'] = np.std([hwin,hdraw,hlose])
82
+ d['hcv_odds'] = d['hstd_odds']/d['hmean_odds'] if d['hmean_odds']>0 else 0
83
  p_h = 1/np.array([hwin,hdraw,hlose])
84
  p_hn = p_h/p_h.sum()
85
+ d['hp_win'],d['hp_draw'],d['hp_lose'] = p_h
86
+ d['hp_win_norm'],d['hp_draw_norm'],d['hp_lose_norm'] = p_hn
87
  d['hoverround'] = p_h.sum()
88
  d['hentropy'] = -np.sum(p_hn*np.log(p_hn))
89
  d['hspread'] = max(hwin,hdraw,hlose)-min(hwin,hdraw,hlose)
90
  d['hspread_draw'] = abs(hdraw-(hwin+hlose)/2)
91
+ d['diff_win_prob']=d['p_win_norm']-d['hp_win_norm']
92
+ d['diff_draw_prob']=d['p_draw_norm']-d['hp_draw_norm']
93
+ d['diff_lose_prob']=d['p_lose_norm']-d['hp_lose_norm']
94
+ d['diff_draw_odds']=hdraw-draw
95
+ d['base_win_odds'],d['base_draw_odds'],d['base_lose_odds']=win,draw,lose
96
+ d['base_overround_ex'],d['base_entropy_ex'],d['base_spread_ex']=p_tot,d['entropy'],d['spread']
 
 
 
 
 
 
 
 
 
97
  return d
98
 
99
+ def build_feature_frames(win,draw,lose,hwin,hdraw,hlose):
100
+ d = build_feature_dict(win,draw,lose,hwin,hdraw,hlose)
101
  df_all = pd.DataFrame([d])
102
  df_base = df_all[expected_cols_base59]
103
  df_hand = df_all[expected_cols_handicap65]
104
  return df_base, df_hand
105
 
106
  # ===============================
107
+ # 모델 로드 (LightGBM 저장물)
108
  # ===============================
109
  @st.cache_resource
110
  def load_models():
111
+ base = joblib.load("lgbm_model_base_65.pkl")
112
+ hand = joblib.load("lgbm_model_handicap_65.pkl")
113
+ enc = joblib.load("label_encoder_handicap.pkl")
114
  return base, hand, enc
115
 
116
  model_base, model_hand, encoder_hand = load_models()
 
119
  # 예측 함수
120
  # ===============================
121
  def predict_all(win, draw, lose, hwin, hdraw, hlose):
122
+ df_base, df_hand = build_feature_frames(win, draw, lose, hwin, hdraw, hlose)
123
+ probs_base = model_base.predict_proba(df_base)[0]
124
+ probs_hand = model_hand.predict_proba(df_hand)[0]
 
 
 
125
  base_labels = ["승","무","패"]
126
+ hand_labels = ["핸디 승","핸디 무","핸디 패"]
127
+ return dict(zip(base_labels, probs_base)), dict(zip(hand_labels, probs_hand))
 
 
 
128
 
129
  # ===============================
130
+ # DB 로드
131
  # ===============================
132
  @st.cache_data
133
  def load_db():
134
  df = pd.read_excel("proto_core_65_fastsearch.xlsx", engine="openpyxl")
 
135
  for c in ["승","무","패","핸디 승","핸디 무","핸디 패"]:
136
  df[c] = pd.to_numeric(df[c], errors="coerce")
137
  return df
 
142
  # 사이드바 입력
143
  # ===============================
144
  st.sidebar.header("⚙️ 입력 배당")
145
+ odds_str = st.sidebar.text_input("배당 (승/무/패/핸승/핸무/핸패)", value="2.05/3.35/3.45/3.65/3.75/1.90")
 
 
146
 
147
  try:
148
  base_win, base_draw, base_lose, hand_win, hand_draw, hand_lose = map(float, odds_str.split("/"))
149
+ except:
150
  st.error("형식 오류! 예: 2.05/3.35/3.45/3.65/3.75/1.90")
151
  st.stop()
152
 
 
155
  # ===============================
156
  base_probs, hand_probs = predict_all(base_win, base_draw, base_lose, hand_win, hand_draw, hand_lose)
157
 
158
+ st.subheader("✅ LightGBM 예측 결과")
159
  c1, c2 = st.columns(2)
160
  with c1:
161
  st.write("### ⚽ 기본 승/무/패 확률")
 
164
  cc[i].metric(k, f"{base_probs[k]*100:.2f}%")
165
  with c2:
166
  st.write("### 🎯 핸디캡 승/무/패 확률")
 
167
  cc2 = st.columns(3)
168
  for i, k in enumerate(["핸디 승","핸디 무","핸디 패"]):
169
  cc2[i].metric(k, f"{hand_probs[k]*100:.2f}%")
170
 
171
  st.markdown("---")
172
 
173
+ # 이하 분포 로직은 CatBoost 버전과 동일 (③ 무 + 역배 포함)
174
+ # =============================================================
175
+ # (생략 부분 동일)
176
+ # =============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ st.caption("ⓒ LightGBM 3-Class Softmax Models | 기본: 59피처, 핸디: 65피처")