d <- read_csv(
"data/shots_dataset_cleaned.csv",
show_col_types = FALSE
)
n_rows <- nrow(d)
n_cols <- ncol(d)
n_duplicates <- sum(duplicated(d))
missing_player_assisted <- sum(is.na(d$player_assisted))
missing_xg <- sum(is.na(d[["xG Understat"]]))
missing_shot_type <- sum(is.na(d$shotType))
missing_situation <- sum(is.na(d$situation))
xg_min <- min(d[["xG Understat"]], na.rm = TRUE)
xg_max <- max(d[["xG Understat"]], na.rm = TRUE)
xg_median <- median(d[["xG Understat"]], na.rm = TRUE)
n_head <- sum(d$shotType == "Head", na.rm = TRUE)
n_left <- sum(d$shotType == "LeftFoot", na.rm = TRUE)
n_other <- sum(d$shotType == "OtherBodyPart", na.rm = TRUE)
n_right <- sum(d$shotType == "RightFoot", na.rm = TRUE)
The dataset is called Football Shots Dataset: Top 5 European
Leagues. I obtained shots_dataset_cleaned.csv from the
Kaggle
dataset page, where it was posted by Mat 126. The page did not list
a version number when I downloaded the file on September 20, 2026.
The Kaggle page does not clearly identify who originally collected the full dataset or exactly how the data were originally obtained. It describes the dataset as suitable for statistical analysis, goal-prediction models, tactical research, and player scouting.
The dataset contains shot events from the Premier League, La Liga, Serie A, Bundesliga, and Ligue 1. The Kaggle page states that the data cover the 2014/15 through 2020/21 seasons. One row represents one individual shot event taken by a player in a match.
The Kaggle page lists the dataset under the Open Database License, with the contents covered by the Database Contents License. This permits reuse subject to the conditions of the Open Data Commons licences.
The file has 442,592 rows and 23 columns. The three variables used in this report are summarized below.
| Column | Meaning | Stored as | Units | Range or levels |
|---|---|---|---|---|
xG Understat |
Expected-goal probability for the shot | dbl | 0–1 probability scale | 0 to 0.98 |
shotType |
Type of shot | chr | category | Head, LeftFoot, OtherBodyPart, RightFoot |
situation |
Situation in which the shot occurred | chr | category | DirectFreekick, FromCorner, OpenPlay, Penalty, SetPiece |
The remaining 20 columns contain player and match identifiers, shot location, team information, match context and outcomes, dates, assisting-player information, preferred foot, and league information.
Missing values are represented by empty cells in the CSV file. The
player_assisted column contains 117,563 missing values. The
three variables used in this report have 0 missing values in total, so
no rows need to be removed for the analyses below. The numeric summaries
also did not show obvious sentinel missing-value codes such as -1, 99,
or 999.
The data contain 0 duplicated rows. The CSV also contains an unnamed
first column that read_csv() renames ...1; it
appears to be an exported row index and is not used in this analysis. In
addition, the Kaggle description says the data cover the 2014/15 through
2020/21 seasons, while the file contains some dates later in 2021, so
the documented time coverage does not perfectly match the CSV.
used_variables <- d |>
select(`xG Understat`, shotType, situation)
used_missing <- colSums(is.na(used_variables))
No analytical transformations were required. I checked the variables used in the report for missing values and left all rows unchanged. The unnamed index column is retained in the file but is not used in any figure or calculation.
Mat 126. (n.d.). Football Shots Dataset: Top 5 European Leagues. Kaggle. https://www.kaggle.com/datasets/mat126/shots-dataset-for-footballsoccer. Accessed September 20, 2026.
Expected goals (xG) measures the estimated probability that a shot will result in a goal. Shot type and shot situation describe two different features of an attempt, and comparing them can show whether the generally low xG values seen across football shots are similar across different kinds of opportunities. Looking at the overall distribution first and then breaking it down by type and situation provides a more complete description of where high- and low-probability shots occur.
Research question: How is expected-goal probability
(xG Understat, on a 0–1 scale) distributed across shots,
and how does it vary across shot type (shotType) and shot
situation (situation) categories in the top five European
leagues?
shot_type_counts <- d |>
count(shotType)
situation_levels <- sort(unique(d$situation))
xG Understat is a continuous variable stored as
dbl. It is the expected-goal probability assigned to each
shot on a 0–1 scale. In this dataset it ranges from 0 to 0.98, with a
median of 0.049.shotType is a nominal variable stored as
chr. Its four levels are Head (76,688 shots), LeftFoot
(131,332), OtherBodyPart (1,397), and RightFoot (233,175).situation is a nominal variable stored as
chr. Its five observed levels are DirectFreekick,
FromCorner, OpenPlay, Penalty, and SetPiece.The analysis uses the data as read from the CSV. I did not recode
variables, remove rows, or create new analytical variables. As checked
above, xG Understat, shotType, and
situation contain no missing values, so all 442,592 shots
are available for the three figures.
low_cut <- 0.10
high_cut <- 0.50
low_share <- mean(
d[["xG Understat"]] < low_cut,
na.rm = TRUE
)
high_share <- mean(
d[["xG Understat"]] >= high_cut,
na.rm = TRUE
)
ggplot(d, aes(x = `xG Understat`)) +
geom_histogram(
binwidth = 0.025,
boundary = 0
) +
labs(
x = "Expected-goal probability (0 to 1)",
y = "Number of shots"
) +
theme_minimal()
Distribution of expected-goal probability across all shots. Each bar counts shots in a 0.025-wide xG bin; most attempts are concentrated near the low end of the 0–1 scale.
Figure 1 shows the overall distribution of expected-goal probability across all shots in the dataset. The main feature is the strong concentration at low xG values: 77.3% of shots have an xG below 0.10, compared with only 5.1% with an xG of at least 0.50. This means that most shots in the dataset were relatively low-probability scoring opportunities, while high-probability chances were much less common. However, Figure 1 alone cannot show whether these differences are related to shot type or shot situation.
shot_type_stats <- d |>
group_by(shotType) |>
summarize(
n = n(),
median_xg = median(
`xG Understat`,
na.rm = TRUE
),
.groups = "drop"
)
other_median <- shot_type_stats |>
filter(shotType == "OtherBodyPart") |>
pull(median_xg)
footed_median <- d |>
filter(
shotType %in% c(
"LeftFoot",
"RightFoot"
)
) |>
summarize(
median_xg = median(
`xG Understat`,
na.rm = TRUE
)
) |>
pull(median_xg)
fig2_caption <- paste0(
"Expected-goal probability by shot type. ",
"Each box summarizes the xG distribution within one shot-type category. ",
"OtherBodyPart has the highest median xG. ",
"Sample sizes differ greatly; OtherBodyPart has n = ",
format(n_other, big.mark = ","),
" shots, compared with n = ",
format(n_right, big.mark = ","),
" RightFoot shots."
)
ggplot(
d,
aes(
x = reorder(
shotType,
`xG Understat`,
FUN = median
),
y = `xG Understat`
)
) +
geom_boxplot(
outlier.shape = NA
) +
labs(
x = "Shot type",
y = "Expected-goal probability (0 to 1)"
) +
theme_minimal()
Expected-goal probability by shot type. Each box summarizes the xG distribution within one shot-type category. OtherBodyPart has the highest median xG. Sample sizes differ greatly; OtherBodyPart has n = 1,397 shots, compared with n = 233,175 RightFoot shots.
Figure 2 builds on Figure 1 by showing how expected-goal probability varies across shot types. OtherBodyPart shots have a median xG of 0.089, compared with 0.047 for shots taken with either foot. This suggests that the xG distribution is not identical across shot types, with OtherBodyPart attempts tending to come from higher-probability scoring opportunities. The figure does not establish that shot type itself causes the difference in xG, because other characteristics of the attempt, including the shot situation, may also be related to these values.
The interactive chart below summarizes the median xG and sample size for each shot type. Hover over a bar to check the values used in the comparison above.
interactive_shot_type <- shot_type_stats |>
mutate(
hover_text = paste0(
"Shot type: ", shotType,
"<br>Median xG: ", round(median_xg, 3),
"<br>Number of shots: ", format(n, big.mark = ",")
)
)
plot_ly(
interactive_shot_type,
x = ~reorder(shotType, median_xg),
y = ~median_xg,
type = "bar",
text = ~hover_text,
hoverinfo = "text"
) |>
layout(
xaxis = list(title = "Shot type"),
yaxis = list(title = "Median expected-goal probability")
)
footed <- d |>
filter(
shotType %in% c(
"LeftFoot",
"RightFoot"
)
)
penalty_median <- footed |>
filter(situation == "Penalty") |>
summarize(
median_xg = median(
`xG Understat`,
na.rm = TRUE
)
) |>
pull(median_xg)
openplay_median <- footed |>
filter(situation == "OpenPlay") |>
summarize(
median_xg = median(
`xG Understat`,
na.rm = TRUE
)
) |>
pull(median_xg)
fig3_caption <- paste0(
"Expected-goal probability by shot situation, shown separately by shot type. ",
"Each box summarizes one situation within a shot-type panel. ",
"Penalties have much higher xG than open-play shots among footed attempts. ",
"Penalty observations appear only in the LeftFoot and RightFoot panels in this dataset."
)
ggplot(
d,
aes(
x = reorder(
situation,
`xG Understat`,
FUN = median
),
y = `xG Understat`
)
) +
geom_boxplot(
outlier.shape = NA
) +
facet_wrap(~ shotType) +
labs(
x = "Shot situation",
y = "Expected-goal probability (0 to 1)"
) +
theme_minimal() +
theme(
axis.text.x = element_text(
angle = 45,
hjust = 1
)
)
Expected-goal probability by shot situation, shown separately by shot type. Each box summarizes one situation within a shot-type panel. Penalties have much higher xG than open-play shots among footed attempts. Penalty observations appear only in the LeftFoot and RightFoot panels in this dataset.
Figure 3 adds shot situation to the comparison in Figure 2 and shows the pattern separately within each shot type. The clearest contrast appears for footed shots: penalties have a median xG of 0.76, compared with 0.047 for open-play footed shots. This means that shot situation is associated with large differences in expected-goal probability within the same broad shot-type group. The figure cannot establish that situation itself causes the difference in xG, because xG may also reflect other characteristics of the shot.
Summary. Figure 1 shows that most shots in the dataset have relatively low expected-goal probabilities, while high-xG opportunities are much less common. Figure 2 shows that the distribution also differs across shot types, with OtherBodyPart shots having a higher median xG than footed shots. Figure 3 then shows that shot situation adds another important distinction within shot type, especially because penalties have much higher xG values than open-play footed shots. Together, the three figures show that xG is concentrated at low values overall but varies across both shot type and shot situation.
Implications. Coaches and analysts should consider the context of a player’s or team’s shots when comparing xG profiles. For example, a player who takes many penalties can naturally have a different xG distribution from a player whose attempts mostly come from open play, so the same number of shots does not necessarily represent the same set of scoring opportunities.
Limitations. The xG Understat variable
is a model-generated probability rather than the observed scoring
outcome, so this analysis describes the model’s assigned probabilities
rather than directly measuring which shots became goals. The shot-type
groups are also very uneven: OtherBodyPart contains only 1,397 shots,
compared with 233,175 RightFoot shots, so comparisons involving that
category are based on a much smaller sample. Finally, the dataset is
limited to the five leagues and seasons included in the file, so its
patterns need not describe other competitions or time periods.
Future directions. Future analysis could use the
result column to calculate the observed goal rate within
each shot-type and situation group, then compare those rates with the
groups’ average predicted xG. That would show how closely the
model-generated probabilities correspond to actual scoring outcomes
across different kinds of shots.
I used ChatGPT to help interpret the assignment requirements, check the R code, and revise the wording in the figure interpretations and Discussion. I also used it to revise figure captions and category ordering to better match the report criteria. Moreover, I also used ChatGPT to help create the interactive web version of the report.