Tech - Traitement des données de Transparence Santé

Comment combiner les langages R et javascript pour obtenir des données adaptées à une visualisation

Introduction

Afin de réaliser un de nos tutoriels (D3JS - Sunburst) nous avons utilisé des données récupérées sur le site data.gouv.fr. Plus particulièrement nous nous sommes intéressés aux données de la transparence santé qui sont fournies par le site du même nom.

Le site de la Transparence Santé

Les données rassemblées par ce site sont volumineuses et particulièrement intéressantes. Elles permettent en théorie de savoir s'il n'y a pas d'accointances entre certaines entreprises pharmaceutiques et des chirurgiens en hôpitaux par exemple. La base permet de connaître chaque transaction financière à partir de 10 euros, un repas, un voyage ou encore un cadeau. Avec une analyse minutieuse il doit être possible d'identifier des disparités ou des anomalies. Le site permet d'effectuer des recherches par entreprise comme par bénéficiaire. Bien sûr si nous parlons de ces données ici c'est parce que data.gouv.fr fournit l'ensemble des données sous forme de fichiers CSV qu'il est plus facile d'exploiter avec des outils informatiques.

Plus de 170 millions de dons de laboratoires privés à des CHU en 2018

Info: Un article récent réalisé par un collectif de datajournalistes montre d'ailleurs ce qu'il est possible de faire avec une telle base. Sur un autre article il est bien indiqué que les données ne sont pas toujours de bonnes qualités et que des vérifications/corrections ont été effectuées dans le cadre de ce travail.

Traitements avec R

Notre objectif est simplement de produire des données qui pourront ensuite être représentées sous forme hiérarchique et il n'est pas de s'assurer de l'exactitude de ces données. Une fois décompressées, les données fournies par le lien du premier paragraphe pèsent quasiment 5 Go, sous la forme de 4 fichiers CSV. On trouve également un fichier de description qui n'est plus à jour et un autre pour la licence qui rappelle que les fichiers CSV contiennent des informations nominatives. Un des fichiers CSV contient les informations de toutes les entreprises mentionnées et les trois autres contiennent rémunérations, avantages et conventions donnés aux bénéficiaires. Comme nous l'avons déjà fait nous utilisons le langage R qui permet de charger de gros fichiers et de rapidement étudier leur contenu.

> require(data.table)
> library(dplyr)
> dataRemuneration <- fread("declaration_remuneration_2020_01_28_04_00.csv", sep=";", encoding = "UTF-8")

Nous commençons par le plus petit fichier pour explorer ses 34 variables. La fonction head peut vite devenir illisible avec autant de colonnes alors nous utilisons la fonction view sous RStudio pour obtenir un affichage plus propre. Ensuite nous réalisons les opérations suivantes et elles seront reproduites à l'identique sur les fichiers conventions et avantages.

# Filtre par date pour conserver les données d'une seule année
> dataRemuneration <- mutate(dataRemuneration, remu_date = as.Date(remu_date, format= "%d/%m/%Y"))
> dataRemuneration <- dataRemuneration %>% filter(remu_date >= as.Date("01/07/2018", "%d/%m/%Y") & remu_date <= as.Date("30/06/2019", "%d/%m/%Y"))

# Suppression des colonnes inutiles, nous ne conservons que le nom de l'entreprise, la catégorie associée au bénéficiaire et le montant.
> dataRemuneration <- dataRemuneration[, c("denomination_sociale", "categorie", "remu_montant_ttc")]

# Renommage des colonnes
> dataRemuneration <- rename(dataRemuneration, remu_montant = remu_montant_ttc, denomination = denomination_sociale)

# Regroupement par dénomination et catégorie, somme des montants et tri décroissant
> dataRemuneration <- dataRemuneration %>% group_by(denomination, categorie) %>% summarise(remu_montant = sum(remu_montant, na.rm = TRUE)) %>% arrange(desc(remu_montant))

# Libération de la mémoire, elle va devenir essentielle avec les deux autres fichiers
gc()

> head(dataRemuneration, 5)
  denomination                             categorie                                                       remu_montant
