1 Introduction

Dans ce projet, nous analysons le marché des transferts de football professionnel sous l’angle de l’analyse des réseaux sociaux. Nous nous appuyons sur une base de données de transferts pour modéliser les relations entre les clubs.

Problématique : Au-delà des simples “clubs tremplins”, comment se structure le marché mondial ? Est-il organisé en communautés nationales étanches ou existe-t-il un “noyau dur” supra-national ? Nous analyserons les différentes propriétés du réseau.

2 Préparation des données

2.1 Chargement et Nettoyage

Nous nous restreignons aux transferts récents (après 2015) et significatifs (> 3M€) pour obtenir une vue claire du marché moderne, et ainsi analyser plus facilement les clubs tremplins (clubs ayant vendus des joueurs chers par rapport aux marchés).

df <- read.csv("transfers.csv", sep = ";")

# 1. Chargement et filtrage de base
df_filtered <- df %>%
    filter(season >= 2015) %>%
    filter(!is.na(transfer_fee_amnt) & transfer_fee_amnt > 3000000)

# 2. Standardisation SANS perdre les exports
edges <- df_filtered %>%
    mutate(
        # On définit qui envoie et qui reçoit selon la direction
        from = ifelse(dir == "in", counter_team_name, team_name),
        to   = ifelse(dir == "in", team_name, counter_team_name),
        amount = as.numeric(transfer_fee_amnt)
    ) %>%
    # On ajoute la ligue si possible (mais attention, on n'a que la ligue de 'team_name')
    mutate(
        league_from = ifelse(dir == "left", league, NA),
        league_to   = ifelse(dir == "in", league, NA)
    ) %>%
    # ÉTAPE CLÉ : On dédoublonne sur les caractéristiques du transfert
    # Si un transfert est présent en 'in' et en 'left', distinct n'en gardera qu'un.
    distinct(from, to, player_name, season, amount, .keep_all = TRUE) %>%
    
    # Nettoyage final
    filter(!is.na(from), !is.na(to), from != "", to != "", from != to)

# 3. Vérification
print(paste("Nombre de transferts :", nrow(edges)))
## [1] "Nombre de transferts : 2536"

2.2 Agrégation du Réseau

Nous agrégeons les liens multiples entre deux mêmes clubs (somme des montants et nombre de transferts).

edges_agg <- edges %>%
    group_by(from, to) %>%
    summarise(
        weight = sum(amount, na.rm = TRUE), # Poids = Montant total des échanges
        count = n(), # Nombre de joueurs échangés
        label = paste0(n(), " joueurs")
    ) %>%
    ungroup()

# Création de la liste des noeuds (Clubs) avec tentative d'attribut "Ligue"
# On prend la ligue majoritaire pour chaque club si disponible dans le dataset original
club_leagues <- df %>%
    select(team_name, league) %>%
    distinct() %>%
    group_by(team_name) %>%
    slice(1)

nodes <- data.frame(id = unique(c(edges_agg$from, edges_agg$to))) %>%
    left_join(club_leagues, by = c("id" = "team_name")) %>%
    mutate(league = ifelse(is.na(league), "Other", league)) %>%
    arrange(id)

3 Construction et Propriétés Macro

Nous construisons un graphe orienté où les arêtes représentent les flux de joueurs.

g <- graph_from_data_frame(d = edges_agg, vertices = nodes, directed = TRUE)

# Informations basiques
densite <- edge_density(g)
reciprocite <- reciprocity(g)
transitivite <- transitivity(g, type = "global")
diametre <- diameter(g, weights = NA)

cat("--- Résumé du Graphe ---\n")
## --- Résumé du Graphe ---
cat("Nombre de noeuds (Clubs):", vcount(g), "\n")
## Nombre de noeuds (Clubs): 423
cat("Nombre d'arêtes (Relations):", ecount(g), "\n")
## Nombre d'arêtes (Relations): 2151
cat("Densité:", round(densite, 5), "\n")
## Densité: 0.01205
cat("Réciprocité:", round(reciprocite, 3), "\n")
## Réciprocité: 0.166
cat("Transitivité (Clustering):", round(transitivite, 3), "\n")
## Transitivité (Clustering): 0.165
cat("Diamètre:", diametre, "\n")
## Diamètre: 8

