3. Raw Data Inspection
glimpse(effects_raw)
## Rows: 1,486
## Columns: 13
## $ year <dbl> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, …
## $ to_country <chr> "Norway", "Luxembourg", "Estonia", "Israel", "Li…
## $ song <chr> "Lighter", "La poupée monte le son", "Espresso M…
## $ artist_name <chr> "Kyle Alessandro", "Laura Thorn", "Tommy Cash", …
## $ round <chr> "finale", "finale", "finale", "finale", "finale"…
## $ performance_order <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1…
## $ fire <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, …
## $ smoke <dbl> 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, …
## $ wind_machine <dbl> 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
## $ confetti <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ special_effects_count <dbl> 2, 0, 0, 0, 2, 0, 0, 0, 1, 1, 1, 0, 1, 0, 2, 0, …
## $ coding_complete <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ Notes_Data_collection <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
glimpse(songs_raw)
## Rows: 1,754
## Columns: 37
## $ year <dbl> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025,…
## $ to_country <chr> "Albania", "Armenia", "Australia", "Austria", "…
## $ song <chr> "Zjerm", "SURVIVOR", "Go-Jo", "Wasted Love", "R…
## $ artist_name <chr> "Shkodra Elektronike", "PARG", "Milkshake Man",…
## $ artist_size <dbl> 2, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1,…
## $ gender <chr> "Mixed", "Male", "Male", "Male", "All-male", "M…
## $ main_singers <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ backing_singers <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,…
## $ backing_dancers <dbl> 0, 0, 2, 0, 3, 3, 4, 4, 4, 4, 5, 0, 0, 4, 3, 1,…
## $ backing_instruments <dbl> 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ number_of_performers <dbl> 2, 1, 3, 1, 6, 4, 5, 5, 5, 5, 6, 1, 1, 5, 6, 2,…
## $ country_debut <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ returning_artist <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ artist_nationality <chr> "Albania", "Armenia", "Australia", "Austria", "…
## $ nationality_match <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1,…
## $ LGBTQ_category <chr> NA, NA, NA, "Queer", NA, "Queer", "Gay", NA, "Q…
## $ LGBTQ_out_time <chr> NA, NA, NA, "Openly", NA, "Openly", "Openly", N…
## $ performance_notes <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ genre <chr> "Pop", "Rock", "Pop", "Pop", "Pop", "Dance", "P…
## $ returning <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ host <chr> "Switzerland", "Switzerland", "Switzerland", "S…
## $ language <chr> "Albanian", "Armenian, English", "English", "En…
## $ dialect_sign_imaginary <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,…
## $ language_count <dbl> 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 1, 1,…
## $ has_english <dbl> 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0,…
## $ tempo <dbl> 9493670654, 1304347839, 1401869202, 625, 141509…
## $ key <dbl> 0, 2, 11, 9, 2, 6, 7, 10, 1, 0, 4, 4, 7, 5, 3, …
## $ mode <dbl> 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0,…
## $ danceability <chr> "0.783782766", "0.746351126", "0.620062281", "0…
## $ energy <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ loudness <chr> "-11.70", "-8.81", "-3.61", "-3.82", "-4.57", "…
## $ speechiness <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ acousticness <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ instrumentalness <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ liveness <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ valence <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ time_signature <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
glimpse(performance_order_raw)
## Rows: 1,756
## Columns: 7
## $ year <dbl> 1956, 1956, 1956, 1956, 1956, 1956, 1956, 1…
## $ to_country <chr> "Switzerland", "Netherlands", "Belgium", "G…
## $ performer <chr> "Lys Assia", "Jetty Paerl", "Fud Leclerc", …
## $ song <chr> "Refrain", "De Vogels Van Holland", "Messie…
## $ semifinal <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ PerformanceOrder_final <dbl> 2, 1, 3, 4, 5, 13, 7, 8, 10, 11, 12, 14, 6,…
## $ PerformanceOrder_semifinal <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
glimpse(votes_old_raw)
## Rows: 51,452
## Columns: 7
## $ year <dbl> 1957, 1957, 1957, 1957, 1957, 1957, 1957, 1957, 1957, …
## $ round <chr> "final", "final", "final", "final", "final", "final", …
## $ from_country <chr> "Austria", "Austria", "Austria", "Austria", "Austria",…
## $ to_country <chr> "Netherlands", "France", "Denmark", "Luxembourg", "Ger…
## $ pairwise_points <dbl> 6, 0, 0, 3, 0, 0, 1, 0, 0, 5, 2, 0, 0, 1, 1, 1, 0, 0, …
## $ tele_points <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ jury_points <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
glimpse(votes_2025_raw)
## Rows: 962
## Columns: 8
## $ year <dbl> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, …
## $ round <chr> "final", "final", "final", "final", "final", "final", …
## $ from_country <chr> "Luxembourg", "Estonia", "Israel", "Lithuania", "Spain…
## $ to_country <chr> "Norway", "Norway", "Norway", "Norway", "Norway", "Nor…
## $ pairwise_points <dbl> 0, 0, 0, 2, 2, 10, 0, 0, 14, 1, 0, 0, 1, 5, 1, 4, 4, 0…
## $ tele_points <dbl> 0, 0, 0, 2, 2, 10, 0, 0, 8, 1, 0, 0, 1, 3, 0, 4, 4, 0,…
## $ jury_points <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 2, 1, 0, 0, 0, …
## $ ...8 <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
# Number of rows with and without a year
effects_raw |>
summarise(
total_rows = n(),
rows_with_year = sum(!is.na(year)),
rows_without_year = sum(is.na(year))
)
## # A tibble: 1 × 3
## total_rows rows_with_year rows_without_year
## <int> <int> <int>
## 1 1486 413 1073
songs_raw |>
summarise(
total_rows = n(),
rows_with_year = sum(!is.na(year)),
rows_without_year = sum(is.na(year))
)
## # A tibble: 1 × 3
## total_rows rows_with_year rows_without_year
## <int> <int> <int>
## 1 1754 1754 0
performance_order_raw |>
summarise(
total_rows = n(),
rows_with_year = sum(!is.na(year)),
rows_without_year = sum(is.na(year))
)
## # A tibble: 1 × 3
## total_rows rows_with_year rows_without_year
## <int> <int> <int>
## 1 1756 1756 0
votes_old_raw |>
summarise(
total_rows = n(),
rows_with_year = sum(!is.na(year)),
rows_without_year = sum(is.na(year))
)
## # A tibble: 1 × 3
## total_rows rows_with_year rows_without_year
## <int> <int> <int>
## 1 51452 51452 0
votes_2025_raw |>
summarise(
total_rows = n(),
rows_with_year = sum(!is.na(year)),
rows_without_year = sum(is.na(year))
)
## # A tibble: 1 × 3
## total_rows rows_with_year rows_without_year
## <int> <int> <int>
## 1 962 962 0
songs_raw |>
select(year, to_country, song, tempo, energy, danceability, valence) |>
slice_head(n = 20)
## # A tibble: 20 × 7
## year to_country song tempo energy danceability valence
## <dbl> <chr> <chr> <dbl> <chr> <chr> <chr>
## 1 2025 Albania Zjerm 9493670654 <NA> 0.783782766 <NA>
## 2 2025 Armenia SURVIVOR 1304347839 <NA> 0.746351126 <NA>
## 3 2025 Australia Go-Jo 1401869202 <NA> 0.620062281 <NA>
## 4 2025 Austria Wasted Love 625 <NA> 0.660020048 <NA>
## 5 2025 Azerbaijan Run With U 1415094299 <NA> 0.767386435 <NA>
## 6 2025 Belgium Strobe Lights 8522727203 <NA> 0.677794755 <NA>
## 7 2025 Croatia Poison Cake 728155365 <NA> 0.899225528 <NA>
## 8 2025 Cyprus Shh 116279068 <NA> 0.97480672 <NA>
## 9 2025 Czechia Kiss Kiss Goodbye 681818161 <NA> 0.351895209 <NA>
## 10 2025 Denmark Hallucination 7731958771 <NA> 0.980092473 <NA>
## 11 2025 Estonia Espresso Macchiato 120 <NA> 0.886701067 <NA>
## 12 2025 Finland ICH KOMME 7894736481 <NA> 0.448452318 <NA>
## 13 2025 France maman 5357143021 <NA> 0.266809981 <NA>
## 14 2025 Georgia Freedom 131578949 <NA> 0.757988785 <NA>
## 15 2025 Germany Baller 7009346008 <NA> 0.994010366 <NA>
## 16 2025 Greece Asteromáta 1219512177 <NA> 0.661153399 <NA>
## 17 2025 Iceland RÓA 1376146851 <NA> 0.990902424 <NA>
## 18 2025 Ireland Laika Party 5357143021 <NA> 0.53104055 <NA>
## 19 2025 Israel New Day Will Rise 8823529053 <NA> 0.588710481 <NA>
## 20 2025 Italy Volevo Essere Un Duro 1485148468 <NA> 0.548016483 <NA>
songs_raw |>
summarise(
tempo_missing = sum(is.na(tempo)),
energy_missing = sum(is.na(energy)),
danceability_missing = sum(is.na(danceability)),
valence_missing = sum(is.na(valence))
)
## # A tibble: 1 × 4
## tempo_missing energy_missing danceability_missing valence_missing
## <int> <int> <int> <int>
## 1 0 211 0 211
songs_raw |>
summarise(
tempo_min = min(tempo, na.rm = TRUE),
tempo_max = max(tempo, na.rm = TRUE)
)
## # A tibble: 1 × 2
## tempo_min tempo_max
## <dbl> <dbl>
## 1 75 9740259552
songs_raw |>
filter(year >= 2016, year <= 2025) |>
group_by(year) |>
summarise(
n = n(),
tempo_missing = sum(is.na(tempo)),
energy_missing = sum(is.na(energy)),
danceability_missing = sum(is.na(danceability)),
valence_missing = sum(is.na(valence)),
.groups = "drop"
)
## # A tibble: 9 × 6
## year n tempo_missing energy_missing danceability_missing valence_missing
## <dbl> <int> <int> <int> <int> <int>
## 1 2016 42 0 0 0 0
## 2 2017 42 0 0 0 0
## 3 2018 43 0 0 0 0
## 4 2019 41 0 1 0 1
## 5 2021 39 0 0 0 0
## 6 2022 40 0 0 0 0
## 7 2023 37 0 0 0 0
## 8 2024 37 0 0 0 0
## 9 2025 37 0 37 0 37
songs_raw |>
filter(year >= 2016, year <= 2025) |>
group_by(year) |>
summarise(
tempo_min = min(tempo, na.rm = TRUE),
tempo_max = max(tempo, na.rm = TRUE),
.groups = "drop"
)
## # A tibble: 9 × 3
## year tempo_min tempo_max
## <dbl> <dbl> <dbl>
## 1 2016 11497 205008
## 2 2017 8488 181758
## 3 2018 10908 189743
## 4 2019 1009 9615384674
## 5 2021 8056 175719
## 6 2022 76905 169985
## 7 2023 13796 160071
## 8 2024 1351 160068
## 9 2025 120 9493670654
# Check duplicate year-country combinations in performance-level datasets
effects_raw |>
filter(!is.na(year)) |>
count(year, to_country) |>
filter(n > 1)
## # A tibble: 0 × 3
## # ℹ 3 variables: year <dbl>, to_country <chr>, n <int>
songs_raw |>
count(year, to_country) |>
filter(n > 1)
## # A tibble: 7 × 3
## year to_country n
## <dbl> <chr> <int>
## 1 1956 Belgium 2
## 2 1956 France 2
## 3 1956 Germany 2
## 4 1956 Italy 2
## 5 1956 Luxembourg 2
## 6 1956 Netherlands 2
## 7 1956 Switzerland 2
performance_order_raw |>
count(year, to_country) |>
filter(n > 1)
## # A tibble: 5 × 3
## year to_country n
## <dbl> <chr> <int>
## 1 1956 Belgium 2
## 2 1956 France 2
## 3 1956 Germany 2
## 4 1956 Italy 2
## 5 1956 Netherlands 2
# Check duplicate voting observations
votes_old_raw |>
count(year, round, from_country, to_country) |>
filter(n > 1)
## # A tibble: 0 × 5
## # ℹ 5 variables: year <dbl>, round <chr>, from_country <chr>, to_country <chr>,
## # n <int>
votes_2025_raw |>
count(year, round, from_country, to_country) |>
filter(n > 1)
## # A tibble: 0 × 5
## # ℹ 5 variables: year <dbl>, round <chr>, from_country <chr>, to_country <chr>,
## # n <int>
# Inspect round labels
unique(effects_raw$round)
## [1] "finale" NA
unique(votes_old_raw$round)
## [1] "final" "semi-final" "semi-final-1" "semi-final-2"
unique(votes_2025_raw$round)
## [1] "final"
4. Data Cleaning
4.1 Special Effects Data
effects_clean <- effects_raw |>
# Standardize column names
clean_names() |>
# Remove empty rows created during the Excel-to-CSV export
filter(!is.na(year)) |>
# Standardize the Grand Final label
mutate(
round = if_else(round == "finale", "final", round)
)
dim(effects_clean)
## [1] 413 13
unique(effects_clean$round)
## [1] "final"
effects_clean |>
count(year) |>
arrange(desc(year))
## # A tibble: 16 × 2
## year n
## <dbl> <int>
## 1 2025 26
## 2 2024 26
## 3 2023 26
## 4 2022 25
## 5 2021 26
## 6 2019 26
## 7 2018 26
## 8 2017 26
## 9 2016 26
## 10 2015 27
## 11 2014 26
## 12 2013 26
## 13 2012 26
## 14 2011 25
## 15 2010 25
## 16 2009 25
# Check allowed values for each binary effect variable
unique(effects_clean$fire)
## [1] 1 0 NA
unique(effects_clean$smoke)
## [1] 1 0 NA
unique(effects_clean$wind_machine)
## [1] 0 1 NA
unique(effects_clean$confetti)
## [1] 0 NA 1
# Check whether the calculated total matches the four component variables
effects_clean |>
mutate(
effects_sum_check = fire + smoke + wind_machine + confetti
) |>
filter(
!is.na(effects_sum_check),
special_effects_count != effects_sum_check
) |>
select(
year,
to_country,
song,
fire,
smoke,
wind_machine,
confetti,
special_effects_count,
effects_sum_check
)
## # A tibble: 0 × 9
## # ℹ 9 variables: year <dbl>, to_country <chr>, song <chr>, fire <dbl>,
## # smoke <dbl>, wind_machine <dbl>, confetti <dbl>,
## # special_effects_count <dbl>, effects_sum_check <dbl>
effects_clean |>
filter(
is.na(fire) |
is.na(smoke) |
is.na(wind_machine) |
is.na(confetti) |
is.na(special_effects_count)
) |>
select(
year,
to_country,
song,
fire,
smoke,
wind_machine,
confetti,
special_effects_count,
coding_complete,
notes_data_collection
)
## # A tibble: 1 × 10
## year to_country song fire smoke wind_machine confetti special_effects_count
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2024 Netherlan… Euro… NA NA NA NA NA
## # ℹ 2 more variables: coding_complete <dbl>, notes_data_collection <chr>
4.2 Voting Data
# Clean historical voting data
votes_old_clean <- votes_old_raw |>
clean_names()
# Clean 2025 voting extension and remove the empty export column
votes_2025_clean <- votes_2025_raw |>
clean_names() |>
select(-x8)
# Compare the column names of both datasets
names(votes_old_clean)
## [1] "year" "round" "from_country" "to_country"
## [5] "pairwise_points" "tele_points" "jury_points"
names(votes_2025_clean)
## [1] "year" "round" "from_country" "to_country"
## [5] "pairwise_points" "tele_points" "jury_points"
# Compare their dimensions
dim(votes_old_clean)
## [1] 51452 7
dim(votes_2025_clean)
## [1] 962 7
# Check 1: pairwise points must equal televote + jury points
votes_2025_clean |>
filter(pairwise_points != tele_points + jury_points)
## # A tibble: 0 × 7
## # ℹ 7 variables: year <dbl>, round <chr>, from_country <chr>, to_country <chr>,
## # pairwise_points <dbl>, tele_points <dbl>, jury_points <dbl>
# Check 2: every finalist should have exactly 37 voting-source rows
votes_2025_clean |>
count(to_country, name = "number_of_voting_rows") |>
filter(number_of_voting_rows != 37)
## # A tibble: 0 × 2
## # ℹ 2 variables: to_country <chr>, number_of_voting_rows <int>
# Check 3: calculate total points received by each finalist
votes_2025_clean |>
group_by(to_country) |>
summarise(
tele_total = sum(tele_points),
jury_total = sum(jury_points),
total_points = sum(pairwise_points),
.groups = "drop"
) |>
arrange(desc(total_points))
## # A tibble: 26 × 4
## to_country tele_total jury_total total_points
## <chr> <dbl> <dbl> <dbl>
## 1 Austria 178 258 436
## 2 Israel 297 60 357
## 3 Estonia 258 98 356
## 4 Sweden 195 126 321
## 5 Italy 97 159 256
## 6 Greece 126 105 231
## 7 France 50 180 230
## 8 Albania 173 45 218
## 9 Ukraine 158 60 218
## 10 Switzerland 0 214 214
## # ℹ 16 more rows
# Combine historical voting data with the 2025 extension
votes_clean <- bind_rows(
votes_old_clean,
votes_2025_clean
)
# Check the dimensions of the combined dataset
dim(votes_clean)
## [1] 52414 7
# Check the year range
range(votes_clean$year, na.rm = TRUE)
## [1] 1957 2025
# Check that 2025 contains exactly 962 observations
votes_clean |>
filter(year == 2025) |>
summarise(
number_of_rows = n()
)
## # A tibble: 1 × 1
## number_of_rows
## <int>
## 1 962
# Check the available round labels
unique(votes_clean$round)
## [1] "final" "semi-final" "semi-final-1" "semi-final-2"
votes_clean |>
filter(
round == "final",
year >= 2009,
year <= 2025
) |>
group_by(year) |>
summarise(
rows = n(),
tele_missing = sum(is.na(tele_points)),
jury_missing = sum(is.na(jury_points)),
tele_available = sum(!is.na(tele_points)),
jury_available = sum(!is.na(jury_points)),
.groups = "drop"
)
## # A tibble: 16 × 6
## year rows tele_missing jury_missing tele_available jury_available
## <dbl> <int> <int> <int> <int> <int>
## 1 2009 1025 1025 1025 0 0
## 2 2010 950 950 950 0 0
## 3 2011 1050 1050 1050 0 0
## 4 2012 1066 1066 1066 0 0
## 5 2013 988 988 988 0 0
## 6 2014 936 936 936 0 0
## 7 2015 1053 1053 1053 0 0
## 8 2016 1066 0 0 1066 1066
## 9 2017 1066 0 0 1066 1066
## 10 2018 1092 0 0 1092 1092
## 11 2019 1040 0 0 1040 1040
## 12 2021 988 0 0 988 988
## 13 2022 975 0 0 975 975
## 14 2023 962 0 26 962 936
## 15 2024 925 0 0 925 925
## 16 2025 962 0 0 962 962
4.3 Musical Features Data
songs_clean <- songs_raw |>
clean_names() |>
mutate(
energy = as.numeric(energy),
danceability = as.numeric(danceability),
valence = as.numeric(valence)
)
# Check the cleaned variable types
songs_clean |>
select(tempo, energy, danceability, valence) |>
glimpse()
## Rows: 1,754
## Columns: 4
## $ tempo <dbl> 9493670654, 1304347839, 1401869202, 625, 1415094299, 8522…
## $ energy <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ danceability <dbl> 0.7837828, 0.7463511, 0.6200623, 0.6600200, 0.7673864, 0.…
## $ valence <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
tempo_check <- songs_clean |>
mutate(
tempo_proposed = case_when(
tempo <= 220 ~ tempo,
tempo / 10 <= 220 ~ tempo / 10,
tempo / 100 <= 220 ~ tempo / 100,
tempo / 1000 <= 220 ~ tempo / 1000,
tempo / 10000 <= 220 ~ tempo / 10000,
tempo / 100000 <= 220 ~ tempo / 100000,
tempo / 1000000 <= 220 ~ tempo / 1000000,
tempo / 10000000 <= 220 ~ tempo / 10000000,
tempo / 100000000 <= 220 ~ tempo / 100000000,
TRUE ~ NA_real_
)
)
# Inspect the first 20 original and proposed tempo values
tempo_check |>
select(year, to_country, song, tempo, tempo_proposed) |>
slice_head(n = 20)
## # A tibble: 20 × 5
## year to_country song tempo tempo_proposed
## <dbl> <chr> <chr> <dbl> <dbl>
## 1 2025 Albania Zjerm 9493670654 94.9
## 2 2025 Armenia SURVIVOR 1304347839 130.
## 3 2025 Australia Go-Jo 1401869202 140.
## 4 2025 Austria Wasted Love 625 62.5
## 5 2025 Azerbaijan Run With U 1415094299 142.
## 6 2025 Belgium Strobe Lights 8522727203 85.2
## 7 2025 Croatia Poison Cake 728155365 72.8
## 8 2025 Cyprus Shh 116279068 116.
## 9 2025 Czechia Kiss Kiss Goodbye 681818161 68.2
## 10 2025 Denmark Hallucination 7731958771 77.3
## 11 2025 Estonia Espresso Macchiato 120 120
## 12 2025 Finland ICH KOMME 7894736481 78.9
## 13 2025 France maman 5357143021 53.6
## 14 2025 Georgia Freedom 131578949 132.
## 15 2025 Germany Baller 7009346008 70.1
## 16 2025 Greece Asteromáta 1219512177 122.
## 17 2025 Iceland RÓA 1376146851 138.
## 18 2025 Ireland Laika Party 5357143021 53.6
## 19 2025 Israel New Day Will Rise 8823529053 88.2
## 20 2025 Italy Volevo Essere Un Duro 1485148468 149.
# Check the resulting range
tempo_check |>
summarise(
proposed_min = min(tempo_proposed, na.rm = TRUE),
proposed_max = max(tempo_proposed, na.rm = TRUE),
proposed_missing = sum(is.na(tempo_proposed))
)
## # A tibble: 1 × 3
## proposed_min proposed_max proposed_missing
## <dbl> <dbl> <int>
## 1 48.5 206. 0
tempo_check |>
filter(year >= 2016, year <= 2025) |>
group_by(year) |>
summarise(
n = n(),
tempo_min = min(tempo_proposed, na.rm = TRUE),
tempo_mean = mean(tempo_proposed, na.rm = TRUE),
tempo_max = max(tempo_proposed, na.rm = TRUE),
.groups = "drop"
)
## # A tibble: 9 × 5
## year n tempo_min tempo_mean tempo_max
## <dbl> <int> <dbl> <dbl> <dbl>
## 1 2016 42 67.9 118. 205.
## 2 2017 42 75.0 123. 182.
## 3 2018 43 74.9 115. 190.
## 4 2019 41 72.0 119. 184.
## 5 2021 39 66.4 114. 176.
## 6 2022 40 76.9 113. 170.
## 7 2023 37 75.0 124. 160.
## 8 2024 37 82.0 124. 160.
## 9 2025 37 53.6 99.9 149.
songs_clean <- songs_clean |>
mutate(
tempo = case_when(
tempo <= 220 ~ tempo,
tempo / 10 <= 220 ~ tempo / 10,
tempo / 100 <= 220 ~ tempo / 100,
tempo / 1000 <= 220 ~ tempo / 1000,
tempo / 10000 <= 220 ~ tempo / 10000,
tempo / 100000 <= 220 ~ tempo / 100000,
tempo / 1000000 <= 220 ~ tempo / 1000000,
tempo / 10000000 <= 220 ~ tempo / 10000000,
tempo / 100000000 <= 220 ~ tempo / 100000000,
TRUE ~ NA_real_
)
)
# Check the final musical-feature variable types
songs_clean |>
select(tempo, energy, danceability, valence) |>
glimpse()
## Rows: 1,754
## Columns: 4
## $ tempo <dbl> 94.93671, 130.43478, 140.18692, 62.50000, 141.50943, 85.2…
## $ energy <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ danceability <dbl> 0.7837828, 0.7463511, 0.6200623, 0.6600200, 0.7673864, 0.…
## $ valence <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
# Check final tempo range
songs_clean |>
summarise(
tempo_min = min(tempo, na.rm = TRUE),
tempo_max = max(tempo, na.rm = TRUE),
tempo_missing = sum(is.na(tempo))
)
## # A tibble: 1 × 3
## tempo_min tempo_max tempo_missing
## <dbl> <dbl> <int>
## 1 48.5 206. 0
# Overall ranges and missing values of the four H2 variables
songs_clean |>
summarise(
tempo_min = min(tempo, na.rm = TRUE),
tempo_max = max(tempo, na.rm = TRUE),
tempo_missing = sum(is.na(tempo)),
energy_min = min(energy, na.rm = TRUE),
energy_max = max(energy, na.rm = TRUE),
energy_missing = sum(is.na(energy)),
danceability_min = min(danceability, na.rm = TRUE),
danceability_max = max(danceability, na.rm = TRUE),
danceability_missing = sum(is.na(danceability)),
valence_min = min(valence, na.rm = TRUE),
valence_max = max(valence, na.rm = TRUE),
valence_missing = sum(is.na(valence))
)
## # A tibble: 1 × 12
## tempo_min tempo_max tempo_missing energy_min energy_max energy_missing
## <dbl> <dbl> <int> <dbl> <dbl> <int>
## 1 48.5 206. 0 0.0314 0.999 211
## # ℹ 6 more variables: danceability_min <dbl>, danceability_max <dbl>,
## # danceability_missing <int>, valence_min <dbl>, valence_max <dbl>,
## # valence_missing <int>
# Missing values by year for the period relevant to our analysis
songs_clean |>
filter(year >= 2016, year <= 2025) |>
group_by(year) |>
summarise(
n = n(),
tempo_missing = sum(is.na(tempo)),
energy_missing = sum(is.na(energy)),
danceability_missing = sum(is.na(danceability)),
valence_missing = sum(is.na(valence)),
.groups = "drop"
)
## # A tibble: 9 × 6
## year n tempo_missing energy_missing danceability_missing valence_missing
## <dbl> <int> <int> <int> <int> <int>
## 1 2016 42 0 0 0 0
## 2 2017 42 0 0 0 0
## 3 2018 43 0 0 0 0
## 4 2019 41 0 1 0 1
## 5 2021 39 0 0 0 0
## 6 2022 40 0 0 0 0
## 7 2023 37 0 0 0 0
## 8 2024 37 0 0 0 0
## 9 2025 37 0 37 0 37
4.4 Performance Order Data
performance_order_clean <- performance_order_raw |>
clean_names() |>
select(
year,
to_country,
performer,
song,
performance_order_final
) |>
rename(
performance_order = performance_order_final
)
# Inspect the cleaned structure
glimpse(performance_order_clean)
## Rows: 1,756
## Columns: 5
## $ year <dbl> 1956, 1956, 1956, 1956, 1956, 1956, 1956, 1956, 1956…
## $ to_country <chr> "Switzerland", "Netherlands", "Belgium", "Germany", …
## $ performer <chr> "Lys Assia", "Jetty Paerl", "Fud Leclerc", "Walter A…
## $ song <chr> "Refrain", "De Vogels Van Holland", "Messieurs Les N…
## $ performance_order <dbl> 2, 1, 3, 4, 5, 13, 7, 8, 10, 11, 12, 14, 6, 8, 9, 2,…
performance_order_check <- effects_clean |>
select(
year,
to_country,
song,
performance_order_effects = performance_order
) |>
left_join(
performance_order_clean |>
select(
year,
to_country,
performance_order_department = performance_order
),
by = c("year", "to_country")
)
# Check whether any Grand Final entries are missing department performance-order data
performance_order_check |>
filter(is.na(performance_order_department))
## # A tibble: 31 × 5
## year to_country song performance_order_ef…¹ performance_order_de…²
## <dbl> <chr> <chr> <dbl> <dbl>
## 1 2025 Norway Lighter 1 NA
## 2 2025 Luxembourg La poupée… 2 NA
## 3 2025 Estonia Espresso … 3 NA
## 4 2025 Israel New Day W… 4 NA
## 5 2025 Lithuania Tavo akys 5 NA
## 6 2025 Spain Esa diva 6 NA
## 7 2025 Ukraine Bird of P… 7 NA
## 8 2025 United Kingdom What the … 8 NA
## 9 2025 Austria Wasted Lo… 9 NA
## 10 2025 Iceland Róa 10 NA
## # ℹ 21 more rows
## # ℹ abbreviated names: ¹performance_order_effects,
## # ²performance_order_department
# Check whether the two performance-order sources disagree
performance_order_check |>
filter(
!is.na(performance_order_department),
performance_order_effects != performance_order_department
)
## # A tibble: 0 × 5
## # ℹ 5 variables: year <dbl>, to_country <chr>, song <chr>,
## # performance_order_effects <dbl>, performance_order_department <dbl>
performance_order_check |>
filter(is.na(performance_order_department)) |>
count(year, name = "missing_department_orders") |>
arrange(desc(year))
## # A tibble: 6 × 2
## year missing_department_orders
## <dbl> <int>
## 1 2025 26
## 2 2024 1
## 3 2022 1
## 4 2019 1
## 5 2018 1
## 6 2016 1
performance_order_check |>
filter(
is.na(performance_order_department),
year < 2025
)
## # A tibble: 5 × 5
## year to_country song performance_order_ef…¹ performance_order_de…²
## <dbl> <chr> <chr> <dbl> <dbl>
## 1 2024 Netherlands Europapa 5 NA
## 2 2022 Czech Republic Lights Off 1 NA
## 3 2019 Czech Republic Friend of … 3 NA
## 4 2018 Czech Republic Lie to Me 14 NA
## 5 2016 Czech Republic I Stand 2 NA
## # ℹ abbreviated names: ¹performance_order_effects,
## # ²performance_order_department
6. Dataset Merging
6.1 Identifier Harmonization
# Effects observations from 2016 onward that do not match voting data
effects_clean |>
filter(year >= 2016) |>
anti_join(
votes_normalized,
by = c("year", "to_country")
) |>
select(year, to_country, song) |>
arrange(desc(year), to_country)
## # A tibble: 5 × 3
## year to_country song
## <dbl> <chr> <chr>
## 1 2024 Netherlands Europapa
## 2 2022 Czech Republic Lights Off
## 3 2019 Czech Republic Friend of a Friend
## 4 2018 Czech Republic Lie to Me
## 5 2016 Czech Republic I Stand
# Effects observations that do not match musical-features data
effects_clean |>
anti_join(
songs_clean,
by = c("year", "to_country")
) |>
select(year, to_country, song) |>
arrange(desc(year), to_country)
## # A tibble: 4 × 3
## year to_country song
## <dbl> <chr> <chr>
## 1 2022 Czech Republic Lights Off
## 2 2019 Czech Republic Friend of a Friend
## 3 2018 Czech Republic Lie to Me
## 4 2016 Czech Republic I Stand
effects_clean <- effects_clean |>
mutate(
to_country = if_else(
to_country == "Czech Republic",
"Czechia",
to_country
)
)
# Recheck unmatched voting identifiers
effects_clean |>
filter(year >= 2016) |>
anti_join(
votes_normalized,
by = c("year", "to_country")
) |>
select(year, to_country, song) |>
arrange(desc(year), to_country)
## # A tibble: 1 × 3
## year to_country song
## <dbl> <chr> <chr>
## 1 2024 Netherlands Europapa
# Recheck unmatched musical-feature identifiers
effects_clean |>
anti_join(
songs_clean,
by = c("year", "to_country")
) |>
select(year, to_country, song) |>
arrange(desc(year), to_country)
## # A tibble: 0 × 3
## # ℹ 3 variables: year <dbl>, to_country <chr>, song <chr>
6.2 Merge Effects and Voting Data
analysis_data <- effects_clean |>
left_join(
votes_normalized |>
select(
year,
to_country,
tele_points,
jury_points,
jury_opportunities,
tele_opportunities,
jury_score_norm,
tele_score_norm,
jury_televote_gap
),
by = c("year", "to_country")
)
# Check dimensions after the merge
dim(analysis_data)
## [1] 413 20
# Inspect the resulting structure
glimpse(analysis_data)
## Rows: 413
## Columns: 20
## $ year <dbl> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, …
## $ to_country <chr> "Norway", "Luxembourg", "Estonia", "Israel", "Li…
## $ song <chr> "Lighter", "La poupée monte le son", "Espresso M…
## $ artist_name <chr> "Kyle Alessandro", "Laura Thorn", "Tommy Cash", …
## $ round <chr> "final", "final", "final", "final", "final", "fi…
## $ performance_order <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1…
## $ fire <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, …
## $ smoke <dbl> 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, …
## $ wind_machine <dbl> 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
## $ confetti <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ special_effects_count <dbl> 2, 0, 0, 0, 2, 0, 0, 0, 1, 1, 1, 0, 1, 0, 2, 0, …
## $ coding_complete <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ notes_data_collection <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
## $ tele_points <dbl> 67, 24, 258, 297, 62, 10, 158, 0, 178, 33, 42, 4…
## $ jury_points <dbl> 22, 23, 98, 60, 34, 27, 60, 88, 258, 0, 116, 133…
## $ jury_opportunities <int> 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, …
## $ tele_opportunities <int> 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, …
## $ jury_score_norm <dbl> 0.05092593, 0.05324074, 0.22685185, 0.13888889, …
## $ tele_score_norm <dbl> 0.150900901, 0.054054054, 0.581081081, 0.6689189…
## $ jury_televote_gap <dbl> 0.0999749750, 0.0008133133, 0.3542292292, 0.5300…
# Count missing normalized voting outcomes by year
analysis_data |>
group_by(year) |>
summarise(
n = n(),
tele_score_missing = sum(is.na(tele_score_norm)),
jury_score_missing = sum(is.na(jury_score_norm)),
gap_missing = sum(is.na(jury_televote_gap)),
.groups = "drop"
) |>
arrange(year)
## # A tibble: 16 × 5
## year n tele_score_missing jury_score_missing gap_missing
## <dbl> <int> <int> <int> <int>
## 1 2009 25 25 25 25
## 2 2010 25 25 25 25
## 3 2011 25 25 25 25
## 4 2012 26 26 26 26
## 5 2013 26 26 26 26
## 6 2014 26 26 26 26
## 7 2015 27 27 27 27
## 8 2016 26 0 0 0
## 9 2017 26 0 0 0
## 10 2018 26 0 0 0
## 11 2019 26 0 0 0
## 12 2021 26 0 0 0
## 13 2022 25 0 0 0
## 14 2023 26 0 0 0
## 15 2024 26 1 1 1
## 16 2025 26 0 0 0
# Inspect post-2015 observations with missing voting outcomes
analysis_data |>
filter(
year >= 2016,
is.na(tele_score_norm) | is.na(jury_score_norm)
) |>
select(
year,
to_country,
song,
tele_score_norm,
jury_score_norm,
jury_televote_gap
)
## # A tibble: 1 × 6
## year to_country song tele_score_norm jury_score_norm jury_televote_gap
## <dbl> <chr> <chr> <dbl> <dbl> <dbl>
## 1 2024 Netherlands Europapa NA NA NA
6.3 Merge Musical Features
analysis_data <- analysis_data |>
left_join(
songs_clean |>
select(
year,
to_country,
tempo,
energy,
danceability,
valence
),
by = c("year", "to_country")
)
# Check dimensions after the merge
dim(analysis_data)
## [1] 413 24
# Inspect the newly added musical-feature variables
analysis_data |>
select(
year,
to_country,
tempo,
energy,
danceability,
valence
) |>
glimpse()
## Rows: 413
## Columns: 6
## $ year <dbl> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2025, 202…
## $ to_country <chr> "Norway", "Luxembourg", "Estonia", "Israel", "Lithuania",…
## $ tempo <dbl> 120.96774, 116.27907, 120.00000, 88.23529, 75.75758, 75.7…
## $ energy <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ danceability <dbl> 0.8429474, 0.8348589, 0.8867011, 0.5887105, 0.1796271, 0.…
## $ valence <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
analysis_data |>
group_by(year) |>
summarise(
n = n(),
tempo_missing = sum(is.na(tempo)),
energy_missing = sum(is.na(energy)),
danceability_missing = sum(is.na(danceability)),
valence_missing = sum(is.na(valence)),
.groups = "drop"
) |>
arrange(year)
## # A tibble: 16 × 6
## year n tempo_missing energy_missing danceability_missing valence_missing
## <dbl> <int> <int> <int> <int> <int>
## 1 2009 25 0 0 0 0
## 2 2010 25 0 0 0 0
## 3 2011 25 0 0 0 0
## 4 2012 26 0 0 0 0
## 5 2013 26 0 0 0 0
## 6 2014 26 0 0 0 0
## 7 2015 27 0 0 0 0
## 8 2016 26 0 0 0 0
## 9 2017 26 0 0 0 0
## 10 2018 26 0 0 0 0
## 11 2019 26 0 0 0 0
## 12 2021 26 0 0 0 0
## 13 2022 25 0 0 0 0
## 14 2023 26 0 0 0 0
## 15 2024 26 0 0 0 0
## 16 2025 26 0 26 0 26
6.4 Merge Validation
# Check final dimensions
dim(analysis_data)
## [1] 413 24
# Check for duplicate year-country identifiers
analysis_data |>
count(year, to_country) |>
filter(n > 1)
## # A tibble: 0 × 3
## # ℹ 3 variables: year <dbl>, to_country <chr>, n <int>
# Check for missing essential identifiers
analysis_data |>
summarise(
year_missing = sum(is.na(year)),
country_missing = sum(is.na(to_country)),
song_missing = sum(is.na(song)),
performance_order_missing = sum(is.na(performance_order))
)
## # A tibble: 1 × 4
## year_missing country_missing song_missing performance_order_missing
## <int> <int> <int> <int>
## 1 0 0 0 0
# Check final year coverage and observations per year
analysis_data |>
count(year, name = "number_of_entries") |>
arrange(year)
## # A tibble: 16 × 2
## year number_of_entries
## <dbl> <int>
## 1 2009 25
## 2 2010 25
## 3 2011 25
## 4 2012 26
## 5 2013 26
## 6 2014 26
## 7 2015 27
## 8 2016 26
## 9 2017 26
## 10 2018 26
## 11 2019 26
## 12 2021 26
## 13 2022 25
## 14 2023 26
## 15 2024 26
## 16 2025 26