Files
graphes-l3/Seance_5_TP_Contamination.ipynb
T

23 KiB

Séance 5 — TP : réseau d'affiliation et chaîne de contamination

Contexte

Dans la première partie de la séance, vous avez travaillé sur le réseau d'ami·es d'Alice et Bob : un réseau où les liens sont directement observés (« Alice connaît Bob »).

Mais en sociologie, on dispose très rarement d'un tel réseau. Ce qu'on observe concrètement, ce sont des participations à des événements : qui était à quelle réunion, qui siège dans quel conseil d'administration, qui a signé quelle pétition, qui était à quel dîner.

Ce type de données forme un réseau d'affiliation (ou réseau biparti) : deux types de nœuds — des personnes et des événements — et des liens qui vont toujours d'une personne vers un événement, jamais de personne à personne.

C'est une structure fondatrice de l'analyse de réseaux. L'étude qui l'a rendue célèbre est celle de Davis, Gardner et Gardner (Deep South, 1941), qui reconstituait la structure sociale d'une petite ville du sud des États-Unis à partir de la seule présence de 18 femmes à 14 événements mondains, relevée dans la presse locale.

Le cas d'étude

Nous reprenons ce principe sur un cas fictif : la commune de Dudley, organisée autour d'une usine agroalimentaire. Plusieurs habitants sont tombés malades d'une maladie transmise par l'alimentation. On ne sait pas qui a contaminé qui — mais on dispose du registre des repas communautaires et de la liste de leurs participants.

Objectif : reconstituer la structure sociale à partir des seules participations, puis identifier le groupe exposé.

Objectifs pédagogiques

  • Construire et lire un réseau d'affiliation (graphe biparti)
  • Comprendre ce qu'est la projection d'un réseau biparti en réseau unimode, et ce qu'elle fait gagner et perdre
  • Réinvestir les composantes connexes et le parcours en largeur vus en cours
  • Interpréter sociologiquement une distance dans un réseau

1. Les données

Nous disposons de deux listes et d'un registre.

Les personnes (habitants de Dudley et de la commune voisine de Millhaven) :

Personne Commune État de santé
Paula, Ray, Bonnie, Jess, Sam, Nancy Dudley malades
Walter, Curtis, Béatrice, Wendell Dudley en bonne santé
Doris, Martha, George Dudley en bonne santé
Marjorie, Harold, Irene Millhaven en bonne santé
Agnès Millhaven en bonne santé

Les repas enregistrés :

Repas Date Lieu
R1988 17 septembre 1988 Ferme Chaco (Dudley)
R1989 24 juin 1989 Salle des fêtes (Dudley)
R1990 15 septembre 1990 Ferme Chaco (Dudley)
R1992 24 décembre 1992 Église (Dudley)
R1995 11 mars 1995 Ferme Chaco (Dudley)
M1991 8 mai 1991 Foyer rural (Millhaven)

Le registre des participations est donné dans la cellule suivante.

In [ ]:
import networkx as nx
import matplotlib.pyplot as plt

# --- Les personnes -----------------------------------------------------
personnes = [
    "Paula", "Ray", "Bonnie", "Jess", "Sam", "Nancy",      # malades
    "Walter", "Curtis", "Beatrice", "Wendell",
    "Doris", "Martha", "George",
    "Marjorie", "Harold", "Irene", "Agnes",
]

malades = ["Paula", "Ray", "Bonnie", "Jess", "Sam", "Nancy"]

# --- Les repas ---------------------------------------------------------
repas = ["R1988", "R1989", "R1990", "R1992", "R1995", "M1991"]

# --- Le registre des participations ------------------------------------
# Chaque couple (personne, repas) signifie : cette personne etait a ce repas.
participations = [
    ("Paula", "R1988"), ("Ray", "R1988"), ("Bonnie", "R1988"),
    ("Jess", "R1988"), ("Sam", "R1988"), ("Nancy", "R1988"),
    ("Walter", "R1988"), ("Doris", "R1988"), ("Martha", "R1988"),
    ("Curtis", "R1988"), ("Wendell", "R1988"),

    ("Paula", "R1989"), ("Ray", "R1989"), ("Bonnie", "R1989"),
    ("Jess", "R1989"), ("Walter", "R1989"), ("Curtis", "R1989"),
    ("Beatrice", "R1989"), ("Wendell", "R1989"),

    ("Paula", "R1990"), ("Ray", "R1990"), ("Sam", "R1990"),
    ("Nancy", "R1990"), ("Walter", "R1990"), ("Curtis", "R1990"),
    ("Wendell", "R1990"), ("George", "R1990"),

    ("Ray", "R1992"), ("Bonnie", "R1992"), ("Jess", "R1992"),
    ("Sam", "R1992"), ("Nancy", "R1992"), ("Beatrice", "R1992"),
    ("Wendell", "R1992"),

    ("Walter", "R1995"), ("Curtis", "R1995"),
    ("Beatrice", "R1995"), ("Wendell", "R1995"),

    ("Marjorie", "M1991"), ("Harold", "M1991"), ("Irene", "M1991"),
]

