Game State Tracking
Beginner
10 min read
1 views
Nov 27, 2025
# Overview
Track momentum shifts and game state changes through possession patterns, territorial control, and scoring probability.
## Python Implementation
```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import savgol_filter
# Load match events
match_events = pd.read_csv('match_events.csv')
# Calculate momentum score
def calculate_momentum(events, window=5):
events = events.sort_values('minute')
events['momentum_score'] = 0
# Assign momentum values to events
momentum_map = {
'goal': 10,
'shot_on_target': 3,
'shot_off_target': 1,
'corner': 2,
'tackle_won': 1,
'interception': 1,
'pass_completed': 0.1
}
for idx, row in events.iterrows():
team_multiplier = 1 if row['team'] == 'home' else -1
events.at[idx, 'momentum_score'] = momentum_map.get(row['event_type'], 0) * team_multiplier
# Calculate rolling momentum
events['cumulative_momentum'] = events['momentum_score'].rolling(window=window, min_periods=1).sum()
return events
# Calculate momentum for match
match_momentum = calculate_momentum(match_events)
# Smooth momentum curve
smoothed_momentum = savgol_filter(match_momentum['cumulative_momentum'],
window_length=11, polyorder=3)
# Identify momentum shifts
momentum_shifts = []
threshold = 5
for i in range(1, len(smoothed_momentum)):
change = smoothed_momentum[i] - smoothed_momentum[i-1]
if abs(change) > threshold:
momentum_shifts.append({
'minute': match_momentum.iloc[i]['minute'],
'shift': change,
'direction': 'home' if change > 0 else 'away'
})
print(f"Significant Momentum Shifts: {len(momentum_shifts)}")
print(pd.DataFrame(momentum_shifts))
# Visualize momentum
plt.figure(figsize=(14, 6))
plt.plot(match_momentum['minute'], match_momentum['cumulative_momentum'],
alpha=0.3, label='Raw Momentum')
plt.plot(match_momentum['minute'], smoothed_momentum,
linewidth=2, label='Smoothed Momentum')
plt.axhline(y=0, color='black', linestyle='--', alpha=0.5)
plt.fill_between(match_momentum['minute'], smoothed_momentum, 0,
where=(smoothed_momentum > 0), alpha=0.3, color='blue', label='Home Advantage')
plt.fill_between(match_momentum['minute'], smoothed_momentum, 0,
where=(smoothed_momentum < 0), alpha=0.3, color='red', label='Away Advantage')
plt.title('Match Momentum Tracking')
plt.xlabel('Minute')
plt.ylabel('Momentum Score')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
```
## R Implementation
```r
library(dplyr)
library(ggplot2)
library(zoo)
# Load match events
match_events <- read.csv("match_events.csv")
# Calculate momentum
calculate_momentum <- function(events, window = 5) {
events <- events %>% arrange(minute)
# Momentum map
momentum_values <- c(
"goal" = 10,
"shot_on_target" = 3,
"shot_off_target" = 1,
"corner" = 2,
"tackle_won" = 1,
"interception" = 1,
"pass_completed" = 0.1
)
events <- events %>%
mutate(
momentum_score = case_when(
event_type %in% names(momentum_values) ~
momentum_values[event_type] * ifelse(team == "home", 1, -1),
TRUE ~ 0
),
cumulative_momentum = rollsum(momentum_score, k = window,
fill = NA, align = "right")
) %>%
tidyr::fill(cumulative_momentum, .direction = "down")
return(events)
}
# Calculate match momentum
match_momentum <- calculate_momentum(match_events)
# Smooth momentum using moving average
match_momentum <- match_momentum %>%
mutate(smoothed_momentum = rollmean(cumulative_momentum, k = 11,
fill = NA, align = "center")) %>%
tidyr::fill(smoothed_momentum, .direction = "downup")
# Identify momentum shifts
threshold <- 5
momentum_shifts <- match_momentum %>%
mutate(shift = smoothed_momentum - lag(smoothed_momentum, default = 0)) %>%
filter(abs(shift) > threshold) %>%
mutate(direction = ifelse(shift > 0, "home", "away")) %>%
select(minute, shift, direction)
cat(sprintf("Significant Momentum Shifts: %d\n", nrow(momentum_shifts)))
print(momentum_shifts)
# Visualize
ggplot(match_momentum, aes(x = minute)) +
geom_line(aes(y = cumulative_momentum), alpha = 0.3, color = "gray") +
geom_line(aes(y = smoothed_momentum), size = 1.2, color = "black") +
geom_hline(yintercept = 0, linetype = "dashed", alpha = 0.5) +
geom_ribbon(aes(ymin = pmin(smoothed_momentum, 0),
ymax = pmax(smoothed_momentum, 0),
fill = ifelse(smoothed_momentum > 0, "Home", "Away")),
alpha = 0.3) +
scale_fill_manual(values = c("Home" = "blue", "Away" = "red")) +
labs(
title = "Match Momentum Tracking",
x = "Minute",
y = "Momentum Score",
fill = "Advantage"
) +
theme_minimal() +
theme(legend.position = "bottom")
```
Discussion
Have questions or feedback? Join our community discussion on
Discord or
GitHub Discussions.
Table of Contents
Related Topics
Quick Actions