1 SANOFI-AVENTIS RECHERCHE & DEVELOPPEMENT Académies, Fondation, sociétés savantes, organismes de conseils     91868459
2 SANOFI-AVENTIS GROUPE                    Académies, Fondation, sociétés savantes, organismes de conseils     29462866
3 SANOFI AVENTIS FRANCE                    Académies, Fondation, sociétés savantes, organismes de conseils     26230932
4 ARROW GENERIQUES SAS                     Académies, Fondation, sociétés savantes, organismes de conseils     18516076
5 NOVARTIS PHARMA SAS                      Académies, Fondation, sociétés savantes, organismes de conseils     16927982

Avec ce travail nous obtenons une structure à 3 niveaux avec un montant en face. On peut lire que SANOFI-AVENTIS R&D (niveau 1) a donné une rémunération (niveau 2) pour la catégorie "Académies, ..." (niveau 3) de 91 M€. Le niveau 2 représente les trois types de fichiers (rémunération, avantages et conventions). La méthode head nous permet aussi de voir que nos données sont segmentées au niveau de la dénomination alors que nous voudrions voir les trois premières lignes regroupées en une seule. Nous tenterons de résoudre ce problème par la suite. Les deux autres fichiers sont traités de la même façon (le nom des colonnes associées au montant et à la date ne sont pas les mêmes).

> dataAvantage <- fread("declaration_avantage_2020_01_28_04_00.csv", sep=";", encoding = "UTF-8")
> dataAvantage <- mutate(dataAvantage, avant_date_signature = as.Date(avant_date_signature, format= "%d/%m/%Y"))
> dataAvantage <- dataAvantage %>% filter(avant_date_signature >= as.Date("01/07/2018", "%d/%m/%Y") & avant_date_signature <= as.Date("30/06/2019", "%d/%m/%Y"))
> dataAvantage <- dataAvantage[, c("denomination_sociale", "categorie", "avant_montant_ttc")]
> dataAvantage <- rename(dataAvantage, avant_montant = avant_montant_ttc, denomination = denomination_sociale)
> dataAvantage <- dataAvantage %>% group_by(denomination, categorie) %>% summarise(avant_montant = sum(avant_montant, na.rm = TRUE)) %>% arrange(desc(avant_montant))
> head(dataAvantage, 5)
  denomination             categorie                                                       avant_montant
1 MicroPort CRM France SAS Professionnel de santé                                               31991002
2 AMGEN SAS                Académies, Fondation, sociétés savantes, organismes de conseils      18401995
3 MSD France               Etablissement de santé                                               14488273
4 BIOGEN FRANCE SAS        Association professionnel de santé                                   10289661
5 NOVARTIS PHARMA SAS      Professionnel de santé                                                9235105


> dataConvention <- fread("declaration_convention_2020_01_28_04_00.csv", sep=";", encoding = "UTF-8")
> dataConvention <- mutate(dataConvention, conv_date_signature = as.Date(conv_date_signature, format= "%d/%m/%Y"))
> dataConvention <- dataConvention %>% filter(conv_date_signature >= as.Date("01/07/2018", "%d/%m/%Y") & conv_date_signature <= as.Date("30/06/2019", "%d/%m/%Y"))
> dataConvention <- dataConvention[, c("denomination_sociale", "categorie", "conv_montant_ttc")]
> dataConvention <- rename(dataConvention, conv_montant = conv_montant_ttc, denomination = denomination_sociale)
> dataConvention <- dataConvention %>% group_by(denomination, categorie) %>% summarise(conv_montant = sum(conv_montant, na.rm = TRUE)) %>% arrange(desc(conv_montant))
> head(dataConvention, 5)
  denomination                       categorie                          conv_montant
1 Roche Diagnostics Operations, Inc. Etablissement de santé                 36782790
2 SA Bristol-Myers Squibb Belgium    Etablissement de santé                 34607706
3 Covidien AG                        Association professionnel de santé     18803031
4 BRISTOL-MYERS SQUIBB               Etablissement de santé                 14782340
5 ASTRAZENECA                        Etablissement de santé                 13298630