Le réseau est peu dense / quasi nul avec une densité de 0,01205. La réciprocité est de 0,166, ce qui veut dire qu’il y a peu d’échanges directs (Club A->Club B et Club B->Club A). La valeur de la transitivité est de 0,165. Cela signifie que dans environ 17 % des cas, lorsqu’un transfert existe via un club intermédiaire, les deux clubs aux extrémités ont aussi une relation directe. Depuis 2015, le nombre de clubs maximum par lesquels est passé un joueur est de 8.

3.1 Distribution des Degrés

# Calcul des degrés
V(g)$degree_total <- degree(g, mode = "all")
V(g)$in_degree <- degree(g, mode = "in") # Achète beaucoup
V(g)$out_degree <- degree(g, mode = "out") # Vend beaucoup

# Dataframe pour ggplot
degrees_df <- data.frame(
    id = V(g)$name,
    Total = V(g)$degree_total,
    In = V(g)$in_degree,
    Out = V(g)$out_degree,
    League = V(g)$league
)

# Histogramme
degrees_df %>%
    pivot_longer(cols = c(In, Out), names_to = "Type", values_to = "Degree") %>%
    ggplot(aes(x = Degree, fill = Type)) +
    geom_histogram(binwidth = 1, alpha = 0.7, position = "identity") +
    labs(
        title = "Distribution des Degrés (In vs Out)",
        y = "Nombre de Clubs", x = "Degré"
    ) +
    theme_minimal() +
    scale_fill_manual(values = c("In" = "blue", "Out" = "red"))

# Log-Log Plot pour vérifier la loi de puissance
deg.dist <- degree_distribution(g, cumulative = TRUE, mode = "all")
plot(
    x = 0:max(degree(g)), y = 1 - deg.dist, pch = 19, cex = 1.2, col = "orange",
    xlab = "Degree", ylab = "Cumulative Frequency", main = "Distribution Cumulative des Degrés (Log-Log)"
)

La majorité des clubs n’ont pas ou très peu de connexions entre eux. C’est assez cohérent, car la majorité des clubs ont peu de budgets et donc n’ont pas pu acheter ou vendre des joueurs à plus de 3M d’euros depuis 2015. Ce sont notamment clubs qui ne se situent pas dans les 5 grands championnats d’Europe (France, Espagne, Italie, Angleterre, Allemagne). 80% des clubs ont moins de 15 transferts avec les critères que nous avons énoncé précedemment. Ensuite l’augmentation du nombre de transferts ne touche que les clubs avec des gros budgets (qui ne sont pas la cible principale de notre étude)

4 Analyse Micro : Centralités et Rôles

Qui sont les acteurs clés ?

Notre étude se concentre principalement sur les clubs tremplins (acheteurs puis vendeurs).

# Calcul des métriques avancées
V(g)$betweenness <- betweenness(g, directed = TRUE, normalized = TRUE)
V(g)$closeness <- closeness(g, mode = "all", normalized = TRUE)
V(g)$pagerank <- page_rank(g)$vector

# Top 15 par Betweenness
metrics_df <- data.frame(
    Club = V(g)$name,
    Ligue = V(g)$league,
    Achats = V(g)$in_degree,
    Ventes = V(g)$out_degree,
    Betweenness = round(V(g)$betweenness, 4),
    PageRank = round(V(g)$pagerank, 4)
)

datatable(metrics_df %>% arrange(desc(Betweenness)),
    caption = "Classement des Clubs par Centralité d'Intermédiarité"
)

Betweenness représente le niveau d’intermediarité de chaque club. Le PageRank mesure l’attractivité indirecte d’un club : un club a un PageRank élevé s’il est une destination de transfert pour des joueurs provenant de clubs qui sont eux-mêmes considérés comme importants au sein du réseau.

On remarque que le top 10 des clubs ayant le niveau d’intermediarité le plus élevé sont des clubs ayant un niveau européen sans pour autant être dans les favoris de ces compétitions. Pour des joueurs prometteurs venants de petits clubs ou clubs formateurs, ces clubs intermediaires sont un excellent tremplin vers des équipes avec de fortes ambitions à l’echelle nationale et européenne. Ceux-ci sont des excellentes destinations pour les joueurs en recherche de temps de jeu voulant se confronter au top niveau européen.

