├── convex_hull_animation.gif ├── README.md ├── convex_hull_animation.R └── my_pitch_plot.R /convex_hull_animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KubaMichalczyk/Tracking-Data/HEAD/convex_hull_animation.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A **convex hull** is a geometrical term that can provide quick insight into team spatial positioning. It is defined as the smallest polygon that contains all points of interest and can be used to derive various summary statistics. Primarily, the area of the convex hull can be used to measure how much stretched a team is. In defensive context, we could immediately filter situations where the whole team shaped was damaged by a player who was out of position (either a defender, who is breaking a line or a forward who is not engaged in the defensive activity at the moment). On the other hand, in the offence coaches often ask wingers or wingbacks to position themselves widely in order to create more space in the middle - any failure of execution can be immediately spotted through the use of convex hull. This can simplify the work of Video Analysts. 2 | 3 | ![convex_hull_animation](https://github.com/KubaMichalczyk/Tracking-Data/blob/master/convex_hull_animation.gif) 4 | 5 | Data source: STATS 2018 (the dataset was previously used in [Le et al. 2007](https://arxiv.org/pdf/1703.03121.pdf)) 6 | -------------------------------------------------------------------------------- /convex_hull_animation.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(gganimate) 3 | library(tweenr) 4 | source('my_pitch_plot.R') 5 | 6 | data <- read_csv('csv_files\\sequence_2.csv') 7 | data <- data %>% select(-X1) 8 | names(data) <- c(paste('team A_', rep(1:11, each = 2), '_', c('x', 'y'), sep = ''), 9 | paste('team B_', rep(1:11, each = 2), '_', c('x', 'y'), sep = ''), 10 | paste('ball_0_', c('x', 'y'), sep = '')) 11 | 12 | data <- data %>% 13 | mutate(time = row_number()) %>% 14 | gather(key, value, -time) %>% 15 | extract(key, into = c('team', 'player', 'coordinate'), regex = '(.+)_(.+)_([xy])') %>% 16 | spread(coordinate, value) 17 | 18 | # coordinates are recorded every 0.1 second. To ensure we have a smooth animation we can interpolate the 19 | # coordinates with tweenr package 20 | data <- data %>% 21 | arrange(time, team, player) %>% 22 | unite(group, team, player) %>% 23 | mutate(ease = 'linear') %>% 24 | tween_elements('time', 'group', 'ease', nframes = 2 * max(.$time)) %>% 25 | separate(col = .group, into = c('team', 'player'), sep = '_') 26 | 27 | # calculating convex hull for each team and .frame. I exclude the goalkeepers as the convex hull with 28 | # only outfield players gives a nice visualisation of defence line disposure and therefore is more informative 29 | data_hull <- data %>% 30 | filter(player != 1) %>% 31 | group_by(team, .frame) %>% 32 | nest() %>% 33 | mutate( 34 | hull = map(data, ~ with(.x, chull(x, y))), 35 | out = map2(data, hull, ~ .x[.y,,drop=FALSE]) 36 | ) %>% 37 | select(-data) %>% 38 | unnest() 39 | 40 | p <- pitch_plot(68, 105) + 41 | geom_polygon(data = filter(data_hull, team == 'team A'), aes(x, y, frame = .frame), fill = 'red', alpha = 0.4) + 42 | geom_polygon(data = filter(data_hull, team == 'team B'), aes(x, y, frame = .frame), fill = 'blue', alpha = 0.4) + 43 | geom_point(data = filter(data, str_detect(team, 'team')), aes(x, y, group = player, fill = team, frame = .frame), shape = 21, size = 6, stroke = 2) + 44 | geom_point(data = filter(data, team == 'ball'), aes(x, y, frame = .frame), shape = 21, fill = 'dark orange', size = 4) + 45 | scale_fill_manual(values = c('team A' = 'red', 'team B' = 'blue')) + 46 | geom_text(data = filter(data, str_detect(team, 'team')), aes(x, y, label = player, frame = .frame), color = 'white') + 47 | guides(fill = FALSE) 48 | 49 | animation::ani.options(ani.width = 735, ani.height = 476) 50 | gganimate(p, 'sequence_2.gif', interval = 0.05, title_frame = FALSE) -------------------------------------------------------------------------------- /my_pitch_plot.R: -------------------------------------------------------------------------------- 1 | pitch_plot <- function(width, length, line_color = 'black'){ 2 | library(ggplot2) 3 | 4 | # the middle of a pitch is a (0,0) point 5 | # x refers to coordinate along football pitch length, y - along width 6 | 7 | p <- ggplot() + 8 | geom_rect(aes(xmin = -length/2, xmax = length/2, ymin = -width/2, ymax = width/2), fill = NA, color = line_color) + 9 | # goals 10 | geom_rect(aes(xmin = -length/2 - 1, xmax = -length/2, ymin = -3.66, ymax = 3.66), fill = NA, color = line_color) + 11 | geom_rect(aes(xmin = length/2, xmax = length/2 + 1, ymin = -3.66, ymax = 3.66), fill = NA, color = line_color) + 12 | # penalty areas 13 | geom_rect(aes(xmin = -length/2, xmax = -length/2 + 16.5, ymin = -20.16, ymax = 20.16), fill = NA, color = line_color) + 14 | geom_rect(aes(xmin = length/2 - 16.5, xmax = length/2, ymin = -20.16, ymax = 20.16), fill = NA, color = line_color) + 15 | # goal areas 16 | geom_rect(aes(xmin = -length/2, xmax = -length/2 + 5.5, ymin = -9.16, ymax = 9.16), fill = NA, color = line_color) + 17 | geom_rect(aes(xmin = length/2 - 5.5, xmax = length/2, ymin = -9.16, ymax = 9.16), fill = NA, color = line_color) + 18 | # half-way line 19 | geom_segment(aes(x = 0, xend = 0, y = -width/2, yend = width/2), color = line_color) + 20 | # centre circle 21 | geom_path(data = data.frame(x = c(-9150:(-1)/1000, 1:9150/1000), 22 | y = c(sqrt(9.15^2 - c(-9150:(-1)/1000, 1:9150/1000)^2))), 23 | aes(x, y), color = line_color) + 24 | geom_path(data = data.frame(x = c(-9150:(-1)/1000, 1:9150/1000), 25 | y = -c(sqrt(9.15^2 - c(-9150:(-1)/1000, 1:9150/1000)^2))), 26 | aes(x, y), color = line_color) + 27 | # penalty arcs 28 | geom_path(data = dplyr::filter(data.frame(x = -length/2 + 11 + c(sqrt(9.15^2 - c(-9150:(-1)/1000, 1:9150/1000)^2)), 29 | y = c(-9150:(-1)/1000, 1:9150/1000)), 30 | x > -length/2 + 16.5), 31 | aes(x, y), color = line_color) + 32 | geom_path(data = dplyr::filter(data.frame(x = length/2 - 11 - c(sqrt(9.15^2 - c(-9150:(-1)/1000, 1:9150/1000)^2)), 33 | y = c(-9150:(-1)/1000, 1:9150/1000)), 34 | x < length/2 - 16.5), 35 | aes(x, y), color = line_color) + 36 | # penalty spots and centre spot 37 | geom_point(aes(-length/2 + 11, 0), color = line_color, size = 1) + 38 | geom_point(aes(length/2 - 11, 0), color = line_color, size = 1) + 39 | geom_point(aes(0, 0), color = line_color, size = 1) + 40 | # set completely empty theme 41 | theme_void() 42 | return(p) 43 | } 44 | --------------------------------------------------------------------------------