Définir ce qu'est une base de données en partant d'une analogie avec Excel
Evoquer les principes de conception qui permettent de créer une base de données
Aborder les notions de tables, de jointures et de schémas
Aborder le SQL, ce langage de programmation qui permet de requêter les bases de données
Principes de conception
Une base de données (BDD) est comme un grand tableur Excel, où chaque feuille représente une table.
Tout l’enjeu d’une base de données est :
d’éviter les redondances des données, pour prendre moins d’espace
de contenir le moins de cases vides possibles, toujours pour occuper moins d’espace
d’être le plus flexible possible, pour pouvoir ajouter de nouvelles données que l’on n’avait pas prévues initialement
Structurer les données en tables
Comment feriez-vous pour stocker dans un tableur Excel les données de biologie et les données démographiques de 5 patients ?
Mettons que l’on ait besoin de stocker le taux d’hémoglobine, les plaquettes et les leucocytes.
Le premier réflexe qui vient à l’esprit est de créer une colonne par paramètre de biologie.
Nous ajoutons une colonne date_biologie pour connaître la date de réalisation du prélèvement biologique.
patient_id
age
sexe
date_admission
date_sortie
date_biologie
hémoglobine
plaquettes
leucocytes
1
45
M
2024-10-01
2024-10-10
2024-10-03
13.5
/
/
1
45
M
2024-10-01
2024-10-10
2024-10-04
/
150,000
7,200
2
60
F
2024-09-25
2024-10-05
2024-09-26
12.8
180,000
8,000
3
38
F
2024-10-05
2024-10-12
2024-10-07
14.0
220,000
/
3
38
F
2024-10-05
2024-10-12
2024-10-08
/
/
6,500
4
52
M
2024-09-20
2024-09-30
2024-09-21
11.5
140,000
9,500
5
29
F
2024-10-08
2024-10-15
2024-10-09
13.2
170,000
7,800
5
29
F
2024-10-16
2024-10-20
2024-10-16
14.2
/
/
Comment lire ce tableau ?
Le patient 1 a un seul séjour (une seule date_admission) et deux dosages biologiques à deux dates différentes durant ce même séjour (deux valeurs pour date_biologie).
Le patient 5 a deux séjours (deux valeurs pour date_admission), avec une biologie prélevée par séjour (deux valeurs différentes de date_biologie).
Nous pouvons remarquer deux choses :
Il existe une redondance des données démographiques (âge, sexe, dates d'admission et de sortie)
Nous avons dû créer une ligne par date de prélèvement biologique, ce qui fait que nous avons des cases vides aux dates où certaines biologies n'ont pas été réalisées
Si l’on revient à nos trois principes de conception (éviter les redondances, moins de cases vides et flexibilité), il semblerait que l’on puisse faire mieux.
Pourquoi ne pas créer une table (ou une feuille Excel pour continuer avec l’analogie) pour les patients ?
patient_id
age
sexe
1
45
M
2
60
F
3
38
F
4
52
M
5
29
F
On gagne ainsi de la place avec 3 lignes en moins.
Pourquoi ne pas avoir intégré les séjours dans cette table ?
Si on avait intégré les séjours dans cette table (avec les colonnes date_admission et date_sortie), nous aurions certes eu une seule ligne pour les patients 1 à 4, mais deux lignes pour le patient 5, qui a deux séjours différents.
Toujours dans une logique de diminuer le nombre de lignes, on préférera créer une table pour les séjours.
patient_id
admission_id
date_admission
date_sortie
1
1
2024-10-01
2024-10-10
2
2
2024-09-25
2024-10-05
3
3
2024-10-05
2024-10-12
4
4
2024-09-20
2024-09-30
5
5
2024-10-08
2024-10-15
5
6
2024-10-16
2024-10-20
Pour finir, nous allons créer une table pour stocker les données de biologie.
patient_id
admission_id
date_biologie
hémoglobine
plaquettes
leucocytes
1
1
2024-10-03
13.5
/
/
1
1
2024-10-04
/
150,000
7,200
2
2
2024-09-26
12.8
180,000
8,000
3
3
2024-10-07
14.0
220,000
/
3
3
2024-10-07
/
/
6,500
4
4
2024-09-21
11.5
140,000
9,500
5
5
2024-10-09
13.2
170,000
7,800
5
6
2024-10-16
14.2
/
/
OK, mais ici nous avons encore des cases vides, nous pourrions optimiser.
La solution est de créer une colonne pour le nom du paramètre biologique, et une colonne pour sa valeur. Ainsi, plus de case vide !
patient_id
admission_id
date_biologie
paramètre
valeur
1
1
2024-10-03
hémoglobine
13.5
1
1
2024-10-04
plaquettes
150,000
1
1
2024-10-04
leucocytes
7,200
2
2
2024-09-26
hémoglobine
12.8
2
2
2024-09-26
plaquettes
180,000
2
2
2024-09-26
leucocytes
8,000
3
3
2024-10-07
hémoglobine
14.0
3
3
2024-10-07
plaquettes
220,000
3
3
2024-10-07
leucocytes
6,500
4
4
2024-09-21
hémoglobine
11.5
4
4
2024-09-21
plaquettes
140,000
4
4
2024-09-21
leucocytes
9,500
5
5
2024-10-09
hémoglobine
13.2
5
5
2024-10-09
plaquettes
170,000
5
5
2024-10-09
leucocytes
7,800
5
6
2024-10-16
hémoglobine
14.2
Nous venons de créer une base de données !
Alors certes, cela peut paraître moins lisible au premier abord, mais quand on a des millions de données, il est nécessaire d’optimiser leur stockage.
Et vous le verrez si vous faites un peu de programmation, cette manière d’organiser les données est finalement bien plus lisible qu’un fichier Excel à 50, 100 colonnes…
Jointures
Les données sont maintenant éparpillées sur plusieurs tables.
Comment les fusionner de nouveau ?
Si l’on veut fusionner plusieurs tables, on fera ce que l’on appelle une jointure.
Par exemple, si on fait une jointure entre les tables patients et séjours, en faisant une correspondance sur la colonne patient_id, nous obtiendrons cette table :
patient_id
age
sexe
admission_id
date_admission
date_sortie
1
45
M
1
2024-10-01
2024-10-10
2
60
F
2
2024-09-25
2024-10-05
3
38
F
3
2024-10-05
2024-10-12
4
52
M
4
2024-09-20
2024-09-30
5
29
F
5
2024-10-08
2024-10-15
5
29
F
6
2024-10-16
2024-10-20
On pourra joindre les données de la table biologie de la même façon, et obtenir le tableau du début de l’article.
Requêter une base de données
Il existe un langage de programmation qui permet spécifiquement de requêter les bases de données.
Ce langage se nomme SQL, pour Structured Query Language.
C’est un langage assez simple et facile d’accès.
Il se compose de quelques mots clefs qui permettent d’obtenir les données que l’on veut, dont (non exhaustif) :
SELECT : vous choisissez les colonnes que vous voulez garder
FROM : de quelle table seront extraites les données ?
WHERE : quels filtres appliquer sur les données ?
Par exemple :
SELECTpatient_id,age,sexeFROMpatientsWHEREage>45
On sélectionne les colonnes patient_id, age, sexe de la table patients où la valeur de la colonne age est supérieur à la valeur 45.
On a le résultat suivant :
patient_id
age
sexe
2
60
F
4
52
M
Schémas de BDD
Ce que l’on appelle un schéma de base de données est la structure des tables qui composent une base de données.
Il spécifie :
le nom des tables
le nom des colonnes de chaque table
le type de données de chaque colonne (si la colonne doit comprendre des données de type texte ou numérique par exemple)
Par exemple, OMOP est un schéma de BDD spécialisée dans les données de santé.
Le schéma de données du modèle OMOP
Cette base est assez complexe, ce qui est nécessaire pour englober toutes les données de santé.
De même que nous l’avons fait plus haut, vous pouvez retrouver la table person qui correspond aux patients et la table visit_detail qui correspond aux séjours.
Conclusion
Les points à retenir :
Une base de données est un ensemble de tables avec un schéma particulier (noms des colonnes et type des données)
Les schémas des bases sont contruits selon des principes : éviter les redondances, optimiser l'espace et permettre la flexibilité
Les tables peuvent être liées entre-elles à l'aide de jointures
Le SQL est un langage de programmation permettant de requêter les bases de données
La base de données MIMIC, pour Medical Information Mart for Intensive Care, est une base de données nord-américaine contenant des données de plus de 50 000 patients admis en réanimation. Il s’agit de l’une des bases de données de réanimation les plus utilisées, du fait de son accès gratuit.
Malgré des données d’une qualité imparfaite, elle constitue un bon socle pour apprendre à manipuler les données issues d’entrepôts de données de santé (EDS).
Elle existe en plusieurs versions, dont la plus récente est la MIMIC-IV.
Données test (publiques)
La base de donneés MIMIC comporte pour les versions III et IV des bases tests, qui contiennent les données anonymisées de 100 patients et qui sont accessibles publiquement.
Vous devez donc commencer par vous inscrire sur le site physionet.org.
Vous devrez faire une demande d’accès à Physionet, en renseignant quelques informations et en donnant les coordonnées d’un superviseur ou d’un collègue, à qui un mail sera envoyé.
Vous devrez ensuite compléter le CITI Course, il s’agit d’une formation nécessaire afin d’accéder aux données hébergées sur le site Physionet. Les différentes étapes sont détaillées ici.
Vous pourrez ensuite télécharger le certificat une fois le CITI Course terminé, vous pourrez le le déposer ici pour validation par l’équipe de Physionet.
Pour chaque table, les requêtes sont disponibles :
en utilisant la librairie dplyr de R (directement depuis LinkR)
en utilisant du SQL (PostgreSQL)
en utilisant la librairie pandas de Python
Pour le code en dplyr, nous utiliserons la fonction join_concepts qui est disponible dans LinkR (pas besoin de les déclarer de nouveau) :
# Permet de faire la jointure des ID et des noms de conceptsjoin_concepts<-function(df,concept_df,cols){for(colincols){key<-paste0(col,"_concept_id")name<-paste0(col,"_concept_name")df<-df%>%dplyr::left_join(concept_df%>%dplyr::select(!!key:=concept_id,!!name:=concept_name),by=key,copy=TRUE)%>%dplyr::relocate(!!name,.after=!!key)}return(df)}
Dans les exemples, les données des tables OMOP sont chargées dans une liste d$.
# Charger les tables dans une liste 'd'd<-list()for(tableinc("person","visit_detail")){sql<-glue::glue_sql("SELECT * FROM {`table`}, .con = con)d[[table]]<-DBI::dbGetQuery(con,sql)}
Comme indiqué ici, il existe différents cas pour calculer les doses des administrations médicamenteuses.
Nous pouvons retenir deux cas :
le champ amount_value est rempli, dans ce cas c’est celui-ci qu’il faut utiliser (cas des médicaments per os)
le champ amount_value n’est pas rempli, dans ce cas il faut utiliser le le champ numerator_value (cas des administrations intra-veineuses, sous-cutanées, en aérosols et en patchs)
Nous allons créer un champ amount, qui sera la synthèse des champs amount_value et numerator_value, ce sera la quantité délivrée du médicament.
Ensuite, nous pourrons déterminer le débit, en rapportant le champ amount au temps entre le début et la fin de l’administration. Nous allons pour cela créer un champ duration_hours, qui sera le nombre d’heures pendant lequel est délivré le médicament.
Nous créons également un champ daily_dose pour calculer la dose quotidienne moyenne sur la période, exprimée en daily_dose_unit.
Le modèle OMOP a initialement été développé pour des analyses épidémiologiques, il n’était pas important de savoir précisément la date et l’heure de l’administration d’un médicament, mais uniquement la quantité reçue globalement sur une période temps.
Ceci pose problème dans le cas des entrepôts de données de santé, où l’heure précise d’administration d’un médicament nous importe.
Il est donc important lors du processus d’ETL de créer une nouvelle ligne à chaque changement de dose, et une ligne par administration intermittente.
Par exemple, si un patient reçoit 1 g de paracétamol toutes les 6 heures pendant 5 jours, plutôt que de créer une ligne avec quantity = 5*4 = 20 et la date de fin à + 5 jours de la date de début, on préférera créer une ligne par administration, soit 20 lignes avec les horaires précis d’administration, avec date de début = date de fin.
d$drug_exposure%>%join_concepts(d$concept,c("drug","drug_type","route"))%>%dplyr::left_join(d$drug_strength%>%join_concepts(d$concept,c("ingredient","amount_unit","numerator_unit","denominator_unit"))%>%dplyr::select(drug_concept_id,ingredient_concept_id,ingredient_concept_name,amount_value,amount_unit_concept_id,amount_unit_concept_name,numerator_value,numerator_unit_concept_id,numerator_unit_concept_name,denominator_value,denominator_unit_concept_id,denominator_unit_concept_name),by="drug_concept_id",copy=TRUE)%>%dplyr::collect()%>%dplyr::arrange(person_id,drug_exposure_start_datetime)%>%dplyr::mutate(amount=dplyr::case_when(!is.na(amount_value)~quantity*amount_value,!is.na(numerator_value)~quantity*numerator_value),amount_unit=dplyr::case_when(!is.na(amount_value)~amount_unit_concept_name,!is.na(numerator_value)~numerator_unit_concept_name),duration_hours=as.numeric(difftime(drug_exposure_end_datetime,drug_exposure_start_datetime,units="hours")),rate=dplyr::case_when(!is.na(numerator_value)&!is.na(duration_hours)&duration_hours>0~amount/duration_hours),rate_unit=dplyr::case_when(!is.na(rate)&!is.na(amount_unit)~paste0(amount_unit," per hour")),daily_dose=dplyr::case_when(is.na(rate)&!is.na(amount)~amount/duration_hours*24),daily_dose_unit=dplyr::case_when(is.na(rate)&!is.na(amount_unit)~paste0(amount_unit," per day")))%>%dplyr::select(person_id,drug_concept_name,drug_exposure_start_datetime,drug_exposure_end_datetime,duration_hours,amount,amount_unit,rate,rate_unit,daily_dose,daily_dose_unit)
L’âge est calculé depuis les variables d$visit_occurrence (hospitalisations) ou d$visit_detail (séjours dans les unités au cours d’une hospitalisation).
Il s’agit de l’âge du patient à l’admission pour chaque hospitalisation ou admission dans une unité.
# Le code de calcul des dates étant mal converti en SQL, nous collectons les données avec dplyr::collect(),# ce qui signifie que les données sont copiées dans un dataframe localement.# Le code dplyr n'est ainsi pas converti en SQL.d$visit_occurrence%>%dplyr::left_join(d$person%>%dplyr::select(person_id,birth_datetime),by="person_id")%>%dplyr::collect()%>%dplyr::mutate(age=round(as.numeric(difftime(visit_start_datetime,birth_datetime,units="days"))/365.25,1))