International Soccer Match Dynamics: Goal Differentials Across Tournaments

Author

David Melchor

Published

December 14, 2026

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:

  1. Historical International Match Results (CSV): Sourced directly via raw GitHub URL from an open-source international football results archive (martj42/international_results).
  2. Supplementary Club/League Match Fixtures (JSON): Retrieved directly via jsonlite from the OpenFootball JSON repository (openfootball/football.json) to demonstrate multi-format data ingestion.
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)

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_descriptive

Interpretation: 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)