Textométrie Webtoon — Réseau webtoon/webtoon

Author

Nom Prénom

1. Libraries

2. Import et préparation des données

2.1 Import

# Chemins 
path_comm  <- "//ad.univ-lille.fr/Personnels/Homedir1/4273/Documents/cours25_26/Master2 ENSP/textometrie/dossier_exam/tc_predictions_3labels_utf8.csv"
path_genre <- "//ad.univ-lille.fr/Personnels/Homedir1/4273/Documents/cours25_26/Master2 ENSP/textometrie/dossier_exam/genre.csv"

comm  <- read.csv(path_comm,  stringsAsFactors = FALSE)
genre <- read.csv(path_genre, stringsAsFactors = FALSE)

# Jointure 
comm <- comm %>% left_join(genre, by = "titre")

dim(comm)
[1] 286505     24
DT::datatable(
  comm %>% dplyr::slice_head(n = 200),
  options = list(scrollX = TRUE)
)

2.2 Répartion des genres

repartition_genre <- genre$genre %>%
  freq() %>%
  as.data.frame()

repartition_genre$genre <- rownames(repartition_genre)
repartition_genre <- repartition_genre[, c(4, 1, 2)] 
repartition_genre<-repartition_genre%>% arrange(desc(n))
rownames(repartition_genre) <- NULL
gt(repartition_genre, row_group_as_column = TRUE)
genre n %
Romance 110 52.1
Fantastique 33 15.6
Drama 22 10.4
Action 16 7.6
Thriller 12 5.7
Comedie 8 3.8
Tranche_de_vie 5 2.4
Horreur 2 0.9
Sport 2 0.9
SF 1 0.5

Certains genres sont très peu représentés. Nous allons donc procéder à un regroupement.

#|label: recode_genre

comm$genre_rec <- comm$genre |>
  fct_recode(
    "Fantastique/SF/Horreur" = "Fantastique",
    "Fantastique/SF/Horreur" = "Horreur",
    "Fantastique/SF/Horreur" = "SF",
    "Tranche_de_vie/Sport" = "Sport",
    "Tranche_de_vie/Sport" = "Tranche_de_vie"
  )

3. Réseau Webtoon/Webtoon

Nous allons créer un réeau entre Webtoon en mesurant la proximité par les publics partagés (nombre d’utilisateurs ayant commenté les 2 webtoons)

3.1 Matrice User/Webtoon

Nous allons créer une SparseMatrix recensant le nombre de commentateurs communs pour chaque paires de webtoon

# Table binaire user-webtoon : un couple user/webtoon unique = 1
uw <- comm %>%
  distinct(user, titre) %>%
  mutate(x = 1L) #data.frame [175153;3]

# Indexation 
users <- sort(unique(uw$user))
toons <- sort(unique(uw$titre))

print(paste0("on compte ",length(users)," utilisateurs pour ",length(toons)," webtoons."))
[1] "on compte 96986 utilisateurs pour 211 webtoons."
uw <- uw %>%
  mutate(
    i = match(user, users), # i=rang de l'individu dans la liste users
    j = match(titre, toons) # j=rang du webtoon dans la liste toons
  )

# Matrice sparse  (users , webtoons)
X <- sparseMatrix(
  i = uw$i,
  j = uw$j,
  x = uw$x,
  dims = c(length(users), length(toons)),
  dimnames = list(users, toons)
) #si l'utilisateur i a commenté le webtoon j alors x(ij) vaut 1, sinon 0 (symbolisé par un ".")

3.2 Similarité cosinus entre Webtoons

Maintenant que nous avons construit la Matrice sparse recensant tous les couples user/Webtoon effectifs, nous pouvons construire un indice de similarité (Similarité cosinus) entre Webtoons. Si deux webtoons sont commentés par un nombre important d’utilisateurs identiques alors ils seront proches.

#Le passage par la similarité cosinus permet de mesurer la proximité relative des publics des oeuvres en neutralisant l'effet de la popularité de certaines oeuvres.

# Co-commentaires : W = t(X) %*% X
W <- crossprod(X)        # 211 x 211 (si 211 webtoons)

# d = sqrt(diag(W)) = norme de chaque colonne (taille du public, racine)
d <- sqrt(diag(W))

# On retire les auto-liens
diag(W) <- 0

# Cosine similarity : COS[i,j] = W[i,j] / ( sqrt(W[i,i]) * sqrt(W[j,j]) )
COS <- W / (d %o% d) 

