AI-анализ тактики и стратегии командных спортивных игр
Тактический анализ — самая сложная часть спортивной аналитики. Распознать, что команда играет в 4-3-3 или использует высокий прессинг — это не детекция объектов, а паттерн-матчинг на временных последовательностях позиций 11 игроков.
Распознавание схемы расстановки
import numpy as np
from sklearn.cluster import KMeans
from scipy.spatial import ConvexHull
from typing import Optional
class FormationAnalyzer:
"""
Схема расстановки определяется по медианным позициям игроков
за период без мяча (когда команда организована).
"""
KNOWN_FORMATIONS = {
'4-3-3': [[0.15, 0.5], [0.3, 0.15], [0.3, 0.38], [0.3, 0.62], [0.3, 0.85],
[0.55, 0.3], [0.55, 0.5], [0.55, 0.7],
[0.75, 0.2], [0.75, 0.5], [0.75, 0.8]],
'4-4-2': [[0.15, 0.5], [0.3, 0.15], [0.3, 0.38], [0.3, 0.62], [0.3, 0.85],
[0.55, 0.2], [0.55, 0.4], [0.55, 0.6], [0.55, 0.8],
[0.75, 0.35], [0.75, 0.65]],
'3-5-2': [[0.15, 0.5], [0.3, 0.25], [0.3, 0.5], [0.3, 0.75],
[0.5, 0.1], [0.5, 0.3], [0.5, 0.5], [0.5, 0.7], [0.5, 0.9],
[0.75, 0.35], [0.75, 0.65]],
}
def __init__(self, field_size: tuple = (105, 68)):
self.field_w, self.field_h = field_size
def detect_formation(self, player_positions: list,
frames_window: int = 300) -> dict:
"""
player_positions: list of dicts {player_id, field_pos, team}
Используем только позиции «без мяча» (команда организована).
"""
if len(player_positions) < 5:
return {'formation': 'unknown', 'confidence': 0}
# Медианная позиция каждого игрока за окно
positions_by_player = {}
for record in player_positions[-frames_window:]:
pid = record['player_id']
pos = record['field_pos']
if pos:
positions_by_player.setdefault(pid, []).append(pos)
median_positions = []
for pid, positions in positions_by_player.items():
if len(positions) > 10: # минимум данных для оценки
median_pos = np.median(positions, axis=0)
median_positions.append(median_pos)
if len(median_positions) < 8:
return {'formation': 'unknown', 'confidence': 0}
# Нормализуем позиции [0..1]
norm_positions = [[p[0] / self.field_w, p[1] / self.field_h]
for p in median_positions[:11]]
# Сортируем по x (глубина поля)
norm_positions.sort(key=lambda p: p[0])
norm_positions = norm_positions[1:] # убираем вратаря
# Сравниваем с эталонными схемами
best_match = 'unknown'
best_score = float('inf')
for formation_name, template in self.KNOWN_FORMATIONS.items():
template_sorted = sorted(template, key=lambda p: p[0])[1:]
score = self._alignment_score(norm_positions, template_sorted)
if score < best_score:
best_score = score
best_match = formation_name
confidence = max(0, 1 - best_score / 2)
return {
'formation': best_match,
'confidence': confidence,
'player_median_positions': norm_positions
}
def _alignment_score(self, positions: list, template: list) -> float:
"""Минимальная сумма расстояний между позициями и шаблоном (assignment problem)"""
from scipy.optimize import linear_sum_assignment
n = min(len(positions), len(template))
cost_matrix = np.zeros((n, n))
for i, pos in enumerate(positions[:n]):
for j, tmpl in enumerate(template[:n]):
cost_matrix[i, j] = np.sqrt((pos[0]-tmpl[0])**2 + (pos[1]-tmpl[1])**2)
row_ind, col_ind = linear_sum_assignment(cost_matrix)
return float(cost_matrix[row_ind, col_ind].mean())
Детекция прессинга и линий обороны
class TacticalPatternDetector:
def detect_high_press(self, team_positions: list,
opponent_ball_pos: tuple,
field_height: float = 68) -> dict:
"""
Высокий прессинг: большинство игроков на чужой половине поля.
PPDA (Passes Per Defensive Action) — стандартная метрика прессинга.
"""
if not team_positions:
return {'pressing': False}
# Игроки на чужой половине (x > 52.5 для атаки слева направо)
half_line = field_height / 2
players_in_opp_half = sum(
1 for p in team_positions
if p.get('field_pos') and p['field_pos'][0] > 52.5
)
pressing_intensity = players_in_opp_half / max(len(team_positions), 1)
# Компактность: ширина и длина блока обороны
positions = [p['field_pos'] for p in team_positions
if p.get('field_pos')]
if positions:
xs = [p[0] for p in positions]
ys = [p[1] for p in positions]
block_depth = max(xs) - min(xs)
block_width = max(ys) - min(ys)
else:
block_depth = block_width = 0
return {
'pressing': pressing_intensity > 0.6,
'pressing_intensity': pressing_intensity,
'players_in_opp_half': players_in_opp_half,
'block_depth_m': block_depth,
'block_width_m': block_width
}
def compute_defensive_line_height(self,
defensive_players: list) -> Optional[float]:
"""Высота линии обороны в метрах от своих ворот"""
if not defensive_players:
return None
positions = [p['field_pos'][0] for p in defensive_players
if p.get('field_pos')]
if not positions:
return None
# Линия обороны = медиана глубинной позиции 4 защитников
return float(np.median(sorted(positions)[:4]))
Кейс: анализ 38 туров чемпионата
Задача аналитического отдела клуба: автоматически строить тактические карты по каждому матчу сезона. Ранее вручную: 6–8 часов на матч.
Система обрабатывала 90-минутные трансляции (видео 1080p@25fps):
- Детекция расстановки: точность 84% совпадений с экспертной оценкой
- Время обработки одного матча: 22 минуты (RTX 3090)
- Автоматически генерировались: тепловые карты по зонам, средние позиции, фазы прессинга с таймкодами
| Тактический показатель | Точность AI | Эталон (эксперт) |
|---|---|---|
| Определение схемы расстановки | 84% | — |
| Детекция высокого прессинга | 81% | — |
| Линия обороны (ошибка ±м) | ±3.2м | ±1.5м |
| Тип проекта | Срок |
|---|---|
| Расстановка + тепловые карты | 6–10 недель |
| Полная тактическая аналитика | 12–18 недель |







