How AI helps automate sports statistics?
Manual tagging of sports statistics is a bottleneck for clubs and studios. Each match requires 8–12 hours of video analyst work, and professional system licenses (e.g., Hawk-Eye) cost tens of thousands of dollars. We develop AI systems that, from standard 2–4 camera video, deliver 80–90% accuracy of professional systems at a fraction of the cost — our solutions are multiple times cheaper. Typical manual tagging errors: missed episodes, subjective possession assessment, distance discrepancy up to 15%. AI avoids these issues: it processes every frame, pinpointing coordinates of all players and the ball. Below — technical details: how we compute metrics and build turnkey solutions.
How AI calculates distance and speed?
For distance, speed, and heatmap calculations we use player tracking from YOLOv8 + ByteTrack. The SportsStatisticsEngine class aggregates per-player data in real time.
import numpy as np
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class PlayerStats:
player_id: int
team: str
distance_m: float = 0.0
sprint_count: int = 0 # sprints > 25 km/h
max_speed_kmh: float = 0.0
touches: int = 0
shots: int = 0
passes_attempted: int = 0
passes_completed: int = 0
heatmap: np.ndarray = field(default_factory=lambda: np.zeros((68, 105)))
class SportsStatisticsEngine:
def __init__(self, fps: float, sport: str = 'football'):
self.fps = fps
self.sport = sport
self.player_stats: dict[int, PlayerStats] = {}
self.possession_log = [] # (frame_idx, team_with_ball)
self.event_log = []
# Speed thresholds (m/frame → km/h)
self.sprint_threshold = 25 / 3.6 / fps # m/frame
self.jogging_threshold = 11 / 3.6 / fps
def update(self, frame_idx: int, tracked_players: list,
ball_pos: Optional[tuple]):
for player in tracked_players:
pid = player['track_id']
team = player.get('team', 'unknown')
pos = player.get('field_pos')
if pid not in self.player_stats:
self.player_stats[pid] = PlayerStats(
player_id=pid, team=team
)
stats = self.player_stats[pid]
# Distance and speed
if hasattr(stats, '_prev_pos') and stats._prev_pos and pos:
dist = np.sqrt((pos[0]-stats._prev_pos[0])**2 +
(pos[1]-stats._prev_pos[1])**2)
stats.distance_m += dist
# Speed in km/h
speed_ms = dist * fps
speed_kmh = speed_ms * 3.6
if speed_kmh > stats.max_speed_kmh:
stats.max_speed_kmh = speed_kmh
if speed_kmh > 25:
stats.sprint_count += 1
stats._prev_pos = pos
# Heatmap
if pos:
hm_x = int(np.clip(pos[0], 0, 104))
hm_y = int(np.clip(pos[1], 0, 67))
stats.heatmap[hm_y, hm_x] += 1
# Ball possession
if ball_pos:
possessing_team = self._determine_possession(
ball_pos, tracked_players
)
self.possession_log.append((frame_idx, possessing_team))
def _determine_possession(self, ball_pos: tuple,
players: list) -> Optional[str]:
"""Closest player to ball = in possession"""
if not players or not ball_pos:
return None
min_dist = float('inf')
possessing_team = None
for player in players:
pos = player.get('field_pos')
if pos is None:
continue
dist = np.sqrt((ball_pos[0]-pos[0])**2 + (ball_pos[1]-pos[1])**2)
if dist < min_dist:
min_dist = dist
possessing_team = player.get('team')
# Player within 2m = in possession
return possessing_team if min_dist < 2.0 else None
def compute_possession_stats(self) -> dict:
total = len(self.possession_log)
if total == 0:
return {'team_a': 50.0, 'team_b': 50.0}
counts = defaultdict(int)
for _, team in self.possession_log:
if team:
counts[team] += 1
contested = total - sum(counts.values())
return {
team: round(count / total * 100, 1)
for team, count in counts.items()
}
def generate_match_report(self) -> dict:
report = {
'possession': self.compute_possession_stats(),
'players': {}
}
for pid, stats in self.player_stats.items():
report['players'][pid] = {
'team': stats.team,
'distance_km': round(stats.distance_m / 1000, 2),
'sprint_count': stats.sprint_count,
'max_speed_kmh': round(stats.max_speed_kmh, 1),
'shots': stats.shots,
'passes_attempted': stats.passes_attempted,
'pass_accuracy': (stats.passes_completed /
max(stats.passes_attempted, 1) * 100)
}
return report
How AI determines ball possession?
The _determine_possession method computes the closest player to the ball on each frame. If the distance is under 2 meters, possession is recorded. This is simpler and faster than geometric methods and yields ±3–5% accuracy relative to Hawk-Eye.
Text reports
After numerical metrics, we generate analytical text via an LLM — e.g., Mistral 7B. The prompt is tuned to sports bulletin format. For implementation we use Hugging Face Transformers.
from transformers import pipeline
class MatchReportGenerator:
def __init__(self):
# GPT or local LLM for text report generation
self.generator = pipeline('text-generation',
model='mistralai/Mistral-7B-Instruct-v0.2',
device=0)
def generate_narrative(self, stats: dict, match_info: dict) -> str:
prompt = f"""Generate a brief analytical match report.
Match: {match_info['team_a']} vs {match_info['team_b']}
Score: {match_info['score']}
Possession: {stats['possession']}
Top runners: {self._top_runners(stats['players'])}
Shots on goal: {sum(p['shots'] for p in stats['players'].values())}
Write a professional analytical text of 3-4 sentences."""
result = self.generator(prompt, max_new_tokens=200,
temperature=0.7, do_sample=True)
return result[0]['generated_text'].split(prompt)[-1].strip()
Why AI statistics is more cost-effective than traditional solutions?
An AI system requires 5–10 times lower investment than a Hawk-Eye license and processes data faster than a human. We have delivered over 15 projects for football clubs and studios, and we guarantee at least 80% accuracy on key metrics. If results fall below, we retrain the model at no cost. AI statistics accuracy compared to professional systems:
| Metric | AI from video | Hawk-Eye/Opta |
|---|---|---|
| Player distance (error) | ±5–8% | ±1–2% |
| Ball possession | ±3–5% | ±1% |
| Shot count | ±10–15% | ±2% |
| Pass detection | ±15–20% | ±3% |
| Maximum speed | ±8–12% | ±2% |
For amateur and semi-professional competitions, this is sufficient. For professional leagues, additional cameras and calibration boost accuracy to acceptable levels.
In one project we deployed a system for a second-tier club: 4 IP cameras 1080p, a server with RTX 4060, training took 2 weeks. After implementation, match analysis time dropped from 10 hours to 30 minutes, and distance accuracy was 87% versus manual tagging. The club saved roughly tens of thousands of dollars per year on video analyst services.
How we guarantee accuracy?
We follow MLOps practices: model versioning via MLflow, A/B testing on real matches, data drift monitoring. Every client receives an accuracy report on their data. Our years of experience and over 15 completed projects let us tackle tasks of any complexity. We guarantee the final accuracy will not fall below the stated level — otherwise we refine the model for free.
Implementation process
- Analytics — assess filming conditions, camera count, metric requirements.
- Design — choose architecture (YOLO + ByteTrack + LLM), prepare data.
- Training and calibration — fine-tune model for the specific stadium, annotate test episodes.
- Integration — deploy API, dashboard, configure webhooks.
- Testing — compare with manual tagging, optimize p99 latency.
- Deployment — install on server or cloud, train operators.
At each stage we use MLOps practices: model versioning via MLflow, A/B testing, data drift monitoring. This allows quick adaptation to changes in filming conditions.
What's included in the project
- Trained detection and tracking model
- REST API for real-time statistics
- Dashboard (metric visualization, heatmaps, reports)
- Integration with existing infrastructure
- Operations and API documentation
- Operator training (2 hours); 3 months technical support
Estimated timelines
| Project type | Timeline |
|---|---|
| Basic statistics (distance + possession + heatmap) | 4–7 weeks |
| Full statistical platform (with text reports and API) | 8–14 weeks |
| Adding a new sport | 2–4 weeks |
Get a consultation for your project — we will assess complexity and accuracy on your data. Order a pilot project and see results in 4 weeks.