# Sécurité
COS[is.na(COS)] <- 0
diag(COS) <- 0

# Résumé
summary(as.numeric(COS))
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
0.000000 0.008674 0.015196 0.019623 0.025895 0.167705 

3.3 Construction du réseau par k-voisins et clustering

Avec la projection webtoon–webtoon basée sur publics partagés et un seuil de similarité relativement permissif, Louvain renvoie 4 communautés assez larges (52/66/25/68). Si on monte le seuil (de 0.02 à 0.055), le graphe devient beaucoup trop dispersé. Nous décidons donc de faire plutôt une sélection des top-k voisins (ici avec k=10 mais les résultats sont relativement stables avec k=8 ou k=12).

k <- 10 

# Edge list : pour chaque webtoon i, garder ses k plus fortes similarités
edgelist_k <- do.call(rbind, lapply(1:nrow(COS), function(i) {

  w <- COS[i, ]   # w est le vecteur des similarités entre le webtoon i et les autres webtoon. Donc w[j] est la similarité cosinus entre les webtoons i et j
  w[i] <- 0       # sécurité : on supprime les auto-liens

  j <- order(w, decreasing = TRUE)[1:k]  # indices des k plus proches

  data.frame( #renvoie un tableau de k lignes
    from = rownames(COS)[i],
    to   = colnames(COS)[j],
    w    = w[j]
  )
})) %>% #les tableaux de k-lignes vont s'empiler grâce au do.call de début de formule
  
  filter(w > 0) %>%  # enlever les éventuels poids nuls
  mutate(
    a = pmin(from, to),  
    b = pmax(from, to)  # réécrit la paire dans le même sens (A,B) et (B,A) -> (A,B). Puisque ici il s'agit de variable texte, le min et le max renvoient à un ordre alphabétique
  ) %>%
  group_by(a, b) %>%     # regroupe les doublons non orientés
  summarise(w = max(w), .groups = "drop") %>%  # garde 1 arête A—B, poids = max. On a fini de désorienté les arêtes.
  rename(from = a, to = b)

# Graphe non orienté
g <- graph_from_data_frame(edgelist_k, directed = FALSE, vertices = toons)
E(g)$weight <- edgelist_k$w

# Diagnostics
print(paste0(gorder(g)," sommets"))
[1] "211 sommets"
print(paste0(gsize(g)," arêtes"))
[1] "1423 arêtes"
print(paste0("densité = ",edge_density(g)))
[1] "densité = 0.0642292936131799"
print(paste0("taille de(s) composante(s) : ",components(g)$csize))
[1] "taille de(s) composante(s) : 211"

Il reste maintenant à établir les clusters, grâce à la méthode de Louvain

cl <- cluster_louvain(g, weights = E(g)$weight)

sizes(cl)        
Community sizes
 1  2  3  4  5  6  7 
22 45 32 24 43 26 19 

Les 211 webtoons peuvent donc être regroupés en 7 clusters de taille variant de 24 à 39. Chaque webtoon appartient donc à un cluster

webtoon_cluster <- tibble(
  titre= names(membership(cl)),
  cluster = as.integer(membership(cl))
)

comm <-comm %>% left_join(webtoon_cluster,by="titre") #pour garder l'information directement dans le dataframe principal

DT::datatable(webtoon_cluster)

Bien que peu lisible, on peut représenter le réseau avec appartenance aux clusters (nous n’affichons les titres que des 15 titres les plus centraux)

deg <- degree(g)
top15 <- names(sort(deg, decreasing = TRUE))[1:15]

V(g)$lab <- ifelse(V(g)$name %in% top15, V(g)$name, NA)

plot(
  g,
  layout = layout_with_fr(g, weights = E(g)$weight),
  vertex.size = 6,
  vertex.label = V(g)$lab,
  vertex.label.cex = 0.6,
  vertex.color = V(g)$comm,
  edge.width = 1 + 5 * (E(g)$weight / max(E(g)$weight))
)

Pour chaque Webtoon, nous pouvons afficher ses webtoons les plus proches

webtoon_cible <- "100"  #ou n'importe quel autre titre

neighbors_tbl <- edgelist_k %>%
  filter(from == webtoon_cible | to == webtoon_cible) %>%
  mutate(other = ifelse(from == webtoon_cible, to, from)) %>%
  arrange(desc(w)) %>%
  select(webtoon_cible = other, cos = w)%>%
  rename("titre"=webtoon_cible) %>% 
  left_join(webtoon_cluster, by = "titre")%>%
  left_join(genre, by = "titre")

