{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Séance 5 — TP : réseau d'affiliation et chaîne de contamination\n", "\n", "## Contexte\n", "\n", "Dans la première partie de la séance, vous avez travaillé sur le réseau d'ami·es\n", "d'Alice et Bob : un réseau où **les liens sont directement observés** (« Alice connaît Bob »).\n", "\n", "Mais en sociologie, on dispose très rarement d'un tel réseau. Ce qu'on observe\n", "concrètement, ce sont des **participations à des événements** : qui était à quelle\n", "réunion, qui siège dans quel conseil d'administration, qui a signé quelle pétition,\n", "qui était à quel dîner.\n", "\n", "Ce type de données forme un **réseau d'affiliation** (ou réseau *biparti*) : deux\n", "types de nœuds — des **personnes** et des **événements** — et des liens qui vont\n", "toujours d'une personne vers un événement, jamais de personne à personne.\n", "\n", "C'est une structure fondatrice de l'analyse de réseaux. L'étude qui l'a rendue\n", "célèbre est celle de Davis, Gardner et Gardner (*Deep South*, 1941), qui reconstituait\n", "la structure sociale d'une petite ville du sud des États-Unis à partir de la seule\n", "présence de 18 femmes à 14 événements mondains, relevée dans la presse locale.\n", "\n", "## Le cas d'étude\n", "\n", "Nous reprenons ce principe sur un cas fictif : la commune de **Dudley**, organisée\n", "autour d'une usine agroalimentaire. Plusieurs habitants sont tombés malades d'une\n", "maladie transmise par l'alimentation. On ne sait pas qui a contaminé qui — mais on\n", "dispose du registre des **repas communautaires** et de la liste de leurs participants.\n", "\n", "**Objectif : reconstituer la structure sociale à partir des seules participations,\n", "puis identifier le groupe exposé.**\n", "\n", "## Objectifs pédagogiques\n", "\n", "- Construire et lire un **réseau d'affiliation** (graphe biparti)\n", "- Comprendre ce qu'est la **projection** d'un réseau biparti en réseau unimode,\n", " et ce qu'elle fait gagner et perdre\n", "- Réinvestir les **composantes connexes** et le **parcours en largeur** vus en cours\n", "- Interpréter sociologiquement une distance dans un réseau\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Les données\n", "\n", "Nous disposons de deux listes et d'un registre.\n", "\n", "**Les personnes** (habitants de Dudley et de la commune voisine de Millhaven) :\n", "\n", "| Personne | Commune | État de santé |\n", "|---|---|---|\n", "| Paula, Ray, Bonnie, Jess, Sam, Nancy | Dudley | **malades** |\n", "| Walter, Curtis, Béatrice, Wendell | Dudley | en bonne santé |\n", "| Doris, Martha, George | Dudley | en bonne santé |\n", "| Marjorie, Harold, Irene | Millhaven | en bonne santé |\n", "| Agnès | Millhaven | en bonne santé |\n", "\n", "**Les repas** enregistrés :\n", "\n", "| Repas | Date | Lieu |\n", "|---|---|---|\n", "| `R1988` | 17 septembre 1988 | Ferme Chaco (Dudley) |\n", "| `R1989` | 24 juin 1989 | Salle des fêtes (Dudley) |\n", "| `R1990` | 15 septembre 1990 | Ferme Chaco (Dudley) |\n", "| `R1992` | 24 décembre 1992 | Église (Dudley) |\n", "| `R1995` | 11 mars 1995 | Ferme Chaco (Dudley) |\n", "| `M1991` | 8 mai 1991 | Foyer rural (Millhaven) |\n", "\n", "Le registre des participations est donné dans la cellule suivante.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import networkx as nx\n", "import matplotlib.pyplot as plt\n", "\n", "# --- Les personnes -----------------------------------------------------\n", "personnes = [\n", " \"Paula\", \"Ray\", \"Bonnie\", \"Jess\", \"Sam\", \"Nancy\", # malades\n", " \"Walter\", \"Curtis\", \"Beatrice\", \"Wendell\",\n", " \"Doris\", \"Martha\", \"George\",\n", " \"Marjorie\", \"Harold\", \"Irene\", \"Agnes\",\n", "]\n", "\n", "malades = [\"Paula\", \"Ray\", \"Bonnie\", \"Jess\", \"Sam\", \"Nancy\"]\n", "\n", "# --- Les repas ---------------------------------------------------------\n", "repas = [\"R1988\", \"R1989\", \"R1990\", \"R1992\", \"R1995\", \"M1991\"]\n", "\n", "# --- Le registre des participations ------------------------------------\n", "# Chaque couple (personne, repas) signifie : cette personne etait a ce repas.\n", "participations = [\n", " (\"Paula\", \"R1988\"), (\"Ray\", \"R1988\"), (\"Bonnie\", \"R1988\"),\n", " (\"Jess\", \"R1988\"), (\"Sam\", \"R1988\"), (\"Nancy\", \"R1988\"),\n", " (\"Walter\", \"R1988\"), (\"Doris\", \"R1988\"), (\"Martha\", \"R1988\"),\n", " (\"Curtis\", \"R1988\"), (\"Wendell\", \"R1988\"),\n", "\n", " (\"Paula\", \"R1989\"), (\"Ray\", \"R1989\"), (\"Bonnie\", \"R1989\"),\n", " (\"Jess\", \"R1989\"), (\"Walter\", \"R1989\"), (\"Curtis\", \"R1989\"),\n", " (\"Beatrice\", \"R1989\"), (\"Wendell\", \"R1989\"),\n", "\n", " (\"Paula\", \"R1990\"), (\"Ray\", \"R1990\"), (\"Sam\", \"R1990\"),\n", " (\"Nancy\", \"R1990\"), (\"Walter\", \"R1990\"), (\"Curtis\", \"R1990\"),\n", " (\"Wendell\", \"R1990\"), (\"George\", \"R1990\"),\n", "\n", " (\"Ray\", \"R1992\"), (\"Bonnie\", \"R1992\"), (\"Jess\", \"R1992\"),\n", " (\"Sam\", \"R1992\"), (\"Nancy\", \"R1992\"), (\"Beatrice\", \"R1992\"),\n", " (\"Wendell\", \"R1992\"),\n", "\n", " (\"Walter\", \"R1995\"), (\"Curtis\", \"R1995\"),\n", " (\"Beatrice\", \"R1995\"), (\"Wendell\", \"R1995\"),\n", "\n", " (\"Marjorie\", \"M1991\"), (\"Harold\", \"M1991\"), (\"Irene\", \"M1991\"),\n", "]\n", "\n", "print(\"Nombre de personnes :\", len(personnes))\n", "print(\"Nombre de repas :\", len(repas))\n", "print(\"Nombre de participations :\", len(participations))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Construire le réseau d'affiliation\n", "\n", "On construit un graphe qui contient **les deux types de nœuds à la fois** : les\n", "personnes et les repas. On marque chaque nœud avec un attribut `bipartite`\n", "(`0` pour les personnes, `1` pour les repas) afin de pouvoir les distinguer ensuite.\n", "\n", "Attention à Agnès : elle n'apparaît dans aucune participation. Il faut donc l'ajouter\n", "explicitement au graphe, sinon elle n'existerait tout simplement pas.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "B = nx.Graph()\n", "\n", "# On declare les deux \"cotes\" du reseau\n", "B.add_nodes_from(personnes, bipartite=0) # cote personnes\n", "B.add_nodes_from(repas, bipartite=1) # cote repas\n", "\n", "# Puis les liens de participation\n", "B.add_edges_from(participations)\n", "\n", "print(\"Le graphe est-il biparti ?\", nx.is_bipartite(B))\n", "print(\"Nombre total de noeuds :\", B.number_of_nodes())\n", "print(\"Nombre de liens :\", B.number_of_edges())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Visualisation\n", "\n", "On place les personnes sur une colonne et les repas sur l'autre. Ce type de\n", "représentation s'appelle un **diagramme en deux modes**.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Positions : personnes a gauche, repas a droite\n", "pos = {}\n", "for i, p in enumerate(personnes):\n", " pos[p] = (0, -i)\n", "for j, r in enumerate(repas):\n", " pos[r] = (2, -j * (len(personnes) - 1) / (len(repas) - 1))\n", "\n", "plt.figure(figsize=(9, 8))\n", "nx.draw_networkx_edges(B, pos, alpha=0.35)\n", "nx.draw_networkx_nodes(B, pos, nodelist=personnes,\n", " node_color=\"lightblue\", node_size=900)\n", "nx.draw_networkx_nodes(B, pos, nodelist=repas,\n", " node_color=\"lightsalmon\", node_shape=\"s\", node_size=1200)\n", "nx.draw_networkx_labels(B, pos, font_size=9)\n", "plt.title(\"Reseau d'affiliation : personnes (bleu) x repas (orange)\")\n", "plt.axis(\"off\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Questions**\n", "\n", "1. Agnès apparaît-elle sur le graphique ? À quoi la reconnaît-on ?\n", "2. Quel repas rassemble le plus de monde ? Lisez-le directement sur la figure.\n", "3. Dans ce graphe, deux personnes peuvent-elles être reliées **directement** par un\n", " lien ? Pourquoi ?\n", "4. Que représenterait, sociologiquement, une personne reliée à un seul repas ?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Passer en réseau unimode : la projection\n", "\n", "Le réseau d'affiliation n'est pas directement exploitable avec les outils vus en cours\n", "(composantes connexes, BFS, centralité), parce qu'il mélange deux types de nœuds.\n", "\n", "On le transforme donc en un réseau qui ne contient plus que des personnes, selon une\n", "règle simple :\n", "\n", "> **Deux personnes sont reliées si elles ont participé à au moins un repas commun.**\n", "\n", "C'est ce qu'on appelle la **projection** du réseau biparti sur le mode « personnes ».\n", "NetworkX la calcule pour nous — la cellule suivante est fournie, vous n'avez pas à\n", "l'écrire.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from networkx.algorithms import bipartite\n", "\n", "# --- CELLULE FOURNIE ---------------------------------------------------\n", "# projected_graph(B, noeuds) construit un nouveau graphe dont les noeuds sont\n", "# ceux passes en second argument, et ou deux noeuds sont relies s'ils avaient\n", "# au moins un voisin commun dans le graphe biparti B.\n", "# Ici : deux personnes sont reliees si elles ont partage au moins un repas.\n", "G = bipartite.projected_graph(B, personnes)\n", "# -----------------------------------------------------------------------\n", "\n", "print(\"Reseau des personnes\")\n", "print(\" noeuds :\", G.number_of_nodes())\n", "print(\" liens :\", G.number_of_edges())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "couleurs = [\"lightcoral\" if p in malades else \"lightblue\" for p in G.nodes()]\n", "\n", "plt.figure(figsize=(9, 7))\n", "pos_g = nx.spring_layout(G, seed=4)\n", "nx.draw(G, pos_g, with_labels=True, node_color=couleurs,\n", " node_size=1100, font_size=9, edge_color=\"gray\")\n", "plt.title(\"Reseau unimode : 'a partage au moins un repas avec'\\n(rouge = malade)\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Questions**\n", "\n", "5. Le réseau biparti comptait 41 liens. Combien le réseau projeté en compte-t-il ?\n", " Le nombre a-t-il augmenté ou diminué ? Proposez une explication.\n", "6. Repérez sur la figure un groupe de personnes toutes reliées entre elles.\n", " À quoi correspond ce groupe dans les données de départ ?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ce que la projection fait perdre\n", "\n", "La projection est commode, mais elle n'est pas neutre. Deux remarques importantes :\n", "\n", "- **Chaque repas devient une clique.** Si dix personnes assistent au même repas,\n", " la projection crée un lien entre *chacune* des paires possibles, soit 45 liens.\n", " Le réseau projeté paraît donc beaucoup plus dense que la réalité sociale : il\n", " affirme que dix personnes présentes dans la même salle se « connaissent » toutes.\n", "- **On ne sait plus *pourquoi* deux personnes sont reliées.** Le lien Paula–Ray ne\n", " dit pas s'il vient d'un seul repas ou de quatre.\n", "\n", "C'est un compromis classique en analyse de réseaux : on simplifie pour pouvoir\n", "appliquer les outils, en acceptant de déformer un peu l'objet. L'important est de\n", "**savoir dans quel sens il est déformé**.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Composantes connexes : qui pouvait être exposé ?\n", "\n", "Une **composante connexe** est un sous-ensemble du réseau où chacun peut atteindre\n", "chacun par une suite de liens, sans aucun lien vers le reste du réseau.\n", "\n", "Ici, l'interprétation est directe : une contamination transmise lors des repas ne peut\n", "**pas** franchir la frontière entre deux composantes.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "composantes = list(nx.connected_components(G))\n", "\n", "print(\"Nombre de composantes connexes :\", len(composantes))\n", "print()\n", "for i, comp in enumerate(sorted(composantes, key=len, reverse=True), start=1):\n", " nb_malades = len(set(comp) & set(malades))\n", " print(f\"Composante {i} ({len(comp)} personnes, {nb_malades} malade(s)) :\")\n", " print(\" \", sorted(comp))\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Questions**\n", "\n", "7. Combien de composantes obtient-on ? Décrivez-les en une phrase chacune.\n", "8. Les malades sont-ils tous dans la même composante ? Était-ce prévisible ?\n", "9. Agnès peut-elle avoir été contaminée lors d'un repas ? Justifiez à partir de la\n", " structure du réseau, sans invoquer d'autre information.\n", "10. Une personne appartient à la même composante que les malades, mais n'est pas\n", " malade. Que peut-on en conclure — et surtout, que ne peut-on **pas** en conclure ?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Parcours en largeur : à quelle distance sociale du cas index ?\n", "\n", "Le premier cas identifié — le **cas index** — est Paula.\n", "\n", "On reprend le parcours en largeur (BFS) de la première partie de la séance, mais\n", "appliqué au **réseau biparti** cette fois. L'intérêt : les distances y sont\n", "interprétables très directement.\n", "\n", "- distance **1** depuis Paula → un **repas** auquel Paula a participé\n", "- distance **2** → une **personne** ayant partagé un repas avec Paula\n", "- distance **3** → un **repas** fréquenté par ces personnes, mais pas par Paula\n", "- distance **4** → des personnes atteignables en deux « sauts » de repas, et ainsi de suite\n", "\n", "Les distances paires désignent donc toujours des personnes, les distances impaires\n", "toujours des repas. C'est une propriété générale des graphes bipartis — et une bonne\n", "façon de vérifier qu'on a bien construit son réseau.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cas_index = \"Paula\"\n", "distances = nx.single_source_shortest_path_length(B, cas_index)\n", "\n", "print(f\"Distances depuis le cas index ({cas_index}) dans le reseau d'affiliation\")\n", "print()\n", "for d in sorted(set(distances.values())):\n", " noms = sorted(n for n, dist in distances.items() if dist == d)\n", " print(f\" distance {d} : {noms}\")\n", "\n", "print()\n", "non_atteints = sorted(set(B.nodes()) - set(distances))\n", "print(\"Noeuds non atteints :\", non_atteints if non_atteints else \"aucun\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Meme chose sur le reseau unimode, pour comparaison\n", "dist_uni = nx.single_source_shortest_path_length(G, cas_index)\n", "\n", "print(f\"Distances depuis {cas_index} dans le reseau unimode\")\n", "print()\n", "for personne in sorted(dist_uni, key=lambda p: (dist_uni[p], p)):\n", " marque = \" (malade)\" if personne in malades else \"\"\n", " print(f\" {personne:<10} : {dist_uni[personne]}{marque}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Questions**\n", "\n", "11. Dans le réseau biparti, quelles personnes sont à distance 2 de Paula ?\n", " Que représente concrètement cet ensemble ?\n", "12. Quels nœuds ne sont pas atteints du tout ? Reliez cette observation à la\n", " question 9.\n", "13. La distance maximale s'arrête ici à 3, alors que le principe énoncé plus haut\n", " autoriserait 4, 5, etc. Qu'est-ce que cela dit de la taille du réseau ?\n", "\n", "**Le résultat le plus intéressant est celui de la seconde cellule. Prenez le temps\n", "de le regarder avant de répondre.**\n", "\n", "14. Dans le réseau unimode, à quelle distance de Paula se trouve chaque personne\n", " de sa composante ? Le résultat est-il celui que vous attendiez ?\n", "\n", "15. Ce résultat n'est pas une erreur : c'est la conséquence directe de ce qui a été\n", " dit au point 3.\n", "\n", " - La composante de Paula compte 13 personnes, soit\n", " 13 × 12 / 2 = **78 paires possibles**. Le réseau projeté compte 74 liens au\n", " total, dont **71 à l'intérieur de cette composante** (les 3 autres forment le\n", " triangle de Millhaven). Quelle proportion des paires possibles est donc\n", " effectivement reliée ?\n", " - Paula a participé à 3 des 5 repas de Dudley. En quoi cela explique-t-il\n", " qu'elle soit à distance 1 de presque tout le monde ?\n", " - **Un réseau où tout le monde est à distance 1 de tout le monde ne permet de\n", " distinguer personne.** Quelles mesures vues en cours (centralité, distance\n", " moyenne, coefficient de clustering) deviennent inutilisables sur un tel\n", " réseau ?\n", " - Le réseau biparti, lui, distinguait encore des niveaux. Que faut-il en\n", " conclure sur l'ordre dans lequel mener une analyse : projeter d'abord,\n", " ou explorer le biparti d'abord ?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Le même outil, deux objets sociologiques\n", "\n", "Vous venez de faire de l'épidémiologie. Mais l'algorithme utilisé — le parcours en\n", "largeur sur un réseau d'affiliation — est exactement celui qu'on emploie pour étudier\n", "la **diffusion d'une information, d'une pratique ou d'une innovation**.\n", "\n", "L'étude fondatrice est celle de Coleman, Katz et Menzel (1957) sur l'adoption d'un\n", "nouvel antibiotique par des médecins de l'Illinois. Leur résultat : la date à laquelle\n", "un médecin se met à prescrire le médicament s'explique bien mieux par sa **position\n", "dans le réseau professionnel** que par son âge, sa formation ou le volume de sa\n", "patientèle.\n", "\n", "Autrement dit : ce qui circule dans un réseau — un virus, une rumeur, une pratique\n", "professionnelle, une candidature — suit les mêmes chemins et obéit aux mêmes\n", "contraintes structurelles. C'est ce qui fait la portée générale de ces outils, et\n", "c'est aussi ce qui invite à la prudence : le même modèle décrit des phénomènes dont\n", "les mécanismes réels n'ont rien à voir entre eux.\n", "\n", "**Question de synthèse (à rédiger, 10 à 15 lignes)**\n", "\n", "16. On vous confie un fichier recensant les participations de 400 salarié·es aux\n", " réunions internes d'une entreprise sur deux ans. On vous demande d'identifier\n", " « les personnes influentes ».\n", "\n", " - Quelles étapes de ce TP reprendriez-vous, et dans quel ordre ?\n", " - Quelles limites signaleriez-vous dans votre rapport ?\n", " - En quoi le fait que la projection transforme chaque réunion en clique\n", " peut-il fausser une mesure d'influence ?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Pour aller plus loin (facultatif)\n", "\n", "1. La fonction `bipartite.weighted_projected_graph(B, personnes)` construit la même\n", " projection, mais en pondérant chaque lien par le **nombre de repas partagés**.\n", " Construisez-la et affichez les cinq liens les plus lourds. En quoi cette version\n", " corrige-t-elle en partie le défaut signalé au point 3 ?\n", "2. Un **point d'articulation** est un nœud dont le retrait fragmente le réseau en\n", " plusieurs composantes. NetworkX les liste avec `list(nx.articulation_points(...))`.\n", "\n", " - Appliquez la fonction au réseau projeté `G`. La liste est **vide**. Au vu de ce\n", " que vous avez découvert à la question 15, pouviez-vous le prévoir ?\n", " - Appliquez-la maintenant au réseau biparti `B`. On en trouve **trois**.\n", " Lesquels ? De quel type sont ces nœuds ?\n", " - Retirez `R1990` de `B` (`B.copy()` d'abord, pour ne pas abîmer le réseau) et\n", " regardez qui se retrouve isolé. Même chose avec `R1988`.\n", " - Concluez : en projetant, qu'est-ce qu'on a rendu invisible ?\n", "3. Reprenez la projection sur l'autre mode : `bipartite.projected_graph(B, repas)`.\n", " Deux repas y sont reliés s'ils ont au moins un participant commun. Qu'est-ce que\n", " ce réseau donne à voir que le précédent ne montrait pas ?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. Synthèse\n", "\n", "- Un **réseau d'affiliation** relie des personnes à des événements. C'est la forme\n", " sous laquelle les données de réseau se présentent le plus souvent en sociologie,\n", " parce que les participations sont observables alors que les relations ne le sont pas.\n", "- La **projection** transforme ce réseau à deux modes en un réseau de personnes,\n", " exploitable avec les outils habituels. Elle a un coût : chaque événement devient\n", " une clique, et le réseau paraît plus dense qu'il ne l'est.\n", "- Les **composantes connexes** délimitent ce qui peut circuler et jusqu'où. Rien ne\n", " franchit la frontière entre deux composantes.\n", "- Le **parcours en largeur** mesure une distance ; dans un réseau biparti, les\n", " distances paires désignent des personnes et les impaires des événements.\n", "- Un même outil formel sert à décrire une contagion, une rumeur ou l'adoption d'une\n", " pratique professionnelle. La structure est commune ; les mécanismes ne le sont pas.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "Auteur : Florian Mathieu — L3 Sociologie quantitative, *Algorithmes des graphes*\n", "\n", "Licence CC BY-NC-SA 4.0\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3" } }, "nbformat": 4, "nbformat_minor": 5 }