The Miracle of Istanbul
(Updated: )
Introduction
On May 25, 2005, the UEFA Champions League final took place at the Atatürk Olympic Stadium in Istanbul. Some 65,000 spectators in the stadium and hundreds of millions of fans worldwide were about to witness a soccer game that would later be called the Miracle of Istanbul. The star-studded AC Milan took the lead within the first minute through their captain, Maldini. They scored two more goals before half-time, making it a 3-0 lead. Rumors said that the Milan players opened bottles of champagne in the locker room during the half-time break to celebrate their soon-to-be second Champions League title in three years. However, in the second half Liverpool launched a comeback and scored 3 goals in a dramatic six-minute spell to level the score at 3-3. The game went to extra time and then to penalties, where Liverpool beat Milan 3-2. The English commentator said, “If this doesn’t prove fate exists, then nothing will” at the end of the game. To be sure, this epic comeback defies any words that try to describe it, but I will try to use visualizations to do this job and, more importantly, to figure out how the comeback happened.
The underdog
The Liverpool captain Steven Gerrard described his team as underdogs before the match, compared to the all-star Milan side, and he was right. The market values below come from transfermarkt.co.uk.
# get the data for the miracle of istanbul
Matches <- FreeMatches(FreeCompetitions())
istanbul <- get.matchFree(Matches[which(Matches$match_id == 2302764),])
milanLineup <- istanbul[[24]][[1]]$player.name
liverpoolLineup <- istanbul[[24]][[2]]$player.name
milanmv <- c(13.5, 2.7, 9.0, 29.7, 2.7, 18.9, 22.5, 16.2, 23.4, 31.5, 13.5)
liverpoolmv <- c(4.05, 3.6, 6.75, 6.08, 1.8, 13.5, 7.88, 27, 11.25, 11.7, 15.75)
mvSums <- data.frame(name = c("Milan", "Liverpool"), value = c(sum(milanmv), sum(liverpoolmv)))
library(RColorBrewer)
coul <- suppressWarnings(brewer.pal(2, "Set2"))
barplot(height=mvSums$value, names=mvSums$name, col=coul, main = "Market value of Starting XI", sub = "184M £ vs 109M £", ylab = "in million pounds", font.sub = 4)
mv <- data.frame(name = c(milanLineup, liverpoolLineup), value = c(milanmv, liverpoolmv))
mvtop10 <- head(mv[order(-mv$value), ], n = 10)
library(forcats)
mvtop10 %>%
mutate(name = fct_reorder(name, value)) %>%
ggplot( aes(x=name, y=value)) +
geom_bar(stat="identity", fill="#f68060", alpha=.6, width=.4) +
coord_flip() +
xlab("") +
theme_bw()
The two charts show that the market value of Milan’s starting XI was significantly higher than Liverpool’s. Individually, Milan’s players occupied 8 of the top 10 places by market value across both squads. The captain Steven Gerrard and the striker Baroš were the 2 exceptions for Liverpool.
After setting the stage, let us go to the actual match.
The Data
Description
The dataset comes from the open-source GitHub repository of the soccer data analytics company StatsBomb. They make public 878 datasets in JSON format, one per game, including this one. Every dataset contains thousands of events, each characterized by 100 or more variables.
dim(istanbul)
## [1] 4648 126
The dataset that records events from the Miracle of Istanbul has 4648 events, each described by 126 variables.
head(colnames(istanbul), n = 10)
## [1] "id" "index" "period" "timestamp"
## [5] "minute" "second" "possession" "duration"
## [9] "related_events" "location"
The above shows the first 10 variables. The first variable id is the
unique identifier of each event; period denotes the time period in
which the event occurs (e.g. 1 = the first half); timestamp records
the exact time of each event; location is another important variable
that records the coordinate information of the event. An example event
therefore would be: player x at time 00:31:52.716 passes the ball at the
pitch coordinate (24, 40) at mid-height with angle y. As we go forward,
we will use and visualize more interesting variables.
Characteristics of the data
- The dataset is really sparse (lots of NULL or NA values). This makes
sense, because at one time point only one player is able to make one
action. For example, the variable
dribble.nutmegis not NA if and only if some player nutmegs someone else (ouch!).
length(which(!is.na(istanbul$dribble.nutmeg)))
## [1] 4
4 nutmegs in 120 minutes (90 mins regular time plus 30 mins extra), not bad!
- We need to take special care of the
locationvariable, which denotes the pitch coordinates of each event. The coordinates are standardized to a 120 * 80 pitch. However, pitch sizes vary, and the Atatürk Olympic Stadium measures 105 meters * 68 meters. We need to rescale the coordinates from the standardized grid to actual meters, so that each event shows up in the correct location on the pitch I draw.
location.x <- location.y <- rep(NA, nrow(istanbul))
for (i in 1:nrow(istanbul)) {
if (! is.null(istanbul$location[[i]])) {
location.x[i] <- istanbul$location[[i]][1] * 105 / 120
location.y[i] <- istanbul$location[[i]][2] * 68 / 80
}
}
istanbul$location.x <- location.x
istanbul$location.y <- location.y
Timeline of the match
To figure out how the game evolved, we need to know the milestones of the match. The milestones are the key events, which by convention include the match start, goals, the end of the 1st half, the start of the 2nd half, cards, substitutions, and penalties.
# get the goals and penalties
goalEvents <- istanbul[which(istanbul$shot.outcome.id == 97 | istanbul$shot.type.name == "Penalty"), ]
# get the subs
subEvents <- istanbul[which(!is.na(istanbul$substitution.outcome.name)), ]
# get the cards
cardEvents <- istanbul[which(!is.na(istanbul$foul_committed.card.name)), ]
keyEvents <- rbind(goalEvents, subEvents, cardEvents)
# Format the data to conform to timevis
contents <- c("First half", "Second half", "1st half extra", "2nd half extra", "Penalty shootout")
contents <- c(contents, "yellow card", "yellow card")
contents <- c(contents, "Kewell out, Smicer in", "Finnan out, Hamann in", "3-1", "3-2", " 3-3", "Baros out, Cisse in", "O","O", "X","O")
contents <- c(contents, "1-0", "2-0", "3-0", "Crespo out, Tomasson in", "Seedorf out, Serginho in", "Gattuso out, Rui Costa in", "X", "X", "O", "O", "X")
contents <- c(contents, "Timeout")
start <- c("2005-5-25 19:45:00", "2005-5-25 20:45:00", "2005-5-25 21:30:00", "2005-5-25 21:45:00", "2005-5-25 22:00:00")
# liverpool
start <- c (start, "2005-5-25 21:15:28", "2005-5-25 21:19:58"
, "2005-5-25 20:07:00", "2005-5-25 20:45:00", "2005-5-25 20:53:04", "2005-5-25 20:55:02", "2005-5-25 20:59:52", "2005-5-25 21:24:28", "2005-5-25 22:00:50", "2005-5-25 22:02:37", "2005-5-25 22:04:11", "2005-5-25 22:05:44")
# Milan
start <- c(start, "2005-5-25 19:45:51", "2005-5-25 20:23:12", "2005-5-25 20:27:57", "2005-5-25 21:24:50", "2005-5-25 21:25:05", "2005-5-25 21:51:20", "2005-5-25 22:00:02", "2005-5-25 22:01:40", "2005-5-25 22:03:17", "2005-5-25 22:05:03", "2005-5-25 22:06:24")
start <- c(start, "2005-5-25 20:30:00")
end <- c("2005-5-25 20:30:00", "2005-5-25 21:30:00", "2005-5-25 21:45:00", "2005-5-25 22:00:00", "2005-5-25 22:07:00", rep(NA, 23), "2005-5-25 20:45:00")
group = c(rep("time", 5), rep("Liverpool", 12), rep("Milan", 11), NA)
style = c(rep(NA, 5), rep("color:yellow;",2), rep(NA, 8), "color:red;", NA, rep(NA, 6), rep("color:red;",2), NA, NA, "color:red;", NA)
data = data.frame(content=contents, start, end, group, style)
timevisDataGroups <- data.frame(
id = c("time", "Liverpool", "Milan"),
content = c("Time", "Liverpool", "Milan")
)
library(timevis)
timevis(data, groups = timevisDataGroups)
[auto-caption] A timeline chart of the 2005 UEFA Champions League Final (Liverpool vs Milan, Wed 25 May) spanning from approximately 19:50 to 22:10. It tracks match events across two rows — Liverpool and Milan — including substitutions (e.g., “Kewell out, Smicer in,” “Gattuso out, Rui Costa in”), scoreline changes (1-0, 2-0, 3-0, 3-1, 3-2, 3-3), yellow cards, and penalty shootout results (marked with O for scored and X for missed). The timeline is divided into periods: First Half, Second Half, 1st Half Extra, 2nd Half Extra, and Penalty Shootout.
To summarize the timeline above: in the first half, Milan scored immediately when the game began, and scored two more goals towards the end of the half. Liverpool, meanwhile, made a substitution because of injury. Before the start of the 2nd half, Liverpool were already 3-0 down and had used one of their three substitutions involuntarily. At the beginning of the 2nd half, Liverpool made yet another substitution. This one was a tactical decision by the Liverpool manager, an attempt to turn things around, and it worked. About 10 minutes later, Liverpool scored their first goal, making it 3-1, and in a six-minute spell they scored three goals in total, levelling the score to 3-3. Neither team scored in the later parts of the game, and Liverpool beat Milan 3-2 in the penalty shootout (the red X denotes a penalty missed or saved by the goalkeeper).
In this timeline plot, all the key events are laid out clearly. Furthermore, we can immediately gain insight into our question, which is, how the miracle happened:
- The three comeback goals were scored in 6 minutes! What happened in those 6 minutes?
- The distribution of goals is quite “even”, in that Milan scored 3 in the first half and Liverpool scored 3 in the second half. Why is that the case?
Another thing that stands out from the timeline graph is that there were only 2 yellow cards (both from Liverpool) and no red cards in the game. That is extremely rare for a soccer game, especially for the Champions League final, the biggest game of the year. It shows how both teams focused on playing actively rather than passively (that is, on fouling a lot to stop the other team’s play). It was a smooth, high-quality game, and a dramatic one.
Heaven or Hell
As the timeline suggested, we see a distinction between the 1st half and the 2nd half. In each half, one team scored three goals and the other scored nothing. Specifically, the 1st half was heaven for Milan and hell for Liverpool; the 2nd half was exactly the reverse. Can we visualize that difference, the difference between heaven and hell?
Shots
is <- tibble::as_tibble(istanbul)
is %>%
filter(minute < 46) %>%
soccerShotmap(theme = "dark")
[auto-caption] A football pitch diagram showing shot locations from the match AC Milan 3–0 Liverpool. Orange circles on the left half represent AC Milan’s shots (xG: 1.29), with two large circles near goal indicating high-quality chances; blue dots on both sides represent Liverpool’s shots (xG: 0.19), all small, indicating low-quality attempts. Milan’s shots were concentrated in dangerous central areas, reflecting their dominant performance.
As we can see, in the 1st half Milan made 8 shots (8 dots on the picture above) and Liverpool made 5. However, most of Milan’s shots (6 out of 8) were inside the box, whereas Liverpool only had 2.
is %>%
filter(minute >= 45 & minute <= 90) %>%
soccerShotmap(theme = "dark")
[auto-caption] A dark-background football pitch diagram showing shot locations from a match where Liverpool beat AC Milan 3–0. Orange circles represent AC Milan’s shots (clustered near the left goal), while blue circles represent Liverpool’s shots (spread across the right half and near the right goal). The numbers 0.81 and 1.03 indicate each team’s expected goals (xG), with circle size reflecting shot quality.
In the 2nd half, we’d expect Liverpool to dominate the shots and the shots in the box. On the contrary, Milan still dominated both counts. Liverpool made only 2 shots inside the box and both found the net, and Liverpool also scored a long-ranger. Milan, however, attempted 6 shots inside the box and 6 more outside it, none of which found the net.
Let’s compare the shots made by both teams in extra time.
is %>%
filter(minute >= 90 & minute <= 120) %>%
soccerShotmap(theme = "dark")
[auto-caption] A dark-themed football pitch diagram showing Liverpool 1:0 AC Milan, with xG values of 0.04 (Liverpool, left half) and 0.71 (AC Milan, right half) displayed at the top. Blue dots of varying sizes are clustered in the right half near AC Milan’s goal, representing shot locations and xG values — the largest dot indicates the highest-quality chance. A small notation reads (+1 P), suggesting a penalty is included.
Surprisingly (or not so surprisingly after seeing the shots made in the 2nd half of the game), Liverpool barely threatened while Milan made 7 more shots, 3 of them inside the box with 1 near the post (so close!), and yet Milan scored nothing.
Passes
Passes are an essential part of a soccer game. By looking at the locations of the passes (both the starting and the ending locations), we can also see the positions of the ball and the players.
One note on names before we start: StatsBomb records players under their full legal names, so the charts label some familiar players in unfamiliar ways. On the Milan side, de Moraes is Cafu, Leite is Kaká, and Silva is Dida; on the Liverpool side, Sanz is Luis García and Olano is Xabi Alonso.
1st half
is %>%
filter(team.name == "AC Milan" & period == 1 & minute <= 24) %>%
soccerPassmap(fill = "lightblue", arrow = "r",
title = "Milan's passing map in the 1st half")
[auto-caption] A tactical passing map for AC Milan’s first half (minutes 1–25), showing player positions on a football pitch with connecting lines whose thickness represents pass frequency (3+ passes shown). Key central players — Pirlo, Seedorf, de Moraes, and Maldini — show the heaviest passing connections, indicating they were the hub of Milan’s build-up play. Total passes: 133, with a 78.9% completion rate.
is %>%
filter(team.name == "AC Milan" & period == 1 & minute > 24) %>%
soccerPassmap(fill = "lightblue", arrow = "r",
title = "Milan's passing map in the 1st half (25' onwards)")
[auto-caption] A passing map for AC Milan’s 1st half (26’–47’), showing player positions on a football pitch with connecting lines indicating pass combinations (3+ passes shown). Thicker, darker lines between Maldini, Seedorf, Pirlo, Nesta, and Gattuso indicate the most frequent passing relationships, with Pirlo and Maldini appearing as central hubs. Milan completed 76.0% of 121 passes in this period, with the team’s shape concentrated in the left-central area of their half.
is %>%
filter(team.name == "Liverpool" & minute <= 24 & period == 1) %>%
soccerPassmap(fill = "lightblue", arrow = "r",
title = "Liverpool's passing map in the 1st half")
[auto-caption] A football passing map showing Liverpool’s passing network from minutes 1–24 of the first half, with player positions marked as blue dots connected by lines whose thickness indicates pass frequency (3+ passes shown). Key players visible include Gerrard (central hub), Finnan, Riise, Carragher, Traoré, and Baroš, with the heaviest passing connections running between Finnan, Gerrard, Riise, and Traoré. Liverpool completed 76.9% of 121 total passes, with the arrow indicating the direction of attack (left to right).
is %>%
filter(team.name == "Liverpool" & minute > 24 & period == 1) %>%
soccerPassmap(fill = "lightblue", arrow = "r",
title = "Liverpool's passing map in the 1st half (25' onwards)")
We break the 1st half into two parts. Picture 1 shows the passing map of AC Milan in the first 24 minutes, with Pirlo at the center of it (the biggest blue dot in the map); Milan’s transition (from attack to defense and vice versa) depended on him. Another thing to notice is that one of Milan’s forwards, Crespo, was not really involved in the game. Picture 2 is the passing map of Milan in the second part of the 1st half. Compared to the first picture, Milan’s formation retreated toward their own goal. For example, in the first 24 minutes, Maldini (left back), Gattuso and Seedorf were standing in front of Pirlo; they were behind him in the later part of the 1st half. I would guess the reason was the increase in Liverpool’s aggressiveness, as well as Milan’s own strategy of contracting their defensive lines and playing on the counterattack, since they led 1-0 as early as 1 minute into the game. And that strategy was fantastic! They scored two more goals on the counterattack at 39’ and 44’, while Liverpool were pushing hard to attack (since they were behind). The scorer of both goals was Crespo, the man who was not “involved” in terms of passing in the first 24 minutes, and he became lethal in the second part.
Pictures 3 and 4 tell Liverpool’s part of the story in the 1st half. The first thing to notice is that Kewell went off and Smicer came on for him (Kewell is in the 3rd picture but not in the 4th, and Smicer the opposite) because Kewell was injured. And comparing the 4th picture with the 3rd, Gerrard and Sanz were pushing forward and towards the center, while the two wingers Riise and Smicer were roughly in the same positions as before.
The six-minute spell
is %>%
filter(team.name == "AC Milan" & minute >= 53 & minute <= 60) %>%
soccerPassmap(fill = "lightblue", arrow = "r",
title = "Milan's passing map in the 6 minute spell", minPass = 1)
[auto-caption] A football passing map for AC Milan during the 54’–61’ spell, showing player positions and pass connections across a pitch diagram. Key players include Pirlo, Crespo, Gattuso, Maldini, Shevchenko, de Moraes, and others, with thick lines indicating high-volume passing routes — notably between Pirlo, Crespo, and Leite. Milan completed 59.3% of their 27 passes in this period, with play concentrated in the central and attacking midfield zones.
passMap(is, "AC Milan", 2, 54, 60)
[auto-caption] A pass map titled “AC Milan’s passes” displays player movements and passes on a football pitch diagram during a six-minute spell. Red lines indicate incomplete/lost passes and teal/cyan lines represent successful passes, with most activity concentrated in AC Milan’s defensive half and midfield. The arrow at the bottom confirms Milan are attacking left to right.
is %>%
filter(team.name == "Liverpool" & minute >= 53 & minute <=60) %>%
soccerPassmap(fill = "lightblue", arrow = "r",
title = "Liverpool's passing map in the 6 minute spell", minPass = 1)
[auto-caption] A passing map of Liverpool’s play between the 54th and 61st minutes, showing player positions connected by lines representing passes on a football pitch diagram. Players including Gerrard, Carragher, Hamann, Riise, Hyypiä, Smicer, Baroš, and others are positioned across the pitch, with thicker lines indicating more frequent passing connections. In total, 41 passes were made during this spell, with a 75.6% completion rate.
passMap(is, "Liverpool", 2, 54, 60)
[auto-caption] A football pass map titled “Liverpool’s passes” showing passing patterns across a pitch diagram during a six-minute spell. Cyan/teal arrows represent successful passes and pink/red arrows represent unsuccessful passes, concentrated heavily in the central and right-hand areas of the pitch. An arrow beneath the pitch indicates Liverpool’s direction of attack (left to right).
We have seen the hell for Liverpool, i.e., the first half of the game, where Milan scored an early goal, played on the counter and scored two more goals while Liverpool struggled to attack. However, as the timeline suggests, in the second half of the game there was a 6-minute spell where Liverpool scored 3 goals. That spell was really heaven for Liverpool and for Liverpool’s fans. What happened? Since we are in the passing section, and analyzing passes is really effective for analyzing the whole game, let’s look at all the passes that happened in that spell. In picture 1, Milan attempted 27 passes and completed less than 60% of them. Only 8 Milan players were involved in passing the ball one or more times. Looking more closely, picture 2 shows that the passes from defense to midfield and from midfield to attack all failed (red: failure, blue: success).
By contrast, picture 3 shows that all 10 Liverpool outfield players were involved in passing, and they achieved more than 75% passing accuracy. Picture 4 shows the exact passes.
In the 1st half, Milan’s passing accuracy was 78%. What caused this decline in passing accuracy and number of passes on the Milan side, while Liverpool somehow maintained theirs? To find out, I looked at the defensive actions by both teams in those six minutes.
d2 <- is %>%
filter(type.name %in% c("Interception", "Block", "Dispossessed", "Ball Recovery") & team.name == "Liverpool" & period == 2 & minute >= 54 & minute <= 60)
soccerPitch(arrow = "r",
title = "Liverpool",
subtitle = "Defensive actions") +
geom_point(data = d2, aes(x = location.x, y = location.y, col = type.name), size = 3, alpha = 0.5)
[auto-caption] A football pitch diagram titled “Liverpool – Defensive actions” tracks defensive events during a six-minute spell, with colored dots marking locations of Ball Recoveries (pink/salmon), Blocks (green), and Interceptions (blue). Most activity is concentrated in Liverpool’s own half and the central midfield area, with a directional arrow at the bottom indicating the direction of play.
d2 <- is %>%
filter(type.name %in% c( "Interception", "Block", "Dispossessed", "Ball Recovery") & team.name == "AC Milan" & period == 2 & minute >= 54 & minute <= 60)
soccerPitch(arrow = "r",
title = "Milan",
subtitle = "Defensive actions") +
geom_point(data = d2, aes(x = location.x, y = location.y, col = type.name), size = 3, alpha = 0.5)
[auto-caption] A football pitch diagram labeled “Milan – Defensive actions” plots five events during a six-minute spell, using color-coded dots: two green (Blocks), one teal (Dispossession), one pink (Ball Recovery), and one purple (Interception). The actions are scattered across both halves, with Milan’s attacking direction indicated by a rightward arrow at the bottom.
Picture 1 shows the defensive actions by Liverpool players in those 6 minutes. What is staggering is that they managed to achieve 7 ball retrievals, which drove the decline in Milan’s passing accuracy. Picture 2 shows the defensive actions by Milan. Their single ball retrieval hardly influenced Liverpool’s passing and possession.
Position
We have already seen some visual information about the positions of the players in the passing maps, enough that we could spot some deliberate position shifts from the Milan side in the 1st half when they contracted their defensive lines. Through the visualizations below, we can see more about the shifts in position at the level of the whole team (i.e. shifts in team formation) and of individual key players.
1st half
is %>%
filter(!is.na(location.x) & team.name == "AC Milan" & period == 1) %>%
soccerPositionMap(id = "player.name", x = "location.x", y = "location.y",
fill1 = "blue", theme = "grass", arrow = "r",
title = "AC Milan",
subtitle = "Average position (1st half)")
[auto-caption] A tactical football pitch diagram showing AC Milan’s average player positions during the 1st half. Eleven blue dots represent players, each labeled with their name: Shevchenko wide right, Crespo and Leite in attacking midfield, Pirlo, Seedorf, and Gattuso in central midfield, de Moraes and Nesta in defense, with Stam, Maldini, and Silva along the back line. An arrow at the bottom indicates the direction of play.
is %>%
filter(!is.na(location.x) & team.name == "Liverpool" & period == 1 & minute >= 25) %>%
soccerPositionMap(id = "player.name", x = "location.x", y = "location.y",
fill1 = "blue", theme = "grass", arrow = "r",
title = "Liverpool",
subtitle = "Average position (1st half)")
[auto-caption] A football tactical diagram showing Liverpool’s average player positions during the 1st half, displayed on a top-down green pitch graphic. Eleven blue dots represent players, each labeled with names including Dudek (goalkeeper), Carragher, Hyypiä, Traoré, Finnan, Smicer, Gerrard, Olano, Riise, Sanz, and Baroš. An arrow at the bottom indicates Liverpool’s direction of play (left to right).
Picture 1 shows Milan’s average positions in the 1st half. The average position is defined as the average coordinates of a player when they take any action, not only passing ones. As we can see, Milan’s formation was really tight in the center of the midfield. In addition to the 4 midfielders, one of the forwards, Crespo, was close to midfield as well. And they were close together in the center for a reason (as explained below).
In picture 2, Liverpool’s formation in the 1st half was rather scattered across the width of the field. This was due to their tactics of playing wide on the sides. Gerrard and Olano sat together as a pair in the midfield. However, their attacks on the sides were not going very well, so Milan decided not to put too many players out wide (as picture 1 shows) and focused their attacks, and later their counterattacks, in the center.
First 15 minutes of the 2nd half
is %>%
filter(!is.na(location.x) & team.name == "AC Milan" & period == 2 & minute <= 60) %>%
soccerPositionMap(id = "player.name", x = "location.x", y = "location.y",
fill1 = "blue", theme = "grass", arrow = "r",
title = "AC Milan",
subtitle = "Average position (2nd half)")
[auto-caption] A tactical football pitch diagram for AC Milan, showing average player positions during the 2nd half. Players labeled include Maldini, Seedorf, Stam, Nesta, Silva (defense), Pirlo, Gattuso, Seedorf (midfield), and Shevchenko, Crespo, de Moraes (attack). The arrow at the bottom indicates Milan’s direction of play, attacking left to right.
is %>%
filter(!is.na(location.x) & team.name == "Liverpool" & period == 2 & minute <= 60) %>%
soccerPositionMap(id = "player.name", x = "location.x", y = "location.y",
fill1 = "blue", theme = "grass", arrow = "r",
title = "Liverpool",
subtitle = "Average position (2nd half)")
[auto-caption] A tactical football pitch diagram showing Liverpool’s average player positions during the first 15 minutes of the 2nd half. Players marked with blue dots are labeled: Dudek (goalkeeper), Hyypiä, Traoré, Carragher, Riise, Smicer, Hamann, Olano, Sanz, Gerrard, Baroš. The team is attacking left-to-right (indicated by the arrow), with a compact defensive shape and Baroš isolated up front.
3-0 down, Liverpool took right back Finnan off (thereby giving up the tactic of attacking on the sides) and put Hamann on right at the beginning of the 2nd half, and the result was astonishing! In picture 2, Hamann took up the position where Gerrard had been in the 1st half, and freed Gerrard. By sacrificing the right back, Liverpool squeezed their formation towards the center. Gerrard, together with Sanz and Baroš, could charge into Milan’s box freely, since Hamann took care of his defensive duties. And boom! Gerrard scored the first goal, a header inside the box, and later won the penalty that became Liverpool’s third.
Conclusion
Some may say that there is no need to visualize a soccer match, since it is a visual art/sport in itself. I have some sympathy for that, since I am a soccer fan and have had so much joy and pain watching the games, and it is especially hard to visualize a miracle. However, I did it anyway, because an EDA process on a soccer match dataset can achieve the following things:
- A visual summary. During a game, the audience sees the players only as a stream of moving images; they do not have good enough memories to remember everything, and they have no time to summarize what they see because they are concentrated on the game itself. After the match, they are given lots of statistics about the game, such as the number of shots, the possession rate, the number of passes, and pass accuracy, which do not make much sense on their own and are not even comparable across games. A 65% possession rate by one side in one game is not the same thing as another team having 65% in another game. Go ask Barcelona fans. Their team used to have 65% possession and win every trophy; now they win nothing with the same possession rate. Visualizations like these combine the visual part with the summary part, giving fans a more immersive and insightful perspective on the game.
- A good starting point for models of soccer games. With the advance
of data science, models of soccer games are in high demand. A
prominent example would be a model that predicts expected goals from
match data. This EDA process is great for:
- Seeing the data and finding its characteristics. In my example, those were the many NA values in the dataset and the mismatched pitch dimensions that I had to rescale manually.
- Visualizing the key events that may lead to goals.
- Finding directions that may be worth pursuing. I identified the six-minute Liverpool spell and think that may be the direction to look into further.
Main lessons learnt. It was without doubt a miracle, because:
- It was super dramatic. Liverpool dominated the game for only 6 minutes and ended up scoring three goals. Milan ran the show for the other 114 minutes, including 7 shots in extra time, and failed to score at all after the 1st half. That inefficiency in front of goal affected their mindset in the penalty shootout, where they missed 3 of 5. And Dudek’s (Liverpool’s goalkeeper) double save to deny Shevchenko in the 117th minute was voted the greatest Champions League moment of all time.
- There are some identifiable reasons behind the comeback. The six-minute spell came about because the introduction of Hamann freed Gerrard to attack. Milan’s success in the 1st half was also due to formation, where Milan were tight in the center and Liverpool scattered.
- The human factor is the hard part to visualize: the psychological change on the Milan side as they went from ecstasy to doubt to fear, and their change in mindset as they attacked repeatedly but scored nothing. On the other side, Gerrard and his influence on the whole team.
Credits
Data
- StatsBomb
Visualization package
- soccermatics
- ggplot2
My working session
sessionInfo()
## R version 4.1.0 (2021-05-18)
## Platform: x86_64-apple-darwin20.4.0 (64-bit)
## Running under: macOS Big Sur 11.4
##
## Matrix products: default
## BLAS: /usr/local/Cellar/openblas/0.3.15_1/lib/libopenblasp-r0.3.15.dylib
## LAPACK: /usr/local/Cellar/r/4.1.0/lib/R/lib/libRlapack.dylib
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## attached base packages:
## [1] parallel stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] forcats_0.5.1 RColorBrewer_1.1-2 soccermatics_0.9.4 StatsBombR_0.1.0
## [5] tidyr_1.1.3 sp_1.4-5 purrr_0.3.4 jsonlite_1.7.2
## [9] httr_1.4.2 doParallel_1.0.16 iterators_1.0.13 foreach_1.5.1
## [13] RCurl_1.98-1.3 rvest_1.0.0 tibble_3.1.2 stringr_1.4.0
## [17] stringi_1.6.2 dplyr_1.0.6 ggplot2_3.3.3
##
## loaded via a namespace (and not attached):
## [1] zoo_1.8-9 tidyselect_1.1.1 xfun_0.23 lattice_0.20-44
## [5] colorspace_2.0-1 vctrs_0.3.8 generics_0.1.0 htmltools_0.5.1.1
## [9] yaml_2.2.1 utf8_1.2.1 rlang_0.4.11 R.oo_1.24.0
## [13] pillar_1.6.1 glue_1.4.2 withr_2.4.2 R.utils_2.10.1
## [17] tweenr_1.0.2 plyr_1.8.6 lifecycle_1.0.0 munsell_0.5.0
## [21] gtable_0.3.0 SDMTools_1.1-221 R.methodsS3_1.8.1 codetools_0.2-18
## [25] evaluate_0.14 knitr_1.33 fansi_0.5.0 xts_0.12.1
## [29] Rcpp_1.0.6 scales_1.1.1 farver_2.1.0 ggforce_0.3.3
## [33] digest_0.6.27 ggrepel_0.9.1 polyclip_1.10-0 grid_4.1.0
## [37] cowplot_1.1.1 tools_4.1.0 bitops_1.0-7 magrittr_2.0.1
## [41] crayon_1.4.1 pkgconfig_2.0.3 ellipsis_0.3.2 MASS_7.3-54
## [45] xml2_1.3.2 rmarkdown_2.8 R6_2.5.0 compiler_4.1.0
Comments