cluster_cible <- webtoon_cluster %>%
  filter(titre == webtoon_cible) %>%
  pull(cluster)

genre_cible <- genre %>%
  filter(titre == webtoon_cible) %>%
  pull(genre)


neighbors_tbl %>%
  slice_head(n = 10) %>%
  gt() %>%
  tab_header(
    title = paste0("Les 10 voisins du webtoon ", webtoon_cible),
    subtitle = paste0(
      "Webtoon de genre ",genre_cible," appartenant au cluster ",
      cluster_cible,
      " (partition Louvain, similarité cosinus des publics)"
    ) 
  )
Les 10 voisins du webtoon 100
Webtoon de genre Action appartenant au cluster 1 (partition Louvain, similarité cosinus des publics)
titre cos cluster genre
Ultra-Alternate Character 0.08351464 1 Action
Night Of Silence 0.07063240 1 Action
Solo Auto-Hunting 0.06544113 1 Action
The Retreats 0.06499172 1 Action
Multi-skill Collector 0.05343246 1 Action
SAMADHI 0.05086688 1 Action
Les pirates d'abord 0.04731112 6 Comedie
Plaza Wars 0.04339023 1 Action
Noise from upstairs 0.04312738 1 Thriller
The Final Raid Boss 0.03550589 1 Action

Cette analyse nous permet de voir si les voisins les plus proches d’un webtoon appartiennent majoritairement au même genre que celui-ci. Il permet également de repérer les ponts entre différents clusters.

Par exemple, le webtoon “100”, de genre “Action”, a pour voisins des webtoons du genre “Action” principalement (7/10) et 9 de ses 10 voisins appartiennent (par construction) au même cluster. Mais un de ses voisins (“Les pirates d’abord”), de genre “Comédie”, appartient à un autre cluster. Il y a ici un pont entre le cluster 1 et le cluster 7.

Nous pouvons également étudié le contenu en genre des clusters. Cela permet d’étudier leur degré d’homogénéité/hétérogénéité de genre.

dist_genre_cluster <- comm %>%
  distinct(titre, cluster, genre_rec) %>%  # 1 ligne = 1 webtoon
  count(cluster, genre_rec, name = "n") %>%
  group_by(cluster) %>%
  mutate(pct = round(n / sum(n) * 100,2)) %>%
  ungroup() %>%
  arrange(cluster, desc(n))

dist_genre_cluster %>%
  gt(groupname_col = "cluster") %>%
  tab_header(
    title = "Distribution des genres par cluster de webtoons",
    subtitle = "en % des titres") %>%
  cols_label(
    genre_rec = "Genre",
    n         = "Nombre de webtoons",
    pct       = "% dans le cluster"
  )
Distribution des genres par cluster de webtoons
en % des titres
Genre Nombre de webtoons % dans le cluster
1
Action 13 59.09
Thriller 5 22.73
Drama 2 9.09
Fantastique/SF/Horreur 2 9.09
2
Romance 34 75.56
Fantastique/SF/Horreur 6 13.33
Drama 4 8.89
Thriller 1 2.22
3
Romance 15 46.88
Fantastique/SF/Horreur 11 34.38
Drama 6 18.75
4
Romance 11 45.83
Drama 3 12.50
Fantastique/SF/Horreur 3 12.50
Comedie 2 8.33
Tranche_de_vie/Sport 2 8.33
Thriller 2 8.33
Action 1 4.17
5
Romance 26 60.47
Drama 6 13.95
Fantastique/SF/Horreur 6 13.95
Thriller 3 6.98
Action 1 2.33
Tranche_de_vie/Sport 1 2.33
6
Fantastique/SF/Horreur 7 26.92
Comedie 6 23.08
Romance 6 23.08
Tranche_de_vie/Sport 4 15.38
Action 1 3.85
Drama 1 3.85
Thriller 1 3.85
7
Romance 18 94.74
Fantastique/SF/Horreur 1 5.26

Par exemple, tandis que le cluster 4 contient près de 85% de titres de genre “Romance” (très homogène), le cluster 6 est formé par des titres de genre divers et aucun genre n’y regroupe plus d’un quart des titres.

4. Réseau user/user

La question centrale à laquelle cette construction vise à répondre est la suivante : À l’intérieur d’un même cluster de webtoons (donc d’un même espace de lectures partageant un important nombre de commentateurs), existe-t-il des sous-groupes d’utilisateurs qui partagent plusieurs œuvres et qui se distinguent par leurs manières de commenter (types de commentaires, registres lexicaux) ?

