library(tidyverse)
library(jsonlite)
library(plotly)
# Source A: CSV file downloaded directly via web URL
csv_url <- "https://raw.githubusercontent.com/martj42/international_results/master/results.csv"
raw_soccer_data <- read_csv(csv_url, show_col_types = FALSE)
# Source B: JSON dataset retrieved from OpenFootball repository
json_url <- "https://raw.githubusercontent.com/openfootball/football.json/master/2021-22/en.1.json"
raw_json_data <- fromJSON(json_url, simplifyVector = TRUE)International Soccer Match Dynamics: Goal Differentials Across Tournaments
Introduction & Research Question
International soccer spans a wide range of competitive environments, from friendly matches to major global championships like the FIFA World Cup just held in Mexico this year. Understanding how scoring margins behave across these different settings offers insight into competitive balance and match predictability.
For this project, I investigate the following research question: How do match goal differentials and scoring distributions vary across different international tournaments? Specifically, I examine whether major international tournaments exhibit tighter competitive margins compared to broader international fixtures, and whether playing at a neutral venue significantly impacts goal differentials.
Data Acquisition
To ensure full reproducibility without relying on local files, this analysis pulls two distinct data formats directly from public online repositories:
- Historical International Match Results (CSV): Sourced directly via raw GitHub URL from an open-source international football results archive (
martj42/international_results). - Supplementary Club/League Match Fixtures (JSON): Retrieved directly via
jsonlitefrom the OpenFootball JSON repository (openfootball/football.json) to demonstrate multi-format data ingestion.
Data Preparation & Tidying
To prepare the data for analysis, I filtered the historical match dataset to focus on modern international matches played since January 1, 2020. I converted date strings into standard Date objects, calculated the absolute goal differential (goal_differential) for every match, and removed records with missing score values.
Additionally, I performed two key structural transformations:
Reshaping Wide to Long: Using pivot_longer(), I transformed home_score and away_score columns into a tidy long format (home_away_status and goals_scored) to examine scoring patterns by venue status.
JSON Flattening: I extracted the nested matches data frame from the JSON payload and standardized its date formatting.
# Filter and transform the CSV dataset
team_data <- raw_soccer_data %>%
filter(date >= "2020-01-01") %>%
mutate(
match_date = as.Date(date),
goal_differential = abs(home_score - away_score)
) %>%
drop_na(home_score, away_score)
# Meaningful Transformation: Reshape wide score columns to long format
team_long <- team_data %>%
pivot_longer(
cols = c(home_score, away_score),
names_to = "home_away_status",
values_to = "goals_scored"
)
# Tidy JSON component
tidy_json_matches <- raw_json_data$matches %>%
select(date, team1, team2) %>%
mutate(match_date = as.Date(date))
glimpse(team_data)Rows: 6,539
Columns: 11
$ date <date> 2020-01-07, 2020-01-09, 2020-01-10, 2020-01-12, 202…
$ home_team <chr> "Barbados", "Moldova", "Barbados", "Kosovo", "Canada…
$ away_team <chr> "Canada", "Sweden", "Canada", "Sweden", "Iceland", "…
$ home_score <dbl> 1, 0, 1, 0, 0, 0, 1, 2, 3, 3, 0, 2, 1, 0, 3, 1, 1, 2…
$ away_score <dbl> 4, 1, 4, 1, 1, 2, 4, 0, 1, 0, 1, 2, 0, 3, 1, 0, 1, 1…
$ tournament <chr> "Friendly", "Friendly", "Friendly", "Friendly", "Fri…
$ city <chr> "Irvine", "Doha", "Irvine", "Doha", "Irvine", "Dhaka…
$ country <chr> "United States", "Qatar", "United States", "Qatar", …
$ neutral <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, TRUE, TRU…
$ match_date <date> 2020-01-07, 2020-01-09, 2020-01-10, 2020-01-12, 202…
$ goal_differential <dbl> 3, 1, 3, 1, 1, 2, 3, 2, 2, 3, 1, 0, 1, 3, 2, 1, 0, 1…
Exploratory & Descriptive Analysis
Before running statistical models, I checked the overall distribution of goal differentials across all filtered matches. Because goal differentials are discrete whole numbers, I constructed a histogram aligned to integer bin widths (binwidth = 1).
plot_descriptive <- ggplot(team_data, aes(x = goal_differential)) +
geom_histogram(binwidth = 1, fill = "#2C3E50", color = "white", boundary = 0.5) +
scale_x_continuous(breaks = 0:15) +
theme_minimal() +
labs(
title = "Distribution of Match Goal Differentials (2020–Present)",
x = "Goal Differential (Absolute Value)",
y = "Match Frequency"
)
plot_descriptiveInterpretation: The distribution is heavily right-skewed. The vast majority of international matches are decided by a margin of 0 goals (draws) or 1 goal, with frequency dropping sharply after a 2-goal differential. Blowouts of 5 or more goals are rare outliers.
Statistical Analysis & Findings
Linear Regression Model
To test whether match context—specifically playing at a neutral venue (neutral)—influences competitive balance, I fitted a linear regression model predicting goal_differential.
model <- lm(goal_differential ~ neutral, data = team_data)
summary(model)
Call:
lm(formula = goal_differential ~ neutral, data = team_data)
Residuals:
Min 1Q Median 3Q Max
-1.6755 -0.6755 -0.5659 0.4341 12.3245
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 1.67546 0.02402 69.759 < 2e-16 ***
neutralTRUE -0.10960 0.04161 -2.634 0.00845 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 1.586 on 6537 degrees of freedom
Multiple R-squared: 0.00106, Adjusted R-squared: 0.0009076
F-statistic: 6.939 on 1 and 6537 DF, p-value: 0.008451
Statistical Findings: The regression output evaluates the baseline goal differential for non-neutral matches (the intercept) and tests whether neutral-site matches produce a statistically significant shift in scoring margins. Evaluating the p-value and R2 demonstrates that while neutral venues slightly alter average scoring spreads, overall match competitiveness remains tightly clustered around low differentials regardless of venue status.
Tournament Comparison & Interactive Visualization (“Something New”)
To compare major international tournaments directly without visual clutter, I filtered the dataset to focus on premier global and regional competitions: the FIFA World Cup, Copa América, UEFA Euro, African Nations Cup, AFC Asian Cup, and CONCACAF Gold Cup.
To satisfy the “Something New” project requirement (introducing a tool or package not covered extensively in class), I used the plotly package (ggplotly()) to turn the boxplot into an interactive HTML widget. This allows readers to hover over the charts to inspect exact metrics, ranges, and individual outlier matches dynamically.
major_tournaments <- c(
"FIFA World Cup", "Copa América", "UEFA Euro",
"African Nations Cup", "AFC Asian Cup", "CONCACAF Gold Cup"
)
filtered_tournament_data <- team_data %>%
filter(tournament %in% major_tournaments)
plot_analytical <- ggplot(filtered_tournament_data, aes(x = tournament, y = goal_differential)) +
geom_boxplot(fill = "#E74C3C", alpha = 0.7, outlier.alpha = 0.5) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 30, hjust = 1, face = "bold"),
panel.grid.minor = element_blank()
) +
labs(
title = "Goal Differentials Across Major International Tournaments",
x = "Tournament",
y = "Goal Differential"
)
# Convert static ggplot into an interactive widget (Something New requirement)
ggplotly(plot_analytical)Analytical Takeaway: Across major international tournaments, the median goal differential sits consistently at 1 goal, with an interquartile range spanning from 0 to 2 goals. This confirms that high-stakes international tournaments maintain high competitive parity, though events like the FIFA World Cup exhibit occasional high-margin outliers during group-stage matchups.
Conclusion & Challenges
Summary of What I Learned
Analyzing modern international soccer matches reveals strong competitive parity across major tournaments. Regardless of the confederation or tournament prestige, matches are predominantly decided by one goal or end in a draw. While occasional blowouts occur in group stages, the core distribution remains tightly bounded between 0 and 2 goals.
Technical Challenges Overcome
During data acquisition, pulling raw JSON directly via strict HTTP header checks (httr2) triggered content-type errors because GitHub serves raw JSON files as text/plain. Switching to jsonlite::fromJSON() resolved the parsing issue cleanly while maintaining 100% reproducible web-based execution. Additionally, plotting all tournaments simultaneously caused severe X-axis label overlap, which was resolved by filtering for major international competitions and rendering interactive elements via plotly.
Acknowledgments & Data Sources
This project relies on publicly available datasets and open-source R packages:
- International Soccer Results Dataset: Historical international match scores and metadata were obtained from Mart Jürisoo’s open-source GitHub repository (
https://github.com/martj42/international_results). - OpenFootball JSON Data: Supplementary fixture data were retrieved from the OpenFootball public JSON repository (
https://github.com/openfootball/football.json). - R Packages & Software Citations:
- Wickham, H., et al. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686.
- Ooms, J. (2014). The jsonlite Package: A Practical and Consistent Mapping Between JSON Data and R Objects. arXiv preprint arXiv:1403.2805.
- Sievert, C. (2020). Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC. ISBN 9781138331457. (
https://plotly-r.com)