├── .Rbuildignore ├── .gitignore ├── .idea └── .gitignore ├── DESCRIPTION ├── NAMESPACE ├── R ├── package.R ├── soccerFlipDirection.R ├── soccerFlow.R ├── soccerHeatmap.R ├── soccerPassmap.R ├── soccerPath.R ├── soccerPitch.R ├── soccerPitchBG.R ├── soccerPitchFG.R ├── soccerPitchHalf.R ├── soccerPositionMap.R ├── soccerResample.R ├── soccerShortenName.R ├── soccerShotmap.R ├── soccerSpokes.R ├── soccerStandardiseCols.R ├── soccerTransform.R ├── soccerVelocity.R ├── soccermatics-deprecated.R ├── soccerxGTimeline.R ├── statsbomb.R ├── tromso.R └── tromso_extra.R ├── README.md ├── data ├── statsbomb.rda ├── tromso.rda └── tromso_extra.rda ├── man ├── soccerFlipDirection.Rd ├── soccerFlow.Rd ├── soccerHeatmap.Rd ├── soccerPassmap.Rd ├── soccerPath.Rd ├── soccerPitch.Rd ├── soccerPitchFG.Rd ├── soccerPitchHalf.Rd ├── soccerPositionMap.Rd ├── soccerResample.Rd ├── soccerShortenName.Rd ├── soccerShotmap.Rd ├── soccerSpokes.Rd ├── soccerStandardiseCols.Rd ├── soccerTransform.Rd ├── soccerVelocity.Rd ├── soccermatics-deprecated.Rd ├── soccerxGTimeline.Rd ├── statsbomb.Rd ├── tromso.Rd └── tromso_extra.Rd ├── pitch_dimensions.csv ├── soccermatics.Rproj └── soccermatics_0.9.5.pdf /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | .Rbuildignore 6 | soccermatics.Rproj 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /.idea/* 3 | 4 | /shelf/ 5 | /workspace.xml 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: soccermatics 2 | Version: 0.9.5 3 | Authors@R: person("Joe", "Gallagher", email = "joedgallagher@gmail.com", role = c("aut", "cre")) 4 | Title: Visualise football (soccer) tracking and event data 5 | Description: Provides tools to visualise x,y-coordinates of soccer players and event data (e.g. passes, shots). Uses ggplot to draw soccer pitch and overplot expected goal maps, pass maps, average player positions, player heatmaps, individual player paths, player flow fields, and more. 6 | Depends: R (>= 3.4.1) 7 | Imports: cowplot, dplyr, forcats, ggforce, ggplot2, ggrepel, magrittr, MASS, plyr, rlang, scales, tidyr, xts, zoo 8 | License: GPL (>=3.0) Note: Use of the name 'soccermatics' was kindly permitted by David Sumpter and is protected from commercial use under EU copyright law. 9 | Encoding: UTF-8 10 | Collate: 11 | 'package.R' 12 | 'soccerFlipDirection.R' 13 | 'soccerPitch.R' 14 | 'soccerHeatmap.R' 15 | 'soccerSpokes.R' 16 | 'soccerFlow.R' 17 | 'soccerPassmap.R' 18 | 'soccerPath.R' 19 | 'soccerPitchBG.R' 20 | 'soccerPitchFG.R' 21 | 'soccerShotmap.R' 22 | 'soccerPitchHalf.R' 23 | 'soccerPositionMap.R' 24 | 'soccerResample.R' 25 | 'soccerShortenName.R' 26 | 'soccerStandardiseCols.R' 27 | 'soccerTransform.R' 28 | 'soccerVelocity.R' 29 | 'soccermatics-deprecated.R' 30 | 'soccerxGTimeline.R' 31 | 'statsbomb.R' 32 | 'tromso.R' 33 | 'tromso_extra.R' 34 | RoxygenNote: 7.1.1 35 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export("%>%") 4 | export(soccerFlipDirection) 5 | export(soccerFlow) 6 | export(soccerHeatmap) 7 | export(soccerPassmap) 8 | export(soccerPath) 9 | export(soccerPitch) 10 | export(soccerPitchBG) 11 | export(soccerPitchFG) 12 | export(soccerPitchHalf) 13 | export(soccerPositionMap) 14 | export(soccerResample) 15 | export(soccerShortenName) 16 | export(soccerShotmap) 17 | export(soccerSpokes) 18 | export(soccerStandardiseCols) 19 | export(soccerTransform) 20 | export(soccerVelocity) 21 | export(soccerxGTimeline) 22 | import(dplyr) 23 | import(ggplot2) 24 | importFrom(MASS,kde2d) 25 | importFrom(cowplot,draw_text) 26 | importFrom(dplyr,filter) 27 | importFrom(forcats,fct_explicit_na) 28 | importFrom(ggforce,geom_arc) 29 | importFrom(ggforce,geom_circle) 30 | importFrom(ggrepel,geom_label_repel) 31 | importFrom(ggrepel,geom_text_repel) 32 | importFrom(magrittr,"%>%") 33 | importFrom(plyr,rbind.fill) 34 | importFrom(rlang,"!!") 35 | importFrom(rlang,":=") 36 | importFrom(rlang,enquo) 37 | importFrom(scales,rescale) 38 | importFrom(tidyr,replace_na) 39 | importFrom(xts,xts) 40 | importFrom(zoo,na.approx) 41 | -------------------------------------------------------------------------------- /R/package.R: -------------------------------------------------------------------------------- 1 | #' @importFrom magrittr %>% 2 | #' @export %>% 3 | NULL -------------------------------------------------------------------------------- /R/soccerFlipDirection.R: -------------------------------------------------------------------------------- 1 | #' @import ggplot2 2 | #' @import dplyr 3 | #' @importFrom magrittr "%>%" 4 | #' @importFrom ggforce geom_arc geom_circle 5 | #' @importFrom rlang enquo ":=" "!!" 6 | NULL 7 | #' Flips x,y-coordinates horizontally in one half to account for changing sides at half-time 8 | #' 9 | #' @description Normalises direction of attack in both halves of both teams by 10 | #' flipping x,y-coordinates horizontally in either the first or second half; 11 | #' i.e. teams attack in the same direction all game despite changing sides at 12 | #' half-time. 13 | #' 14 | #' @param df dataframe containing unnormalised x,y-coordinates 15 | #' @param lengthPitch,widthPitch length, width of pitch in metres 16 | #' @param teamToFlip character, name of team to flip. If \code{NULL}, all x,y-coordinates in \code{df} will be flipped 17 | #' @param periodToFlip integer, period(s) to flip 18 | #' @param team character, name of variables containing x,y-coordinates 19 | #' @param period character, name of variable containing period labels 20 | #' @param x,y character, name of variables containing x,y-coordinates 21 | #' @return a dataframe 22 | #' @examples 23 | #' library(dplyr) 24 | #' 25 | #' # flip x,y-coords of France in both halves of statsbomb data 26 | #' data(statsbomb) 27 | #' statsbomb %>% 28 | #' soccerFlipDirection(team = "team.name", x = "location.x", y = "location.y", 29 | #' teamToFlip = "France") 30 | #' 31 | #' # flip x,y-coords in 2nd half of Tromso, based on a dummy period variable 32 | #' data(tromso) 33 | #' tromso %>% 34 | #' mutate(period = if_else(t > as.POSIXct("2013-11-07 21:14:00 GMT"), 1, 2)) %>% 35 | #' soccerFlipDirection(periodToFlip = 2) 36 | #' 37 | #' @export 38 | soccerFlipDirection <- function(df, lengthPitch = 105, widthPitch = 68, teamToFlip = NULL, periodToFlip = 1:2, team = "team", period = "period", x = "x", y = "y") { 39 | 40 | # flip all x,y-coordinates in df 41 | if(is.null(teamToFlip)) { 42 | df <- df %>% 43 | mutate(!!x := if_else(!!sym(period) %in% periodToFlip, lengthPitch - !!sym(x), !!sym(x)), 44 | !!y := if_else(!!sym(period) %in% periodToFlip, widthPitch - !!sym(y), !!sym(y))) 45 | 46 | # flip x,y-coords of only one team 47 | } else { 48 | df <- df %>% 49 | mutate(!!x := if_else(!!sym(team) == teamToFlip & !!sym(period) %in% periodToFlip, lengthPitch - !!sym(x), !!sym(x)), 50 | !!y := if_else(!!sym(team) == teamToFlip & !!sym(period) %in% periodToFlip, widthPitch - !!sym(y), !!sym(y))) 51 | } 52 | 53 | return(df) 54 | } 55 | -------------------------------------------------------------------------------- /R/soccerFlow.R: -------------------------------------------------------------------------------- 1 | #' @include soccerHeatmap.R 2 | #' @include soccerSpokes.R 3 | #' @import ggplot2 4 | #' @import dplyr 5 | #' @importFrom magrittr "%>%" 6 | NULL 7 | #' Draw a flow field of passing direction on a soccer pitch 8 | #' @description A flow field to show the mean angle and distance of passes in zones of the pitch 9 | #' 10 | #' @param df dataframe of event data containing fields of start x,y-coordinates, pass distance, and pass angle 11 | #' @param lengthPitch,widthPitch numeric, length and width of pitch in metres. 12 | #' @param xBins,yBins integer, the number of horizontal (length-wise) and vertical (width-wise) bins the soccer pitch is to be divided up into; if \code{yBins} is NULL (default), it will take the value of \code{xBins} 13 | #' @param x,y,angle,distance names of variables containing pass start x,y-coordinates, angle, and distance 14 | #' @param col colour of arrows 15 | #' @param lwd thickness of arrow segments 16 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 17 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 18 | #' @param theme palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass} 19 | #' @param plot base plot to add path layer to; NULL by default 20 | #' @return a ggplot object of a heatmap on a soccer pitch 21 | #' @examples 22 | #' library(dplyr) 23 | #' data(statsbomb) 24 | #' 25 | #' # transform x,y-coords, filter only France pass events, 26 | #' # draw flow field showing mean angle, distance of passes per pitch zone 27 | #' statsbomb %>% 28 | #' soccerTransform(method = 'statsbomb') %>% 29 | #' filter(team.name == "France" & type.name == "Pass") %>% 30 | #' soccerFlow(xBins=7, yBins=5, 31 | #' x="location.x", y="location.y", angle="pass.angle", distance="pass.length") 32 | #' 33 | #' # transform x,y-coords, standarise column names, 34 | #' # filter only France pass events 35 | #' my_df <- statsbomb %>% 36 | #' soccerTransform(method = 'statsbomb') %>% 37 | #' soccerStandardiseCols(method = 'statsbomb') %>% 38 | #' filter(team_name == "France" & event_name == "Pass") 39 | #' 40 | #' # overlay flow field onto heatmap showing proportion of team passes per pitch zone 41 | #' soccerHeatmap(my_df, xBins=7, yBins=5) %>% 42 | #' soccerFlow(my_df, xBins=7, yBins=5, plot = .) 43 | #' 44 | #' @seealso \code{\link{soccerHeatmap}} for drawing a heatmap of player position, or \code{\link{soccerSpokes}} for drawing spokes to show all directions in each area of the pitch. 45 | #' @export 46 | soccerFlow <- function(df, lengthPitch = 105, widthPitch = 68, xBins = 5, yBins = NULL, x = "x", y = "y", angle = "angle", distance = "distance", col = "black", lwd = 0.5, arrow = c("none", "r", "l"), title = NULL, subtitle = NULL, theme = c("light", "dark", "grey", "grass"), plot = NULL) { 47 | x.bin<-y.bin<-bin<-x.bin.coord<-y.bin.coord<-angle.mean<-radius.mean<-NULL 48 | 49 | # check value for vertical bins and match to horizontal bins if NULL 50 | if(is.null(yBins)) yBins <- xBins 51 | 52 | # adjust range and n bins 53 | x.range <- seq(0, lengthPitch, length.out = xBins+1) 54 | y.range <- seq(0, widthPitch, length.out = yBins+1) 55 | 56 | # bin plot values 57 | x.bin.coords <- data.frame(x.bin = 1:xBins, 58 | x.bin.coord = (x.range + (lengthPitch / (xBins) / 2))[1:xBins]) 59 | y.bin.coords <- data.frame(y.bin = 1:yBins, 60 | y.bin.coord = (y.range + (widthPitch / (yBins) / 2))[1:yBins]) 61 | 62 | # bin data 63 | suppressWarnings( #suppress warnings about empty bins 64 | df <- df %>% 65 | rowwise() %>% 66 | mutate(x.bin = max(which(!!sym(x) > x.range)), 67 | y.bin = max(which(!!sym(y) > y.range)), 68 | bin = paste(x.bin, y.bin, sep = "_")) %>% 69 | ungroup() %>% 70 | filter(is.finite(x.bin) & is.finite(y.bin)) 71 | ) 72 | 73 | # summarise angle for each bin 74 | df <- df %>% 75 | group_by(bin) %>% 76 | mutate(x.mean = mean(!!sym(x), na.rm=T), 77 | y.mean = mean(!!sym(y), na.rm=T), 78 | angle.mean = mean(!!sym(angle), na.rm=T), 79 | radius.mean = mean(!!sym(distance), na.rm=T)) %>% 80 | ungroup() 81 | 82 | # add bin centre x,y-coords for plotting 83 | df <- left_join(df, x.bin.coords, by = "x.bin") 84 | df <- left_join(df, y.bin.coords, by = "y.bin") 85 | 86 | # scale radius 87 | df$radius.mean <- scales::rescale(df$radius.mean, c(2, lengthPitch / xBins)) 88 | 89 | if(missing(plot)) { 90 | soccerPitch(lengthPitch = lengthPitch, widthPitch = widthPitch, theme = theme) + 91 | geom_spoke(data = df, aes(x = x.bin.coord, y = y.bin.coord, 92 | angle = angle.mean, radius = radius.mean), 93 | size = lwd, col = col, arrow=arrow(length = unit(0.2,"cm"))) + 94 | guides(fill="none") 95 | } else { 96 | plot + 97 | geom_spoke(data = df, aes(x = x.bin.coord, y = y.bin.coord, 98 | angle = angle.mean, radius = radius.mean), 99 | size = lwd, col = col, arrow=arrow(length = unit(0.2,"cm"))) + 100 | guides(fill="none") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /R/soccerHeatmap.R: -------------------------------------------------------------------------------- 1 | #' @include soccerPitch.R 2 | #' @import ggplot2 3 | #' @import dplyr 4 | #' @importFrom magrittr "%>%" 5 | #' @importFrom MASS kde2d 6 | NULL 7 | #' Draw a heatmap on a soccer pitch using any event or tracking data. 8 | #' @description Draws a heatmap showing player position frequency in each area of the pitch and adds soccer pitch outlines. 9 | #' 10 | #' @param df dataframe containing x,y-coordinates of player position 11 | #' @param xBins,yBins integer, the number of horizontal (length-wise) and vertical (width-wise) bins the soccer pitch is to be divided up into. If no value for \code{yBins} is provided, it will take the value of \code{xBins}. 12 | #' @param kde use kernel density estimates for a smoother heatmap; FALSE by default 13 | #' @param lengthPitch,widthPitch numeric, length and width of pitch in metres. 14 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 15 | #' @param colLow,colHigh character, colours for the low and high ends of the heatmap gradient; white and red respectively by default 16 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 17 | #' @param x,y name of variables containing x,y-coordinates 18 | #' @return a ggplot object of a heatmap on a soccer pitch. 19 | #' @details uses \code{ggplot2::geom_bin2d} to map 2D bin counts 20 | #' @examples 21 | #' library(dplyr) 22 | #' 23 | #' # tracking data heatmap with 21x5 zones(~5x5m) 24 | #' data(tromso) 25 | #' tromso %>% 26 | #' filter(id == 8) %>% 27 | #' soccerHeatmap(xBins = 10) 28 | #' 29 | #' # transform x,y-coords, filter only France pressure events, 30 | #' # heatmap with 6x3 zones 31 | #' data(statsbomb) 32 | #' statsbomb %>% 33 | #' soccerTransform(method='statsbomb') %>% 34 | #' filter(type.name == "Pressure" & team.name == "France") %>% 35 | #' soccerHeatmap(x = "location.x", y = "location.y", 36 | #' xBins = 6, yBins = 3, arrow = "r", 37 | #' title = "France (vs Argentina, 30th June 2016)", 38 | #' subtitle = "Defensive pressure heatmap") 39 | #' 40 | #' # transform x,y-coords, standardise column names, 41 | #' # filter player defensive actions, plot kernel density estimate heatmap 42 | #' statsbomb %>% 43 | #' soccerTransform(method='statsbomb') %>% 44 | #' soccerStandardiseCols() %>% 45 | #' filter(event_name %in% c("Duel", "Interception", "Clearance", "Block") & 46 | #' player_name == "Samuel Yves Umtiti") %>% 47 | #' soccerHeatmap(kde = TRUE, arrow = "r", 48 | #' title = "Umtiti (vs Argentina, 30th June 2016)", 49 | #' subtitle = "Defensive actions heatmap") 50 | #' 51 | #' @export 52 | soccerHeatmap <- function(df, lengthPitch = 105, widthPitch = 68, xBins = 10, yBins = NULL, kde = FALSE, arrow = c("none", "r", "l"), colLow = "white", colHigh = "red", title = NULL, subtitle = NULL, x = "x", y = "y") { 53 | z <- NULL 54 | 55 | # ensure input is dataframe 56 | df <- as.data.frame(df) 57 | 58 | # rename variables 59 | df$x <- df[,x] 60 | df$y <- df[,y] 61 | 62 | # zonal heatmap 63 | if(!kde) { 64 | # check value for vertical bins and match to horizontal bins if NULL 65 | if(is.null(yBins)) yBins <- xBins 66 | 67 | # filter invalid values outside pitch limits 68 | df <- df[df$x > 0 & df$x < lengthPitch & df$y > 0 & df$y < widthPitch,] 69 | 70 | # define bin ranges 71 | x.range <- seq(0, lengthPitch, length.out = xBins+1) 72 | y.range <- seq(0, widthPitch, length.out = yBins+1) 73 | 74 | # plot heatmap on blank pitch lines 75 | p <- soccerPitch(lengthPitch, widthPitch, arrow = arrow, title = title, subtitle = subtitle, theme = "blank") + 76 | geom_bin2d(data = df, aes(x, y), binwidth = c(diff(x.range)[1], diff(y.range)[1])) + 77 | scale_fill_gradient(low = colLow, high = colHigh) + 78 | guides(fill="none") 79 | 80 | # redraw pitch lines 81 | p <- soccerPitchFG(p, title = !is.null(subtitle), subtitle = !is.null(title)) 82 | 83 | # kernel density estimate heatmap 84 | } else { 85 | dens <- kde2d(df$x, df$y, n=200, lims=c(c(0.25, lengthPitch-0.25), c(0.25, widthPitch-0.25))) 86 | dens_df <- data.frame(expand.grid(x = dens$x, y = dens$y), z = as.vector(dens$z)) 87 | 88 | p <- soccerPitch(lengthPitch, widthPitch, arrow = arrow, title = title, subtitle = subtitle, theme = "light") + 89 | geom_tile(data = dens_df, aes(x = x, y = y, fill = z)) + 90 | scale_fill_distiller(palette="Spectral", na.value="white") + 91 | guides(fill="none") 92 | 93 | p <- soccerPitchFG(p, title = !is.null(subtitle), subtitle = !is.null(title)) 94 | } 95 | 96 | return(p) 97 | 98 | } 99 | -------------------------------------------------------------------------------- /R/soccerPassmap.R: -------------------------------------------------------------------------------- 1 | #' @include soccerPitch.R 2 | #' @import ggplot2 3 | #' @import dplyr 4 | #' @importFrom magrittr "%>%" 5 | #' @importFrom ggrepel geom_label_repel 6 | #' @importFrom forcats fct_explicit_na 7 | #' @importFrom scales rescale 8 | NULL 9 | #' Draw a passing network using StatsBomb data 10 | #' 11 | #' @description Draw an undirected passing network of completed passes on pitch from StatsBomb data. Nodes are scaled by number of successful passes; edge width is scaled by number of successful passes between each node pair. Only passes made until first substition shown (ability to specify custom minutes will be added soon). Total number of passes attempted and percentage of completed passes shown. Compatability with other (non-StatsBomb) shot data will be added soon. 12 | #' 13 | #' @param df dataframe containing x,y-coordinates of player passes 14 | #' @param lengthPitch,widthPitch numeric, length and width of pitch, in metres 15 | #' @param minPass minimum number of passes between players for edge to be drawn 16 | #' @param fill,col fill and border colour of nodes 17 | #' @param edgeCol colour of edge lines. Default is complementary to \code{theme} colours. 18 | #' @param edgeAlpha transparency of edge lines, from \code{0} - \code{1}. Defaults to \code{0.6} so overlapping edges are visible. 19 | #' @param label boolean, draw labels 20 | #' @param shortNames shorten player names to display last name as label 21 | #' @param maxNodeSize maximum size of nodes 22 | #' @param maxEdgeSize maximum width of edge lines 23 | #' @param labelSize size of player name labels 24 | #' @param arrow optional, adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}) 25 | #' @param theme draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch 26 | #' @param title adds custom title to plot. Defaults to team name. 27 | #' @examples 28 | #' # France vs. Argentina, minimum of three passes 29 | #' library(dplyr) 30 | #' data(statsbomb) 31 | #' 32 | #' # transform x,y-coords, 33 | #' # Argentina pass map until first substituton with transparent edges 34 | #' statsbomb %>% 35 | #' soccerTransform(method='statsbomb') %>% 36 | #' filter(team.name == "Argentina") %>% 37 | #' soccerPassmap(fill = "lightblue", arrow = "r", 38 | #' title = "Argentina (vs France, 30th June 2018)") 39 | #' 40 | #' # transform x,y-coords, 41 | #' # France pass map until first substitution with opaque edges 42 | #' statsbomb %>% 43 | #' filter(team.name == "France") %>% 44 | #' soccerTransform(method='statsbomb') %>% 45 | #' soccerPassmap(fill = "blue", minPass = 3, 46 | #' maxEdgeSize = 30, edgeCol = "grey40", edgeAlpha = 1, 47 | #' title = "France (vs Argentina, 30th June 2018)") 48 | #' @export 49 | soccerPassmap <- function(df, lengthPitch = 105, widthPitch = 68, minPass = 3, fill = "red", col = "black", edgeAlpha = 0.6, edgeCol = NULL, label = TRUE, shortNames = TRUE, maxNodeSize = 30, maxEdgeSize = 30, labelSize = 4, arrow = c("none", "r", "l"), theme = c("light", "dark", "grey", "grass"), title = NULL) { 50 | type.name<-pass.outcome.name<-period<-timestamp<-player.name<-pass.recipient.name<-from<-to<-xend<-yend<-events<-NULL 51 | 52 | if(length(unique(df$team.name)) > 1) stop("Data contains more than one team") 53 | 54 | # define colours by theme 55 | if(theme[1] == "grass") { 56 | colText <- "white" 57 | if(is.null(edgeCol)) edgeCol <- "black" 58 | } else if(theme[1] == "light") { 59 | colText <- "black" 60 | if(is.null(edgeCol)) edgeCol <- "black" 61 | } else if(theme[1] %in% c("grey", "gray")) { 62 | colText <- "black" 63 | if(is.null(edgeCol)) edgeCol <- "black" 64 | } else { 65 | colText <- "white" 66 | if(is.null(edgeCol)) edgeCol <- "white" 67 | } 68 | 69 | # ensure input is dataframe 70 | df <- as.data.frame(df) 71 | 72 | # set variable names 73 | x <- "location.x" 74 | y <- "location.y" 75 | id <- "player.id" 76 | name <- "player.name" 77 | team <- "team.name" 78 | 79 | df$x <- df[,x] 80 | df$y <- df[,y] 81 | df$id <- df[,id] 82 | df$name <- df[,name] 83 | df$team <- df[,team] 84 | 85 | 86 | # full game passing stats for labels 87 | passes <- df %>% 88 | filter(type.name == "Pass") %>% 89 | group_by(pass.outcome.name) %>% 90 | tally() %>% 91 | filter(!pass.outcome.name %in% c("Injury Clearance", "Unknown")) %>% 92 | mutate(pass.outcome.name = fct_explicit_na(pass.outcome.name, "Complete")) 93 | pass_n <- sum(passes$n) 94 | pass_pc <- passes[passes$pass.outcome.name == "Complete",]$n / pass_n * 100 95 | 96 | 97 | # filter events before time of first substitution, if at least one substitution 98 | min_events <- df %>% 99 | group_by(id) %>% 100 | dplyr::summarise(period = min(period), timestamp = min(timestamp)) %>% 101 | stats::na.omit() %>% 102 | arrange(period, timestamp) 103 | 104 | if(nrow(min_events) > 11) { 105 | max_event <- min_events[12,] 106 | idx <- which(df$period == max_event$period & df$timestamp == max_event$timestamp) - 1 107 | df <- df[1:idx,] 108 | } 109 | 110 | 111 | # get nodes and edges for plotting 112 | # node position and size based on touches 113 | nodes <- df %>% 114 | filter(type.name %in% c("Pass", "Ball Receipt*", "Ball Recovery", "Shot", "Dispossessed", "Interception", "Clearance", "Dribble", "Shot", "Goal Keeper", "Miscontrol", "Error")) %>% 115 | group_by(id, name) %>% 116 | dplyr::summarise(x = mean(x, na.rm=T), y = mean(y, na.rm=T), events = n()) %>% 117 | stats::na.omit() %>% 118 | as.data.frame() 119 | 120 | # edges based only on completed passes 121 | edgelist <- df %>% 122 | mutate(pass.outcome.name = fct_explicit_na(pass.outcome.name, "Complete")) %>% 123 | filter(type.name == "Pass" & pass.outcome.name == "Complete") %>% 124 | select(from = player.name, to = pass.recipient.name) %>% 125 | group_by(from, to) %>% 126 | dplyr::summarise(n = n()) %>% 127 | stats::na.omit() 128 | 129 | edges <- left_join(edgelist, 130 | nodes %>% select(id, name, x, y), 131 | by = c("from" = "name")) 132 | 133 | edges <- left_join(edges, 134 | nodes %>% select(id, name, xend = x, yend = y), 135 | by = c("to" = "name")) 136 | 137 | edges <- edges %>% 138 | group_by(player1 = pmin(from, to), player2 = pmax(from, to)) %>% 139 | dplyr::summarise(n = sum(n), x = x[1], y = y[1], xend = xend[1], yend = yend[1]) 140 | 141 | 142 | # filter minimum number of passes and rescale line width 143 | nodes <- nodes %>% 144 | mutate(events = rescale(events, c(2, maxNodeSize), c(1, 200))) 145 | 146 | # rescale node size 147 | edges <- edges %>% 148 | filter(n >= minPass) %>% 149 | mutate(n = rescale(n, c(1, maxEdgeSize), c(minPass, 75))) 150 | 151 | 152 | # shorten player name 153 | if(shortNames) { 154 | nodes$name <- soccerShortenName(nodes$name) 155 | } 156 | 157 | # if no title given, use team 158 | if(is.null(title)) { 159 | title <- unique(df$team) 160 | } 161 | 162 | subtitle <- paste0(min(df$minute)+1, "' - ", max(df$minute)+1, "', ", minPass, "+ passes shown") 163 | 164 | # plot network 165 | p <- soccerPitch(lengthPitch, widthPitch, 166 | arrow = arrow[1], theme = theme[1], 167 | title = title, 168 | subtitle = subtitle) + 169 | geom_segment(data = edges, aes(x, y, xend = xend, yend = yend, size = n), col = edgeCol, alpha = edgeAlpha) + 170 | geom_point(data = nodes, aes(x, y, size = events), pch = 21, fill = fill, col = col) + 171 | scale_size_identity() + 172 | guides(size="none") + 173 | annotate("text", 104, 1, label = paste0("Passes: ", pass_n, "\nCompleted: ", sprintf("%.1f", pass_pc), "%"), hjust = 1, vjust = 0, size = labelSize * 7/8, col = colText) 174 | 175 | # add labels 176 | if(label) { 177 | p <- p + 178 | geom_label_repel(data = nodes, aes(x, y, label = name), size = labelSize) 179 | } 180 | 181 | return(p) 182 | 183 | } 184 | -------------------------------------------------------------------------------- /R/soccerPath.R: -------------------------------------------------------------------------------- 1 | #' @import ggplot2 2 | #' @import dplyr 3 | #' @importFrom magrittr "%>%" 4 | NULL 5 | #' Draw a path of player trajectory on a soccer pitch using any tracking data 6 | #' 7 | #' @description Draws a path connecting consecutive x,y-coordinates of a player on a soccer pitch. 8 | #' 9 | #' @param df dataframe containing x,y-coordinates of player position 10 | #' @param lengthPitch,widthPitch length and width of pitch in metres 11 | #' @param col colour of path if no \code{'id'} is provided; if an \code{'id'} is present, uses ColorBrewer's 'Paired' palette by default 12 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 13 | #' @param theme draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch 14 | #' @param lwd player path thickness 15 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 16 | #' @param legend boolean, include legend 17 | #' @param x,y name of variables containing x,y-coordinates 18 | #' @param id character, the name of the column containing player identity (only required if \code{'df'} contains multiple players) 19 | #' @param plot base plot to add path layer to; NULL by default 20 | #' @return a ggplot object 21 | #' @examples 22 | #' library(dplyr) 23 | #' data(tromso) 24 | #' 25 | #' # draw path of Tromso #8 over first 3 minutes (1800 frames) 26 | #' tromso %>% 27 | #' filter(id == 8) %>% 28 | #' top_n(1800) %>% 29 | #' soccerPath(col = "red", theme = "grass", arrow = "r") 30 | #' 31 | #' # draw path of all Tromso players over first minute (600 frames) 32 | #' tromso %>% 33 | #' group_by(id) %>% 34 | #' slice(1:1200) %>% 35 | #' soccerPath(id = "id", theme = "light") 36 | #' 37 | #' @export 38 | soccerPath <- function(df, lengthPitch = 105, widthPitch = 68, col = "black", arrow = c("none", "r", "l"), theme = c("light", "dark", "grey", "grass"), lwd = 1, title = NULL, subtitle = NULL, legend = FALSE, x = "x", y = "y", id = NULL, plot = NULL) { 39 | 40 | # ensure input is dataframe 41 | df <- as.data.frame(df) 42 | 43 | if(is.null(id)) { 44 | # one player 45 | if(missing(plot)) { 46 | p <- soccerPitch(lengthPitch, widthPitch, arrow = arrow, theme = theme[1], title = title, subtitle = subtitle) + 47 | geom_path(data = df, aes(x, y), col = col, lwd = lwd) 48 | } else { 49 | p <- plot + 50 | geom_path(data = df, aes(x, y), col = col, lwd = lwd) 51 | } 52 | } else { 53 | # multiple players 54 | if(missing(plot)) { 55 | p <- soccerPitch(lengthPitch, widthPitch, arrow = arrow, theme = theme[1], title = title, subtitle = subtitle) + 56 | geom_path(data = df, aes_string("x", "y", group = id, colour = id), lwd = lwd) + 57 | scale_colour_brewer(type = "seq", palette = "Paired", labels = 1:12) 58 | } else { 59 | p <- plot + 60 | geom_path(data = df, aes_string("x", "y", group = id, colour = id), lwd = lwd) + 61 | scale_colour_brewer(type = "seq", palette = "Paired", labels = 1:12) 62 | } 63 | 64 | #legend 65 | if(legend == FALSE) { 66 | p <- p + 67 | guides(colour="none") 68 | } 69 | } 70 | 71 | return(p) 72 | 73 | } 74 | -------------------------------------------------------------------------------- /R/soccerPitch.R: -------------------------------------------------------------------------------- 1 | #' @import ggplot2 2 | #' @importFrom dplyr filter 3 | #' @importFrom magrittr "%>%" 4 | #' @importFrom ggforce geom_arc geom_circle 5 | #' @importFrom cowplot draw_text 6 | NULL 7 | #' Plot a full soccer pitch 8 | #' 9 | #' @description Draws a soccer pitch as a ggplot object for the purpose of adding layers such as player positions, player trajectories, etc.. 10 | #' 11 | #' @param lengthPitch,widthPitch length and width of pitch in metres 12 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 13 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 14 | #' @param theme palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass} 15 | #' @param data a default dataset for plotting in subsequent layers; NULL by default 16 | #' @return a ggplot object 17 | #' @examples 18 | #' library(ggplot2) 19 | #' data(statsbomb) 20 | #' 21 | #' # transform Statsbomb coordinates to metre units for plotting 22 | #' my_df <- soccerTransform(statsbomb, method = "statsbomb") 23 | #' 24 | #' # filter events of interest (France defensive pressure events vs. Argentina) 25 | #' my_df <- my_df %>% 26 | #' dplyr::filter(team.name == "France" & type.name == "Pressure") 27 | #' 28 | #' # add custom layers to soccerPitch base 29 | #' soccerPitch(data = my_df, 30 | #' arrow = "r", theme = "grass", 31 | #' title = "France (vs. Argentina)", 32 | #' subtitle = "Pressure events") + 33 | #' geom_point(aes(x = location.x, y = location.y), 34 | #' col = "blue", alpha = 0.5) 35 | #' 36 | #' @export 37 | soccerPitch <- function(lengthPitch = 105, widthPitch = 68, arrow = c("none", "r", "l"), title = NULL, subtitle = NULL, theme = c("light", "dark", "grey", "grass"), data = NULL) { 38 | start<-end<-NULL 39 | 40 | # define colours by theme 41 | if(theme[1] == "grass") { 42 | fill1 <- "#008000" 43 | fill2 <- "#328422" 44 | colPitch <- "grey85" 45 | arrowCol <- "white" 46 | colText <- "white" 47 | } else if(theme[1] == "light") { 48 | fill1 <- "grey98" 49 | fill2 <- "grey98" 50 | colPitch <- "grey60" 51 | arrowCol = "black" 52 | colText <- "black" 53 | } else if(theme[1] %in% c("grey", "gray")) { 54 | fill1 <- "#A3A1A3" 55 | fill2 <- "#A3A1A3" 56 | colPitch <- "white" 57 | arrowCol <- "white" 58 | colText <- "black" 59 | } else if(theme[1] == "dark") { 60 | fill1 <- "#1a1e2c" 61 | fill2 <- "#1a1e2c" 62 | colPitch <- "#F0F0F0" 63 | arrowCol <- "#F0F0F0" 64 | colText <- "#F0F0F0" 65 | } else if(theme[1] == "blank") { 66 | fill1 <- "white" 67 | fill2 <- "white" 68 | colPitch <- "white" 69 | arrowCol <- "black" 70 | colText <- "black" 71 | } 72 | lwd <- 0.5 73 | 74 | # outer border (t,r,b,l) 75 | border <- c(10, 6, 5, 6) 76 | 77 | # mowed grass lines 78 | lines <- (lengthPitch + border[2] + border[4]) / 13 79 | boxes <- data.frame(start = lines * 0:12 - border[4], end = lines * 1:13 - border[2])[seq(2, 12, 2),] 80 | 81 | # draw pitch 82 | p <- ggplot(data) + 83 | # background 84 | geom_rect(aes(xmin = -border[4], xmax = lengthPitch + border[2], ymin = -border[3], ymax = widthPitch + border[1]), fill = fill1) + 85 | # mowed pitch lines 86 | geom_rect(data = boxes, aes(xmin = start, xmax = end, ymin = -border[3], ymax = widthPitch + border[1]), fill = fill2) + 87 | # perimeter line 88 | geom_rect(aes(xmin = 0, xmax = lengthPitch, ymin = 0, ymax = widthPitch), fill = NA, col = colPitch, lwd = lwd) + 89 | # centre circle 90 | geom_circle(aes(x0 = lengthPitch/2, y0 = widthPitch/2, r = 9.15), col = colPitch, lwd = lwd) + 91 | # kick off spot 92 | geom_circle(aes(x0 = lengthPitch/2, y0 = widthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 93 | # halfway line 94 | geom_segment(aes(x = lengthPitch/2, y = 0, xend = lengthPitch/2, yend = widthPitch), col = colPitch, lwd = lwd) + 95 | # penalty arcs 96 | geom_arc(aes(x0= 11, y0 = widthPitch/2, r = 9.15, start = pi/2 + 0.9259284, end = pi/2 - 0.9259284), col = colPitch, lwd = lwd) + 97 | geom_arc(aes(x0 = lengthPitch - 11, y0 = widthPitch/2, r = 9.15, start = pi/2*3 - 0.9259284, end = pi/2*3 + 0.9259284), col = colPitch, lwd = lwd) + 98 | # penalty areas 99 | geom_rect(aes(xmin = 0, xmax = 16.5, ymin = widthPitch/2 - 20.15, ymax = widthPitch/2 + 20.15), fill = NA, col = colPitch, lwd = lwd) + 100 | geom_rect(aes(xmin = lengthPitch - 16.5, xmax = lengthPitch, ymin = widthPitch/2 - 20.15, ymax = widthPitch/2 + 20.15), fill = NA, col = colPitch, lwd = lwd) + 101 | # penalty spots 102 | geom_circle(aes(x0 = 11, y0 = widthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 103 | geom_circle(aes(x0 = lengthPitch - 11, y0 = widthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 104 | # six yard boxes 105 | geom_rect(aes(xmin = 0, xmax = 5.5, ymin = (widthPitch/2) - 9.16, ymax = (widthPitch/2) + 9.16), fill = NA, col = colPitch, lwd = lwd) + 106 | geom_rect(aes(xmin = lengthPitch - 5.5, xmax = lengthPitch, ymin = (widthPitch/2) - 9.16, ymax = (widthPitch/2) + 9.16), fill = NA, col = colPitch, lwd = lwd) + 107 | # goals 108 | geom_rect(aes(xmin = -2, xmax = 0, ymin = (widthPitch/2) - 3.66, ymax = (widthPitch/2) + 3.66), fill = NA, col = colPitch, lwd = lwd) + 109 | geom_rect(aes(xmin = lengthPitch, xmax = lengthPitch + 2, ymin = (widthPitch/2) - 3.66, ymax = (widthPitch/2) + 3.66), fill = NA, col = colPitch, lwd = lwd) + 110 | coord_fixed() + 111 | theme(rect = element_blank(), 112 | line = element_blank(), 113 | axis.text = element_blank(), 114 | axis.title = element_blank()) 115 | 116 | # add arrow 117 | if(arrow[1] == "r") { 118 | p <- p + 119 | geom_segment(aes(x = 0, y = -2, xend = lengthPitch / 3, yend = -2), colour = arrowCol, size = 1.5, arrow = arrow(length = unit(0.2, "cm"), type="closed"), linejoin='mitre') 120 | } else if(arrow[1] == "l") { 121 | p <- p + 122 | geom_segment(aes(x = lengthPitch, y = -2, xend = lengthPitch / 3 * 2, yend = -2), colour = arrowCol, size = 1.5, arrow = arrow(length = unit(0.2, "cm"), type="closed"), linejoin='mitre') 123 | } 124 | 125 | # add title and/or subtitle 126 | theme_buffer <- ifelse(theme[1] == "light", 0, 4) 127 | if(!is.null(title) & !is.null(subtitle)) { 128 | p <- p + 129 | draw_text(title, 130 | x = 0, y = widthPitch + 9, hjust = 0, vjust = 1, 131 | size = 15, fontface = 'bold', col = colText) + 132 | draw_text(subtitle, 133 | x = 0, y = widthPitch + 4.5, hjust = 0, vjust = 1, 134 | size = 13, col = colText) + 135 | theme(plot.margin = unit(c(-0.525,-0.9,-0.7,-0.9), "cm")) 136 | } else if(!is.null(title) & is.null(subtitle)) { 137 | p <- p + 138 | draw_text(title, 139 | x = 0, y = widthPitch + 4.5, hjust = 0, vjust = 1, 140 | size = 15, fontface = 'bold', col = colText) + 141 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 142 | } else if(is.null(title) & !is.null(subtitle)) { 143 | p <- p + 144 | draw_text(subtitle, 145 | x = 0, y = widthPitch + 4.5, hjust = 0, vjust = 1, 146 | size = 13, col = colText) + 147 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 148 | } else if(is.null(title) & is.null(subtitle)){ 149 | p <- p + 150 | theme(plot.margin = unit(c(-1.2,-0.9,-0.7,-0.9), "cm")) 151 | } 152 | 153 | 154 | return(p) 155 | 156 | } 157 | -------------------------------------------------------------------------------- /R/soccerPitchBG.R: -------------------------------------------------------------------------------- 1 | #' @import ggplot2 2 | #' @importFrom dplyr filter 3 | #' @importFrom magrittr "%>%" 4 | #' @importFrom ggforce geom_arc geom_circle 5 | #' @importFrom cowplot draw_text 6 | NULL 7 | #' Plot a full soccer pitch 8 | #' 9 | #' @description Draws a soccer pitch as a ggplot object for the purpose of adding layers such as player positions, player trajectories, etc.. 10 | #' 11 | #' @param lengthPitch,widthPitch length and width of pitch in metres 12 | #' @param fillPitch,colPitch pitch fill and line colour 13 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 14 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 15 | #' @param theme palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass} 16 | #' @param data a default dataset for plotting in subsequent layers; NULL by default 17 | #' @return a ggplot object 18 | #' @seealso \code{\link{soccermatics-deprecated}} 19 | #' @keywords internal 20 | #' @rdname soccermatics-deprecated 21 | #' @export 22 | soccerPitchBG <- function(lengthPitch = 105, widthPitch = 68, arrow = c("none", "r", "l"), title = NULL, subtitle = NULL, theme = c("light", "dark", "grey", "grass"), data = NULL) { 23 | .Deprecated("soccerPitch") 24 | start<-end<-NULL 25 | 26 | # define colours by theme 27 | if(theme[1] == "grass") { 28 | fill1 <- "#008000" 29 | fill2 <- "#328422" 30 | colPitch <- "grey85" 31 | arrowCol <- "white" 32 | colText <- "white" 33 | } else if(theme[1] == "light") { 34 | fill1 <- "grey98" 35 | fill2 <- "grey98" 36 | colPitch <- "grey60" 37 | arrowCol = "black" 38 | colText <- "black" 39 | } else if(theme[1] %in% c("grey", "gray")) { 40 | fill1 <- "#A3A1A3" 41 | fill2 <- "#A3A1A3" 42 | colPitch <- "white" 43 | arrowCol <- "white" 44 | colText <- "black" 45 | } else if(theme[1] == "dark") { 46 | fill1 <- "#1C1F26" 47 | fill2 <- "#1C1F26" 48 | colPitch <- "white" 49 | arrowCol <- "white" 50 | colText <- "white" 51 | } else if(theme[1] == "blank") { 52 | fill1 <- "white" 53 | fill2 <- "white" 54 | colPitch <- "white" 55 | arrowCol <- "black" 56 | colText <- "black" 57 | } 58 | lwd <- 0.5 59 | 60 | # outer border (t,r,b,l) 61 | border <- c(10, 6, 5, 6) 62 | 63 | # mowed grass lines 64 | lines <- (lengthPitch + border[2] + border[4]) / 13 65 | boxes <- data.frame(start = lines * 0:12 - border[4], end = lines * 1:13 - border[2])[seq(2, 12, 2),] 66 | 67 | # draw pitch 68 | p <- ggplot(data) + 69 | # background 70 | geom_rect(aes(xmin = -border[4], xmax = lengthPitch + border[2], ymin = -border[3], ymax = widthPitch + border[1]), fill = fill1) + 71 | # mowed pitch lines 72 | geom_rect(data = boxes, aes(xmin = start, xmax = end, ymin = -border[3], ymax = widthPitch + border[1]), fill = fill2) + 73 | # perimeter line 74 | geom_rect(aes(xmin = 0, xmax = lengthPitch, ymin = 0, ymax = widthPitch), fill = NA, col = colPitch, lwd = lwd) + 75 | # centre circle 76 | geom_circle(aes(x0 = lengthPitch/2, y0 = widthPitch/2, r = 9.15), col = colPitch, lwd = lwd) + 77 | # kick off spot 78 | geom_circle(aes(x0 = lengthPitch/2, y0 = widthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 79 | # halfway line 80 | geom_segment(aes(x = lengthPitch/2, y = 0, xend = lengthPitch/2, yend = widthPitch), col = colPitch, lwd = lwd) + 81 | # penalty arcs 82 | geom_arc(aes(x0= 11, y0 = widthPitch/2, r = 9.15, start = pi/2 + 0.9259284, end = pi/2 - 0.9259284), col = colPitch, lwd = lwd) + 83 | geom_arc(aes(x0 = lengthPitch - 11, y0 = widthPitch/2, r = 9.15, start = pi/2*3 - 0.9259284, end = pi/2*3 + 0.9259284), col = colPitch, lwd = lwd) + 84 | # penalty areas 85 | geom_rect(aes(xmin = 0, xmax = 16.5, ymin = widthPitch/2 - 20.15, ymax = widthPitch/2 + 20.15), fill = NA, col = colPitch, lwd = lwd) + 86 | geom_rect(aes(xmin = lengthPitch - 16.5, xmax = lengthPitch, ymin = widthPitch/2 - 20.15, ymax = widthPitch/2 + 20.15), fill = NA, col = colPitch, lwd = lwd) + 87 | # penalty spots 88 | geom_circle(aes(x0 = 11, y0 = widthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 89 | geom_circle(aes(x0 = lengthPitch - 11, y0 = widthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 90 | # six yard boxes 91 | geom_rect(aes(xmin = 0, xmax = 5.5, ymin = (widthPitch/2) - 9.16, ymax = (widthPitch/2) + 9.16), fill = NA, col = colPitch, lwd = lwd) + 92 | geom_rect(aes(xmin = lengthPitch - 5.5, xmax = lengthPitch, ymin = (widthPitch/2) - 9.16, ymax = (widthPitch/2) + 9.16), fill = NA, col = colPitch, lwd = lwd) + 93 | # goals 94 | geom_rect(aes(xmin = -2, xmax = 0, ymin = (widthPitch/2) - 3.66, ymax = (widthPitch/2) + 3.66), fill = NA, col = colPitch, lwd = lwd) + 95 | geom_rect(aes(xmin = lengthPitch, xmax = lengthPitch + 2, ymin = (widthPitch/2) - 3.66, ymax = (widthPitch/2) + 3.66), fill = NA, col = colPitch, lwd = lwd) + 96 | coord_fixed() + 97 | theme(rect = element_blank(), 98 | line = element_blank(), 99 | axis.text = element_blank(), 100 | axis.title = element_blank()) 101 | 102 | # add arrow 103 | if(arrow[1] == "r") { 104 | p <- p + 105 | geom_segment(aes(x = 0, y = -2, xend = lengthPitch / 3, yend = -2), colour = arrowCol, size = 1.5, arrow = arrow(length = unit(0.2, "cm"), type="closed"), linejoin='mitre') 106 | } else if(arrow[1] == "l") { 107 | p <- p + 108 | geom_segment(aes(x = lengthPitch, y = -2, xend = lengthPitch / 3 * 2, yend = -2), colour = arrowCol, size = 1.5, arrow = arrow(length = unit(0.2, "cm"), type="closed"), linejoin='mitre') 109 | } 110 | 111 | # add title and/or subtitle 112 | theme_buffer <- ifelse(theme[1] == "light", 0, 4) 113 | if(!is.null(title) & !is.null(subtitle)) { 114 | p <- p + 115 | draw_text(title, 116 | x = 0, y = widthPitch + 9, hjust = 0, vjust = 1, 117 | size = 15, fontface = 'bold', col = colText) + 118 | draw_text(subtitle, 119 | x = 0, y = widthPitch + 4.5, hjust = 0, vjust = 1, 120 | size = 13, col = colText) + 121 | theme(plot.margin = unit(c(-0.525,-0.9,-0.7,-0.9), "cm")) 122 | } else if(!is.null(title) & is.null(subtitle)) { 123 | p <- p + 124 | draw_text(title, 125 | x = 0, y = widthPitch + 4.5, hjust = 0, vjust = 1, 126 | size = 15, fontface = 'bold', col = colText) + 127 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 128 | } else if(is.null(title) & !is.null(subtitle)) { 129 | p <- p + 130 | draw_text(subtitle, 131 | x = 0, y = widthPitch + 4.5, hjust = 0, vjust = 1, 132 | size = 13, col = colText) + 133 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 134 | } else if(is.null(title) & is.null(subtitle)){ 135 | p <- p + 136 | theme(plot.margin = unit(c(-1.2,-0.9,-0.7,-0.9), "cm")) 137 | } 138 | 139 | 140 | return(p) 141 | 142 | } 143 | -------------------------------------------------------------------------------- /R/soccerPitchFG.R: -------------------------------------------------------------------------------- 1 | #' @include soccerPitchFG.R 2 | #' @import ggplot2 3 | #' @import dplyr 4 | #' @importFrom magrittr "%>%" 5 | #' @importFrom ggforce geom_arc geom_circle 6 | NULL 7 | #' Helper function to draw soccer pitch outlines over an existing ggplot object 8 | #' 9 | #' @description Adds soccer pitch outlines (with transparent fill) to an existing ggplot object (e.g. heatmaps, passing maps, etc..) 10 | #' 11 | #' @param plot an existing ggplot object to add pitch lines layer to 12 | #' @param lengthPitch,widthPitch length and width of pitch in metres 13 | #' @param colPitch colour of pitch markings 14 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 15 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 16 | #' @return a ggplot object 17 | #' 18 | #' @seealso \code{\link{soccerPitch}} for plotting a soccer pitch as background layer 19 | #' @export 20 | soccerPitchFG <- function(plot, lengthPitch = 105, widthPitch = 68, colPitch = "black", arrow = c("none", "r", "l"), title = NULL, subtitle = NULL) { 21 | 22 | lwd <- 0.5 23 | 24 | p <- plot + 25 | geom_rect(aes(xmin = -4, xmax = lengthPitch + 4, ymin = -4, ymax = widthPitch + 4), fill = "NA") + 26 | # outer lines 27 | geom_rect(aes(xmin = 0, xmax = lengthPitch, ymin = 0, ymax = widthPitch), fill = "NA", col = colPitch, lwd = lwd) + 28 | # centre circle 29 | geom_circle(aes(x0 = lengthPitch / 2, y0 = widthPitch / 2, r = 9.15), fill = "NA", col = colPitch, lwd = lwd) + 30 | # kick off spot 31 | geom_circle(aes(x0 = lengthPitch / 2, y0 = widthPitch / 2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 32 | # halfway line 33 | geom_segment(aes(x = lengthPitch / 2, y = 0, xend = lengthPitch / 2, yend = widthPitch), col = colPitch, lwd = lwd) + 34 | # penalty areas 35 | geom_rect(aes(xmin = 0, xmax = 16.5, ymin = widthPitch / 2 - (40.3 / 2), ymax = widthPitch / 2 + (40.3 / 2)), fill = "NA", col = colPitch, lwd = lwd) + 36 | geom_rect(aes(xmin = lengthPitch - 16.5, xmax = lengthPitch, ymin = widthPitch / 2 - (40.3 / 2), ymax = widthPitch / 2 + (40.3 / 2)), fill = "NA", col = colPitch, lwd = lwd) + 37 | # penalty spots 38 | geom_circle(aes(x0 = 11, y0 = widthPitch / 2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 39 | geom_circle(aes(x0 = lengthPitch - 11, y0 = widthPitch / 2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 40 | # penalty arcs 41 | geom_arc(aes(x0= 11, y0 = widthPitch/2, r = 9.15, start = pi/2 + 0.9259284, end = pi/2 - 0.9259284), col = colPitch, lwd = lwd) + 42 | geom_arc(aes(x0 = lengthPitch - 11, y0 = widthPitch/2, r = 9.15, start = pi/2*3 - 0.9259284, end = pi/2*3 + 0.9259284), col = colPitch, lwd = lwd) + 43 | # six yard boxes 44 | geom_rect(aes(xmin = 0, xmax = 5.5, ymin = (widthPitch / 2) - 9.16, ymax = (widthPitch / 2) + 9.16), fill = "NA", col = colPitch, lwd = lwd) + 45 | geom_rect(aes(xmin = lengthPitch - 5.5, xmax = lengthPitch, ymin = (widthPitch / 2) - 9.16, ymax = (widthPitch / 2) + 9.16), fill = "NA", col = colPitch, lwd = lwd) + 46 | # goals 47 | geom_rect(aes(xmin = -2, xmax = 0, ymin = (widthPitch / 2) - 3.66, ymax = (widthPitch / 2) + 3.66), fill = "NA", col = colPitch, lwd = lwd) + 48 | geom_rect(aes(xmin = lengthPitch, xmax = lengthPitch + 2, ymin = (widthPitch / 2) - 3.66, ymax = (widthPitch / 2) + 3.66), fill = "NA", col = colPitch, lwd = lwd) + 49 | theme(rect = element_blank(), 50 | line = element_blank(), 51 | axis.text = element_blank(), 52 | axis.title = element_blank()) 53 | 54 | # add title and/or subtitle 55 | if(title & subtitle) { 56 | p <- p + 57 | theme(plot.margin = unit(c(-0.525,-0.9,-0.7,-0.9), "cm")) 58 | } else if(title & !subtitle) { 59 | p <- p + 60 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 61 | } else if(!title & subtitle) { 62 | p <- p + 63 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 64 | } else if(!title & !subtitle) { 65 | p <- p + 66 | theme(plot.margin = unit(c(-1.2,-0.9,-0.7,-0.9), "cm")) 67 | } 68 | 69 | return(p) 70 | 71 | } 72 | -------------------------------------------------------------------------------- /R/soccerPitchHalf.R: -------------------------------------------------------------------------------- 1 | #' @include soccerPitch.R 2 | #' @include soccerShotmap.R 3 | #' @import ggplot2 4 | #' @import dplyr 5 | #' @importFrom magrittr "%>%" 6 | #' @importFrom ggforce geom_arc geom_circle 7 | #' @importFrom cowplot draw_text 8 | NULL 9 | #' Draws a vertical half soccer pitch for the purpose of plotting shotmaps 10 | #' 11 | #' @description Adds soccer pitch outlines (with transparent fill) to an existing ggplot object (e.g. heatmaps, passing maps, etc..) 12 | #' 13 | #' @param lengthPitch,widthPitch length and width of pitch in metres 14 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 15 | #' @param theme palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass}; 16 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 17 | #' @param data a default dataset for plotting in subsequent layers; NULL by default 18 | #' @return a ggplot object 19 | #' @seealso \code{\link{soccerShotmap}} for plotting a shotmap on a half pitch for a single player or \code{\link{soccerPitch}} for drawing a full size soccer pitch 20 | #' @examples 21 | #' library(ggplot2) 22 | #' library(dplyr) 23 | #' data(statsbomb) 24 | #' 25 | #' # normalise data, get non-penalty shots for France, 26 | #' # add boolean variable 'goal' for plotting 27 | #' my_df <- statsbomb %>% 28 | #' soccerTransform(method = 'statsbomb') %>% 29 | #' filter(team.name == "France" & 30 | #' type.name == "Shot" & 31 | #' shot.type.name != 'penalty') %>% 32 | #' mutate(goal = as.factor(if_else(shot.outcome.name == "Goal", 1, 0))) 33 | #' 34 | #' soccerPitchHalf(data = my_df, theme = 'light') + 35 | #' geom_point(aes(x = location.y, y = location.x, 36 | #' size = shot.statsbomb_xg, colour = goal), 37 | #' alpha = 0.7) 38 | #' 39 | #' @export 40 | soccerPitchHalf <- function(lengthPitch = 105, widthPitch = 68, arrow = c("none", "r", "l"), theme = c("light", "dark", "grey", "grass"), title = NULL, subtitle = NULL, data = NULL) { 41 | start<-end<-NULL 42 | 43 | # define colours by theme 44 | if(theme[1] == "grass") { 45 | fill1 <- "#008000" 46 | fill2 <- "#328422" 47 | colPitch <- "grey85" 48 | arrowCol <- "white" 49 | colText <- "white" 50 | } else if(theme[1] == "light") { 51 | fill1 <- "white" 52 | fill2 <- "white" 53 | colPitch <- "grey60" 54 | arrowCol = "black" 55 | colText <- "black" 56 | } else if(theme[1] %in% c("grey", "gray")) { 57 | fill1 <- "#A3A1A3" 58 | fill2 <- "#A3A1A3" 59 | colPitch <- "white" 60 | arrowCol <- "white" 61 | colText <- "black" 62 | } else { 63 | fill1 <- "#1a1e2c" 64 | fill2 <- "#1a1e2c" 65 | colPitch <- "#F0F0F0" 66 | arrowCol <- "#F0F0F0" 67 | colText <- "#F0F0F0" 68 | } 69 | lwd <- 0.5 70 | 71 | # outer border (t,r,b,l) 72 | border <- c(12, 6, 1, 6) 73 | 74 | # mowed grass lines 75 | lines <- (lengthPitch + border[2] + border[4]) / 13 76 | boxes <- data.frame(start = lines * 0:12 - border[4], end = lines * 1:13 - border[2])[seq(2, 12, 2),] 77 | 78 | # draw pitch 79 | p <- ggplot(data) + 80 | # background 81 | geom_rect(aes(xmin = -border[4], xmax = widthPitch + border[2], ymin = lengthPitch/2 - border[3], ymax = lengthPitch + border[1]), fill = fill1) + 82 | # mowed pitch lines 83 | geom_rect(data = boxes, aes(ymin = start, ymax = end, xmin = -border[4], xmax = widthPitch + border[2]), fill = fill2) + 84 | # perimeter line 85 | geom_rect(aes(xmin = 0, xmax = widthPitch, ymin = lengthPitch/2, ymax = lengthPitch), fill = NA, col = colPitch, lwd = lwd) + 86 | # centre circle 87 | geom_arc(aes(x0 = widthPitch/2, y0 = lengthPitch/2, r = 9.15, start = pi/2, end = -pi/2), col = colPitch, lwd = lwd) + 88 | # kick off spot 89 | geom_circle(aes(x0 = widthPitch/2, y0 = lengthPitch/2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 90 | # halfway line 91 | geom_segment(aes(x = 0, y = lengthPitch/2, xend = widthPitch, yend = lengthPitch/2), col = colPitch, lwd = lwd) + 92 | # penalty arc 93 | geom_arc(aes(x0 = widthPitch/2, y0 = lengthPitch - 11, r = 9.15, start = pi * 0.705, end = 1.295 * pi), col = colPitch, lwd = lwd) + 94 | # penalty area 95 | geom_rect(aes(xmin = widthPitch/2 - 20.15, xmax = widthPitch/2 + 20.15, ymin = lengthPitch - 16.5, ymax = lengthPitch), fill = NA, col = colPitch, lwd = lwd) + 96 | # penalty spot 97 | geom_circle(aes(x0 = widthPitch/2, y0 = lengthPitch - 11, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) + 98 | # six yard box 99 | geom_rect(aes(xmin = (widthPitch/2) - 9.16, xmax = (widthPitch/2) + 9.16, ymin = lengthPitch - 5.5, ymax = lengthPitch), fill = NA, col = colPitch, lwd = lwd) + 100 | # goal 101 | geom_rect(aes(xmin = (widthPitch/2) - 3.66, xmax = (widthPitch/2) + 3.66, ymin = lengthPitch, ymax = lengthPitch + 2), fill = NA, col = colPitch, lwd = lwd) + 102 | coord_fixed(ylim = c(lengthPitch/2 - border[3], lengthPitch + border[1])) + 103 | theme(rect = element_blank(), 104 | line = element_blank(), 105 | axis.text = element_blank(), 106 | axis.title = element_blank()) 107 | 108 | # add title and/or subtitle 109 | theme_buffer <- ifelse(theme[1] == "light", 0, 4) 110 | if(!is.null(title) & !is.null(subtitle)) { 111 | p <- p + 112 | draw_text(title, 113 | x = widthPitch/2, y = lengthPitch + 10, hjust = 0.5, vjust = 1, 114 | size = 15, fontface = 'bold', col = colText) + 115 | draw_text(subtitle, 116 | x = widthPitch/2, y = lengthPitch + 6.5, hjust = 0.5, vjust = 1, 117 | size = 13, col = colText) + 118 | theme(plot.margin = unit(c(-0.7,-1.4,-0.7,-1.4), "cm")) 119 | } else if(!is.null(title) & is.null(subtitle)) { 120 | p <- p + 121 | draw_text(title, 122 | x = widthPitch/2, y = lengthPitch + 6.5, hjust = 0.5, vjust = 1, 123 | size = 15, fontface = 'bold', col = colText) + 124 | theme(plot.margin = unit(c(-1.2,-1.4,-0.7,-1.4), "cm")) 125 | } else if(is.null(title) & !is.null(subtitle)) { 126 | p <- p + 127 | draw_text(subtitle, 128 | x = widthPitch/2, y = lengthPitch + 6.5, hjust = 0.5, vjust = 1, 129 | size = 13, col = colText) + 130 | theme(plot.margin = unit(c(-1.2,-1.4,-0.7,-1.4), "cm")) 131 | } else if(is.null(title) & is.null(subtitle)){ 132 | p <- p + 133 | theme(plot.margin = unit(c(-1.85,-1.4,-0.7,-1.4), "cm")) 134 | } 135 | 136 | 137 | return(p) 138 | 139 | } 140 | -------------------------------------------------------------------------------- /R/soccerPositionMap.R: -------------------------------------------------------------------------------- 1 | #' @include soccerPitch.R 2 | #' @import ggplot2 3 | #' @import dplyr 4 | #' @importFrom magrittr "%>%" 5 | #' @importFrom ggrepel geom_text_repel geom_label_repel 6 | NULL 7 | #' Plot average player position using any event or tracking data 8 | #' @description Draws the average x,y-positions of each player from one or both teams on a soccer pitch. 9 | #' 10 | #' @param df a dataframe containing x,y-coordinates of player position and a player identifier variable 11 | #' @param lengthPitch,widthPitch numeric, length and width of pitch in metres 12 | #' @param fill1,fill2 character, fill colour of position points of team 1, team 2 (team 2 \code{NULL} by default) 13 | #' @param col1,col2 character, border colour of position points of team 1, team 2 (team 2 \code{NULL} by default) 14 | #' @param labelCol character, label text colour 15 | #' @param homeTeam if \code{df} contains two teams, the name of the home team to be displayed on the left hand side of the pitch (i.e. attacking from left to right). If \code{NULL}, infers home team as the team of the first event in \code{df}. 16 | #' @param flipAwayTeam flip x,y-coordinates of away team so attacking from right to left 17 | #' @param label type of label to draw, player names (\code{name}), jersey numbers (\code{number}), or \code{none} 18 | #' @param labelBox add box around label text 19 | #' @param shortNames shorten player names to display last name as label 20 | #' @param nodeSize numeric, size of position points 21 | #' @param labelSize numeric, size of labels 22 | #' @param arrow optional, adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}) 23 | #' @param theme draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch 24 | #' @param title,subtitle optional, adds title and subtitle to plot 25 | #' @param source if \code{statsbomb}, uses StatsBomb definitions of required variable names (i.e. `location.x`, `location.y`, `player.id`, `team.name`); if \code{manual} (default), respects variable names defined in function arguments \code{x}, \code{y}, \code{id}, \code{name}, and \code{team}. 26 | #' @param x,y,id,name,team names of variables containing x,y-coordinates, unique player ids, player names, and team names, respectively; \code{name} and \code{team} NULL by default 27 | #' @examples 28 | #' library(dplyr) 29 | #' data(statsbomb) 30 | #' 31 | #' # average player position from tracking data for one team 32 | #' # w/ jersey numbers labelled 33 | #' data(tromso) 34 | #' tromso %>% 35 | #' soccerPositionMap(label = "number", id ="id", 36 | #' labelCol = "white", nodeSize = 8, 37 | #' arrow = "r", theme = "grass", 38 | #' title = "Tromso IL (vs. Stromsgodset, 3rd Nov 2013)", 39 | #' subtitle = "Average player position (1' - 16')") 40 | #' 41 | #' # transform x,y-coords, standarise column names, 42 | #' # average pass position for one team using 'statsbomb' method 43 | #' # w/ player name as labels 44 | #' statsbomb %>% 45 | #' soccerTransform(method='statsbomb') %>% 46 | #' filter(type.name == "Pass" & team.name == "France" & period == 1) %>% 47 | #' soccerPositionMap(source = "statsbomb", 48 | #' fill1 = "blue", arrow = "r", theme = "light", 49 | #' title = "France (vs Argentina, 30th June 2018)", 50 | #' subtitle = "Average pass position (1' - 45')") 51 | #' 52 | #' # transform x,y-coords, standarise column names, 53 | #' # average pass position for two teams using 'manual' method 54 | #' # w/ player names labelled 55 | #' statsbomb %>% 56 | #' soccerTransform(method='statsbomb') %>% 57 | #' soccerStandardiseCols(method='statsbomb') %>% 58 | #' filter(event_name == "Pass" & period == 1) %>% 59 | #' soccerPositionMap(fill1 = "lightblue", fill2 = "blue", 60 | #' title = "Argentina vs France, 30th June 2018", 61 | #' subtitle = "Average pass position (1' - 45')") 62 | #' 63 | #' @export 64 | soccerPositionMap <- function(df, lengthPitch = 105, widthPitch = 68, fill1 = "red", col1 = NULL, fill2 = "blue", col2 = NULL, labelCol = "black", homeTeam = NULL, flipAwayTeam = TRUE, label = c("name", "number", "none"), labelBox = TRUE, shortNames = TRUE, nodeSize = 5, labelSize = 4, arrow = c("none", "r", "l"), theme = c("light", "dark", "grey", "grass"), title = NULL, subtitle = NULL, source = c("manual", "statsbomb"), x = "x", y = "y", id = "player_id", name = "player_name", team = "team_name") { 65 | x.mean<-y.mean<-NULL 66 | 67 | # define colours by theme 68 | if(theme[1] == "grass") { 69 | colText <- "white" 70 | } else if(theme[1] == "light") { 71 | colText <- "black" 72 | } else if(theme[1] %in% c("grey", "gray")) { 73 | colText <- "black" 74 | } else { 75 | colText <- "white" 76 | } 77 | if(is.null(col1)) col1 <- fill1 78 | if(is.null(col2)) col2 <- fill2 79 | 80 | # ensure input is dataframe 81 | df <- as.data.frame(df) 82 | 83 | # set variable names 84 | if(source[1] == "statsbomb") { 85 | x <- "location.x" 86 | y <- "location.y" 87 | id <- "player.id" 88 | team <- "team.name" 89 | name <- "player.name" 90 | } 91 | 92 | df$x <- df[,x] 93 | df$y <- df[,y] 94 | df$id <- df[,id] 95 | if(!name %in% colnames(df)) { 96 | name <- id 97 | } 98 | df$name <- df[,name] 99 | if(team %in% colnames(df)) { 100 | df$team <- df[,team] 101 | } else { 102 | team <- "Team A" 103 | df$team <- team 104 | } 105 | 106 | # shorten player name 107 | if(!is.null(name) & shortNames == TRUE) { 108 | df$name <- soccerShortenName(df$name) 109 | } 110 | 111 | # if two teams in df 112 | if(length(unique(df$team)) > 1) { 113 | 114 | # home team taken as first team in df if unspecified 115 | if(is.null(homeTeam)) homeTeam <- df[,team][1] 116 | 117 | # flip x,y-coordinates of home team 118 | if(flipAwayTeam) { 119 | df <- df %>% 120 | soccerFlipDirection(teamToFlip = homeTeam, periodToFlip = 1:2) 121 | } 122 | 123 | # get average positions 124 | pos <- df %>% 125 | group_by(team, id, name) %>% 126 | dplyr::summarise(x.mean = mean(x), y.mean = mean(y)) %>% 127 | ungroup() %>% 128 | mutate(team = as.factor(team), id = as.factor(id)) %>% 129 | as.data.frame() 130 | 131 | # plot 132 | p <- soccerPitch(theme = theme[1], title = title, subtitle = subtitle) + 133 | geom_point(data = pos, aes(x.mean, y.mean, group = team, fill = team, colour = team), shape = 21, size = 6, stroke = 1.3) + 134 | scale_colour_manual(values = c(col1, col2)) + 135 | scale_fill_manual(values = c(fill1, fill2)) + 136 | guides(colour="none", fill="none") 137 | 138 | # if one team 139 | } else { 140 | # get average positions 141 | pos <- df %>% 142 | group_by(id, name) %>% 143 | dplyr::summarise(x.mean = mean(x), y.mean = mean(y)) %>% 144 | # ungroup() %>% 145 | # mutate(id = as.factor(id)) %>% 146 | as.data.frame() 147 | 148 | # plot 149 | p <- soccerPitch(arrow = arrow, theme = theme[1], title = title, subtitle = subtitle) + 150 | geom_point(data = pos, aes(x.mean, y.mean), col = col1, fill = fill1, shape = 21, size = nodeSize, stroke = 1.3) 151 | } 152 | 153 | # add non-overlapping names as labels 154 | if(label[1] == "name") { 155 | if(labelBox) { 156 | p <- p + 157 | geom_label_repel(data = pos, aes(x.mean, y.mean, label = name), segment.colour = colText, segment.size = 0.3, max.iter = 1000, size = labelSize, fontface = "bold") 158 | } else { 159 | p <- p + 160 | geom_text_repel(data = pos, aes(x.mean, y.mean, label = name), col = labelCol, segment.colour = colText, segment.size = 0.3, max.iter = 1000, size = labelSize, fontface = "bold") 161 | } 162 | # add jersey numbers directly to points 163 | } else if(label[1] == "number") { 164 | p <- p + 165 | geom_text(data = pos, aes(x.mean, y.mean, label = name), col = labelCol, fontface = "bold") 166 | } 167 | 168 | return(p) 169 | 170 | } 171 | -------------------------------------------------------------------------------- /R/soccerResample.R: -------------------------------------------------------------------------------- 1 | #' @import ggplot2 2 | #' @import dplyr 3 | #' @importFrom zoo na.approx 4 | #' @importFrom plyr rbind.fill 5 | #' @importFrom xts xts 6 | NULL 7 | #' Resample the frames per second of any tracking data using linear interpolation 8 | #' 9 | #' @description Downsample or upsample any tracking data containing x,y,t data using linear interpolation of x,y-coordinates (plus constant interpolation of all other variables in dataframe) 10 | #' 11 | #' @param df a dataframe containing x,y-coordinates and time variable 12 | #' @param r resampling rate in frames per second 13 | #' @param x,y name of variables containing x,y-coordinates 14 | #' @param t name of variable containing time data 15 | #' @param id name of variable containing player identifier 16 | #' @return a dataframe with interpolated rows added 17 | #' @examples 18 | #' data(tromso) 19 | #' 20 | #' # resample tromso dataset from ~21 fps to 10 fps 21 | #' soccerResample(tromso, r=10) 22 | #' 23 | #' @export 24 | soccerResample <- function(df, r = 10, x = "x", y = "y", t = "t", id = "id") { 25 | Index<-NULL 26 | 27 | # create new time index 28 | time.index <- seq(min(df[,t]), max(df[,t]), by = as.difftime(1/r, units='secs')) 29 | 30 | # remove all rows that have duplicated timestamps 31 | df <- df %>% 32 | group_by(!!sym(id)) %>% 33 | filter(!(duplicated(!!sym(t)) | duplicated(!!sym(t), fromLast = TRUE))) %>% 34 | ungroup() 35 | 36 | # resample and interpolate for each id 37 | ids <- as.numeric(as.vector(unique(df[[id]]))) 38 | df_resampled <- lapply(ids, function(i) { 39 | #subset 40 | ss <- df[df[,id] == i,] 41 | 42 | # convert data to xts object 43 | ss.xts <- xts(ss[, names(ss) != t], ss[[t]]) 44 | 45 | # join to time index 46 | ss.join <- merge(ss.xts, time.index, all=TRUE) %>% 47 | ggplot2::fortify() %>% 48 | rename_at(vars(Index),~"t") 49 | 50 | # linear interpolatation of x,y-coords with omission of leading / lagging NAs; constant interpolation of other variables 51 | ss.join %>% 52 | mutate_at(vars(-one_of(t, x, y)), function(x) na.approx(x, method = "constant", na.rm=FALSE)) %>% 53 | mutate_at(vars(one_of(x, y)), function(x) na.approx(x, na.rm=FALSE)) %>% 54 | filter(!!sym(t) %in% time.index) 55 | 56 | }) %>% 57 | plyr::rbind.fill() 58 | 59 | # generate frame variable 60 | time.index2 <- data.frame(t = time.index, frame = 1:length(time.index)) 61 | names(time.index2)[names(time.index2) == "t"] <- t 62 | df_resampled <- left_join(df_resampled, time.index2, by = t) 63 | 64 | return(df_resampled) 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /R/soccerShortenName.R: -------------------------------------------------------------------------------- 1 | #' Extract player surname 2 | #' @description Helper function to extract last name (including common nobiliary particles) from full player names 3 | #' 4 | #' @param names vector of strings containing full player name 5 | #' @examples 6 | #' data(statsbomb) 7 | #' statsbomb$name <- soccerShortenName(statsbomb$player.name) 8 | #' 9 | #' @export 10 | soccerShortenName <- function(names) { 11 | 12 | # collapse special cases with >1 prefix separated by a space 13 | names <- sub("van der", "_vander_", names) 14 | names <- sub("van de", "_vande_", names) 15 | 16 | # define prefixes 17 | prefixes <- " (Di|di|De|de|El|el|Da|da|Dos|dos|Van|van|Von|von|Le|le|La|la|N'|_vander_|_vande_) " 18 | 19 | # remove all of string before last name and any defined prefixes 20 | names <- sub(":", " ", sub(".* ", "", sub(prefixes, " \\1:", names))) 21 | 22 | # expand special cases 23 | names <- sub("_vander_", "van der", names) 24 | names <- sub("_vande_", "van de", names) 25 | 26 | return(names) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /R/soccerShotmap.R: -------------------------------------------------------------------------------- 1 | #' @include soccerPitch.R 2 | #' @include soccerPitchHalf.R 3 | #' @import ggplot2 4 | #' @import dplyr 5 | #' @importFrom magrittr "%>%" 6 | #' @importFrom cowplot draw_text 7 | NULL 8 | #' Draw an individual, team, or two team shotmap using StatsBomb data 9 | #' 10 | #' @description If \code{df} contains two teams, draws a shotmap of each team at either end of a full pitch. If \code{df} contains one or more players from a single team, draws a vertical half pitch. Currently only works with StatsBomb data but compatability with other (non-StatsBomb) shot data will be added soon. 11 | #' 12 | #' @param df dataframe containing x,y-coordinates of player passes 13 | #' @param lengthPitch,widthPitch length and width of pitch, in metres 14 | #' @param homeTeam if \code{df} contains two teams, the name of the home team to be displayed on the left hand side of the pitch. If \code{NULL}, infers home team as the team of the first event in \code{df}. 15 | #' @param adj adjust xG using conditional probability to account for multiple shots per possession 16 | #' @param n_players number of highest xG players to display 17 | #' @param size_lim minimum and maximum size of points, \code{c(min, max)} 18 | #' @param theme draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch with appropriate point colours 19 | #' @param title,subtitle optional, adds title and subtitle to half pitch plot. Title defaults to scoreline and team identity when two teams are defined in \code{df}. 20 | #' @return a ggplot object 21 | #' @examples 22 | #' data(statsbomb) 23 | #' 24 | #' # shot map of two teams on full pitch 25 | #' statsbomb %>% 26 | #' soccerTransform(method='statsbomb') %>% 27 | #' soccerShotmap(theme = "gray") 28 | #' 29 | #' # shot map of one player on half pitch 30 | #' statsbomb %>% 31 | #' dplyr::filter(player.name == "Antoine Griezmann") %>% 32 | #' soccerTransform(method='statsbomb') %>% 33 | #' soccerShotmap(theme = "grass", 34 | #' title = "Antoine Griezmann", 35 | #' subtitle = "vs. Argentina, World Cup 2018") 36 | #' 37 | #' @export 38 | soccerShotmap <- function(df, lengthPitch = 105, widthPitch = 68, homeTeam = NULL, adj = TRUE, n_players = 0, size_lim = c(2,15), title = NULL, subtitle = NULL, theme = c("light", "dark", "grey", "grass")) { 39 | shot.type.name<-team.name<-shot.statsbomb_xg<-type.name<-shot.outcome<-penalty<-possession<-xg_cond<-xg_adj<-size<-location.x<-location.y<-player.name<-name<-rowid<-x<-y<-label<-hjust<-.<-position_name<-shot.outcome.name<-position.name<-NULL 40 | 41 | # define colours by theme 42 | if(theme[1] == "grass") { 43 | colGoal <- "#E77100" 44 | colMiss <- "#234987" 45 | colText <- "white" 46 | } else if(theme[1] == "light") { 47 | colGoal <- "#E77100" 48 | colMiss <- "#93a5c1" 49 | colText <- "black" 50 | } else if(theme[1] %in% c("grey", "gray")) { 51 | colGoal <- "#efa340" 52 | colMiss <- "#4c6896" 53 | colText <- "black" 54 | } else { 55 | colGoal <- "#E77100" 56 | colMiss <- "#88adea" 57 | colText <- "white" 58 | } 59 | 60 | # ensure input is dataframe 61 | df <- as.data.frame(df) 62 | 63 | # full pitch shotmap for two teams 64 | if(length(unique(df$team.name)) > 1) { 65 | 66 | # home team taken as first team in df if unspecified 67 | if(is.null(homeTeam)) homeTeam <- df$team.name[1] 68 | awayTeam <- unique(df$team.name)[unique(df$team.name) != homeTeam] 69 | 70 | # flip x,y-coordinates of home team and factorise variables 71 | df <- df %>% 72 | soccerFlipDirection(teamToFlip = homeTeam, x = "location.x", y = "location.y", team = "team.name") %>% 73 | mutate(shot.outcome = as.factor(if_else(shot.outcome.name == "Goal", 1, 0)), 74 | penalty = as.factor(if_else(shot.type.name == "Penalty", 1, 0)), 75 | team.name = factor(team.name, levels = c(homeTeam, awayTeam))) %>% 76 | rename(xg = shot.statsbomb_xg) 77 | 78 | # actual goals (including own goals) 79 | goals <- df %>% 80 | group_by(team.name) %>% 81 | filter(type.name == "Shot") %>% 82 | dplyr::summarise(g = length(shot.outcome.name[shot.outcome.name == "Goal"]) + length(type.name[type.name == "Own Goal For"])) 83 | 84 | # penalties 85 | pen_totals <- df %>% 86 | group_by(team.name) %>% 87 | filter(type.name == "Shot") %>% 88 | dplyr::summarise(pen = length(shot.outcome[penalty == 1 & shot.outcome == 1])) 89 | 90 | # own goals 91 | og_totals <- df %>% 92 | group_by(team.name) %>% 93 | dplyr::summarise(og = length(type.name[type.name == "Own Goal For"])) 94 | 95 | # adjust xG using conditional probability when there are multiple shots in a single possession 96 | if(adj) { 97 | df <- df %>% 98 | filter(type.name == "Shot" & penalty == 0) %>% 99 | group_by(team.name, possession) %>% 100 | mutate(xg_cond = (1 - prod(1 - xg))) %>% 101 | mutate(xg_adj = xg_cond * (xg / sum(xg))) %>% 102 | ungroup() %>% 103 | select(-xg, -xg_cond) %>% 104 | rename(xg = xg_adj) 105 | } 106 | 107 | # expected goals 108 | xg_totals <- df %>% 109 | filter(penalty == 0) %>% 110 | group_by(team.name) %>% 111 | dplyr::summarise(xg = sum(xg)) 112 | 113 | # labels 114 | score1 <- goals$g[1] + og_totals$og[1] 115 | score2 <- goals$g[2] + og_totals$og[2] 116 | 117 | xg1 <- sprintf("%.2f", xg_totals$xg[1]) 118 | if(pen_totals$pen[1] > 0 & og_totals$og[1] == 0) { 119 | xg1 <- paste0("(+", pen_totals$pen[1], " P) ", xg1) 120 | } else if(pen_totals$pen[1] == 0 & og_totals$og[1] > 0) { 121 | xg1 <- paste0("(+", og_totals$og[1], " OG) ", xg1) 122 | } else if(pen_totals$pen[1] > 0 & og_totals$og[1] > 0) { 123 | xg1 <- paste0("(+", pen_totals$pen[1], " P, +", og_totals$og[1], " OG) ", xg1) 124 | } 125 | xg2 <- sprintf("%.2f", xg_totals$xg[2]) 126 | if(pen_totals$pen[2] > 0) { 127 | xg2 <- paste0(xg2, " (+", pen_totals$pen[2], " P)") 128 | } else if(pen_totals$pen[2] == 0 & og_totals$og[2] > 0) { 129 | xg2 <- paste0(xg2, " (+", og_totals$og[2], " OG)") 130 | } else if(pen_totals$pen[2] > 0 & og_totals$og[2] > 0) { 131 | xg2 <- paste0(xg2, " (+", pen_totals$pen[2], " P, +", og_totals$og[2], " OG) )") 132 | } 133 | 134 | # subset shots for plotting 135 | df <- df %>% 136 | filter(type.name == "Shot" & penalty == 0) %>% 137 | mutate(size = scales::rescale(xg, size_lim, c(0, 1))) %>% 138 | arrange(as.numeric(shot.outcome), size) 139 | 140 | # plot 141 | p <- soccerPitch(lengthPitch, widthPitch, theme = theme[1]) + 142 | geom_point(data = df, aes(x = location.x, y = location.y, size = size, colour = shot.outcome), alpha = 0.8) + 143 | scale_size_identity() + 144 | scale_colour_manual(name = "Outcome", breaks = c(0,1), values = c(colMiss, colGoal)) + 145 | guides(colour="none", size="none") 146 | 147 | # add labels 148 | p <- p + 149 | draw_text(paste0(xg_totals$team.name[1], " ", score1), x = lengthPitch / 2 - 1, y = widthPitch + 5, hjust = 1, vjust = 1, size = 15, fontface = 'bold', colour = colText) + 150 | draw_text(":", x = lengthPitch / 2, y = widthPitch + 5, hjust = 0.5, vjust = 1, size = 15, fontface = 'bold', colour = colText) + 151 | draw_text(paste0(score2, " ", xg_totals$team.name[2]), x = lengthPitch / 2 + 1, y = widthPitch + 5, hjust = 0, vjust = 1, size = 15, fontface = 'bold', colour = colText) + 152 | draw_text(xg1, x = lengthPitch / 2 - 1, y = widthPitch - 5, hjust = 1, vjust = 0, size = 15, colour = colText) + 153 | draw_text(xg2, x = lengthPitch / 2 + 1, y = widthPitch - 5, hjust = 0, vjust = 0, size = 15, colour = colText) + 154 | theme(plot.margin = unit(c(-0.9,-0.9,-0.7,-0.9), "cm")) 155 | 156 | # top xG by player 157 | if(n_players > 0) { 158 | top_xgs <- df %>% 159 | group_by(player.name, team.name) %>% 160 | summarise(xg = sum(xg, na.rm=T)) %>% 161 | ungroup() %>% 162 | mutate(name = soccerShortenName(player.name)) %>% 163 | arrange(-xg) %>% 164 | utils::head(n_players) %>% 165 | group_by(team.name) %>% 166 | mutate(rowid = 1:n()) %>% 167 | ungroup() %>% 168 | mutate(label = if_else(team.name == homeTeam, 169 | sprintf("%s %s", name, sprintf("%.2f", xg)), 170 | sprintf("%s %s", sprintf("%.2f", xg), name)), 171 | x = if_else(team.name == homeTeam, lengthPitch/2 - 1, lengthPitch/2 + 1), 172 | hjust = if_else(team.name == homeTeam, 1, 0), 173 | y = widthPitch - 5 - (rowid * 2.5)) 174 | 175 | p <- p + 176 | geom_text(data = top_xgs[top_xgs$team.name == homeTeam,], aes(x, y, label = label, hjust = hjust), size = 4, colour = colText) + 177 | geom_text(data = top_xgs[top_xgs$team.name != homeTeam,], aes(x, y, label = label, hjust = hjust), size = 4, colour = colText) 178 | } 179 | 180 | # half pitch if one team 181 | } else { 182 | 183 | df <- df %>% 184 | mutate(shot.outcome = as.factor(if_else(shot.outcome.name == "Goal", 1, 0)), 185 | penalty = as.factor(if_else(shot.type.name == "Penalty", 1, 0))) %>% 186 | rename(xg = shot.statsbomb_xg) 187 | 188 | # goals 189 | goals <- df %>% 190 | filter(type.name == "Shot") %>% 191 | dplyr::summarise(g = length(shot.outcome.name[shot.outcome.name == "Goal"]) + length(type.name[type.name == "Own Goal For"])) 192 | 193 | # penalties 194 | pen_totals <- df %>% 195 | filter(type.name == "Shot") %>% 196 | dplyr::summarise(pen = length(shot.outcome[penalty == 1 & shot.outcome == 1])) 197 | 198 | # adjust xG using conditional probability when multiple shots in a single possession 199 | if(adj) { 200 | df <- df %>% 201 | filter(type.name == "Shot" & penalty == 0) %>% 202 | group_by(team.name, possession) %>% 203 | mutate(xg_cond = (1 - prod(1 - xg))) %>% 204 | mutate(xg_adj = xg_cond * (xg / sum(xg))) %>% 205 | ungroup() %>% 206 | select(-xg, -xg_cond) %>% 207 | rename(xg = xg_adj) 208 | } 209 | 210 | # expected goals 211 | xg <- df %>% 212 | filter(penalty == 0) %>% 213 | dplyr::summarise(xg = sum(xg)) %>% 214 | pull %>% 215 | sprintf("%.2f", .) 216 | 217 | df <- df %>% 218 | filter(type.name == "Shot" & penalty == 0) %>% 219 | mutate(size = scales::rescale(xg, size_lim, c(0, 1))) %>% 220 | arrange(as.numeric(shot.outcome), size) 221 | 222 | p <- soccerPitchHalf(lengthPitch, widthPitch, theme = theme[1], title = title, subtitle = subtitle) + 223 | geom_point(data = df, aes(x = location.y, y = location.x, size = size, colour = shot.outcome), alpha = 0.7) + 224 | scale_size_identity() + 225 | scale_colour_manual(name = "Outcome", breaks = c(0,1), values = c(colMiss, colGoal)) + 226 | guides(colour="none", size="none") 227 | 228 | # top xG by player 229 | if(n_players > 0) { 230 | top_xgs <- df %>% 231 | group_by(player.name, position.name) %>% 232 | summarise(xg = sum(xg, na.rm=T)) %>% 233 | ungroup() %>% 234 | mutate(name = soccerShortenName(player.name)) %>% 235 | arrange(-xg) %>% 236 | slice(1:n_players) %>% 237 | arrange(xg) %>% 238 | mutate(rowid = 1:n()) %>% 239 | mutate(label = sprintf("%s %s", sprintf("%.2f", xg), name), 240 | y = lengthPitch/2 + (rowid * 2.5)) 241 | 242 | p <- p + 243 | geom_text(data = top_xgs, aes(x = 2, y, label = label, hjust = 0), size = 4, colour = colText) + 244 | annotate("text", x = 2, y = lengthPitch/2 + 6 + max(top_xgs$rowid * 2.5), label = paste0("Goals: ", goals$g), hjust = 0, vjust = 0, size = 5, colour = colText) + 245 | annotate("text", x = 2, y = lengthPitch/2 + 2 + max(top_xgs$rowid * 2.5), label = paste0("xG: ", xg), hjust = 0, vjust = 0, size = 5, colour = colText) 246 | } else { 247 | p <- p + 248 | annotate("text", x = 2, y = lengthPitch/2 + 6, label = paste0("Goals: ", goals$g), hjust = 0, vjust = 0, size = 5, colour = colText) + 249 | annotate("text", x = 2, y = lengthPitch/2 + 2, label = paste0("xG: ", xg), hjust = 0, vjust = 0, size = 5, colour = colText) 250 | } 251 | 252 | } 253 | 254 | return(p) 255 | 256 | } 257 | -------------------------------------------------------------------------------- /R/soccerSpokes.R: -------------------------------------------------------------------------------- 1 | #' @include soccerHeatmap.R 2 | #' @include soccerFlow.R 3 | #' @import ggplot2 4 | #' @import dplyr 5 | NULL 6 | #' Draw spokes of passing direction on a soccer pitch 7 | #' 8 | #' @description Multiple arrows to show the distribution of pass angle and distance in zones of the pitch; similar to a radar plot but grouped by pitch location rather than player 9 | #' 10 | #' @param df a dataframe of event data containing fields of start x,y-coordinates, pass distance, and pass angle 11 | #' @param lengthPitch,widthPitch numeric, length and width of pitch in metres 12 | #' @param xBins,yBins integer, the number of horizontal (length-wise) and vertical (width-wise) bins the soccer pitch is to be divided up into; if \code{yBins} is NULL (default), it will take the value of \code{xBins} 13 | #' @param angleBins integer, the number of arrows to draw in each zone of the pitch; for example, a value of 4 clusters has direction vectors up, down, left, and right 14 | #' @param x,y,angle names of variables containing pass start x,y-coordinates and angle 15 | #' @param minLength numeric, ratio between size of shortest arrow and longest arrow depending on number of events 16 | #' @param minAlpha,minWidth numeric, minimum alpha and line width of arrows drawn 17 | #' @param col colour of arrows 18 | #' @param legend if \code{TRUE}, adds legend for arrow transparency 19 | #' @param arrow adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default 20 | #' @param title,subtitle adds title and subtitle to plot; NULL by default 21 | #' @param theme palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass} 22 | #' @param plot base plot to add path layer to; NULL by default 23 | #' @return a ggplot object of a heatmap on a soccer pitch 24 | #' @examples 25 | #' library(dplyr) 26 | #' data(statsbomb) 27 | #' 28 | #' # transform x,y-coords, filter only France pass events, 29 | #' # draw flow field showing mean angle, distance of passes per pitch zone 30 | #' statsbomb %>% 31 | #' soccerTransform(method = 'statsbomb') %>% 32 | #' filter(team.name == "France" & type.name == "Pass") %>% 33 | #' soccerSpokes(xBins=7, yBins=5, angleBins=12, legend=FALSE) 34 | #' 35 | #' # transform x,y-coords, standarise column names, 36 | #' # filter only France pass events 37 | #' my_df <- statsbomb %>% 38 | #' soccerTransform(method = 'statsbomb') %>% 39 | #' soccerStandardiseCols(method = 'statsbomb') %>% 40 | #' filter(team_name == "France" & event_name == "Pass") 41 | #' 42 | #' # overlay flow field onto heatmap showing proportion of team passes per pitch zone 43 | #' soccerHeatmap(my_df, xBins=7, yBins=5, 44 | #' title = "France passing radar") %>% 45 | #' soccerSpokes(my_df, xBins=7, yBins=5, angleBins=8, legend=FALSE, plot = .) 46 | #' 47 | #' @seealso \code{\link{soccerHeatmap}} for drawing a heatmap of player position, or \code{\link{soccerFlow}} for drawing a single arrow for pass distance and angle per pitch zone. 48 | #' @export 49 | soccerSpokes <- function(df, lengthPitch=105, widthPitch=68, xBins=5, yBins=NULL, angleBins=8, x="x", y="y", angle="angle", minLength=0.6, minAlpha=0.5, minWidth=0.5, col="black", legend=TRUE, arrow=c("none", "r", "l"), title=NULL, subtitle=NULL, theme=c("light", "dark", "grey", "grass"), plot=NULL) { 50 | x.bin<-y.bin<-bin<-x.bin.coord<-y.bin.coord<-angle.theta<-radius<-lwd<-NULL 51 | 52 | border <- c(4, 4, 4, 4) 53 | 54 | # check value for vertical bins and match to horizontal bins if NULL 55 | if(is.null(yBins)) yBins <- xBins 56 | 57 | x.range <- seq(0, lengthPitch, length.out=xBins+1) 58 | y.range <- seq(0, widthPitch, length.out=yBins+1) 59 | 60 | angle.bin <- seq(-pi, pi, length.out=angleBins+1) 61 | 62 | # bin plot values 63 | x.bin.coords <- data.frame(x.bin=1:xBins, 64 | x.bin.coord=(x.range + (lengthPitch / (xBins) / 2))[1:xBins]) 65 | y.bin.coords <- data.frame(y.bin=1:yBins, 66 | y.bin.coord=(y.range + (widthPitch / (yBins) / 2))[1:yBins]) 67 | 68 | angle.bin.theta <- data.frame(angle.bin=1:angleBins, 69 | angle.theta=angle.bin[1:angleBins]) 70 | 71 | # bin by x,y 72 | suppressWarnings( #suppress warnings about empty bins 73 | df <- df %>% 74 | rowwise() %>% 75 | mutate(x.bin=max(which(!!sym(x) > x.range)), 76 | y.bin=max(which(!!sym(y) > y.range)), 77 | bin=paste(x.bin, y.bin, sep="_")) %>% 78 | ungroup() %>% 79 | filter(is.finite(x.bin) & is.finite(y.bin)) 80 | ) 81 | 82 | # bin by angle 83 | suppressWarnings( #suppress warnings about empty bins 84 | df <- df %>% 85 | group_by(bin) %>% 86 | rowwise() %>% 87 | mutate(angle.bin=max(which(!!sym(angle) > angle.bin))) %>% 88 | ungroup() %>% 89 | filter(is.finite(angle.bin)) 90 | ) 91 | 92 | # count number of events in each angle bin 93 | df <- df %>% 94 | group_by(bin, angle.bin) %>% 95 | summarise(n.angles=n(), 96 | x.bin=x.bin[1], 97 | y.bin=y.bin[1]) %>% 98 | ungroup() 99 | 100 | # join x,y-coords and theta to each bin 101 | df <- left_join(df, x.bin.coords, by="x.bin") 102 | df <- left_join(df, y.bin.coords, by="y.bin") 103 | df <- left_join(df, angle.bin.theta, by="angle.bin") 104 | 105 | df$alpha <- minAlpha + ((1-minAlpha) / max(df$n.angles) * df$n.angles) 106 | df$lwd <- 0.5 + ((1.6-0.5) / max(df$n.angles) * df$n.angles) 107 | df$radius <- (minLength * widthPitch / (yBins+5)) + ((1-minLength) * (df$n.angles / max(df$n.angles)) * widthPitch / (yBins+5)) 108 | 109 | # plot 110 | if(missing(plot)) { 111 | plot <- soccerPitch(lengthPitch=lengthPitch, widthPitch=widthPitch, 112 | title=title, subtitle=subtitle, 113 | arrow=arrow, theme=theme) 114 | } 115 | 116 | plot <- plot + 117 | geom_spoke(data=df, 118 | aes(x=x.bin.coord, y=y.bin.coord, 119 | angle=angle.theta, radius=radius, 120 | size=lwd, alpha=alpha), 121 | col=col, arrow=arrow(length=unit(0.15,"cm"))) + 122 | scale_size_continuous(range=c(minWidth, 1.6)) + 123 | scale_alpha(range=c(minAlpha, 1)) 124 | 125 | # add legend 126 | if(!legend) { 127 | plot <- plot + 128 | guides(radius="none", size="none", alpha="none") 129 | } 130 | 131 | return(plot) 132 | } 133 | -------------------------------------------------------------------------------- /R/soccerStandardiseCols.R: -------------------------------------------------------------------------------- 1 | #' @import dplyr 2 | #' @importFrom magrittr "%>%" 3 | NULL 4 | #' Rename columns in a dataframe for easier use with other {soccermatics} functions 5 | #' 6 | #' @description Rename columns (e.g. \code{"location.x"} -> \code{"x"}, \code{"team.name"} -> \code{"team"}, etc...) to interface directly with other soccermatics functions without having to explicitly define column names as arguments. Currently only supports Statsbomb data. 7 | #' 8 | #' @param df a dataframe of Statsbomb event data 9 | #' @param method source of data; only \code{"statsbomb"} currently supported 10 | #' @return a dataframe with column names x, y, distance, angle, player_id, player_name, team_name, event_name 11 | #' @examples 12 | #' library(dplyr) 13 | #' data(statsbomb) 14 | #' 15 | #' # transform x,y-coords, standardise column names 16 | #' my_df <- statsbomb %>% 17 | #' soccerTransform(method = 'statsbomb') %>% 18 | #' soccerStandardiseCols(method = 'statsbomb') 19 | #' 20 | #' # feed to other functions without defining variables, 21 | #' # x, y, id,distance, angle, etc... 22 | #' soccerHeatmap(my_df) 23 | #' 24 | #' @export 25 | soccerStandardiseCols <- function(df, method = c("statsbomb")) { 26 | location.x<-location.y<-pass.length<-pass.angle<-player.id<-player.name<-team.name<-type.name<-NULL 27 | 28 | if(method[1] == "statsbomb") { 29 | 30 | df <- df %>% 31 | select(-id) %>% 32 | rename(x = location.x, 33 | y = location.y, 34 | distance = pass.length, 35 | angle = pass.angle, 36 | player_id = player.id, 37 | player_name = player.name, 38 | team_name = team.name, 39 | event_name = type.name) 40 | 41 | } 42 | 43 | return(df) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /R/soccerTransform.R: -------------------------------------------------------------------------------- 1 | #' @import dplyr 2 | #' @importFrom magrittr "%>%" 3 | #' @importFrom rlang enquo ":=" "!!" 4 | NULL 5 | #' Normalises x,y-coordinates to metres units for use with soccermatics functions 6 | #' 7 | #' @description Normalise x,y-coordinates from between arbitrary limits to metre units bounded by [0 < \code{"x"} < \code{"pitchLength"}, 0 < \code{"y"} < \code{"pitchWidth"}] 8 | #' 9 | #' @param df dataframe containing arbitrary x,y-coordinates 10 | #' @param xMin,xMax,yMin,yMax range of possible x,y-coordinates in the raw dataframe 11 | #' @param lengthPitch,widthPitch length, width of pitch in metres 12 | #' @param method source of data, either \code{"opta"}, \code{"statsbomb"}, \code{"chyronhego"} (ChryonHego), or \code{"manual"} 13 | #' @param x,y variable names of x,y-coordinates. Not required when \code{method} other than \code{"manual"} is defined; defaults to \code{"x"} and \code{"y"} if manual. 14 | #' @return a dataframe 15 | #' 16 | #' @examples 17 | #' # Three examples with true pitch dimensions (in metres): 18 | #' lengthPitch <- 105 19 | #' widthPitch <- 68 20 | #' 21 | #' # Example 1. Opta ---------------------------------------------------------- 22 | #' 23 | #' # limits = [0 < x < 100, 0 < y < 100] 24 | #' opta_df <- data.frame(team_id = as.factor(c(1, 1, 1, 2, 2)), 25 | #' x = c(50.0, 41.2, 44.4, 78.6, 76.7), 26 | #' y = c(50.0, 55.8, 47.5, 55.1, 45.5), 27 | #' endx = c(42.9, 40.2, 78.0, 80.5, 72.4), 28 | #' endy = c(57.6, 47.2, 55.6, 48.1, 26.3)) 29 | #' 30 | #' soccerTransform(opta_df, method = "opta") 31 | #' 32 | #' 33 | #' # Example 2. StatsBomb ----------------------------------------------------- 34 | #' 35 | #' # limits = [0 < x < 120, 0 < y < 80] 36 | #' data(statsbomb) 37 | #' soccerTransform(statsbomb, method = "statsbomb") 38 | #' 39 | #' 40 | #' # Example 3. ChyronHego -------------------------------------------------------- 41 | #' 42 | #' # limits = [-5250 < x < 5250, -3400 < y < 3400] 43 | #' 44 | #' xMin <- -5250 45 | #' xMax <- 5250 46 | #' yMin <- -3400 47 | #' yMax <- 3400 48 | #' 49 | #' ch_df <- data.frame(x = c(0,-452,-982,-1099,-1586,-2088,-2422,-2999,-3200,-3857), 50 | #' y = c(0,150,300,550,820,915,750,620,400,264)) 51 | #' 52 | #' soccerTransform(ch_df, -5250, 5250, -3400, 3400, method = "chyronhego") 53 | #' 54 | #' 55 | #' # Example 4. Manual ----------------------------------------------------- 56 | #' 57 | #' # limits = [0 < x < 420, -136 < y < 136] 58 | #' 59 | #' my_df <- data.frame(team = as.factor(c(1, 1, 1, 2, 2)), 60 | #' my_x = c(210, 173, 187, 330, 322), 61 | #' my_y = c(0, 16, -7, 14, -12), 62 | #' my_endx = c(180, 169, 328, 338, 304), 63 | #' my_endy = c(21, -8, 15, -5, -65)) 64 | #' 65 | #' soccerTransform(my_df, 0, 420, -136, 136, x = c("my_x", "my_endx"), y = c("my_y", "my_endy")) 66 | #' 67 | #' @export 68 | soccerTransform <- function(df, xMin, xMax, yMin, yMax, lengthPitch = 105, widthPitch = 68, method = c("manual", "statsbomb", "opta", "chyronhego", "ch"), x = "x", y = "y") { 69 | 70 | if(method[1] == "statsbomb") { 71 | 72 | xMin <- 1 73 | xMax <- 120 74 | yMin <- 1 75 | yMax <- 80 76 | 77 | df <- df %>% 78 | mutate_at(vars(contains('.x')), list(~ (. - xMin) / diff(c(xMin, xMax)) * lengthPitch)) %>% 79 | mutate_at(vars(contains('.y')), list(~ (. - yMin) / diff(c(yMin, yMax)) * widthPitch)) 80 | 81 | } else if(method[1] == "opta") { 82 | 83 | xMin <- 0 84 | xMax <- 100 85 | yMin <- 0 86 | yMax <- 100 87 | 88 | df$x <- (df$x - xMin) / diff(c(xMin,xMax)) * lengthPitch 89 | df$y <- (df$y - yMin) / diff(c(yMin,yMax)) * widthPitch 90 | df$endx <- (df$endx - xMin) / diff(c(xMin,xMax)) * lengthPitch 91 | df$endy <- (df$endy - yMin) / diff(c(yMin,yMax)) * widthPitch 92 | 93 | } else if(method[1] %in% c("chyronhego", "ch")) { 94 | 95 | xMin <- -5250 96 | xMax <- 5250 97 | yMin <- -3400 98 | yMax <- 3400 99 | 100 | df$x <- (df$x - xMin) / diff(c(xMin,xMax)) * lengthPitch 101 | df$y <- (df$y - yMin) / diff(c(yMin,yMax)) * widthPitch 102 | 103 | } else { 104 | 105 | df <- df %>% 106 | mutate_at(vars(!!enquo(x)), list(~ (. - xMin) / diff(c(xMin,xMax)) * lengthPitch)) %>% 107 | mutate_at(vars(!!enquo(y)), list(~ (. - yMin) / diff(c(yMin,yMax)) * widthPitch)) 108 | 109 | } 110 | 111 | return(df) 112 | 113 | } 114 | -------------------------------------------------------------------------------- /R/soccerVelocity.R: -------------------------------------------------------------------------------- 1 | #' @import dplyr 2 | #' @importFrom magrittr "%>%" 3 | NULL 4 | #' Compute instantaneous distance, speed and direction from x,y-coordinates 5 | #' 6 | #' @description Compute instantaneous distance moved (in metres), speed (in metres per second), and direction (in radians) between subsequent frames in a dataframe of x,y-coordinates. 7 | #' 8 | #' @param dat dataframe containing unnormalised x,y-coordinates \code{x} and \code{y}, time variable \code{'t'}, and player identifier \code{'id'} 9 | #' @return a dataframe with columns \code{'dist'}, \code{'speed'}, and \code{'direction'} added 10 | #' @examples 11 | #' data(tromso) 12 | #' 13 | #' # calculate distance, speed, and direction for \code{tromso} dataset 14 | #' soccerVelocity(tromso) 15 | #' 16 | #' @export 17 | soccerVelocity <- function(dat) { 18 | delta_t<-x<-y<-velocity<-distance<-NULL 19 | 20 | dat %>% 21 | group_by(id) %>% 22 | mutate(delta_t = c(NA, diff(t)), 23 | velocity = c(NA, diff(complex(real = x, imaginary = y))), 24 | direction = c(diff(Arg(velocity)) %% (2*pi), NA) - pi, 25 | distance = Mod(velocity), 26 | speed = distance / delta_t) %>% 27 | select(-delta_t, -velocity) %>% 28 | ungroup() 29 | 30 | } -------------------------------------------------------------------------------- /R/soccermatics-deprecated.R: -------------------------------------------------------------------------------- 1 | ## soccermatics-deprecated.R 2 | #' @title Deprecated functions in package \pkg{soccermatics}. 3 | #' @description The functions listed below are deprecated and will be defunct in 4 | #' the near future. When possible, alternative functions with similar 5 | #' functionality are also mentioned. Help pages for deprecated functions are 6 | #' available at \code{help("soccermatics-deprecated")}. 7 | #' @name soccermatics-deprecated 8 | #' @keywords internal 9 | NULL -------------------------------------------------------------------------------- /R/soccerxGTimeline.R: -------------------------------------------------------------------------------- 1 | #' @include soccerShortenName.R 2 | #' @import ggplot2 3 | #' @import dplyr 4 | #' @importFrom magrittr "%>%" 5 | #' @importFrom tidyr replace_na 6 | NULL 7 | #' Draw a timeline showing cumulative expected goals (xG) over the course of a match using StatsBomb data. 8 | #' 9 | #' @description Draw a timeline showing cumulative expected goals (xG, excluding penalties and own goals) by two teams over the course of a match, as well as plotting the scoreline and goalscorer at goal events. Currently only works with StatsBomb data but compatability with other (non-StatsBomb) shot data will be added soon. 10 | #' 11 | #' @param df a dataframe containing StatsBomb data from one full match 12 | #' @param homeCol,awayCol colours of the home and away team, respectively 13 | #' @param adj adjust xG using conditional probability to account for multiple shots per possession 14 | #' @param labels include scoreline and goalscorer labels for goals 15 | #' @param y_buffer vertical space to add at the top of the y-axis (a quick and dirty way to ensure text annotations are not cropped). 16 | #' @return a ggplot object 17 | #' @examples 18 | #' library(dplyr) 19 | #' data(statsbomb) 20 | #' 21 | #' # xG timeline of France vs. Argentina 22 | #' # w/ goalscorer labels, adjusted xG data 23 | #' statsbomb %>% 24 | #' soccerxGTimeline(homeCol = "blue", awayCol = "lightblue", y_buffer = 0.4) 25 | #' 26 | #' # no goalscorer labels, raw xG data 27 | #' statsbomb %>% 28 | #' soccerxGTimeline(homeCol = "blue", awayCol = "lightblue", adj = FALSE) 29 | #' 30 | #' @export 31 | soccerxGTimeline <- function(df, homeCol = "red", awayCol = "blue", adj = TRUE, labels = TRUE, y_buffer = 0.3) { 32 | minute<-second<-type.name<-shot.type.name<-type<-shot.outcome.name<-shot.statsbomb_xg<-team.name<-possession<-xg_total<-xg_adj<-outcome<-player.name<-hg<-ag<-name<-label<-NULL 33 | 34 | # ensure input is dataframe 35 | df <- as.data.frame(df) 36 | 37 | # preprocess data 38 | df <- df %>% 39 | mutate(t = minute * 60 + second) %>% 40 | mutate(type = if_else(type.name == "Own Goal For", "OG", 41 | if_else(shot.type.name == "Penalty", "Pen", 42 | if_else(type.name == "Shot", "Open", "NA")))) %>% 43 | mutate(outcome = if_else(type == "OG", 1, 44 | if_else(shot.outcome.name == "Goal", 1, 0))) 45 | 46 | # set variable names 47 | home_team <- df$team.name[1] 48 | away_team <- df[df$team.name != home_team,]$team.name %>% unique 49 | ht_t <- df[df$type.name == "Half End",]$t[1] #HT in seconds 50 | et_first <- ht_t - (45 * 60) #1H stoppage time 51 | df[df$period == 2,]$t <- df[df$period == 2,]$t + et_first #add 1H stoppage time to 2H 52 | ft_t <- df[df$type.name == "Half End",]$t[4] # FT in seconds 53 | 54 | # shot types 55 | df <- df %>% 56 | filter(!is.na(type)) %>% 57 | mutate(xg = shot.statsbomb_xg, 58 | xg = if_else(type %in% c("Pen", "OG"), 0, xg)) 59 | 60 | # adjust xG using conditional probability when there are multiple shots in a single possession 61 | if(adj) { 62 | xg <- df %>% 63 | filter(type == "Open") %>% 64 | group_by(team.name, possession) %>% 65 | mutate(xg_total = (1 - prod(1 - xg))) %>% 66 | mutate(xg_adj = xg_total * (xg / sum(xg))) %>% 67 | ungroup() %>% 68 | select(id, xg_adj) 69 | 70 | df <- left_join(df, xg, by = "id") %>% 71 | mutate(xg_adj = replace_na(xg_adj, 0)) %>% 72 | select(-xg) %>% 73 | rename(xg = xg_adj) 74 | } 75 | 76 | # compute cumulative xG (non-penalty, non-OG goals only) 77 | shots <- df %>% 78 | group_by(team.name) %>% 79 | mutate(xg = cumsum(xg)) %>% 80 | ungroup() %>% 81 | select(team.name, xg, t, id) 82 | # dummy first row at KO for start of geom_line 83 | shots_start <- shots %>% 84 | group_by(team.name) %>% 85 | summarise(xg = 0, 86 | t = 0, 87 | id = NA) 88 | # dummy final row at FT for end of geom_line 89 | shots_end <- shots %>% 90 | group_by(team.name) %>% 91 | summarise(xg = max(xg), 92 | t = ft_t, 93 | id = NA) 94 | # join shots data 95 | shots <- rbind(shots, shots_start, shots_end) 96 | 97 | # get goals 98 | goals <- df %>% 99 | filter(outcome == 1) %>% 100 | select(id, team.name, player.name, t, type) 101 | 102 | # join cumulative xG 103 | goals <- left_join(goals, 104 | shots %>% select(id, xg), 105 | by = "id") 106 | 107 | 108 | # set plotting options and labels 109 | # score and goalscorer labels 110 | goals <- goals %>% 111 | mutate(hg = if_else(team.name == home_team, 1, 0), 112 | ag = if_else(team.name != home_team, 1, 0)) %>% 113 | mutate(hg = cumsum(hg), 114 | ag = cumsum(ag)) %>% 115 | mutate(name = soccermatics::soccerShortenName(player.name)) %>% 116 | mutate(label = paste0(hg, "-", ag, " ", name)) %>% 117 | mutate(label = if_else(type == "Pen", paste0(label, " (P)"), 118 | if_else(type == "OG", paste0(label, " (OG)"), 119 | label))) 120 | 121 | y_lim <- ifelse(labels, max(shots$xg) + y_buffer, max(shots$xg) + 0.2) 122 | title_a <- paste0(home_team, " ", max(goals$hg), " (", sprintf("%.1f", max(shots[shots$team.name == home_team,]$xg)), ")") 123 | title_b <- paste0(away_team, " ", max(goals$ag), " (", sprintf("%.1f", max(shots[shots$team.name == away_team,]$xg)), ")") 124 | 125 | shots$team.name <- factor(shots$team.name, levels = c(home_team, away_team)) 126 | goals$team.name <- factor(goals$team.name, levels = c(home_team, away_team)) 127 | 128 | # plot 129 | p <- ggplot() + 130 | geom_vline(xintercept = ht_t, linetype = "longdash", col = "grey70") + 131 | geom_vline(xintercept = ft_t, linetype = "longdash", col = "grey70") + 132 | annotate("text", ht_t - 30, y_lim - 0.02, label = "HT", hjust = 1, vjust = 1, col = "grey70") + 133 | annotate("text", ft_t - 30, y_lim - 0.02, label = "FT", hjust = 1, vjust = 1, col = "grey70") + 134 | geom_step(data = shots, aes(t, xg, group = team.name, colour = team.name), lwd = 1) + 135 | geom_point(data = goals, aes(t, xg, group = team.name, colour = team.name), size = 3) + 136 | scale_y_continuous(limits = c(0, y_lim), expand = c(0,0)) + 137 | scale_x_continuous(breaks = c(seq(0, 45*60, 15*60), ht_t + seq(0, 45*60, 15*60)), labels = c("0'","15'","30'","","45'","60'","75'","90'"), limits = c(0, ft_t+60), expand = c(0, 0)) + 138 | scale_color_manual(breaks = c(home_team, away_team), values = c(homeCol, awayCol)) + 139 | guides(col="none") + 140 | labs(title = title_a, 141 | subtitle = title_b, 142 | y = "xG") + 143 | theme_bw(base_size = 14) + 144 | theme(panel.grid.minor = element_blank(), 145 | axis.title.x = element_blank(), 146 | plot.title = element_text(size = 16, face = 'bold', colour = homeCol), 147 | plot.subtitle = element_text(size = 16, face = 'bold', colour = awayCol)) 148 | 149 | if(labels) { 150 | p <- p + 151 | geom_text(data = goals, aes(t, xg + (y_lim/30), group = team.name, colour = team.name, label = label), angle = 90, hjust = 0) 152 | } 153 | 154 | return(p) 155 | 156 | } 157 | -------------------------------------------------------------------------------- /R/statsbomb.R: -------------------------------------------------------------------------------- 1 | #' Sample StatsBomb event data containing the x,y-locations and identity of players involved in pass events, shot events, defensive actions, and more. 2 | #' 3 | #' Sample StatsBomb event data from the France vs. Argentina World Cup 2018 game on the 30th June 2018, made publicly available by StatsBomb \href{https://github.com/statsbomb/open-data}{here}. Data contains 145 variables in total, including x,y-coordinates (\code{location.x}, \code{location.y}). StatsBomb pitch dimensions are 120m long and 80m wide, meaning \code{lengthPitch} should be specified as \code{120} and \code{widthPitch} as \code{80}. Event data for all World Cup games (and other competitions) are accessible via the StatsBombR package available \href{https://github.com/statsbomb/StatsBombR}{here}. 4 | #' @docType data 5 | #' @name statsbomb 6 | #' @format A dataframe containing 12000 frames of x,y-coordinates and timestamps from 11 players. 7 | #' @source \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking} 8 | #' @references \href{https://github.com/statsbomb/open-data}{StatsBomb Open Data} 9 | #' @usage data(statsbomb) 10 | NULL 11 | -------------------------------------------------------------------------------- /R/tromso.R: -------------------------------------------------------------------------------- 1 | #' x,y-coordinates of 11 soccer players over 12000 frames each 2 | #' 3 | #' x,y-coordinates of 11 soccer players over 10 minutes (Tromsø IL vs. Anzhi, 2013-11-07), captured at 20 Hz using the ZXY Sport Tracking system and made available in the publication \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking}. 4 | #' @docType data 5 | #' @name tromso 6 | #' @format A dataframe containing 12000 frames of x,y-coordinates and timestamps from 11 players. 7 | #' @source \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking} 8 | #' @references \href{http://home.ifi.uio.no/paalh/publications/files/mmsys2014-dataset.pdf}{Pettersen et al. (2014)} Proceedings of the International Conference on Multimedia Systems (MMSys) 9 | #' @usage data(tromso) 10 | NULL 11 | -------------------------------------------------------------------------------- /R/tromso_extra.R: -------------------------------------------------------------------------------- 1 | #' x,y-coordinates and additional positional information on 11 soccer players over 12000 frames each 2 | #' 3 | #' x,y-coordinates of 11 soccer players over 10 minutes (Tromsø IL vs. Anzhi, 2013-11-07), plus additional information on player heading, direction, energy, speed, and total distance. Data captured at 20 Hz using the ZXY Sport Tracking system and made available in the publication \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking}. 4 | #' @docType data 5 | #' @name tromso_extra 6 | #' @format A dataframe containing 12000 frames of x,y-coordinates and timestamps from 11 players. 7 | #' @source \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking} 8 | #' @references Pettersen et al. (2014) Proceedings of the International Conference on Multimedia Systems (MMSys) 9 | #' (\href{http://home.ifi.uio.no/paalh/publications/files/mmsys2014-dataset.pdf}{pdf}) 10 | #' @usage data(tromso_extra) 11 | NULL 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | soccermatics 2 | ===== 3 | 4 | soccermatics provides tools to visualise spatial tracking and event data from football (soccer) matches. There are currently functions to visualise shot maps (with xG), average positions, heatmaps, and individual player trajectories. There are also helper functions to smooth, interpolate, and prepare x,y-coordinate tracking data for plotting and calculating further metrics. 5 | 6 | Many more functions are planned - see [To Do List](https://github.com/JoGall/soccermatics/issues/8) - suggestions and/or help welcomed! 7 | 8 | The sample x,y-coordinate tracking data in `tromso` and `tromso_extra` were made available by [Pettersen et al. (2014)](http://home.ifi.uio.no/paalh/dataset/alfheim/), whilst the event data in `statsbomb` is taken from the World Cup 2018 data [made public by StatsBomb](https://github.com/statsbomb/open-data). 9 | 10 | Use of the name soccermatics kindly permitted by the eponymous book's author, [David Sumpter](https://www.bloomsbury.com/uk/soccermatics-9781472924124/). 11 | 12 | soccermatics is built on R v3.4.2. 13 | 14 | --- 15 | 16 | ### Installation 17 | You can install `soccermatics` from GitHub in R using [`devtools`](https://github.com/hadley/devtools): 18 | 19 | ```{r} 20 | if (!require("devtools")) install.packages("devtools") 21 | devtools::install_github("jogall/soccermatics") 22 | 23 | library(soccermatics) 24 | ``` 25 | 26 | --- 27 | 28 | ### Examples 29 | 30 | Below are some sample visualisations produced by `soccermetrics` with code snippets underneath. See the individual help files for each function (e.g. `?soccerHeatmap`) for more information. 31 | 32 | #### Shotmaps (showing xG) 33 | 34 | Dark theme: 35 | 36 | 37 | 38 | ```{r} 39 | statsbomb %>% 40 | filter(team.name == "France") %>% 41 | soccerShotmap(theme = "dark") 42 | ``` 43 | 44 | 45 | Grass theme with custom colours: 46 | 47 | 48 | 49 | ```{r} 50 | statsbomb %>% 51 | filter(team.name == "Argentina") %>% 52 | soccerShotmap(theme = "grass", colGoal = "yellow", colMiss = "blue", legend = T) 53 | ``` 54 | 55 | 56 | #### Passing networks 57 | 58 | Default aesthetics: 59 | 60 | 61 | 62 | ```{r} 63 | statsbomb %>% 64 | filter(team.name == "Argentina") %>% 65 | soccerPassmap(fill = "lightblue", arrow = "r", 66 | title = "Argentina (vs France, 30th June 2018)") 67 | ``` 68 | 69 | Grass background, non-transparent edges: 70 | 71 | 72 | 73 | ```{r} 74 | statsbomb %>% 75 | filter(team.name == "France") %>% 76 | soccerPassmap(fill = "blue", minPass = 3, 77 | edge_max_width = 30, edge_col = "grey40", edge_alpha = 1, 78 | title = "France (vs Argentina, 30th June 2018)") 79 | ``` 80 | 81 | 82 | #### Heatmaps 83 | 84 | Passing heatmap with approx 10x10m bins: 85 | 86 | 87 | 88 | ```{r} 89 | statsbomb %>% 90 | filter(type.name == "Pass" & team.name == "France") %>% 91 | soccerHeatmap(x = "location.x", y = "location.y", 92 | title = "France (vs Argentina, 30th June 2016)", 93 | subtitle = "Passing heatmap") 94 | ``` 95 | 96 | 97 | Defensive pressure heatmap with approx 5x5m bins: 98 | 99 | 100 | 101 | ```{r} 102 | statsbomb %>% 103 | filter(type.name == "Pressure" & team.name == "France") %>% 104 | soccerHeatmap(x = "location.x", y = "location.y", xBins = 21, yBins = 14, 105 | title = "France (vs Argentina, 30th June 2016)", 106 | subtitle = "Defensive pressure heatmap") 107 | ``` 108 | 109 | Player position heatmaps also possible using TRACAB-style x,y-location data. 110 | 111 | 112 | #### Average position 113 | 114 | Average pass position: 115 | 116 | 117 | 118 | ```{r} 119 | statsbomb %>% 120 | filter(type.name == "Pass" & team.name == "France" & minute < 43) %>% 121 | soccerPositionMap(id = "player.name", x = "location.x", y = "location.y", 122 | fill1 = "blue", grass = T, 123 | arrow = "r", 124 | title = "France (vs Argentina, 30th June 2016)", 125 | subtitle = "Average pass position (1' - 42')") 126 | ``` 127 | 128 | 129 | Average pass position (both teams): 130 | 131 | 132 | 133 | ```{r} 134 | statsbomb %>% 135 | filter(type.name == "Pass" & minute < 43) %>% 136 | soccerPositionMap(id = "player.name", team = "team.name", x = "location.x", y = "location.y", 137 | fill1 = "lightblue", fill2 = "blue", label_col = "black", 138 | repel = T, teamToFlip = 2, 139 | title = "France vs Argentina, 30th June 2018", 140 | subtitle = "Average pass position (1' - 42')") 141 | ``` 142 | 143 | 144 | Average player position using TRACAB-style x,y-location data: 145 | 146 | 147 | 148 | ```{r} 149 | tromso_extra[1:11,] %>% 150 | soccerPositionMap(grass = T, title = "Tromsø IL (vs. Strømsgodset, 3rd Nov 2013)", subtitle = "Average player position (1' - 16')") 151 | ``` 152 | 153 | 154 | #### Custom plots 155 | 156 | Inbuilt functions for many of these will be added soon. 157 | 158 | 159 | Locations of multiple events: 160 | 161 | 162 | 163 | ```{r} 164 | d2 <- statsbomb %>% 165 | filter(type.name %in% c("Pressure", "Interception", "Block", "Dispossessed", "Ball Recovery") & team.name == "France") 166 | 167 | soccerPitch(arrow = "r", 168 | title = "France (vs Argentina, 30th June 2016)", 169 | subtitle = "Defensive actions") + 170 | geom_point(data = d2, aes(x = location.x, y = location.y, col = type.name), size = 3, alpha = 0.5) 171 | ``` 172 | 173 | 174 | Start and end locations of passes: 175 | 176 | 177 | 178 | 179 | ```{r} 180 | d3 <- statsbomb %>% 181 | filter(type.name == "Pass" & team.name == "France") %>% 182 | mutate(pass.outcome = as.factor(if_else(is.na(pass.outcome.name), 1, 0))) 183 | 184 | soccerPitch(arrow = "r", 185 | title = "France (vs Argentina, 30th June 2016)", 186 | subtitle = "Pass map") + 187 | geom_segment(data = d3, aes(x = location.x, xend = pass.end_location.x, y = location.y, yend = pass.end_location.y, col = pass.outcome), alpha = 0.75) + 188 | geom_point(data = d3, aes(x = location.x, y = location.y, col = pass.outcome), alpha = 0.5) + 189 | guides(colour = FALSE) 190 | ``` 191 | 192 | 193 | #### Player paths 194 | 195 | Path of a single player: 196 | 197 | 198 | 199 | ```{r} 200 | subset(tromso, id == 8)[1:1800,] %>% 201 | soccerPath(col = "red", grass = TRUE, arrow = "r", 202 | title = "Tromsø IL (vs. Strømsgodset, 3rd Nov 2013)", 203 | subtitle = "Player #8 path (1' - 3')") 204 | ``` 205 | 206 | 207 | Path of multiple players: 208 | 209 | 210 | 211 | ```{r} 212 | tromso %>% 213 | dplyr::group_by(id) %>% 214 | dplyr::slice(1:1200) %>% 215 | soccerPath(id = "id", arrow = "r", 216 | title = "Tromsø IL (vs. Strømsgodset, 3rd Nov 2013)", 217 | subtitle = "Player paths (1')") 218 | ``` 219 | 220 | --- 221 | -------------------------------------------------------------------------------- /data/statsbomb.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoGall/soccermatics/4dfbfebc345b28103bfa08db89884626dcbe2a80/data/statsbomb.rda -------------------------------------------------------------------------------- /data/tromso.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoGall/soccermatics/4dfbfebc345b28103bfa08db89884626dcbe2a80/data/tromso.rda -------------------------------------------------------------------------------- /data/tromso_extra.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoGall/soccermatics/4dfbfebc345b28103bfa08db89884626dcbe2a80/data/tromso_extra.rda -------------------------------------------------------------------------------- /man/soccerFlipDirection.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerFlipDirection.R 3 | \name{soccerFlipDirection} 4 | \alias{soccerFlipDirection} 5 | \title{Flips x,y-coordinates horizontally in one half to account for changing sides at half-time} 6 | \usage{ 7 | soccerFlipDirection( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | teamToFlip = NULL, 12 | periodToFlip = 1:2, 13 | team = "team", 14 | period = "period", 15 | x = "x", 16 | y = "y" 17 | ) 18 | } 19 | \arguments{ 20 | \item{df}{dataframe containing unnormalised x,y-coordinates} 21 | 22 | \item{lengthPitch, widthPitch}{length, width of pitch in metres} 23 | 24 | \item{teamToFlip}{character, name of team to flip. If \code{NULL}, all x,y-coordinates in \code{df} will be flipped} 25 | 26 | \item{periodToFlip}{integer, period(s) to flip} 27 | 28 | \item{team}{character, name of variables containing x,y-coordinates} 29 | 30 | \item{period}{character, name of variable containing period labels} 31 | 32 | \item{x, y}{character, name of variables containing x,y-coordinates} 33 | } 34 | \value{ 35 | a dataframe 36 | } 37 | \description{ 38 | Normalises direction of attack in both halves of both teams by 39 | flipping x,y-coordinates horizontally in either the first or second half; 40 | i.e. teams attack in the same direction all game despite changing sides at 41 | half-time. 42 | } 43 | \examples{ 44 | library(dplyr) 45 | 46 | # flip x,y-coords of France in both halves of statsbomb data 47 | data(statsbomb) 48 | statsbomb \%>\% 49 | soccerFlipDirection(team = "team.name", x = "location.x", y = "location.y", 50 | teamToFlip = "France") 51 | 52 | # flip x,y-coords in 2nd half of Tromso, based on a dummy period variable 53 | data(tromso) 54 | tromso \%>\% 55 | mutate(period = if_else(t > as.POSIXct("2013-11-07 21:14:00 GMT"), 1, 2)) \%>\% 56 | soccerFlipDirection(periodToFlip = 2) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /man/soccerFlow.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerFlow.R 3 | \name{soccerFlow} 4 | \alias{soccerFlow} 5 | \title{Draw a flow field of passing direction on a soccer pitch} 6 | \usage{ 7 | soccerFlow( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | xBins = 5, 12 | yBins = NULL, 13 | x = "x", 14 | y = "y", 15 | angle = "angle", 16 | distance = "distance", 17 | col = "black", 18 | lwd = 0.5, 19 | arrow = c("none", "r", "l"), 20 | title = NULL, 21 | subtitle = NULL, 22 | theme = c("light", "dark", "grey", "grass"), 23 | plot = NULL 24 | ) 25 | } 26 | \arguments{ 27 | \item{df}{dataframe of event data containing fields of start x,y-coordinates, pass distance, and pass angle} 28 | 29 | \item{lengthPitch, widthPitch}{numeric, length and width of pitch in metres.} 30 | 31 | \item{xBins, yBins}{integer, the number of horizontal (length-wise) and vertical (width-wise) bins the soccer pitch is to be divided up into; if \code{yBins} is NULL (default), it will take the value of \code{xBins}} 32 | 33 | \item{x, y, angle, distance}{names of variables containing pass start x,y-coordinates, angle, and distance} 34 | 35 | \item{col}{colour of arrows} 36 | 37 | \item{lwd}{thickness of arrow segments} 38 | 39 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 40 | 41 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 42 | 43 | \item{theme}{palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass}} 44 | 45 | \item{plot}{base plot to add path layer to; NULL by default} 46 | } 47 | \value{ 48 | a ggplot object of a heatmap on a soccer pitch 49 | } 50 | \description{ 51 | A flow field to show the mean angle and distance of passes in zones of the pitch 52 | } 53 | \examples{ 54 | library(dplyr) 55 | data(statsbomb) 56 | 57 | # transform x,y-coords, filter only France pass events, 58 | # draw flow field showing mean angle, distance of passes per pitch zone 59 | statsbomb \%>\% 60 | soccerTransform(method = 'statsbomb') \%>\% 61 | filter(team.name == "France" & type.name == "Pass") \%>\% 62 | soccerFlow(xBins=7, yBins=5, 63 | x="location.x", y="location.y", angle="pass.angle", distance="pass.length") 64 | 65 | # transform x,y-coords, standarise column names, 66 | # filter only France pass events 67 | my_df <- statsbomb \%>\% 68 | soccerTransform(method = 'statsbomb') \%>\% 69 | soccerStandardiseCols(method = 'statsbomb') \%>\% 70 | filter(team_name == "France" & event_name == "Pass") 71 | 72 | # overlay flow field onto heatmap showing proportion of team passes per pitch zone 73 | soccerHeatmap(my_df, xBins=7, yBins=5) \%>\% 74 | soccerFlow(my_df, xBins=7, yBins=5, plot = .) 75 | 76 | } 77 | \seealso{ 78 | \code{\link{soccerHeatmap}} for drawing a heatmap of player position, or \code{\link{soccerSpokes}} for drawing spokes to show all directions in each area of the pitch. 79 | } 80 | -------------------------------------------------------------------------------- /man/soccerHeatmap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerHeatmap.R 3 | \name{soccerHeatmap} 4 | \alias{soccerHeatmap} 5 | \title{Draw a heatmap on a soccer pitch using any event or tracking data.} 6 | \usage{ 7 | soccerHeatmap( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | xBins = 10, 12 | yBins = NULL, 13 | kde = FALSE, 14 | arrow = c("none", "r", "l"), 15 | colLow = "white", 16 | colHigh = "red", 17 | title = NULL, 18 | subtitle = NULL, 19 | x = "x", 20 | y = "y" 21 | ) 22 | } 23 | \arguments{ 24 | \item{df}{dataframe containing x,y-coordinates of player position} 25 | 26 | \item{lengthPitch, widthPitch}{numeric, length and width of pitch in metres.} 27 | 28 | \item{xBins, yBins}{integer, the number of horizontal (length-wise) and vertical (width-wise) bins the soccer pitch is to be divided up into. If no value for \code{yBins} is provided, it will take the value of \code{xBins}.} 29 | 30 | \item{kde}{use kernel density estimates for a smoother heatmap; FALSE by default} 31 | 32 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 33 | 34 | \item{colLow, colHigh}{character, colours for the low and high ends of the heatmap gradient; white and red respectively by default} 35 | 36 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 37 | 38 | \item{x, y}{name of variables containing x,y-coordinates} 39 | } 40 | \value{ 41 | a ggplot object of a heatmap on a soccer pitch. 42 | } 43 | \description{ 44 | Draws a heatmap showing player position frequency in each area of the pitch and adds soccer pitch outlines. 45 | } 46 | \details{ 47 | uses \code{ggplot2::geom_bin2d} to map 2D bin counts 48 | } 49 | \examples{ 50 | library(dplyr) 51 | 52 | # tracking data heatmap with 21x5 zones(~5x5m) 53 | data(tromso) 54 | tromso \%>\% 55 | filter(id == 8) \%>\% 56 | soccerHeatmap(xBins = 10) 57 | 58 | # transform x,y-coords, filter only France pressure events, 59 | # heatmap with 6x3 zones 60 | data(statsbomb) 61 | statsbomb \%>\% 62 | soccerTransform(method='statsbomb') \%>\% 63 | filter(type.name == "Pressure" & team.name == "France") \%>\% 64 | soccerHeatmap(x = "location.x", y = "location.y", 65 | xBins = 6, yBins = 3, arrow = "r", 66 | title = "France (vs Argentina, 30th June 2016)", 67 | subtitle = "Defensive pressure heatmap") 68 | 69 | # transform x,y-coords, standardise column names, 70 | # filter player defensive actions, plot kernel density estimate heatmap 71 | statsbomb \%>\% 72 | soccerTransform(method='statsbomb') \%>\% 73 | soccerStandardiseCols() \%>\% 74 | filter(event_name \%in\% c("Duel", "Interception", "Clearance", "Block") & 75 | player_name == "Samuel Yves Umtiti") \%>\% 76 | soccerHeatmap(kde = TRUE, arrow = "r", 77 | title = "Umtiti (vs Argentina, 30th June 2016)", 78 | subtitle = "Defensive actions heatmap") 79 | 80 | } 81 | -------------------------------------------------------------------------------- /man/soccerPassmap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPassmap.R 3 | \name{soccerPassmap} 4 | \alias{soccerPassmap} 5 | \title{Draw a passing network using StatsBomb data} 6 | \usage{ 7 | soccerPassmap( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | minPass = 3, 12 | fill = "red", 13 | col = "black", 14 | edgeAlpha = 0.6, 15 | edgeCol = NULL, 16 | label = TRUE, 17 | shortNames = TRUE, 18 | maxNodeSize = 30, 19 | maxEdgeSize = 30, 20 | labelSize = 4, 21 | arrow = c("none", "r", "l"), 22 | theme = c("light", "dark", "grey", "grass"), 23 | title = NULL 24 | ) 25 | } 26 | \arguments{ 27 | \item{df}{dataframe containing x,y-coordinates of player passes} 28 | 29 | \item{lengthPitch, widthPitch}{numeric, length and width of pitch, in metres} 30 | 31 | \item{minPass}{minimum number of passes between players for edge to be drawn} 32 | 33 | \item{fill, col}{fill and border colour of nodes} 34 | 35 | \item{edgeAlpha}{transparency of edge lines, from \code{0} - \code{1}. Defaults to \code{0.6} so overlapping edges are visible.} 36 | 37 | \item{edgeCol}{colour of edge lines. Default is complementary to \code{theme} colours.} 38 | 39 | \item{label}{boolean, draw labels} 40 | 41 | \item{shortNames}{shorten player names to display last name as label} 42 | 43 | \item{maxNodeSize}{maximum size of nodes} 44 | 45 | \item{maxEdgeSize}{maximum width of edge lines} 46 | 47 | \item{labelSize}{size of player name labels} 48 | 49 | \item{arrow}{optional, adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'})} 50 | 51 | \item{theme}{draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch} 52 | 53 | \item{title}{adds custom title to plot. Defaults to team name.} 54 | } 55 | \description{ 56 | Draw an undirected passing network of completed passes on pitch from StatsBomb data. Nodes are scaled by number of successful passes; edge width is scaled by number of successful passes between each node pair. Only passes made until first substition shown (ability to specify custom minutes will be added soon). Total number of passes attempted and percentage of completed passes shown. Compatability with other (non-StatsBomb) shot data will be added soon. 57 | } 58 | \examples{ 59 | # France vs. Argentina, minimum of three passes 60 | library(dplyr) 61 | data(statsbomb) 62 | 63 | # transform x,y-coords, 64 | # Argentina pass map until first substituton with transparent edges 65 | statsbomb \%>\% 66 | soccerTransform(method='statsbomb') \%>\% 67 | filter(team.name == "Argentina") \%>\% 68 | soccerPassmap(fill = "lightblue", arrow = "r", 69 | title = "Argentina (vs France, 30th June 2018)") 70 | 71 | # transform x,y-coords, 72 | # France pass map until first substitution with opaque edges 73 | statsbomb \%>\% 74 | filter(team.name == "France") \%>\% 75 | soccerTransform(method='statsbomb') \%>\% 76 | soccerPassmap(fill = "blue", minPass = 3, 77 | maxEdgeSize = 30, edgeCol = "grey40", edgeAlpha = 1, 78 | title = "France (vs Argentina, 30th June 2018)") 79 | } 80 | -------------------------------------------------------------------------------- /man/soccerPath.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPath.R 3 | \name{soccerPath} 4 | \alias{soccerPath} 5 | \title{Draw a path of player trajectory on a soccer pitch using any tracking data} 6 | \usage{ 7 | soccerPath( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | col = "black", 12 | arrow = c("none", "r", "l"), 13 | theme = c("light", "dark", "grey", "grass"), 14 | lwd = 1, 15 | title = NULL, 16 | subtitle = NULL, 17 | legend = FALSE, 18 | x = "x", 19 | y = "y", 20 | id = NULL, 21 | plot = NULL 22 | ) 23 | } 24 | \arguments{ 25 | \item{df}{dataframe containing x,y-coordinates of player position} 26 | 27 | \item{lengthPitch, widthPitch}{length and width of pitch in metres} 28 | 29 | \item{col}{colour of path if no \code{'id'} is provided; if an \code{'id'} is present, uses ColorBrewer's 'Paired' palette by default} 30 | 31 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 32 | 33 | \item{theme}{draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch} 34 | 35 | \item{lwd}{player path thickness} 36 | 37 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 38 | 39 | \item{legend}{boolean, include legend} 40 | 41 | \item{x, y}{name of variables containing x,y-coordinates} 42 | 43 | \item{id}{character, the name of the column containing player identity (only required if \code{'df'} contains multiple players)} 44 | 45 | \item{plot}{base plot to add path layer to; NULL by default} 46 | } 47 | \value{ 48 | a ggplot object 49 | } 50 | \description{ 51 | Draws a path connecting consecutive x,y-coordinates of a player on a soccer pitch. 52 | } 53 | \examples{ 54 | library(dplyr) 55 | data(tromso) 56 | 57 | # draw path of Tromso #8 over first 3 minutes (1800 frames) 58 | tromso \%>\% 59 | filter(id == 8) \%>\% 60 | top_n(1800) \%>\% 61 | soccerPath(col = "red", theme = "grass", arrow = "r") 62 | 63 | # draw path of all Tromso players over first minute (600 frames) 64 | tromso \%>\% 65 | group_by(id) \%>\% 66 | slice(1:1200) \%>\% 67 | soccerPath(id = "id", theme = "light") 68 | 69 | } 70 | -------------------------------------------------------------------------------- /man/soccerPitch.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPitch.R 3 | \name{soccerPitch} 4 | \alias{soccerPitch} 5 | \title{Plot a full soccer pitch} 6 | \usage{ 7 | soccerPitch( 8 | lengthPitch = 105, 9 | widthPitch = 68, 10 | arrow = c("none", "r", "l"), 11 | title = NULL, 12 | subtitle = NULL, 13 | theme = c("light", "dark", "grey", "grass"), 14 | data = NULL 15 | ) 16 | } 17 | \arguments{ 18 | \item{lengthPitch, widthPitch}{length and width of pitch in metres} 19 | 20 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 21 | 22 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 23 | 24 | \item{theme}{palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass}} 25 | 26 | \item{data}{a default dataset for plotting in subsequent layers; NULL by default} 27 | } 28 | \value{ 29 | a ggplot object 30 | } 31 | \description{ 32 | Draws a soccer pitch as a ggplot object for the purpose of adding layers such as player positions, player trajectories, etc.. 33 | } 34 | \examples{ 35 | library(ggplot2) 36 | data(statsbomb) 37 | 38 | # transform Statsbomb coordinates to metre units for plotting 39 | my_df <- soccerTransform(statsbomb, method = "statsbomb") 40 | 41 | # filter events of interest (France defensive pressure events vs. Argentina) 42 | my_df <- my_df \%>\% 43 | dplyr::filter(team.name == "France" & type.name == "Pressure") 44 | 45 | # add custom layers to soccerPitch base 46 | soccerPitch(data = my_df, 47 | arrow = "r", theme = "grass", 48 | title = "France (vs. Argentina)", 49 | subtitle = "Pressure events") + 50 | geom_point(aes(x = location.x, y = location.y), 51 | col = "blue", alpha = 0.5) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /man/soccerPitchFG.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPitchFG.R 3 | \name{soccerPitchFG} 4 | \alias{soccerPitchFG} 5 | \title{Helper function to draw soccer pitch outlines over an existing ggplot object} 6 | \usage{ 7 | soccerPitchFG( 8 | plot, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | colPitch = "black", 12 | arrow = c("none", "r", "l"), 13 | title = NULL, 14 | subtitle = NULL 15 | ) 16 | } 17 | \arguments{ 18 | \item{plot}{an existing ggplot object to add pitch lines layer to} 19 | 20 | \item{lengthPitch, widthPitch}{length and width of pitch in metres} 21 | 22 | \item{colPitch}{colour of pitch markings} 23 | 24 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 25 | 26 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 27 | } 28 | \value{ 29 | a ggplot object 30 | } 31 | \description{ 32 | Adds soccer pitch outlines (with transparent fill) to an existing ggplot object (e.g. heatmaps, passing maps, etc..) 33 | } 34 | \seealso{ 35 | \code{\link{soccerPitch}} for plotting a soccer pitch as background layer 36 | } 37 | -------------------------------------------------------------------------------- /man/soccerPitchHalf.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPitchHalf.R 3 | \name{soccerPitchHalf} 4 | \alias{soccerPitchHalf} 5 | \title{Draws a vertical half soccer pitch for the purpose of plotting shotmaps} 6 | \usage{ 7 | soccerPitchHalf( 8 | lengthPitch = 105, 9 | widthPitch = 68, 10 | arrow = c("none", "r", "l"), 11 | theme = c("light", "dark", "grey", "grass"), 12 | title = NULL, 13 | subtitle = NULL, 14 | data = NULL 15 | ) 16 | } 17 | \arguments{ 18 | \item{lengthPitch, widthPitch}{length and width of pitch in metres} 19 | 20 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 21 | 22 | \item{theme}{palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass};} 23 | 24 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 25 | 26 | \item{data}{a default dataset for plotting in subsequent layers; NULL by default} 27 | } 28 | \value{ 29 | a ggplot object 30 | } 31 | \description{ 32 | Adds soccer pitch outlines (with transparent fill) to an existing ggplot object (e.g. heatmaps, passing maps, etc..) 33 | } 34 | \examples{ 35 | library(ggplot2) 36 | library(dplyr) 37 | data(statsbomb) 38 | 39 | # normalise data, get non-penalty shots for France, 40 | # add boolean variable 'goal' for plotting 41 | my_df <- statsbomb \%>\% 42 | soccerTransform(method = 'statsbomb') \%>\% 43 | filter(team.name == "France" & 44 | type.name == "Shot" & 45 | shot.type.name != 'penalty') \%>\% 46 | mutate(goal = as.factor(if_else(shot.outcome.name == "Goal", 1, 0))) 47 | 48 | soccerPitchHalf(data = my_df, theme = 'light') + 49 | geom_point(aes(x = location.y, y = location.x, 50 | size = shot.statsbomb_xg, colour = goal), 51 | alpha = 0.7) 52 | 53 | } 54 | \seealso{ 55 | \code{\link{soccerShotmap}} for plotting a shotmap on a half pitch for a single player or \code{\link{soccerPitch}} for drawing a full size soccer pitch 56 | } 57 | -------------------------------------------------------------------------------- /man/soccerPositionMap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPositionMap.R 3 | \name{soccerPositionMap} 4 | \alias{soccerPositionMap} 5 | \title{Plot average player position using any event or tracking data} 6 | \usage{ 7 | soccerPositionMap( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | fill1 = "red", 12 | col1 = NULL, 13 | fill2 = "blue", 14 | col2 = NULL, 15 | labelCol = "black", 16 | homeTeam = NULL, 17 | flipAwayTeam = TRUE, 18 | label = c("name", "number", "none"), 19 | labelBox = TRUE, 20 | shortNames = TRUE, 21 | nodeSize = 5, 22 | labelSize = 4, 23 | arrow = c("none", "r", "l"), 24 | theme = c("light", "dark", "grey", "grass"), 25 | title = NULL, 26 | subtitle = NULL, 27 | source = c("manual", "statsbomb"), 28 | x = "x", 29 | y = "y", 30 | id = "player_id", 31 | name = "player_name", 32 | team = "team_name" 33 | ) 34 | } 35 | \arguments{ 36 | \item{df}{a dataframe containing x,y-coordinates of player position and a player identifier variable} 37 | 38 | \item{lengthPitch, widthPitch}{numeric, length and width of pitch in metres} 39 | 40 | \item{fill1, fill2}{character, fill colour of position points of team 1, team 2 (team 2 \code{NULL} by default)} 41 | 42 | \item{col1, col2}{character, border colour of position points of team 1, team 2 (team 2 \code{NULL} by default)} 43 | 44 | \item{labelCol}{character, label text colour} 45 | 46 | \item{homeTeam}{if \code{df} contains two teams, the name of the home team to be displayed on the left hand side of the pitch (i.e. attacking from left to right). If \code{NULL}, infers home team as the team of the first event in \code{df}.} 47 | 48 | \item{flipAwayTeam}{flip x,y-coordinates of away team so attacking from right to left} 49 | 50 | \item{label}{type of label to draw, player names (\code{name}), jersey numbers (\code{number}), or \code{none}} 51 | 52 | \item{labelBox}{add box around label text} 53 | 54 | \item{shortNames}{shorten player names to display last name as label} 55 | 56 | \item{nodeSize}{numeric, size of position points} 57 | 58 | \item{labelSize}{numeric, size of labels} 59 | 60 | \item{arrow}{optional, adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'})} 61 | 62 | \item{theme}{draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch} 63 | 64 | \item{title, subtitle}{optional, adds title and subtitle to plot} 65 | 66 | \item{source}{if \code{statsbomb}, uses StatsBomb definitions of required variable names (i.e. `location.x`, `location.y`, `player.id`, `team.name`); if \code{manual} (default), respects variable names defined in function arguments \code{x}, \code{y}, \code{id}, \code{name}, and \code{team}.} 67 | 68 | \item{x, y, id, name, team}{names of variables containing x,y-coordinates, unique player ids, player names, and team names, respectively; \code{name} and \code{team} NULL by default} 69 | } 70 | \description{ 71 | Draws the average x,y-positions of each player from one or both teams on a soccer pitch. 72 | } 73 | \examples{ 74 | library(dplyr) 75 | data(statsbomb) 76 | 77 | # average player position from tracking data for one team 78 | # w/ jersey numbers labelled 79 | data(tromso) 80 | tromso \%>\% 81 | soccerPositionMap(label = "number", id ="id", 82 | labelCol = "white", nodeSize = 8, 83 | arrow = "r", theme = "grass", 84 | title = "Tromso IL (vs. Stromsgodset, 3rd Nov 2013)", 85 | subtitle = "Average player position (1' - 16')") 86 | 87 | # transform x,y-coords, standarise column names, 88 | # average pass position for one team using 'statsbomb' method 89 | # w/ player name as labels 90 | statsbomb \%>\% 91 | soccerTransform(method='statsbomb') \%>\% 92 | filter(type.name == "Pass" & team.name == "France" & period == 1) \%>\% 93 | soccerPositionMap(source = "statsbomb", 94 | fill1 = "blue", arrow = "r", theme = "light", 95 | title = "France (vs Argentina, 30th June 2018)", 96 | subtitle = "Average pass position (1' - 45')") 97 | 98 | # transform x,y-coords, standarise column names, 99 | # average pass position for two teams using 'manual' method 100 | # w/ player names labelled 101 | statsbomb \%>\% 102 | soccerTransform(method='statsbomb') \%>\% 103 | soccerStandardiseCols(method='statsbomb') \%>\% 104 | filter(event_name == "Pass" & period == 1) \%>\% 105 | soccerPositionMap(fill1 = "lightblue", fill2 = "blue", 106 | title = "Argentina vs France, 30th June 2018", 107 | subtitle = "Average pass position (1' - 45')") 108 | 109 | } 110 | -------------------------------------------------------------------------------- /man/soccerResample.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerResample.R 3 | \name{soccerResample} 4 | \alias{soccerResample} 5 | \title{Resample the frames per second of any tracking data using linear interpolation} 6 | \usage{ 7 | soccerResample(df, r = 10, x = "x", y = "y", t = "t", id = "id") 8 | } 9 | \arguments{ 10 | \item{df}{a dataframe containing x,y-coordinates and time variable} 11 | 12 | \item{r}{resampling rate in frames per second} 13 | 14 | \item{x, y}{name of variables containing x,y-coordinates} 15 | 16 | \item{t}{name of variable containing time data} 17 | 18 | \item{id}{name of variable containing player identifier} 19 | } 20 | \value{ 21 | a dataframe with interpolated rows added 22 | } 23 | \description{ 24 | Downsample or upsample any tracking data containing x,y,t data using linear interpolation of x,y-coordinates (plus constant interpolation of all other variables in dataframe) 25 | } 26 | \examples{ 27 | data(tromso) 28 | 29 | # resample tromso dataset from ~21 fps to 10 fps 30 | soccerResample(tromso, r=10) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /man/soccerShortenName.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerShortenName.R 3 | \name{soccerShortenName} 4 | \alias{soccerShortenName} 5 | \title{Extract player surname} 6 | \usage{ 7 | soccerShortenName(names) 8 | } 9 | \arguments{ 10 | \item{names}{vector of strings containing full player name} 11 | } 12 | \description{ 13 | Helper function to extract last name (including common nobiliary particles) from full player names 14 | } 15 | \examples{ 16 | data(statsbomb) 17 | statsbomb$name <- soccerShortenName(statsbomb$player.name) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /man/soccerShotmap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerShotmap.R 3 | \name{soccerShotmap} 4 | \alias{soccerShotmap} 5 | \title{Draw an individual, team, or two team shotmap using StatsBomb data} 6 | \usage{ 7 | soccerShotmap( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | homeTeam = NULL, 12 | adj = TRUE, 13 | n_players = 0, 14 | size_lim = c(2, 15), 15 | title = NULL, 16 | subtitle = NULL, 17 | theme = c("light", "dark", "grey", "grass") 18 | ) 19 | } 20 | \arguments{ 21 | \item{df}{dataframe containing x,y-coordinates of player passes} 22 | 23 | \item{lengthPitch, widthPitch}{length and width of pitch, in metres} 24 | 25 | \item{homeTeam}{if \code{df} contains two teams, the name of the home team to be displayed on the left hand side of the pitch. If \code{NULL}, infers home team as the team of the first event in \code{df}.} 26 | 27 | \item{adj}{adjust xG using conditional probability to account for multiple shots per possession} 28 | 29 | \item{n_players}{number of highest xG players to display} 30 | 31 | \item{size_lim}{minimum and maximum size of points, \code{c(min, max)}} 32 | 33 | \item{title, subtitle}{optional, adds title and subtitle to half pitch plot. Title defaults to scoreline and team identity when two teams are defined in \code{df}.} 34 | 35 | \item{theme}{draws a \code{light}, \code{dark}, \code{grey}, or \code{grass} coloured pitch with appropriate point colours} 36 | } 37 | \value{ 38 | a ggplot object 39 | } 40 | \description{ 41 | If \code{df} contains two teams, draws a shotmap of each team at either end of a full pitch. If \code{df} contains one or more players from a single team, draws a vertical half pitch. Currently only works with StatsBomb data but compatability with other (non-StatsBomb) shot data will be added soon. 42 | } 43 | \examples{ 44 | data(statsbomb) 45 | 46 | # shot map of two teams on full pitch 47 | statsbomb \%>\% 48 | soccerTransform(method='statsbomb') \%>\% 49 | soccerShotmap(theme = "gray") 50 | 51 | # shot map of one player on half pitch 52 | statsbomb \%>\% 53 | dplyr::filter(player.name == "Antoine Griezmann") \%>\% 54 | soccerTransform(method='statsbomb') \%>\% 55 | soccerShotmap(theme = "grass", 56 | title = "Antoine Griezmann", 57 | subtitle = "vs. Argentina, World Cup 2018") 58 | 59 | } 60 | -------------------------------------------------------------------------------- /man/soccerSpokes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerSpokes.R 3 | \name{soccerSpokes} 4 | \alias{soccerSpokes} 5 | \title{Draw spokes of passing direction on a soccer pitch} 6 | \usage{ 7 | soccerSpokes( 8 | df, 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | xBins = 5, 12 | yBins = NULL, 13 | angleBins = 8, 14 | x = "x", 15 | y = "y", 16 | angle = "angle", 17 | minLength = 0.6, 18 | minAlpha = 0.5, 19 | minWidth = 0.5, 20 | col = "black", 21 | legend = TRUE, 22 | arrow = c("none", "r", "l"), 23 | title = NULL, 24 | subtitle = NULL, 25 | theme = c("light", "dark", "grey", "grass"), 26 | plot = NULL 27 | ) 28 | } 29 | \arguments{ 30 | \item{df}{a dataframe of event data containing fields of start x,y-coordinates, pass distance, and pass angle} 31 | 32 | \item{lengthPitch, widthPitch}{numeric, length and width of pitch in metres} 33 | 34 | \item{xBins, yBins}{integer, the number of horizontal (length-wise) and vertical (width-wise) bins the soccer pitch is to be divided up into; if \code{yBins} is NULL (default), it will take the value of \code{xBins}} 35 | 36 | \item{angleBins}{integer, the number of arrows to draw in each zone of the pitch; for example, a value of 4 clusters has direction vectors up, down, left, and right} 37 | 38 | \item{x, y, angle}{names of variables containing pass start x,y-coordinates and angle} 39 | 40 | \item{minLength}{numeric, ratio between size of shortest arrow and longest arrow depending on number of events} 41 | 42 | \item{minAlpha, minWidth}{numeric, minimum alpha and line width of arrows drawn} 43 | 44 | \item{col}{colour of arrows} 45 | 46 | \item{legend}{if \code{TRUE}, adds legend for arrow transparency} 47 | 48 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 49 | 50 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 51 | 52 | \item{theme}{palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass}} 53 | 54 | \item{plot}{base plot to add path layer to; NULL by default} 55 | } 56 | \value{ 57 | a ggplot object of a heatmap on a soccer pitch 58 | } 59 | \description{ 60 | Multiple arrows to show the distribution of pass angle and distance in zones of the pitch; similar to a radar plot but grouped by pitch location rather than player 61 | } 62 | \examples{ 63 | library(dplyr) 64 | data(statsbomb) 65 | 66 | # transform x,y-coords, filter only France pass events, 67 | # draw flow field showing mean angle, distance of passes per pitch zone 68 | statsbomb \%>\% 69 | soccerTransform(method = 'statsbomb') \%>\% 70 | filter(team.name == "France" & type.name == "Pass") \%>\% 71 | soccerSpokes(xBins=7, yBins=5, angleBins=12, legend=FALSE) 72 | 73 | # transform x,y-coords, standarise column names, 74 | # filter only France pass events 75 | my_df <- statsbomb \%>\% 76 | soccerTransform(method = 'statsbomb') \%>\% 77 | soccerStandardiseCols(method = 'statsbomb') \%>\% 78 | filter(team_name == "France" & event_name == "Pass") 79 | 80 | # overlay flow field onto heatmap showing proportion of team passes per pitch zone 81 | soccerHeatmap(my_df, xBins=7, yBins=5, 82 | title = "France passing radar") \%>\% 83 | soccerSpokes(my_df, xBins=7, yBins=5, angleBins=8, legend=FALSE, plot = .) 84 | 85 | } 86 | \seealso{ 87 | \code{\link{soccerHeatmap}} for drawing a heatmap of player position, or \code{\link{soccerFlow}} for drawing a single arrow for pass distance and angle per pitch zone. 88 | } 89 | -------------------------------------------------------------------------------- /man/soccerStandardiseCols.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerStandardiseCols.R 3 | \name{soccerStandardiseCols} 4 | \alias{soccerStandardiseCols} 5 | \title{Rename columns in a dataframe for easier use with other {soccermatics} functions} 6 | \usage{ 7 | soccerStandardiseCols(df, method = c("statsbomb")) 8 | } 9 | \arguments{ 10 | \item{df}{a dataframe of Statsbomb event data} 11 | 12 | \item{method}{source of data; only \code{"statsbomb"} currently supported} 13 | } 14 | \value{ 15 | a dataframe with column names x, y, distance, angle, player_id, player_name, team_name, event_name 16 | } 17 | \description{ 18 | Rename columns (e.g. \code{"location.x"} -> \code{"x"}, \code{"team.name"} -> \code{"team"}, etc...) to interface directly with other soccermatics functions without having to explicitly define column names as arguments. Currently only supports Statsbomb data. 19 | } 20 | \examples{ 21 | library(dplyr) 22 | data(statsbomb) 23 | 24 | # transform x,y-coords, standardise column names 25 | my_df <- statsbomb \%>\% 26 | soccerTransform(method = 'statsbomb') \%>\% 27 | soccerStandardiseCols(method = 'statsbomb') 28 | 29 | # feed to other functions without defining variables, 30 | # x, y, id,distance, angle, etc... 31 | soccerHeatmap(my_df) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /man/soccerTransform.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerTransform.R 3 | \name{soccerTransform} 4 | \alias{soccerTransform} 5 | \title{Normalises x,y-coordinates to metres units for use with soccermatics functions} 6 | \usage{ 7 | soccerTransform( 8 | df, 9 | xMin, 10 | xMax, 11 | yMin, 12 | yMax, 13 | lengthPitch = 105, 14 | widthPitch = 68, 15 | method = c("manual", "statsbomb", "opta", "chyronhego", "ch"), 16 | x = "x", 17 | y = "y" 18 | ) 19 | } 20 | \arguments{ 21 | \item{df}{dataframe containing arbitrary x,y-coordinates} 22 | 23 | \item{xMin, xMax, yMin, yMax}{range of possible x,y-coordinates in the raw dataframe} 24 | 25 | \item{lengthPitch, widthPitch}{length, width of pitch in metres} 26 | 27 | \item{method}{source of data, either \code{"opta"}, \code{"statsbomb"}, \code{"chyronhego"} (ChryonHego), or \code{"manual"}} 28 | 29 | \item{x, y}{variable names of x,y-coordinates. Not required when \code{method} other than \code{"manual"} is defined; defaults to \code{"x"} and \code{"y"} if manual.} 30 | } 31 | \value{ 32 | a dataframe 33 | } 34 | \description{ 35 | Normalise x,y-coordinates from between arbitrary limits to metre units bounded by [0 < \code{"x"} < \code{"pitchLength"}, 0 < \code{"y"} < \code{"pitchWidth"}] 36 | } 37 | \examples{ 38 | # Three examples with true pitch dimensions (in metres): 39 | lengthPitch <- 105 40 | widthPitch <- 68 41 | 42 | # Example 1. Opta ---------------------------------------------------------- 43 | 44 | # limits = [0 < x < 100, 0 < y < 100] 45 | opta_df <- data.frame(team_id = as.factor(c(1, 1, 1, 2, 2)), 46 | x = c(50.0, 41.2, 44.4, 78.6, 76.7), 47 | y = c(50.0, 55.8, 47.5, 55.1, 45.5), 48 | endx = c(42.9, 40.2, 78.0, 80.5, 72.4), 49 | endy = c(57.6, 47.2, 55.6, 48.1, 26.3)) 50 | 51 | soccerTransform(opta_df, method = "opta") 52 | 53 | 54 | # Example 2. StatsBomb ----------------------------------------------------- 55 | 56 | # limits = [0 < x < 120, 0 < y < 80] 57 | data(statsbomb) 58 | soccerTransform(statsbomb, method = "statsbomb") 59 | 60 | 61 | # Example 3. ChyronHego -------------------------------------------------------- 62 | 63 | # limits = [-5250 < x < 5250, -3400 < y < 3400] 64 | 65 | xMin <- -5250 66 | xMax <- 5250 67 | yMin <- -3400 68 | yMax <- 3400 69 | 70 | ch_df <- data.frame(x = c(0,-452,-982,-1099,-1586,-2088,-2422,-2999,-3200,-3857), 71 | y = c(0,150,300,550,820,915,750,620,400,264)) 72 | 73 | soccerTransform(ch_df, -5250, 5250, -3400, 3400, method = "chyronhego") 74 | 75 | 76 | # Example 4. Manual ----------------------------------------------------- 77 | 78 | # limits = [0 < x < 420, -136 < y < 136] 79 | 80 | my_df <- data.frame(team = as.factor(c(1, 1, 1, 2, 2)), 81 | my_x = c(210, 173, 187, 330, 322), 82 | my_y = c(0, 16, -7, 14, -12), 83 | my_endx = c(180, 169, 328, 338, 304), 84 | my_endy = c(21, -8, 15, -5, -65)) 85 | 86 | soccerTransform(my_df, 0, 420, -136, 136, x = c("my_x", "my_endx"), y = c("my_y", "my_endy")) 87 | 88 | } 89 | -------------------------------------------------------------------------------- /man/soccerVelocity.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerVelocity.R 3 | \name{soccerVelocity} 4 | \alias{soccerVelocity} 5 | \title{Compute instantaneous distance, speed and direction from x,y-coordinates} 6 | \usage{ 7 | soccerVelocity(dat) 8 | } 9 | \arguments{ 10 | \item{dat}{dataframe containing unnormalised x,y-coordinates \code{x} and \code{y}, time variable \code{'t'}, and player identifier \code{'id'}} 11 | } 12 | \value{ 13 | a dataframe with columns \code{'dist'}, \code{'speed'}, and \code{'direction'} added 14 | } 15 | \description{ 16 | Compute instantaneous distance moved (in metres), speed (in metres per second), and direction (in radians) between subsequent frames in a dataframe of x,y-coordinates. 17 | } 18 | \examples{ 19 | data(tromso) 20 | 21 | # calculate distance, speed, and direction for \code{tromso} dataset 22 | soccerVelocity(tromso) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /man/soccermatics-deprecated.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerPitchBG.R, R/soccermatics-deprecated.R 3 | \name{soccerPitchBG} 4 | \alias{soccerPitchBG} 5 | \alias{soccermatics-deprecated} 6 | \title{Plot a full soccer pitch} 7 | \usage{ 8 | soccerPitchBG( 9 | lengthPitch = 105, 10 | widthPitch = 68, 11 | arrow = c("none", "r", "l"), 12 | title = NULL, 13 | subtitle = NULL, 14 | theme = c("light", "dark", "grey", "grass"), 15 | data = NULL 16 | ) 17 | } 18 | \arguments{ 19 | \item{lengthPitch, widthPitch}{length and width of pitch in metres} 20 | 21 | \item{arrow}{adds team direction of play arrow as right (\code{'r'}) or left (\code{'l'}); \code{'none'} by default} 22 | 23 | \item{title, subtitle}{adds title and subtitle to plot; NULL by default} 24 | 25 | \item{theme}{palette of pitch background and lines, either \code{light} (default), \code{dark}, \code{grey}, or \code{grass}} 26 | 27 | \item{data}{a default dataset for plotting in subsequent layers; NULL by default} 28 | 29 | \item{fillPitch, colPitch}{pitch fill and line colour} 30 | } 31 | \value{ 32 | a ggplot object 33 | } 34 | \description{ 35 | Draws a soccer pitch as a ggplot object for the purpose of adding layers such as player positions, player trajectories, etc.. 36 | 37 | The functions listed below are deprecated and will be defunct in 38 | the near future. When possible, alternative functions with similar 39 | functionality are also mentioned. Help pages for deprecated functions are 40 | available at \code{help("soccermatics-deprecated")}. 41 | } 42 | \seealso{ 43 | \code{\link{soccermatics-deprecated}} 44 | } 45 | \keyword{internal} 46 | -------------------------------------------------------------------------------- /man/soccerxGTimeline.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/soccerxGTimeline.R 3 | \name{soccerxGTimeline} 4 | \alias{soccerxGTimeline} 5 | \title{Draw a timeline showing cumulative expected goals (xG) over the course of a match using StatsBomb data.} 6 | \usage{ 7 | soccerxGTimeline( 8 | df, 9 | homeCol = "red", 10 | awayCol = "blue", 11 | adj = TRUE, 12 | labels = TRUE, 13 | y_buffer = 0.3 14 | ) 15 | } 16 | \arguments{ 17 | \item{df}{a dataframe containing StatsBomb data from one full match} 18 | 19 | \item{homeCol, awayCol}{colours of the home and away team, respectively} 20 | 21 | \item{adj}{adjust xG using conditional probability to account for multiple shots per possession} 22 | 23 | \item{labels}{include scoreline and goalscorer labels for goals} 24 | 25 | \item{y_buffer}{vertical space to add at the top of the y-axis (a quick and dirty way to ensure text annotations are not cropped).} 26 | } 27 | \value{ 28 | a ggplot object 29 | } 30 | \description{ 31 | Draw a timeline showing cumulative expected goals (xG, excluding penalties and own goals) by two teams over the course of a match, as well as plotting the scoreline and goalscorer at goal events. Currently only works with StatsBomb data but compatability with other (non-StatsBomb) shot data will be added soon. 32 | } 33 | \examples{ 34 | library(dplyr) 35 | data(statsbomb) 36 | 37 | # xG timeline of France vs. Argentina 38 | # w/ goalscorer labels, adjusted xG data 39 | statsbomb \%>\% 40 | soccerxGTimeline(homeCol = "blue", awayCol = "lightblue", y_buffer = 0.4) 41 | 42 | # no goalscorer labels, raw xG data 43 | statsbomb \%>\% 44 | soccerxGTimeline(homeCol = "blue", awayCol = "lightblue", adj = FALSE) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /man/statsbomb.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/statsbomb.R 3 | \docType{data} 4 | \name{statsbomb} 5 | \alias{statsbomb} 6 | \title{Sample StatsBomb event data containing the x,y-locations and identity of players involved in pass events, shot events, defensive actions, and more.} 7 | \format{ 8 | A dataframe containing 12000 frames of x,y-coordinates and timestamps from 11 players. 9 | } 10 | \source{ 11 | \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking} 12 | } 13 | \usage{ 14 | data(statsbomb) 15 | } 16 | \description{ 17 | Sample StatsBomb event data from the France vs. Argentina World Cup 2018 game on the 30th June 2018, made publicly available by StatsBomb \href{https://github.com/statsbomb/open-data}{here}. Data contains 145 variables in total, including x,y-coordinates (\code{location.x}, \code{location.y}). StatsBomb pitch dimensions are 120m long and 80m wide, meaning \code{lengthPitch} should be specified as \code{120} and \code{widthPitch} as \code{80}. Event data for all World Cup games (and other competitions) are accessible via the StatsBombR package available \href{https://github.com/statsbomb/StatsBombR}{here}. 18 | } 19 | \references{ 20 | \href{https://github.com/statsbomb/open-data}{StatsBomb Open Data} 21 | } 22 | -------------------------------------------------------------------------------- /man/tromso.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tromso.R 3 | \docType{data} 4 | \name{tromso} 5 | \alias{tromso} 6 | \title{x,y-coordinates of 11 soccer players over 12000 frames each} 7 | \format{ 8 | A dataframe containing 12000 frames of x,y-coordinates and timestamps from 11 players. 9 | } 10 | \source{ 11 | \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking} 12 | } 13 | \usage{ 14 | data(tromso) 15 | } 16 | \description{ 17 | x,y-coordinates of 11 soccer players over 10 minutes (Tromsø IL vs. Anzhi, 2013-11-07), captured at 20 Hz using the ZXY Sport Tracking system and made available in the publication \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking}. 18 | } 19 | \references{ 20 | \href{http://home.ifi.uio.no/paalh/publications/files/mmsys2014-dataset.pdf}{Pettersen et al. (2014)} Proceedings of the International Conference on Multimedia Systems (MMSys) 21 | } 22 | -------------------------------------------------------------------------------- /man/tromso_extra.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tromso_extra.R 3 | \docType{data} 4 | \name{tromso_extra} 5 | \alias{tromso_extra} 6 | \title{x,y-coordinates and additional positional information on 11 soccer players over 12000 frames each} 7 | \format{ 8 | A dataframe containing 12000 frames of x,y-coordinates and timestamps from 11 players. 9 | } 10 | \source{ 11 | \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking} 12 | } 13 | \usage{ 14 | data(tromso_extra) 15 | } 16 | \description{ 17 | x,y-coordinates of 11 soccer players over 10 minutes (Tromsø IL vs. Anzhi, 2013-11-07), plus additional information on player heading, direction, energy, speed, and total distance. Data captured at 20 Hz using the ZXY Sport Tracking system and made available in the publication \href{http://home.ifi.uio.no/paalh/dataset/alfheim/}{ZXY Sport Tracking}. 18 | } 19 | \references{ 20 | Pettersen et al. (2014) Proceedings of the International Conference on Multimedia Systems (MMSys) 21 | (\href{http://home.ifi.uio.no/paalh/publications/files/mmsys2014-dataset.pdf}{pdf}) 22 | } 23 | -------------------------------------------------------------------------------- /pitch_dimensions.csv: -------------------------------------------------------------------------------- 1 | team,alt_name,stadium,length,width 2 | Arsenal,Arsenal,Emirates Stadium,105,68 3 | Bournemouth,AFC Bournemouth,Dean Court,105,78 4 | Chelsea,Chelsea,Stamford Bridge,103,67 5 | Crystal Palace,Crystal Palace,Selhurst Park,100.58,67.67 6 | Everton,Everton,Goodison Park,100.48,68 7 | Hull City,Hull,KCOM Stadium,114,74 8 | Leicester City,Leicester,King Power Stadium,102,67 9 | Liverpool,Liverpool,Anfield,101,68 10 | Manchester City,Man City,Etihad Stadium,105,68 11 | Manchester United,Man United,Old Trafford,105,68 12 | Middlesbrough,Middlesbrough,Riverside Stadium,105,69 13 | Southampton,Southampton,St Mary's Stadium,105,68 14 | Stoke City,Stoke,bet365 Stadium,105,68 15 | Sunderland,Sunderland,Stadium of Light,105,68 16 | Swansea City,Swansea,Liberty Stadium,105.16,67.67 17 | Tottenham Hotspur,Tottenham,White Hart Lane,100,67 18 | Watford,Watford,Vicarage Road,109.73,73.15 19 | West Bromwich Albion,West Brom,The Hawthorns,105,68 20 | West Ham United,West Ham,Olympic Stadium,105,68 21 | Tromso,Tromsø IL,Alfheim Stadion,105,68 22 | Brighton & Hove Albion,Brighton,Falmer Stadium,105,69 23 | Huddersfield Town,Huddersfield,Kirklees Stadium,105,69 24 | Burnley,Burnley,Turf Moor,105,68 25 | Newcastle United,Newcastle,St James' Park,105,68 26 | Ipswich Town,Ipswich,Portman Road,102,75 27 | Nottingham Forest,Nott'm Forest,The City Ground,105,71 28 | Sheffield Wednesday,Sheffield Weds,Hillsborough,106,69 29 | Blackburn Rovers,Blackburn,Ewood Park,105,69 30 | Millwall,Millwall,The Den,106,68 31 | Leeds United,Leeds,Elland Road,105,68 32 | Reading,Reading,Madjeski Stadium,105,68 33 | Bristol City,Bristol City,Ashton Gate,105,68 34 | Derby County,Derby,Pride Park,105,68 35 | Aston Villa FC,Aston Villa,Villa Park,105,68 36 | Wigan Athletic,Wigan,DW Stadium,105,68 37 | Norwich City FC,Norwich,Carrow Road,104,68 38 | Preston North End,Preston,Deepdale,101,69 39 | Queens Park Rangers,QPR,Loftus Road,102,66 40 | Rotherham United,Rotherham,New York Stadium,102,66 41 | Brentford,Brentford,Griffin Park,100,67 42 | Sheffield United,Sheffield United,Bramall Lane,102,65 43 | Birmingham City,Birmingham,St Andrew's,100,66 44 | Bolton Wanderers,Bolton,The Macron Stadium,100,66 45 | Real Sociedad,Sociedad,anoeta stadium,105,68 46 | Celta Vigo,Celta,balaidos,105,68 47 | FC Barcelona,Barcelona,camp nou,105,68 48 | Villarreal CF,Villarreal,estadio el madrigal,105,68 49 | SD Eibar,Eibar,ipurua municipal stadium,103,65 50 | Deportivo Alaves,Alaves,mendizorroza stadium,105,67 51 | Sevilla,Sevilla,ramon sanchez pizjuan stadium,105,68 52 | Espanyol,Espanol,rcde stadium,105,68 53 | Athletic Bilbao,Ath Bilbao,san mames,105,68 54 | Real Madrid,Real Madrid,santiago bernabeu,105,68 55 | Valencia,Valencia,the mestalla,105,70 56 | Atletico Madrid,Ath Madrid,wanda metropolitano,105,68 57 | Real Betis,Betis,Estadio Benito Villamarín,107,64 58 | Getafe,Getafe,Coliseum Alfonso Pérez,105,71 59 | Girona,Girona,Estadi Montilivi,100,68 60 | Levante,Levante,Estadi Ciutat de València,107,68 61 | Real Valladolid,Valladolid,Estadio José Zorrilla,105,68 62 | -------------------------------------------------------------------------------- /soccermatics.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | -------------------------------------------------------------------------------- /soccermatics_0.9.5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoGall/soccermatics/4dfbfebc345b28103bfa08db89884626dcbe2a80/soccermatics_0.9.5.pdf --------------------------------------------------------------------------------