Finalement nous regroupons ces trois jeux de données en un seul et le sauvegardons dans un fichier CSV.

> mergedData <- merge(dataAvantage, dataConvention, by.x = c("denomination", "categorie"), by.y = c("denomination", "categorie"), all = TRUE)
> mergedData <- merge(mergedData, dataRemuneration, by.x = c("denomination", "categorie"), by.y = c("denomination", "categorie"), all = TRUE)
> mergedData <- mergedData %>% replace(is.na(.), 0) %>%  mutate(total = rowSums(.[3:5]))
> mergedData <- arrange(mergedData, desc(total))
> head(mergedData, 10)
denomination                                categorie           avant_montant conv_montant remu_montant    total
1  SANOFI-AVENTIS RECHERCHE & DEVELOPPEMENT Académies, ...                  1000            0     91868459 91869459
2        Roche Diagnostics Operations, Inc. Etablissement de santé             0     36782790            0 36782790
3           SA Bristol-Myers Squibb Belgium Etablissement de santé             0     34607706      1425421 36033127
4                  MicroPort CRM France SAS Professionnel de santé      31991002       434049       328706 32753757
5                     SANOFI-AVENTIS GROUPE Académies, ...                  4088            0     29462866 29466954
6                      BRISTOL-MYERS SQUIBB Etablissement de santé        296515     14782340     12768095 27846950
7                     SANOFI AVENTIS FRANCE Académies, ...                801444            0     26230932 27032376
8                                    AbbVie Académies, ...               1187500     12451106      9717956 23356562
9                       NOVARTIS PHARMA SAS Académies, ...                601739      5625255     16927982 23154976
10                      NOVARTIS PHARMA SAS  Professionnel de santé      9235105      9532935      2587648 21355688

> fwrite(mergedData, "transparence_data_2018_2019.csv", sep = ";")

Traitements en Javascript

A partir du fichier CSV produit en R nous allons pouvoir construire notre structure arborescente. La partie création de la visualisation est décrite dans le tutoriel cité précédemment.

d3.text("d3js/sunburst-chart/transparence_data_2018_2019.csv").then(function(raw) {
	let dsv = d3.dsvFormat(';');
	let data = dsv.parse(raw);
	let json = buildHierarchy(data);
	//createVisualization(json);
});

La construction de la hiérarchie se déroule en trois étapes :

  1. Identification des entreprises que l'on souhaite mettre dans notre visualisation. Comme le graphique est hiérarchique nous nous sommes limités à 9 entreprises car chacune va ensuite être divisée en trois sous-parties (avantage, convention et rémunération) et chacune de ces sous-parties peut être divisée en 12 catégories. Par ailleurs nous avons dû effectuer quelques recherches pour trouver le nom à afficher en fonction des dénominations des entreprises. Pour "Bristol-Myers Squibb" il existe 29 dénominations différentes, par exemple "Bristol-Myers Squibb Brazil". Mais comme nous voulons afficher une dénomination connue et regrouper les filiales nous avons dû trouver les chaînes de caractères qui permettaient de les regrouper (ici "bristol" suffit à retrouver les 29 dénominations sans sélectionner d'autres entreprises). La dénomination "Autres" permet de regrouper toutes les données qui ne correspondent pas à ces 9 entreprises. Elle s'avère nécessaire si le graphique affiche un pourcentage ou alors un montant dont on veut connaitre la répartition globale. Dans notre cas ce n'est pas essentiel car ces données ne font que servir le graphique qui se retrouverait avec un "Autres" représentant un peu plus de 50% des montants et écraserait donc les 9 entreprises qu'on souhaite visualiser.
  2. Initialiser la hiérarchie de l'arborescence. On commence par construire un noeud root qui contient un nom et un tableau de fils vide. La boucle for qui suit va ajouter les niveaux 1 (entreprise) et niveaux 2 (type de montant).
  3. Parcourir toutes les lignes de notre fichier CSV pour associer son montant au bon endroit de la hiérarchie.
    1. Conversion des montants en nombre
    2. Recherche du parent grâce à la fonction getParentNode qui va utiliser nos chaines de caractères matchingNames et si la ligne ne correspond pas à l'une de nos 9 entreprises on continue.
    3. Ensuite on effectue le même traitement que ce soit pour les avantages, les conventions et les rémunérations. On vérifie si la catégorie existe en troisième niveau sinon on la crée puis on ajoute le montant de notre ligne à cette catégorie.