4.1 Comparaison par Ligue

Les clubs anglais sont-ils structurellement différents des autres ?

# Focus sur les ligues principales pour la lisibilité
top_leagues <- c("GB1", "ES1", "IT1", "L1", "L1", "PO1", "NL1") # Ang, Esp, Ita, Fra, All, Por, Hol
# Note: Vérifier les codes exacts dans votre CSV. Ici j'assume des codes standards type GB1.
# Adaptation dynamique aux tops 5 ligues présentes dans les données
top_leagues_detected <- names(sort(table(V(g)$league), decreasing = TRUE))[1:6]

metrics_df %>%
    filter(Ligue %in% top_leagues_detected) %>%
    ggplot(aes(x = Ligue, y = Betweenness, fill = Ligue)) +
    geom_boxplot() +
    labs(title = "Centralité d'Intermédiarité par Ligue", subtitle = "Quel championnat joue le rôle de pivot ?") +
    theme_minimal() +
    theme(legend.position = "none")

On pouvait s’imaginer une differentiation entre la ligue anglaise et les autres championnats européens au vu de leur budget reconnu plus élevé. Ils se situent dans la norme en proportion d’achat et revente. Cependant on remarque que la ligue allemande (L1) semble être un excellent tremplin au vu de la distribution présente.

5 Communautés et Assortativité

5.1 Assortativité Nominale

Les clubs ont-ils tendance à transférer des joueurs vers des clubs de la même ligue ? L’assortativité nominale varie de -1 (hétérophilie parfaite) à 1 (homophilie parfaite). Si l’assortativité est superieur à 0.3, on peut en déduire qu’il y a une forte tendance à échanger au sein de la même ligue.

# Assortativité par Ligue
V(g)$league_num <- as.numeric(as.factor(V(g)$league))
assort_ligue <- assortativity_nominal(g, V(g)$league_num, directed = TRUE)

cat("Assortativité par Ligue :", round(assort_ligue, 3), "\n")
## Assortativité par Ligue : 0.207

L’assortativité par ligue est de 0.2, on peut en déduire qu’il y a de nombreux tranferts dans la même ligue mais il y a toujours la majorité des joueurs qui changent plutôt de championnat.

5.2 Détection de Communautés

Utilisons l’algorithme de Louvain pour détecter les “clusters” naturels d’échanges.

# On utilise l'algorithme de Louvain sur une version non-dirigée du graphe
g_undir <- as.undirected(g, mode = "collapse")
cluster <- cluster_louvain(g_undir)

V(g)$community <- cluster$membership
nodes$group <- cluster$membership

# Taille des communautés
sizes(cluster) %>%
    as.data.frame() %>%
    rename(Communauté = Community.sizes, Taille = Freq) %>%
    datatable(caption = "Taille des communautés détectées")

Les clubs d’un même cluster Louvain ont plus de transferts entre eux que ce que le hasard prédirait. Analysons cela concrètement :

# Calcul des transferts INTERNES vs EXTERNES pour chaque communauté
edges_with_community <- edges_agg %>%
    left_join(data.frame(from = V(g)$name, comm_from = V(g)$community), by = "from") %>%
    left_join(data.frame(to = V(g)$name, comm_to = V(g)$community), by = "to") %>%
    mutate(
        type_transfert = ifelse(comm_from == comm_to, "Interne", "Externe"),
        communaute = ifelse(comm_from == comm_to, as.character(comm_from), NA)
    )

# Résumé par communauté
cohesion_stats <- edges_with_community %>%
    filter(!is.na(communaute)) %>%
    group_by(communaute) %>%
    summarise(
        Transferts_Internes = n(),
        Montant_Interne_M = round(sum(weight, na.rm = TRUE) / 1e6, 1)
    ) %>%
    rename(Communauté = communaute)

# Nombre total de transferts impliquant chaque communauté
total_by_comm <- edges_with_community %>%
    pivot_longer(cols = c(comm_from, comm_to), values_to = "comm") %>%
    mutate(comm = as.character(comm)) %>%
    filter(comm %in% cohesion_stats$Communauté) %>%
    group_by(comm) %>%
    summarise(Total_Transferts = n()) %>%
    rename(Communauté = comm)