print("Nombre de personnes    :", len(personnes))
print("Nombre de repas        :", len(repas))
print("Nombre de participations :", len(participations))

2. Construire le réseau d'affiliation

On construit un graphe qui contient les deux types de nœuds à la fois : les personnes et les repas. On marque chaque nœud avec un attribut bipartite (0 pour les personnes, 1 pour les repas) afin de pouvoir les distinguer ensuite.

Attention à Agnès : elle n'apparaît dans aucune participation. Il faut donc l'ajouter explicitement au graphe, sinon elle n'existerait tout simplement pas.

In [ ]:
B = nx.Graph()

# On declare les deux "cotes" du reseau
B.add_nodes_from(personnes, bipartite=0)   # cote personnes
B.add_nodes_from(repas, bipartite=1)       # cote repas

# Puis les liens de participation
B.add_edges_from(participations)

print("Le graphe est-il biparti ?", nx.is_bipartite(B))
print("Nombre total de noeuds  :", B.number_of_nodes())
print("Nombre de liens         :", B.number_of_edges())

Visualisation

On place les personnes sur une colonne et les repas sur l'autre. Ce type de représentation s'appelle un diagramme en deux modes.

In [ ]:
# Positions : personnes a gauche, repas a droite
pos = {}
for i, p in enumerate(personnes):
    pos[p] = (0, -i)
for j, r in enumerate(repas):
    pos[r] = (2, -j * (len(personnes) - 1) / (len(repas) - 1))

plt.figure(figsize=(9, 8))
nx.draw_networkx_edges(B, pos, alpha=0.35)
nx.draw_networkx_nodes(B, pos, nodelist=personnes,
                       node_color="lightblue", node_size=900)
nx.draw_networkx_nodes(B, pos, nodelist=repas,
                       node_color="lightsalmon", node_shape="s", node_size=1200)
nx.draw_networkx_labels(B, pos, font_size=9)
plt.title("Reseau d'affiliation : personnes (bleu) x repas (orange)")
plt.axis("off")
plt.show()

Questions

  1. Agnès apparaît-elle sur le graphique ? À quoi la reconnaît-on ?
  2. Quel repas rassemble le plus de monde ? Lisez-le directement sur la figure.
  3. Dans ce graphe, deux personnes peuvent-elles être reliées directement par un lien ? Pourquoi ?
  4. Que représenterait, sociologiquement, une personne reliée à un seul repas ?

3. Passer en réseau unimode : la projection

Le réseau d'affiliation n'est pas directement exploitable avec les outils vus en cours (composantes connexes, BFS, centralité), parce qu'il mélange deux types de nœuds.

On le transforme donc en un réseau qui ne contient plus que des personnes, selon une règle simple :

Deux personnes sont reliées si elles ont participé à au moins un repas commun.

C'est ce qu'on appelle la projection du réseau biparti sur le mode « personnes ». NetworkX la calcule pour nous — la cellule suivante est fournie, vous n'avez pas à l'écrire.

In [ ]:
from networkx.algorithms import bipartite

# --- CELLULE FOURNIE ---------------------------------------------------
# projected_graph(B, noeuds) construit un nouveau graphe dont les noeuds sont
# ceux passes en second argument, et ou deux noeuds sont relies s'ils avaient
# au moins un voisin commun dans le graphe biparti B.
# Ici : deux personnes sont reliees si elles ont partage au moins un repas.
G = bipartite.projected_graph(B, personnes)
# -----------------------------------------------------------------------

print("Reseau des personnes")
print("  noeuds :", G.number_of_nodes())
print("  liens  :", G.number_of_edges())
In [ ]:
couleurs = ["lightcoral" if p in malades else "lightblue" for p in G.nodes()]

plt.figure(figsize=(9, 7))
pos_g = nx.spring_layout(G, seed=4)
nx.draw(G, pos_g, with_labels=True, node_color=couleurs,
        node_size=1100, font_size=9, edge_color="gray")
plt.title("Reseau unimode : 'a partage au moins un repas avec'\n(rouge = malade)")
plt.show()

Questions

  1. Le réseau biparti comptait 41 liens. Combien le réseau projeté en compte-t-il ? Le nombre a-t-il augmenté ou diminué ? Proposez une explication.
  2. Repérez sur la figure un groupe de personnes toutes reliées entre elles. À quoi correspond ce groupe dans les données de départ ?