Il nous faut donc:

  1. Repérer des communautés d’utilisateurs dans un espace de lecture déjà homogènes (clusters de webtoon)

  2. Caractériser ces communautés en fonction du type de commentaires voire des termes abordés ou encore des registres lexicaux utilisés.

Si nous établissons des spécificités pour certaines communautés notre analyse ne nous permettra pas de déterminer le sens de la causalité : la communauté d’utilisateurs est-elle formée par ou forme-t-elle la communauté de commentaires ?

Nous allons donc repérer les communautés d’utilisateurs en fonction de l’importance de leur relation. La relation est ici mesurée par le fait de commenter des webtoons communs au sein d’un cluster de webtoon. La relation mesure donc, non pas nécessairement une interaction directe, mais une proximité de lectures commentées. Il s’agit d’une relation non orientée mais valuée (nombre de webtoons commentés en commun).

4.1 Construction du réseau user/user pour chaque cluster de Webtoon

#on définit ici les paramètres du modèle. On peut changer les paramètres sans avoir à réécrire tout le modèle.

chunk_size <- 20  # nombre de titres traités par "paquet" ;selon la RAM

max_users_per_titre <- Inf         # sert de garde-fou : au-delà, un titre génère trop de paires (quadratique). Ici on laisse Inf mais on pourrait mettre 500 par exemple. Cela excluerait 3 titres très populaires

min_titre_partage <- 3 # seuil : nb minimum de webtoons communs pour garder un lien user/user. Si on change le seuil, inutile de relancer tout le code. On peut repartir au 4.2

min_edges_analyse <- 10 #nb minimum de liens dans un cluster de webtoon pour executer une analyse des communautés d'utilisateurs

out_dir <- "//ad.univ-lille.fr/Personnels/Homedir1/4273/Documents/cours25_26/Master2 ENSP/textometrie/dossier_exam"

Nous allons commencer par établir une boucle sur les clusters de Webtoon afin d’obtenir les liens entre utilisateurs

for (cluster_cible in sort(unique(webtoon_cluster$cluster))) {

  ## titres du cluster
  titres_cluster <- as.data.table(webtoon_cluster)[cluster == cluster_cible, titre]

  ## user–titre restreint au cluster (binaire : un user compte 1 fois par titre)
  uw <- unique(as.data.table(comm)[titre %in% titres_cluster, .(user, titre)])

  ## nb d'users uniques par titre
  n_u <- uw[, .(n_users = uniqueN(user)), by = titre]

  ## filtrer les titres trop populaires (paramètre max_users_per_titre)
  titres_ok <- n_u[n_users <= max_users_per_titre, titre] #inutile si Inf dans les paramètres
  uw <- uw[titre %in% titres_ok]

  ## partition de la liste en chunk
  titres_ok <- sort(unique(uw$titre))
  chunks <- split(titres_ok, ceiling(seq_along(titres_ok) / chunk_size)) #découpe titres_ok en x chunks. l'objet chunks est donc une liste de vecteurs de titres. Chaque vecteur, sauf éventuellement le dernier, contient chunk_size titres

  ## BOUCLE : POUR CHAQUE chunk, GÉNÉRER LES PAIRES user/user ET LES COMPTER
  tmp_files <- character(length(chunks))

  for (c in seq_along(chunks)) {

    titres_chunk <- chunks[[c]]
    uwc <- uw[titre %in% titres_chunk]
    setkey(uwc, titre)  # clé sur titre pour accélérer la jointure

    # Self-join sur "titre" : pour chaque titre, on associe tous les users entre eux (on fait le produit cartésien par titre)
    pairs <- uwc[uwc, on = "titre", allow.cartesian = TRUE, nomatch = 0L] #allow.cartesian=TRUE car par défaut R ne le fera pas car ça produit beaucoup de lignes #nomatch=0L supprime les lignes sans correspondance. Puisque c'est un self-join ça ne devrait aps arriver.

    # On ne garde qu'une seule orientation (user < i.user) pour :
    # - éviter les doublons (A,B) et (B,A)
    # - éviter les auto-liens (A,A)
    # On garde aussi "titre" pour compter explicitement les titres en commun
    pairs <- pairs[user < i.user, .(titre, u1 = user, u2 = i.user)]
    
  #On a donc maintenant, pour chaque webtoon,toutes les paires possibles d’utilisateurs ayant commenté un même titre. 

    # On compte le nombre de TITRES DISTINCTS en commun DANS CE CHUNK
    edges_chunk <- pairs[, .(w = uniqueN(titre)), by = .(u1, u2)]

    # On écrit un fichier temporaire pour ce paquet (pour ne pas tout garder en RAM)
    tmp_path <- file.path(out_dir, sprintf("tmp_edges_cl%02d_chunk%03d.csv", cluster_cible, c)) #chemin de type (out_dir,"tmp_edges_cl06_chunk%002.csv)
    fwrite(edges_chunk, tmp_path) #sauvegarde edges_chunk sur le disque à tmp_path
    tmp_files[c] <- tmp_path # stocke le chemin dans le vecteur tmp_files à l'indice c

    cat("Chunk", c, "/", length(chunks), "écrit :", tmp_path, "\n")
  }

  ## agrégation des paquets
  tmp_files_ok <- tmp_files[file.exists(tmp_files)] #au cas où il n'y ait aucune paire dans un chunk et donc que le fichier ne soit pas créé

  edges_all <- rbindlist(lapply(tmp_files_ok, fread)) #on charge chaque tableau lapply(fread) et on les colle (rbindlist)
  edges_all <- edges_all[, .(w = sum(w)), by = .(u1, u2)]   # agrégation finale : somme des poids par paire

  final_path <- file.path(out_dir, sprintf("edges_user_user_cluster%02d.csv", cluster_cible))
  fwrite(edges_all, final_path)
  cat("Edge list finale écrite :", final_path, "\n")

  # (Optionnel) Nettoyer les fichiers temporaires avant le nouveau passage de la boucle
  file.remove(tmp_files_ok)
}

