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.