cohesion_stats <- cohesion_stats %>%
    mutate(Communauté = as.character(Communauté)) %>%
    left_join(total_by_comm, by = "Communauté") %>%
    mutate(
        Pct_Interne = round(Transferts_Internes / Total_Transferts * 100, 1),
        Communauté = as.factor(Communauté)
    ) %>%
    arrange(desc(Pct_Interne))

# Graphique : % de transferts internes par cluster
ggplot(cohesion_stats, aes(x = reorder(Communauté, Pct_Interne), y = Pct_Interne, fill = Pct_Interne)) +
    geom_col() +
    geom_text(aes(label = paste0(Pct_Interne, "%")), hjust = -0.1, size = 3.5) +
    coord_flip() +
    scale_fill_gradient(low = "lightcoral", high = "darkgreen") +
    labs(
        title = "Cohésion des Clusters : % de Transferts Internes",
        subtitle = "Plus le % est élevé, plus les clubs du cluster s'échangent des joueurs entre eux",
        x = "Communauté (Cluster)",
        y = "% des transferts qui restent dans le cluster"
    ) +
    theme_minimal() +
    theme(legend.position = "none") +
    ylim(0, max(cohesion_stats$Pct_Interne) * 1.15)

5.3 Composition des Communautés

Analysons la structure interne de chaque communauté pour comprendre ce qui les relie : sont-elles organisées par ligue, par région géographique, ou par niveau sportif ?

# Création d'un dataframe enrichi avec les communautés
community_df <- data.frame(
    Club = V(g)$name,
    Ligue = V(g)$league,
    Communauté = as.factor(V(g)$community),
    Betweenness = V(g)$betweenness,
    PageRank = V(g)$pagerank,
    InDegree = V(g)$in_degree,
    OutDegree = V(g)$out_degree
)

# 1. Graphique : Composition par Ligue de chaque Communauté
# On garde les top 8 ligues pour la lisibilité
top_ligues <- names(sort(table(community_df$Ligue), decreasing = TRUE))[1:8]
community_df_filtered <- community_df %>%
    mutate(Ligue_grouped = ifelse(Ligue %in% top_ligues, Ligue, "Autres"))

# Barplot empilé
ggplot(community_df_filtered, aes(x = Communauté, fill = Ligue_grouped)) +
    geom_bar(position = "fill") +
    scale_y_continuous(labels = scales::percent_format()) +
    labs(
        title = "Composition des Communautés Louvain par Ligue",
        subtitle = "Chaque barre représente la répartition des ligues au sein de la communauté",
        x = "Communauté (Cluster)",
        y = "Proportion",
        fill = "Ligue"
    ) +
    scale_fill_brewer(palette = "Set3") +
    theme_minimal() +
    theme(
        legend.position = "bottom",
        plot.title = element_text(size = 14, face = "bold")
    )

On remarque que dans tous les clusters, les principaux clubs proviennent des championnats Others car ils sont plus en plus grand nombre (englobe plusieurs championnats moins connus).

6 Visualisation Interactive Complète

Visualisation du réseau filtré pour plus de clarté (on cache les équipes n’ayant aucun tranfert).

# Filtrage pour la visu : on ne garde que la composante géante ou les noeuds avec degré > k
# Ici on garde tout mais on joue sur la taille
nodes_vis <- nodes %>%
    mutate(
        label = id,
        value = V(g)$betweenness, # Taille = Intermédiarité
        group = V(g)$league, # Couleur = Ligue (pour comparer visuellement avec les communautés calculées)
        title = paste0(
            "<b>", id, "</b><br>",
            "Ligue: ", league, "<br>",
            "Achats: ", V(g)$in_degree, "<br>",
            "Ventes: ", V(g)$out_degree
        )
    )

edges_vis <- edges_agg %>%
    mutate(
        value = weight,
        title = paste0("Total: ", round(weight / 1e6, 1), " M€")
    )

visNetwork(nodes_vis, edges_vis, main = "Réseau Mondial des Transferts") %>%
    visNodes(
        scaling = list(min = 10, max = 50),
        font = list(size = 20, face = "bold")
    ) %>%
    visEdges(
        arrows = "to",
        color = list(opacity = 0.5)
    ) %>%
    visOptions(
        highlightNearest = list(enabled = TRUE, degree = 1, hover = TRUE),
        nodesIdSelection = TRUE,
        selectedBy = "group"
    ) %>%
    visPhysics(
        solver = "forceAtlas2Based",
        forceAtlas2Based = list(gravitationalConstant = -100)
    ) %>%
    visLayout(randomSeed = 42) %>%
    visLegend()

