Euroviisut 2026: Äänestysverkosto on jakauma – kuka äänestää ketä?

Blokkiäänestyksen rakenne, naapuruuskorrelaaatiot ja Dirichlet-malli

euroviisut
bayesian
verkostoanalyysi
avoin data
Tekijä

Kristian Vepsäläinen

Julkaistu

14.5.2026

Tänä iltana kello 22 alkaa toinen semifinaali. Eilen katsoimme positioefektejä. Tänään kysymys on syvempi: kenen ääni on painokkaampi – ja kenelle?

Euroviisut on äänestysverkosto. Jokainen osallistujamaa äänestää, mutta ei omaa maataan. Tuloksena on painotettu suunnattu verkko, jossa naapuruus, kielialue ja diaspora luovat systemaattisia kuvioita.

Data: TidyTuesday Eurovision-äänestysdata

Koodi
# Lähde: TidyTuesday Eurovision -datasetti (CC0)
# https://github.com/rfordatascience/tidytuesday/tree/master/data/2022/2022-05-17
#
# eurovision-votes.csv – maa-maa-äänestysdata
# Sarakkeet: year, semi_final, edition, jury_or_televoting,
#            from_country, to_country, points, duplicate
#
# Tämä on staattinen raw-CSV GitHubissa – ei JS-renderöintiä.

base_url <- paste0("https://raw.githubusercontent.com/rfordatascience/",
                   "tidytuesday/master/data/2022/2022-05-17/")

votes_raw <- read_csv(paste0(base_url, "eurovision-votes.csv"),
                      show_col_types = FALSE)

cat("Rivejä:", nrow(votes_raw), "\n")
Rivejä: 56312 
Koodi
cat("Sarakkeet:", paste(names(votes_raw), collapse = ", "), "\n\n")
Sarakkeet: year, semi_final, edition, jury_or_televoting, from_country, to_country, points, duplicate 
Koodi
# Diagnostiikka: näytetään avainmuuttujien arvot
cat("semi_final-sarakkeen arvot:\n")
semi_final-sarakkeen arvot:
Koodi
print(count(votes_raw, semi_final))
# A tibble: 4 × 2
  semi_final     n
  <chr>      <int>
1 f          38305
2 sf          3817
3 sf1         6950
4 sf2         7240
Koodi
# Suodata: finaalit (semi_final == "f"), vuodesta 2010+
# Normalisoidaan myös maanimi: "The Netherlands" → "Netherlands"
votes_final <- votes_raw |>
  filter(
    semi_final == "f",
    year >= 2010,
    !is.na(points)
  ) |>
  mutate(
    from_country = str_replace(from_country, "^The Netherlands$", "Netherlands"),
    to_country   = str_replace(to_country,   "^The Netherlands$", "Netherlands")
  )

# duplicate-sarake on chr: "x" = duplikaatti, NA = ei duplikaatti
if ("duplicate" %in% names(votes_final)) {
  votes_final <- filter(votes_final, duplicate != "x" | is.na(duplicate))
}

# Aggregoi: kuinka paljon A on antanut B:lle (kaikki vuodet yhteensä)
vote_summary <- votes_final |>
  group_by(from_country, to_country) |>
  summarise(
    total_pts = sum(points, na.rm = TRUE),
    n_years   = n_distinct(year),
    avg_pts   = mean(points, na.rm = TRUE),
    n_12pts   = sum(points == 12, na.rm = TRUE),
    .groups   = "drop"
  ) |>
  filter(n_years >= 3)  # vähintään 3 yhteistä finaalivuotta

cat("Äänestyspareja (≥ 3 vuotta yhdessä finaalissa):", nrow(vote_summary), "\n")
Äänestyspareja (≥ 3 vuotta yhdessä finaalissa): 1713 

Äänestysverkosto: Louvain-yhteisöalgoritmi

Koodi
# iGraph: suunnattu verkko merkittävistä äänestyssuhteista
# HUOM: graph_from_data_frame() ottaa kaikki sarakkeet kaarien attribuuteiksi.
# as.undirected() + as_tbl_graph() kaatuu jos attribuutit eivät yhdisty
# siististi (tuottaa NULL-sarakkeita). Ratkaisu: valitaan vain avg_pts.
g_dir <- vote_summary |>
  filter(avg_pts >= 3) |>
  select(from_country, to_country, avg_pts) |>   # vain tarvittavat sarakkeet
  graph_from_data_frame(directed = TRUE)

# Symmetrinen versio Louvain-algoritmia varten
g_und <- as.undirected(g_dir, mode = "collapse",
                       edge.attr.comb = list(avg_pts = "mean"))

set.seed(42)
communities <- cluster_louvain(g_und)
V(g_und)$community <- membership(communities)
cat("Louvain löysi", max(membership(communities)), "äänestysyhteisöä\n")
Louvain löysi 3 äänestysyhteisöä
Koodi
# tidygraph-visualisointi
tg <- as_tbl_graph(g_und) |>
  activate(nodes) |>
  mutate(
    degree    = centrality_degree(mode = "all"),
    community = as.factor(V(g_und)$community)
  )

bloc_colors <- c(clr_red, clr_green, clr_orange, clr_blue, clr_navy,
                 "#9b2226", "#606c38", "#8338ec")