4.2 Caractéristiques des liens et des communautés pour chaque cluster

Il s’agit ici de faire un premier diagnostic de la structure des réseaux user/user pour chaque cluster de webtoons. En fonction des résultats de ce diagnostic on passera soit à l’étape suivante (production du résultat final) soit à un ajustement des paramètres du modèle, en particulier min_titre_partage et min_edges_analyse.

dossier <- out_dir
files <- list.files(dossier, pattern = "edges_user_user_cluster", full.names = TRUE)

for (f in files) {

  cat("\n============================\n")
  cat("Fichier:", basename(f), "\n")

  ELU <- fread(f)

  # filtre liens "solides"
  ELU2 <- ELU[w >= min_titre_partage]

  cat("Liens bruts:", nrow(ELU), " | Liens w>=", min_titre_partage, ":", nrow(ELU2), "\n")

  # s'il n'y pas assez de liens après filtre, on s'arrête là
  if (nrow(ELU2) < min_edges_analyse) {
    cat("Trop peu de liens après filtre -> cluster suivant\n")
    next
  }

  g <- graph_from_data_frame(ELU2, directed = FALSE)
  E(g)$weight <- ELU2$w

  comp <- components(g)
  cl <- cluster_louvain(g, weights = E(g)$weight)
  sz <- sizes(cl)

  cat("Noeuds:", gorder(g),
      "| Aretes:", gsize(g),
      "| Densite:", round(edge_density(g), 4),
      "| Composantes:", comp$no,
      "| Max comp:", max(comp$csize),
      "| Nb comms:", length(sz), "\n")

  cat("Top tailles:", paste(head(sort(sz, decreasing = TRUE), 10), collapse = ", "), "\n")
}

============================
Fichier: edges_user_user_cluster01.csv 
Liens bruts: 1157959  | Liens w>= 3 : 750 
Noeuds: 186 | Aretes: 750 | Densite: 0.0436 | Composantes: 1 | Max comp: 186 | Nb comms: 6 
Top tailles: 56, 55, 31, 26, 13, 5 

============================
Fichier: edges_user_user_cluster02.csv 
Liens bruts: 10606379  | Liens w>= 3 : 23708 
Noeuds: 1307 | Aretes: 23708 | Densite: 0.0278 | Composantes: 1 | Max comp: 1307 | Nb comms: 5 
Top tailles: 369, 353, 314, 162, 109 

============================
Fichier: edges_user_user_cluster03.csv 
Liens bruts: 14359751  | Liens w>= 3 : 22878 
Noeuds: 956 | Aretes: 22878 | Densite: 0.0501 | Composantes: 1 | Max comp: 956 | Nb comms: 6 
Top tailles: 188, 184, 180, 144, 139, 121 