On peut filtrer la visualisation pour afficher un championnat précis, et même de sélectionner qu’une équipe en particulier.

7 Analyse de Coeur (K-Core)

Identification du “Noyau Dur” (Core) des clubs qui sont tous interconnectés à un niveau k.

cores <- coreness(g, mode = "all")
max_core <- max(cores)
g_core <- induced_subgraph(g, vids = which(cores == max_core))
# 1. On réduit les marges de la zone de dessin (bas, gauche, haut, droite)
par(mar = c(0, 0, 2, 0)) 

# 2. On prépare les attributs
pr_core <- page_rank(g_core)$vector
V(g_core)$size <- pr_core * 500 + 8
V(g_core)$label <- ifelse(pr_core > mean(pr_core), V(g_core)$name, NA)

# 3. On utilise un layout avec une force de répulsion plus forte
# On multiplie les coordonnées par un facteur (ex: 2) pour écarter les points
layout_expand <- layout_with_fr(g_core)
layout_expand <- norm_coords(layout_expand, ymin=-1.2, ymax=1.2, xmin=-1.2, xmax=1.2)

# 4. Visualisation large
plot(g_core,
     layout = layout_expand,               # Utilise le layout élargi
     rescale = FALSE,                      # Empêche igraph de tout resserrer au centre
     vertex.label = V(g_core)$label,
     vertex.label.cex = 0.9,
     vertex.label.color = "black",
     vertex.label.font = 2,
     vertex.label.dist = 1.2,              # Écarte un peu le texte des nœuds
     
     edge.label = NA,
     edge.arrow.size = 0.2,
     edge.color = adjustcolor("gray75", alpha.f = 0.3),
     edge.curved = 0.1,
     
     vertex.color = "gold",
     vertex.frame.color = "white",
     main = ""
)

Ce graphique présente le k-core maximal, soit l’élite hyper-connectée du marché des transferts. Contrairement au reste du réseau, chaque club ici est lié à au moins k autres membres, formant une clique financière et sportive.

Poids des leaders : Les nœuds les plus gros (PageRank élevé) révèlent les centres névralgiques (ex: Man City, Real Madrid). Ils ne sont pas de simples acheteurs, mais les moteurs qui dictent les flux mondiaux.

Marché globalisé : La proximité des nœuds montre que pour ces géants, les frontières nationales s’effacent. Ils préfèrent échanger entre eux (marché de “haut étage”) plutôt qu’avec la périphérie.

Effet Domino : La forte densité des liens suggère qu’un transfert majeur au sein de ce groupe provoque instantanément des réactions en chaîne sur les autres membres.

Rôle de filtrage : Ce noyau agit comme un entonnoir où convergent les meilleurs talents mondiaux après avoir transité par des clubs plus modestes.

8 Conclusion Générale

Cette analyse du marché des transferts depuis 2015 révèle une structure de réseau hautement hiérarchisée, typique des systèmes dits « petit monde ». Bien que la densité globale soit faible, le marché s’articule autour d’un noyau dur (K-core) de clubs majeurs dont l’influence dépasse largement les frontières nationales. Ce centre stratégique, dominé par des entités à fort PageRank, dicte les flux financiers et sportifs mondiaux.

L’étude met en lumière le rôle crucial des clubs pivots et tremplins, identifiés par leur forte centralité d’intermédiarité (Betweenness). Ces clubs, notamment en Allemagne (Bundesliga) et au Portugal, ne sont pas de simples acheteurs, mais de véritables connecteurs stratégiques qui facilitent la circulation des talents vers l’élite. Malgré une globalisation évidente, l’assortativité de 0.2 montre que les marchés domestiques conservent une influence notable, bien que secondaire face à l’attraction du noyau supra-national.

En conclusion, le marché des transferts moderne n’est pas un ensemble de ligues isolées, mais un écosystème intégré où une élite hyper-connectée agit comme un entonnoir pour les talents mondiaux, structurant ainsi durablement la hiérarchie du football professionnel.