Ce que la projection fait perdre

La projection est commode, mais elle n'est pas neutre. Deux remarques importantes :

  • Chaque repas devient une clique. Si dix personnes assistent au même repas, la projection crée un lien entre chacune des paires possibles, soit 45 liens. Le réseau projeté paraît donc beaucoup plus dense que la réalité sociale : il affirme que dix personnes présentes dans la même salle se « connaissent » toutes.
  • On ne sait plus pourquoi deux personnes sont reliées. Le lien Paula–Ray ne dit pas s'il vient d'un seul repas ou de quatre.

C'est un compromis classique en analyse de réseaux : on simplifie pour pouvoir appliquer les outils, en acceptant de déformer un peu l'objet. L'important est de savoir dans quel sens il est déformé.

4. Composantes connexes : qui pouvait être exposé ?

Une composante connexe est un sous-ensemble du réseau où chacun peut atteindre chacun par une suite de liens, sans aucun lien vers le reste du réseau.

Ici, l'interprétation est directe : une contamination transmise lors des repas ne peut pas franchir la frontière entre deux composantes.

In [ ]:
composantes = list(nx.connected_components(G))

print("Nombre de composantes connexes :", len(composantes))
print()
for i, comp in enumerate(sorted(composantes, key=len, reverse=True), start=1):
    nb_malades = len(set(comp) & set(malades))
    print(f"Composante {i} ({len(comp)} personnes, {nb_malades} malade(s)) :")
    print("   ", sorted(comp))
    print()

Questions

  1. Combien de composantes obtient-on ? Décrivez-les en une phrase chacune.
  2. Les malades sont-ils tous dans la même composante ? Était-ce prévisible ?
  3. Agnès peut-elle avoir été contaminée lors d'un repas ? Justifiez à partir de la structure du réseau, sans invoquer d'autre information.
  4. Une personne appartient à la même composante que les malades, mais n'est pas malade. Que peut-on en conclure — et surtout, que ne peut-on pas en conclure ?

5. Parcours en largeur : à quelle distance sociale du cas index ?

Le premier cas identifié — le cas index — est Paula.

On reprend le parcours en largeur (BFS) de la première partie de la séance, mais appliqué au réseau biparti cette fois. L'intérêt : les distances y sont interprétables très directement.

  • distance 1 depuis Paula → un repas auquel Paula a participé
  • distance 2 → une personne ayant partagé un repas avec Paula
  • distance 3 → un repas fréquenté par ces personnes, mais pas par Paula
  • distance 4 → des personnes atteignables en deux « sauts » de repas, et ainsi de suite

Les distances paires désignent donc toujours des personnes, les distances impaires toujours des repas. C'est une propriété générale des graphes bipartis — et une bonne façon de vérifier qu'on a bien construit son réseau.

In [ ]:
cas_index = "Paula"
distances = nx.single_source_shortest_path_length(B, cas_index)

print(f"Distances depuis le cas index ({cas_index}) dans le reseau d'affiliation")
print()
for d in sorted(set(distances.values())):
    noms = sorted(n for n, dist in distances.items() if dist == d)
    print(f"  distance {d} : {noms}")

print()
non_atteints = sorted(set(B.nodes()) - set(distances))
print("Noeuds non atteints :", non_atteints if non_atteints else "aucun")
In [ ]:
# Meme chose sur le reseau unimode, pour comparaison
dist_uni = nx.single_source_shortest_path_length(G, cas_index)

print(f"Distances depuis {cas_index} dans le reseau unimode")
print()
for personne in sorted(dist_uni, key=lambda p: (dist_uni[p], p)):
    marque = "  (malade)" if personne in malades else ""
    print(f"  {personne:<10} : {dist_uni[personne]}{marque}")

Questions

  1. Dans le réseau biparti, quelles personnes sont à distance 2 de Paula ? Que représente concrètement cet ensemble ?
  2. Quels nœuds ne sont pas atteints du tout ? Reliez cette observation à la question 9.
  3. La distance maximale s'arrête ici à 3, alors que le principe énoncé plus haut autoriserait 4, 5, etc. Qu'est-ce que cela dit de la taille du réseau ?