============================
Fichier: edges_user_user_cluster04.csv 
Liens bruts: 66899793  | Liens w>= 3 : 114823 
Noeuds: 2009 | Aretes: 114823 | Densite: 0.0569 | Composantes: 1 | Max comp: 2009 | Nb comms: 5 
Top tailles: 553, 522, 517, 278, 139 

============================
Fichier: edges_user_user_cluster05.csv 
Liens bruts: 59217516  | Liens w>= 3 : 193617 
Noeuds: 3562 | Aretes: 193617 | Densite: 0.0305 | Composantes: 1 | Max comp: 3562 | Nb comms: 5 
Top tailles: 1173, 907, 682, 508, 292 

============================
Fichier: edges_user_user_cluster06.csv 
Liens bruts: 6101314  | Liens w>= 3 : 11825 
Noeuds: 671 | Aretes: 11825 | Densite: 0.0526 | Composantes: 1 | Max comp: 671 | Nb comms: 5 
Top tailles: 259, 194, 94, 86, 38 

============================
Fichier: edges_user_user_cluster07.csv 
Liens bruts: 16428106  | Liens w>= 3 : 29174 
Noeuds: 875 | Aretes: 29174 | Densite: 0.0763 | Composantes: 1 | Max comp: 875 | Nb comms: 5 
Top tailles: 287, 245, 170, 102, 71 

Deux seuils ont été testés pour définir les liens entre utilisateurs : au moins deux titres partagés (w≥2) et au moins trois titres partagés (w≥3). Dans les deux cas, les réseaux restent connectés (une composante principale unique), ce qui indique l’existence d’un noyau de publics communs au sein de chaque cluster. Le seuil w≥3 réduit fortement le nombre de liens et d’utilisateurs retenus (par exemple dans le cluster 7 le passage du minimum de titres partagés de 2 à 3 fait passer le nombre de noeuds de 2620 à 875), conduisant à une analyse centrée sur un noyau dur d’utilisateurs commentant les mêmes titres. w≥2 permet une couverture plus large des publics. Autrement dit avec w≥2 on établit une cartographie générale des communautés tandis qu’avec w≥3 on peut effectuer une analyse d’un noyeau dur, de communautés “fortes”.

Nous utiliserons les communautés “fortes” (w≥3) pour mener des comparaisons de discours.

4.3 Communautés d’utilisateurs par clusters (Louvain)

Lorsque le diagnostic indique des structures de réseaux jugées suffisamment robustes et interprétables, les partitions communautaires sont produites et conservées pour les analyses ultérieures. C’est ce que nous faisons dans cette section.

for (f in files) {

  ELU <- fread(f)
  ELU2 <- ELU[w >= min_titre_partage]

  # g si vide, on saute pour éviter un bug
  if (nrow(ELU2) < 1) {
    cat("NEXT (aucun lien solide) :", basename(f), "\n")
    next
  }

  g <- graph_from_data_frame(ELU2, directed = FALSE)
  if (gsize(g) < 1) {
    cat("NEXT (graphe sans arêtes) :", basename(f), "\n")
    next
  }

  E(g)$weight <- ELU2$w
  cl <- cluster_louvain(g, weights = E(g)$weight)

  # table user avec communauté de users
  memb <- data.table(
    user = names(membership(cl)),
    commu_user = as.integer(membership(cl)))

  # nom de sortie (on garde le numéro du cluster dans le nom)
  out <- sub("edges_user_user_", "users_commu_user_", f)
out <- sub("\\.csv$", paste0("_wge", min_titre_partage, ".csv"), out) #on aurait pu faire un sprintf mais il aurait fallu extraire le numéro de cluster avant, cela aurait été plus compliqué
  fwrite(memb, out)

  cat("Ecrit:", basename(out), "| lignes =", nrow(memb), "\n")
}
Ecrit: users_commu_user_cluster01_wge3.csv | lignes = 186 
Ecrit: users_commu_user_cluster02_wge3.csv | lignes = 1307 
Ecrit: users_commu_user_cluster03_wge3.csv | lignes = 956 
Ecrit: users_commu_user_cluster04_wge3.csv | lignes = 2009 
Ecrit: users_commu_user_cluster05_wge3.csv | lignes = 3562 
Ecrit: users_commu_user_cluster06_wge3.csv | lignes = 671 
Ecrit: users_commu_user_cluster07_wge3.csv | lignes = 875