Files
predictV1/routes/predict.py
mamonov.ep 8a134239d7 Initial commit: добавление проекта predictV1
Включает модели ML для предсказаний, API маршруты, скрипты обучения и данные.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 17:22:58 +03:00

200 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Request
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from catboost import CatBoostClassifier
import pandas as pd
import numpy as np
from routes.predict_bag_of_heroes import predict_bag_of_heroes
from routes.predict_with_players import predict_with_players
router = APIRouter()
# =========================
# Загрузка модели
# =========================
modelPro = CatBoostClassifier()
modelPro.load_model("artifacts/model_from_db_pro_v3.cbm")
# =========================
# Загрузка порядка фич
# =========================
def load_feature_order(path: str) -> list[str]:
fo = pd.read_csv(path)
first_col = fo.columns[0]
return fo[first_col].tolist()
FEATURE_ORDER_PRO: list[str] = load_feature_order("artifacts/feature_order_db.csv")
# =========================
# Дефолты для недостающих фич
# =========================
DEFAULTS: Dict[str, Any] = {
"is_first_pick_radiant": 0,
# Radiant heroes
"r_h1": -1, "r_h2": -1, "r_h3": -1, "r_h4": -1, "r_h5": -1,
# Dire heroes
"d_h1": -1, "d_h2": -1, "d_h3": -1, "d_h4": -1, "d_h5": -1,
# # Radiant players
"r_p1": -1, "r_p2": -1, "r_p3": -1, "r_p4": -1, "r_p5": -1,
# # Dire players
"d_p1": -1, "d_p2": -1, "d_p3": -1, "d_p4": -1, "d_p5": -1,
# Radiant positions
"rp_h1": -1, "rp_h2": -1, "rp_h3": -1, "rp_h4": -1, "rp_h5": -1,
# Dire positions
"dp_h1": -1, "dp_h2": -1, "dp_h3": -1, "dp_h4": -1, "dp_h5": -1,
}
# =========================
# Входная схема
# =========================
class DraftPayload(BaseModel):
# флаг первого пика (0 — Dire first pick/неизвестно, 1 — Radiant first pick)
is_first_pick_radiant: Optional[int] = Field(default=DEFAULTS["is_first_pick_radiant"])
# герои (IDs)
r_h1: Optional[int] = Field(default=DEFAULTS["r_h1"])
r_h2: Optional[int] = Field(default=DEFAULTS["r_h2"])
r_h3: Optional[int] = Field(default=DEFAULTS["r_h3"])
r_h4: Optional[int] = Field(default=DEFAULTS["r_h4"])
r_h5: Optional[int] = Field(default=DEFAULTS["r_h5"])
d_h1: Optional[int] = Field(default=DEFAULTS["d_h1"])
d_h2: Optional[int] = Field(default=DEFAULTS["d_h2"])
d_h3: Optional[int] = Field(default=DEFAULTS["d_h3"])
d_h4: Optional[int] = Field(default=DEFAULTS["d_h4"])
d_h5: Optional[int] = Field(default=DEFAULTS["d_h5"])
# игроки (IDs)
r_p1: Optional[int] = Field(default=DEFAULTS["r_p1"])
r_p2: Optional[int] = Field(default=DEFAULTS["r_p2"])
r_p3: Optional[int] = Field(default=DEFAULTS["r_p3"])
r_p4: Optional[int] = Field(default=DEFAULTS["r_p4"])
r_p5: Optional[int] = Field(default=DEFAULTS["r_p5"])
d_p1: Optional[int] = Field(default=DEFAULTS["d_p1"])
d_p2: Optional[int] = Field(default=DEFAULTS["d_p2"])
d_p3: Optional[int] = Field(default=DEFAULTS["d_p3"])
d_p4: Optional[int] = Field(default=DEFAULTS["d_p4"])
d_p5: Optional[int] = Field(default=DEFAULTS["d_p5"])
# позиции героев (1-5)
rp_h1: Optional[int] = Field(default=DEFAULTS["rp_h1"])
rp_h2: Optional[int] = Field(default=DEFAULTS["rp_h2"])
rp_h3: Optional[int] = Field(default=DEFAULTS["rp_h3"])
rp_h4: Optional[int] = Field(default=DEFAULTS["rp_h4"])
rp_h5: Optional[int] = Field(default=DEFAULTS["rp_h5"])
dp_h1: Optional[int] = Field(default=DEFAULTS["dp_h1"])
dp_h2: Optional[int] = Field(default=DEFAULTS["dp_h2"])
dp_h3: Optional[int] = Field(default=DEFAULTS["dp_h3"])
dp_h4: Optional[int] = Field(default=DEFAULTS["dp_h4"])
dp_h5: Optional[int] = Field(default=DEFAULTS["dp_h5"])
# =========================
# Хелперы
# =========================
def build_long_format_input(payload: dict) -> pd.DataFrame:
"""
Конвертирует payload в hero+position combination features для модели.
Создаёт бинарные признаки вида radiant_h{hero_id}_p{position} и dire_h{hero_id}_p{position}
"""
features = {}
# Инициализируем все признаки нулями
for feat in FEATURE_ORDER_PRO:
features[feat] = 0
# Radiant heroes с позициями
for i in range(1, 6):
hero_id = int(payload.get(f"r_h{i}", -1))
position = int(payload.get(f"rp_h{i}", -1))
if hero_id >= 0 and position >= 0:
feature_name = f"radiant_h{hero_id}_p{position}"
if feature_name in features:
features[feature_name] = 1
# Dire heroes с позициями
for i in range(1, 6):
hero_id = int(payload.get(f"d_h{i}", -1))
position = int(payload.get(f"dp_h{i}", -1))
if hero_id >= 0 and position >= 0:
feature_name = f"dire_h{hero_id}_p{position}"
if feature_name in features:
features[feature_name] = 1
# Создаём DataFrame с одной строкой в правильном порядке
df = pd.DataFrame([features], columns=FEATURE_ORDER_PRO)
return df
def proba_percent(p: float) -> float:
"""Перевод вероятности в проценты (0..100) с отсечкой."""
return round(float(np.clip(p * 100.0, 0.0, 100.0)))
# =========================
# Роут
# =========================
@router.post("/draft/predict")
async def predict(request: Request):
body = await request.json()
# Конвертируем все значения героев, игроков и позиций в int
for key in body:
if key.startswith(("r_h", "d_h", "is_first_pick_radiant")):
if body[key] is not None and body[key] != "":
try:
body[key] = int(body[key])
except (ValueError, TypeError):
body[key] = -1
else:
body[key] = -1
elif key.startswith(("rp_h", "dp_h")):
if body[key] == 0:
body[key] = -1
else:
body[key] = -1
# Hero+position combination предсказание
X_pro = build_long_format_input(body)
# Получаем предсказание для матча (одна строка)
radiant_pro = float(modelPro.predict_proba(X_pro)[0, 1])
rp = proba_percent(radiant_pro)
rd = 100.0 - rp
# Предсказание bag-of-heroes модели
bag_prediction = predict_bag_of_heroes(body)
# Предсказание модели с игроками
players_prediction = predict_with_players(body)
# Предсказание стекинг модели (ленивый импорт для избежания циклической зависимости)
try:
from routes.predict_stacking import predict_stacking
stacking_prediction = predict_stacking(body)
except Exception:
stacking_prediction = {"radiant_win": 50, "dire_win": 50}
return {
"pro-with-pos": {
"radiant_win": rp,
"dire_win": rd
},
"pro": {
"radiant_win": bag_prediction["radiant_win"],
"dire_win": bag_prediction["dire_win"]
},
"with-players": {
"radiant_win": players_prediction["radiant_win"],
"dire_win": players_prediction["dire_win"]
},
"stacking": {
"radiant_win": stacking_prediction["radiant_win"],
"dire_win": stacking_prediction["dire_win"]
}
}