Le résultat le plus intéressant est celui de la seconde cellule. Prenez le temps de le regarder avant de répondre.

  1. Dans le réseau unimode, à quelle distance de Paula se trouve chaque personne de sa composante ? Le résultat est-il celui que vous attendiez ?

  2. Ce résultat n'est pas une erreur : c'est la conséquence directe de ce qui a été dit au point 3.

    • La composante de Paula compte 13 personnes, soit 13 × 12 / 2 = 78 paires possibles. Le réseau projeté compte 74 liens au total, dont 71 à l'intérieur de cette composante (les 3 autres forment le triangle de Millhaven). Quelle proportion des paires possibles est donc effectivement reliée ?
    • Paula a participé à 3 des 5 repas de Dudley. En quoi cela explique-t-il qu'elle soit à distance 1 de presque tout le monde ?
    • Un réseau où tout le monde est à distance 1 de tout le monde ne permet de distinguer personne. Quelles mesures vues en cours (centralité, distance moyenne, coefficient de clustering) deviennent inutilisables sur un tel réseau ?
    • Le réseau biparti, lui, distinguait encore des niveaux. Que faut-il en conclure sur l'ordre dans lequel mener une analyse : projeter d'abord, ou explorer le biparti d'abord ?

6. Le même outil, deux objets sociologiques

Vous venez de faire de l'épidémiologie. Mais l'algorithme utilisé — le parcours en largeur sur un réseau d'affiliation — est exactement celui qu'on emploie pour étudier la diffusion d'une information, d'une pratique ou d'une innovation.

L'étude fondatrice est celle de Coleman, Katz et Menzel (1957) sur l'adoption d'un nouvel antibiotique par des médecins de l'Illinois. Leur résultat : la date à laquelle un médecin se met à prescrire le médicament s'explique bien mieux par sa position dans le réseau professionnel que par son âge, sa formation ou le volume de sa patientèle.

Autrement dit : ce qui circule dans un réseau — un virus, une rumeur, une pratique professionnelle, une candidature — suit les mêmes chemins et obéit aux mêmes contraintes structurelles. C'est ce qui fait la portée générale de ces outils, et c'est aussi ce qui invite à la prudence : le même modèle décrit des phénomènes dont les mécanismes réels n'ont rien à voir entre eux.

Question de synthèse (à rédiger, 10 à 15 lignes)

  1. On vous confie un fichier recensant les participations de 400 salarié·es aux réunions internes d'une entreprise sur deux ans. On vous demande d'identifier « les personnes influentes ».

    • Quelles étapes de ce TP reprendriez-vous, et dans quel ordre ?
    • Quelles limites signaleriez-vous dans votre rapport ?
    • En quoi le fait que la projection transforme chaque réunion en clique peut-il fausser une mesure d'influence ?

7. Pour aller plus loin (facultatif)

  1. La fonction bipartite.weighted_projected_graph(B, personnes) construit la même projection, mais en pondérant chaque lien par le nombre de repas partagés. Construisez-la et affichez les cinq liens les plus lourds. En quoi cette version corrige-t-elle en partie le défaut signalé au point 3 ?

  2. Un point d'articulation est un nœud dont le retrait fragmente le réseau en plusieurs composantes. NetworkX les liste avec list(nx.articulation_points(...)).

    • Appliquez la fonction au réseau projeté G. La liste est vide. Au vu de ce que vous avez découvert à la question 15, pouviez-vous le prévoir ?
    • Appliquez-la maintenant au réseau biparti B. On en trouve trois. Lesquels ? De quel type sont ces nœuds ?
    • Retirez R1990 de B (B.copy() d'abord, pour ne pas abîmer le réseau) et regardez qui se retrouve isolé. Même chose avec R1988.
    • Concluez : en projetant, qu'est-ce qu'on a rendu invisible ?
  3. Reprenez la projection sur l'autre mode : bipartite.projected_graph(B, repas). Deux repas y sont reliés s'ils ont au moins un participant commun. Qu'est-ce que ce réseau donne à voir que le précédent ne montrait pas ?

8. Synthèse

  • Un réseau d'affiliation relie des personnes à des événements. C'est la forme sous laquelle les données de réseau se présentent le plus souvent en sociologie, parce que les participations sont observables alors que les relations ne le sont pas.
  • La projection transforme ce réseau à deux modes en un réseau de personnes, exploitable avec les outils habituels. Elle a un coût : chaque événement devient une clique, et le réseau paraît plus dense qu'il ne l'est.
  • Les composantes connexes délimitent ce qui peut circuler et jusqu'où. Rien ne franchit la frontière entre deux composantes.
  • Le parcours en largeur mesure une distance ; dans un réseau biparti, les distances paires désignent des personnes et les impaires des événements.
  • Un même outil formel sert à décrire une contagion, une rumeur ou l'adoption d'une pratique professionnelle. La structure est commune ; les mécanismes ne le sont pas.

Auteur : Florian Mathieu — L3 Sociologie quantitative, Algorithmes des graphes

Licence CC BY-NC-SA 4.0