ggraph(tg, layout = "fr") +
  geom_edge_link(aes(width = avg_pts, alpha = avg_pts),
                 color = "grey60", show.legend = FALSE) +
  geom_node_point(aes(size = degree, color = community),
                  show.legend = c(size = FALSE, color = TRUE)) +
  geom_node_text(aes(label = name), repel = TRUE, size = 2.8,
                 color = clr_navy, max.overlaps = 25) +
  scale_edge_width(range = c(0.3, 2.5)) +
  scale_edge_alpha(range = c(0.15, 0.65)) +
  scale_size(range = c(2, 8)) +
  scale_color_manual(values = bloc_colors, name = "Äänestysblokki") +
  labs(
    title    = "Eurovision äänestysverkosto finaalissa 2010–2022",
    subtitle = paste0(
      "Louvain-yhteisöalgoritmi tunnistaa luonnolliset blokit.\n",
      "Viivojen paksuus = keskimääräiset pisteet | Solmun koko = yhteyksien määrä"
    ),
    caption = "Lähde: TidyTuesday / eurovision-votes.csv (CC0)"
  ) +
  theme_graph(base_size = 13) +
  theme(
    plot.title      = element_text(face = "bold", color = clr_navy),
    plot.subtitle   = element_text(color = "grey40"),
    legend.position = "bottom"
  )

Suomi äänestäjänä: minne pisteet menevät?

Koodi
fi_votes <- votes_final |>
  filter(from_country == "Finland") |>
  group_by(to_country) |>
  summarise(
    yht_pisteet = sum(points, na.rm = TRUE),
    n_12pts     = sum(points == 12, na.rm = TRUE),
    n_vuotta    = n_distinct(year),
    .groups     = "drop"
  ) |>
  arrange(desc(yht_pisteet)) |>
  slice_head(n = 15)

fi_votes |>
  mutate(to_country = fct_reorder(to_country, yht_pisteet)) |>
  ggplot(aes(x = to_country, y = yht_pisteet)) +
  geom_col(fill = clr_blue, alpha = 0.8) +
  geom_col(
    data = fi_votes |>
      filter(n_12pts > 0) |>
      mutate(to_country = fct_reorder(to_country, yht_pisteet)),
    aes(y = n_12pts * 12),
    fill = clr_red, alpha = 0.7
  ) +
  coord_flip() +
  labs(
    title    = "Suomen pisteet Eurovision finaaleissa 2010–2022",
    subtitle = "Sininen = kokonaispisteet | Punainen = 12p-antokerrat × 12",
    x        = NULL,
    y        = "Kokonaispisteet",
    caption  = "Lähde: TidyTuesday / eurovision-votes.csv (CC0)"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title    = element_text(face = "bold", color = clr_navy),
    plot.subtitle = element_text(color = "grey40"),
    panel.grid.minor = element_blank()
  )

Korrelaatiomatriisi: kuka äänestää samoin?

Koodi
# Pivotoi leveään muotoon: rivit = äänestäjämaat, sarakkeet = kohdemaat
voter_agg <- votes_final |>
  group_by(from_country, to_country) |>
  summarise(avg_pts = mean(points, na.rm = TRUE), .groups = "drop") |>
  pivot_wider(
    id_cols     = from_country,
    names_from  = to_country,
    values_from = avg_pts,
    values_fill = 0
  )

mat           <- as.matrix(select(voter_agg, -from_country))
rownames(mat) <- voter_agg$from_country

# Poistetaan maat joilla hyvin vähän varianssia (lähes aina 0)
row_var <- apply(mat, 1, var)
mat_f   <- mat[row_var > 0.5, ]

cor_mat <- cor(t(mat_f), method = "spearman")

corrplot(
  cor_mat,
  method  = "color",
  type    = "upper",
  tl.cex  = 0.55,
  tl.col  = clr_navy,
  col     = colorRampPalette(c(clr_red, "white", clr_blue))(200),
  title   = "Äänestäjämaiden Spearman-korrelaatio (finaalit 2010–2022)",
  mar     = c(0, 0, 2, 0),
  diag    = FALSE,
  order   = "hclust"  # ryhmittelee samanlaiset äänestäjät vierekkäin
)

Mitä tämä tarkoittaa tänä iltana?

Toisen semifinaalin maiden (Bulgaria, Azerbaidžan, Romania, Luxemburg, Tšekki, Armenia, Sveitsi, Kypros, Latvia, Tanska, Australia, Ukraina, Albania, Malta, Norja) kannattaa huomioida:

  • Itä-Euroopan blokki (Romania, Bulgaria, Armenia) äänestää vahvasti toisiaan – Louvain ryhmittelee nämä tyypillisesti yhteen yhteisöön
  • Pohjoismaat (Tanska, Norja) korreloivat keskenään voimakkaasti
  • Diaspora-efekti: Armenia saa pisteitä maista, joissa on suuri armenialainen diaspora
HuomautusJakauma-ajattelun ydin

Yksittäinen äänestyslinkkipari (“Kreikka antaa Kyprokselle 12p”) on epäinformatiivinen. Olennainen rakenne näkyy vasta koko äänestysmatriisin jakaumasta — ja erityisesti blokkien sisäisestä korrelaatiosta verrattuna blokkien väliseen korrelaatioon.


Kristian Vepsäläinen on data science -konsultti Siilinjärveltä. Kiinnostuitko? → kristianvepsalainen.com