function buildHierarchy(data) {
	var bigOnes = [
		{"displayName": "Sanofi", "matchingNames": ["sanofi"]},
		{"displayName": "Astrazeneca", "matchingNames": ["astrazeneca"]}, 
		{"displayName": "Novartis", "matchingNames": ["novartis"]}, 
		{"displayName": "Bristol-Myers Squibb", "matchingNames": ["bristol"]}, 
		{"displayName": "AbbVie", "matchingNames": ["abbvie"]},
		{"displayName": "Roche", "matchingNames": ["roche"]},
		{"displayName": "Merck Sharp and Dohme", "matchingNames": ["MSD France", "merck"]}, 
		{"displayName": "Microport CRM", "matchingNames": ["Microport"]}, 
		{"displayName": "GlaxoSmithKline", "matchingNames": ["glaxosmithkline"]}
		//{"displayName": "Autres"}
	];

	var root = {"name": "root", "children": []};

    // Ajout des entreprises dont les montants sont les plus importants
	for (let i = 0; i < bigOnes.length; ++i) {
		root.children.push({"name": bigOnes[i].displayName, "matchingNames": bigOnes[i].matchingNames, "position" : i, "children": [
			{"name": "Avantage", "children": []}, {"name": "Convention", "children": []}, {"name": "Rémunération", "children": []}
		]});
	}

	for (let i = 0; i < data.length; ++i) {
		data[i].avant_montant = +data[i].avant_montant;
		data[i].conv_montant = +data[i].conv_montant;
		data[i].remu_montant = +data[i].remu_montant;

		let parentNode = getParentNode(root, data[i]);
		if (parentNode === undefined) {
			continue;
		}

        // Avantage, parentNode.children[0] correspond à la première entrée ajoutée à chaque entreprise dans la boucle for du dessus.
        // Pour Convention il faudra utiliser parentNode.children[1] et pour Rémunération parentNode.children[2]
		if (data[i].avant_montant !== "" && data[i].avant_montant !== 0) {
			let foundCategory = undefined;
			for (var iCat = 0; iCat < parentNode.children[0].children.length; ++iCat) {
				if (parentNode.children[0].children[iCat].name === data[i].categorie) {
					foundCategory = parentNode.children[0].children[iCat];
				}
			}
			if (foundCategory === undefined) {
				parentNode.children[0].children.push({"name": data[i].categorie, "amount": 0});
				foundCategory = parentNode.children[0].children[parentNode.children[0].children.length - 1];
			}
			foundCategory.amount = foundCategory.amount + data[i].avant_montant;
		}

        // Le même traitement est répété pour la colonne conv_montant de data[i] avec parentNode.children[1]
            
		// Le même traitement est répété pour la colonne remu_montant de data[i] avec parentNode.children[2]
	}

	return root;
}

function getParentNode(root, current) {
	let parentNode = undefined;
	for (let j = 0; j < root.children.length; ++j) {
		for (let k = 0; k < root.children[j].matchingNames.length; ++k) {
			if (current.denomination.search(new RegExp(root.children[j].matchingNames[k], "i")) >= 0) {
				return root.children[j];
			}
		}
	}
	return undefined;
	//return root.children[root.children.length - 1]; // Autres
}

Conclusion

Nous terminons ici ce tutoriel qui montre comment récupérer des données volumineuses, les traiter avec un langage adapté pour finalement construire une représentation qui servira à une visualisation. Il y aurait beaucoup à faire avec ces trois fichiers qui contiennent presque 20 millions de lignes et nous reviendrons peut-être sur ce jeu de données ultérieurement.

COMMENTAIRES