
Les flowcharts – ou diagrammes de flux – sont très importants en recherche clinique, car ils permettent de représenter le parcours des participants : inclusion, exclusions, traitements randomisés, populations analysées (ITT, PP, sécurité, etc.).
Dans les essais randomisés, les études d’observation et les évaluations de dispositifs médicaux, ils sont très utiles pour expliquer la sélection effectuée entre le recrutement et les résultats définitifs. C’est d’ailleurs pour standardiser cette transparence, du recrutement jusqu’aux résultats définitifs, que la norme CONSORT a été établie pour les essais randomisés.
Mais leur utilité dépasse largement le champ de la santé. Dans l’industrie, l’énergie, la logistique ou tout projet de data science, les flowcharts sont un outil extrêmement lisible pour visualiser les étapes de sélection, suivre des règles de filtrage ou garder une trace reproductible de la préparation des données.
L’objectif de cet article est de vous montrer comment automatiser la création d’un flowchart à partir d’un data.frame grâce au package flowchart. Nous illustrerons cette méthode en traçant le suivi de patients dans un essai simulé, complété par des exemples sur le jeu de données iris.
Un grand merci à Éric L., lecteur fidèle du blog, qui m’a signalé ce package et a généreusement partagé une partie du code utilisé ici.
Le package flowchart est téléchargeable sur CRAN
#install.packages("flowchart")
library(flowchart) fc`,fc_filter(), fc_split(), etc.),as_fc()` : convertit un data.frame en objet flowchart.fc_split()` : crée des branches selon les modalités d’une variable.fc_filter()` : ajoute un filtre avec affichage automatique des exclus.fc_merge()` : fusionne horizontalement deux flowcharts.fc_stack()` : empile verticalement deux flowcharts.fc_modify() : modifie les paramètres graphiques internes.fc_draw()` : dessine le diagramme.fc_export() : exporte le résultat.|> ou le pipe tidyverse %>%.Pour illustrer l’usage du package dans un contexte de recherche clinique, nous allons essayer de reproduire le flowchart de l’essai clinique de phase III SAFO (Cloxacilline ± Fosfomycine pour traiter les bactériémies à Staphylococcus aureus sensible à la méticilline), publié dans Grillo, S., Pujol, M., Miró, J.M. et al. Cloxacillin plus fosfomycin versus cloxacillin alone for methicillin-susceptible Staphylococcus aureus bacteremia: a randomized trial. Nat Med 29, 2518–2525 (2023). https://doi.org/10.1038/s41591-023-02569-0↩︎)
Flowchart de la publication

Le jeu de données permettant de reproduire ce diagramme est inclus dans le package flowchart ; il se nomme safo.
Lorsqu’on explore les variables de ce dataset (avec la fonction glimpse()), on peut facilement identifier les variables qui vont nous permettrent de réaliser le flowchart :
group qui correspond au traitement randomisé (lorsque la variable est NA cela signifie que le patient n’a pas été randomisé, et donc qu’il a été exclu avant cette étape)ittqui correspond à la population en intention de traitépp qui correspond à la population per-protocolelibrary(dplyr)
# affiche la structure des donnérd safo
glimpse(safo)
Rows: 925
Columns: 21
$ id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, …
$ inclusion_crit <fct> Yes, No, No, No, No, No, No, No, No, No…
$ exclusion_crit <fct> No, No, No, Yes, No, Yes, No, Yes, No, …
$ chronic_heart_failure <fct> No, No, No, No, No, No, No, No, No, No,…
$ expected_death_24h <fct> No, No, No, No, No, No, No, Yes, No, No…
$ polymicrobial_bacteremia <fct> No, No, No, No, No, No, No, No, No, No,…
$ conditions_affect_adhrence <fct> No, No, No, No, No, No, No, No, No, No,…
$ susp_prosthetic_valve_endocard <fct> No, No, No, No, No, Yes, No, No, No, No…
$ severe_liver_cirrhosis <fct> No, No, No, Yes, No, No, No, No, No, No…
$ acute_sars_cov2 <fct> No, No, No, No, No, No, No, No, No, No,…
$ blactam_fosfomycin_hypersens <fct> No, No, No, No, No, No, No, No, No, No,…
$ other_clinical_trial <fct> No, No, No, No, No, No, No, No, No, No,…
$ pregnancy_or_breastfeeding <fct> No, No, No, No, No, No, No, No, No, No,…
$ previous_participation <fct> No, No, No, No, No, No, No, No, No, No,…
$ myasthenia_gravis <fct> No, No, No, No, No, No, No, No, No, No,…
$ decline_part <fct> NA, Yes, No, NA, No, NA, Yes, NA, No, Y…
$ group <fct> NA, NA, cloxacillin plus fosfomycin, NA…
$ itt <fct> NA, NA, Yes, NA, Yes, NA, NA, NA, Yes, …
$ reason_itt <fct> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ pp <fct> NA, NA, Yes, NA, Yes, NA, NA, NA, Yes, …
$ reason_pp Le flowchart en lui-même est réalisé avec :
fc_filter() pour filtrer les patients qui ont atteint la randomisationfc_split(group) pour marquer les deux bras de traitementfc_filter() sur les variables itt et pp pour spécifier les populations en intention de traiter et per-protocole
À noter que la fonction fc_filter() possèdent trois arguments importants :
label pour spécifier l’étiquette de la boite principalelabel_excpour spécifier l’étiquette de la boite d’exclusion (sur le côtétext_pattern_exc qui permet de contrôler finement le texte affiché dans la boite d’exclusion (voir son utilisation dans le second exemple)Code employé pour le reproduire
safo |>
as_fc(label = "Patients assessed for eligibility") |> # Point de départ du flowchart : nombre total de patients évalués
fc_filter( # --- 1er filtre : exclusions initiales ---
!is.na(group), # Conserve uniquement les patients avec un groupe renseigné (= randomisés)
label = "Randomized", # Texte de la boîte conservée
show_exc = TRUE, # Affiche la boîte d'exclusion à droite
label_exc = "Excluded for several reasons :\n - not meet inclusion criteria\n - declined to participate\n - met exclusion criteria"
# Texte listant les motifs d'exclusion
) |>
fc_split(group) |> # Sépare le flowchart en deux branches selon le groupe de traitement
fc_filter( # --- 2e filtre : population ITT ---
itt == "Yes", # Conserve les patients inclus en intention de traiter
label = "Included in intention-to-treat population",
show_exc = TRUE,
label_exc = "withdrew consent" # Motif d’exclusion : retrait de consentement
) |>
fc_filter( # --- 3e filtre : population per-protocol ---
pp == "Yes", # Conserve les patients PP
label = "Included in per-protocol population",
show_exc = TRUE,
label_exc = "excluded for several reason" # Motifs d’exclusion PP
) |>
fc_draw() |> # Génère le flowchart final
fc_export( # Exporte l’image du flowchart
here::here("images/safo_flowchart.png"), # Nom du fichier de sortie
width = 4000, # Largeur en pixels
height = 3000, # Hauteur en pixels
res = 300 # Résolution (dpi)
) Flowchart obtenu :

Pour illustrer une utilisation hors santé, prenons un exemple très simple : appliquer une série de filtres successifs au dataset iris, puis les représenter sous forme de flowchart.
Je vous propose les filtres suivants :
Avec fc_filter(), chaque étape devient un bloc, et les observations exclues sont automatiquement comptabilisées et affichées.
Remarque:
Les expressions dynamiques ({n}, {N}, {perc}), utilisées dans l’argument text_pattern_exc permettent aux labels de s’adapter automatiquement aux données.
u <- iris |>
as.data.frame()
fc <- u |>
as_fc() |> # Transforme le data.frame u en objet flowchart,
# point de départ pour construire le diagramme
fc_filter( # --- 1er filtre ---
Sepal.Length > 5, # Condition : on conserve Sepal.Length > 5
show_exc = TRUE, # Affiche dans le flowchart les valeurs exclues
label_exc = "Exclus : Sepal.Length ≤ 5", # Texte affiché dans la boîte d'exclusion
text_pattern_exc = "{label} (n = {n} ; {perc}%)", # Format dynamique : n et %
border_color_exc = "orange" # Couleur de la boîte d'exclusion
) |>
fc_filter( # --- 2e filtre ---
Sepal.Width > 3, # Condition : on conserve Sepal.Width > 3
show_exc = TRUE,
label_exc = "Exclus : Sepal.Width ≤ 3", # Ce qui est exclu apparaît clairement
text_pattern_exc = "{label} (n = {n} ; {perc}%)",
border_color_exc = "red" # Boîte rouge pour cette étape
) |>
fc_filter( # --- 3e filtre ---
Petal.Length > 5, # Condition : on conserve Petal.Length > 5
show_exc = TRUE,
label_exc = "Exclus : Petal.Length ≤ 5", # Libellé de l’exclusion
text_pattern_exc = "{label} (n = {n} ; {perc}%)",
border_color_exc = "green" # Boîte verte pour cette étape
) |>
fc_draw(title = "Flowchart des filtres appliqués") |>
fc_export( # Exporte l’image du flowchart
here::here("images/flowchart_iris_simple.png"), # Nom du fichier de sortie
width = 2500, # Largeur en pixels
height = 2000, # Hauteur en pixels
res = 300 # Résolution (dpi)
)

Vous pouvez soutenir mon travail en faisant un don libre sur le Tipeee du blog
Pour aller plus loin, Éric a proposé un exemple plus élaboré, avec des règles de filtrage plus complexes :
Le flowchart généré affiche clairement, pour chaque étape, le nombre de lignes exclues, le nombre restant et le pourcentage correspondant.
# permet de faire une copie de iris
u <- iris |>
as.data.frame()
u_traite <- # On crée un nouvel objet contenant les données filtrées
u |> # Point de départ : le data.frame u (copie de iris)
as_fc() |> # Convertit u en objet flowchart utilisable par le package
fc_filter( # --- 1ère étape de filtrage ---
Sepal.Length >= 5, # Condition : on exclut les lignes où Sepal.Length < 5
show_exc = TRUE, # Affiche dans le flowchart les valeurs exclues
label_exc = "Filtrage Sepal.Length exclut ", # Texte du bloc d’exclusion
text_pattern_exc = "{label} {n} sur {N} \n({perc}%)", # Format du texte affiché
border_color_exc = "orange" # Bordure orange pour ce bloc d’exclusion
) |>
fc_filter( # --- 2ème étape de filtrage ---
Petal.Width <= 1.9, # Condition : on exclut les lignes où Petal.Width > 1.9
show_exc = TRUE,
label_exc = "Filtrage Petal.Width exclut ",
text_pattern_exc = "{label} {n} sur {N} \n({perc}%)",
border_color_exc = "red" # Bordure rouge pour cette exclusion
) |>
fc_filter( # --- 3ème étape de filtrage ---
Petal.Width >= 2.25 - 0.25 * Sepal.Length, # Condition basée sur une droite de coupure
show_exc = TRUE,
label_exc = "Filtrage droite de coupure exclut ",
text_pattern_exc = "{label} {n} sur {N} \n({perc}%)",
border_color_exc = "green" # Bordure verte pour ce filtre
) |>
fc_draw() |> # Génère le flowchart final
fc_export( # Exporte l’image du flowchart
"flowchart.png", # Nom du fichier de sortie
width = 3000, # Largeur en pixels
height = 4000, # Hauteur en pixels
res = 700 # Résolution (dpi)
) 
Eric a créé une
fonction filter_df_flowchart_expr() qui permet de définir une liste de filtres sous forme de chaînes de caractères, puis :
Voici le code de cette fonction :
#' filter_df_flowchart_expr
#'
#' @param z data.frame contenant les données (colonnes) à filtrer
#' @param filters liste de filtre (éléments à écarter par mise à NA)
#' exemple : list("valeurs de A trop faibles"="A < 3", "B en dehors de (10;20)"="B<10 | B>20")
#' @param preserve_cols colonne
#' @param count_col colonne(s) à préserver de la mise à NA
#' @param flowchart_file nom du fichier d'image du graphique à sauvegarder
#' @param prct_round nb de chiffres après la virgule pour le prcent (2 par défaut)
#' @param couleurs vecteurs des couleurs à utiliser (par défaut
#' \code{palette.colors(n = 30, palette = "Polychrome 36")})
#' @param width largeur de l'image
#' @param height hauteur de l'image
#' @param res résolution de l'image
#' @param title titre
#' @param draw_flowchart TRUE (par défaut) si on veut tracer le flowchart à l'écran
#' @param silent TRUE si on veut inhiber la sortie print() (FALSE par défaut)
#' @param kable_format format utilisé pour knitr::kable ("rst" par défaut)
#'
#' @returns
#' une longue liste avec les résultats
#' @export
#'
#' @examples
#' filter_df_flowchart_expr(iris,
#' list("SL>6"=" Sepal.Length >6", "setosa seules"="Species=='setosa'"),
#' couleurs=c("blue", "beige"))
filter_df_flowchart_expr <- function(
z,
filters = list(),
preserve_cols = "Cycle",
count_col = NULL,
flowchart_file = NULL,
prct_round = 2,
couleurs = palette.colors(n = 30, palette = "Polychrome 36"),
width = 3000, height = 4000, res = 600,
title = "Filtrage successif des données",
draw_flowchart = TRUE,
silent = FALSE,
kable_format = "rst"
) {
if (!is.data.frame(z)) stop("z doit être un objet xts")
if(is.null(count_col)){
count_col <- tail(make.names(c(names(z), "ZZZ"), unique=T),1)
eval(parse(text=sprintf("z$%s <- 1:(dim(z)[1])", count_col)))
}
if (!count_col %in% colnames(z)) stop(paste(count_col, "absente dans z"))
if (is.null(names(filters))) names(filters) <- filters
z_original <- z
z_filtered <- z
df <- as.data.frame(z)
# === Ligne "initial" demandée ===
initial_total_lignes <- nrow(z) # nb total de lignes
initial_na_count_col <- sum(is.na(z[, count_col])) # déjà NA dans count_col
initial_exclus <- initial_na_count_col # partie imaginaire = NA initiau
initial_restants <- initial_total_lignes - initial_na_count_col
initial_pourcent <- round(100 * initial_restants / initial_total_lignes, 2)
# On garde la valeur "classique" utilisée ensuite pour les pourcentages relatifs
initial_n <- sum(!is.na(z_filtered[, count_col])) # individus valides au départ pour les calculs suivants
# Flowchart de départ
fc <- df |> as_fc()
stats <- list()
# Ajout de la ligne initiale
stats[["initial"]] <- list(
expression_NA_faiseuse = "État initial",
exclus = initial_exclus, # complexe : partie réelle = 0, imaginaire = nb NA initiaux
restants = initial_restants,
pourcent_restant = initial_pourcent
)
for (i in seq_along(filters)) {
expr_str <- filters[[i]]
col_ref <- names(filters)[[i]]
na_before <- sum(is.na(z_filtered[, count_col]))
keep <- tryCatch({
!eval(parse(text = expr_str), envir = df, enclos = parent.frame())
}, error = function(e) {
warning(sprintf("Expression invalide ignorée : %s\nErreur : %s", expr_str, e$message))
rep(TRUE, nrow(df))
})
if (!is.logical(keep) || length(keep) != nrow(df)) {
warning(sprintf("Expression '%s' n'a pas renvoyé un vecteur logique de bonne taille → ignorée", expr_str))
keep <- rep(TRUE, nrow(df))
}
keep[is.na(keep)] <- FALSE
if (!all(keep)) {
cols_to_na <- setdiff(colnames(z_filtered), preserve_cols)
z_filtered[!keep, cols_to_na] <- NA
df[!keep, cols_to_na] <- NA
}
exclus <- sum(is.na(z_filtered[, count_col])) - na_before
restants <- initial_n - sum(is.na(z_filtered[, count_col]))
stats[[col_ref]] <- list(
expression_NA_faiseuse = expr_str,
exclus = exclus ,
restants = restants,
pourcent_restant = round(100 * restants / initial_n, prct_round)
)
if ( draw_flowchart | !is.null(flowchart_file)) {
cond_display <- if (grepl("^is\\.na\\(", expr_str)) {
"Absence de valeur pour certaines variables"
} else if (grepl("&|\\|", expr_str)) {
expr_str
} else {
gsub("([A-Za-z][A-Za-z0-9_]*)", "`\\1`", expr_str)
}
lab <- paste0("Filtre : ", col_ref)
call_text <- sprintf(
'fc <- fc |> fc_filter(!(%s),
show_exc = TRUE,
label_exc = "%s",
text_pattern=paste0("{label}\\n{n} soit ", stats[[col_ref]]$pourcent_restant, "%%initial"),
text_pattern_exc = "{label}\\n{n} exclus ({perc}%%)",
bg_fill_exc = "%s")',
expr_str, lab, couleurs[(i-1) %% length(couleurs) + 1]
)
eval(parse(text = call_text))
}
}
if (!is.null(flowchart_file)) {
xx <- fc |> fc_draw(title = title) |>
fc_export(flowchart_file, width = width, height = height, res = res)
cat("*** Flowchart généré avec succès --> ", flowchart_file, "\n")
}
# Conversion en data.frame pour affichage propre
uu <- data.frame(t(data.frame(lapply(stats, FUN=unlist), check.names = F)))
stats_as_df <- data.frame(critere=row.names(uu),
expression_NA_faiseuse=uu[,1],
uu[,-1] |> lapply(as.numeric) )
# Affichage spécial pour la ligne "initial" : on montre les NA initiaux séparément
if (!silent) {
# On crée une version lisible
nice_df <- stats_as_df
nice_df[1, "exclus"] <- paste0("0 (", initial_na_count_col, " NA déjà présents)")
print(knitr::kable(nice_df, format = kable_format))
}
if (draw_flowchart) {
fc |> fc_draw(title = title)
}
return( invisible( list(
z_original = z_original,
z_filtered = z_filtered,
stats = stats,
stats_as_df = stats_as_df,
initial_total = initial_total_lignes,
initial_na_in_count_col = initial_na_count_col,
initial_valid = initial_n,
final_remaining = max(0, restants),
flowchart = if (!is.null(flowchart_file)) flowchart_file else NULL,
fc=fc
) )
)
} Cette fonction permet de produire à la fois un tableau d’exclusion, un flowchart et un jeu de données filtré, en une seule commande.
filter_df_flowchart_expr(
iris,
list("SL>6"=" Sepal.Length >6", "setosa seules"="Species=='setosa'"),
couleurs=c("blue", "beige"),
title="petit exemple") 
============= ====================== ====================== ======== ================
critere expression_NA_faiseuse exclus restants pourcent_restant
============= ====================== ====================== ======== ================
initial État initial 0 (0 NA déjà présents) 150 100.00
SL>6 Sepal.Length >6 61 89 59.33
setosa seules Species=='setosa' 50 39 26.00
============= ====================== ====================== ======== ================
res <- filter_df_flowchart_expr(
z = iris,
filters = list(
"Sepal.Length trop bas" = "Sepal.Length < 5",
"Petal.Width hors intervalle" = "Petal.Width < 1 | Petal.Width > 2.2",
"Sepal.Width élevée" = "Sepal.Width > 3.2"
),
couleurs=c("pink4", "beige", "lightblue3"),
preserve_cols = "Species",
flowchart_file = "filtrage_iris.png",
title = "exemple de filtrage sur iris",
draw_flowchart = FALSE
)
knitr::kable(res$stats_as_df) 
critere expression_NA_faiseuse exclus restants pourcent_restant
=========================== =================================== ====================== ======== ================
initial État initial 0 (0 NA déjà présents) 150 100.00
Sepal.Length trop bas Sepal.Length < 5 22 128 85.33
Petal.Width hors intervalle Petal.Width < 1 | Petal.Width > 2.2 44 84 56.00
Sepal.Width élevée Sepal.Width > 3.2 5 79 52.67
=========================== =================================== ================= set.seed(321)
irisNA <- iris
irisNA$Sepal.Length[sample(1:dim(iris)[1], 20)] <- NA
filter_df_flowchart_expr(
z = irisNA,
filters = list(
"Sepal.Length a des trous" = "is.na(Sepal.Length)",
"Petal.Length trop bas" = "Petal.Length < 4.5",
"Sepal.Width élevée" = "Sepal.Width > 3.2"
),
title = "exemple avec des NA initiaux") 
======================== ====================== ====================== ======== ================
critere expression_NA_faiseuse exclus restants pourcent_restant
======================== ====================== ====================== ======== ================
initial État initial 0 (0 NA déjà présents) 150 100.00
Sepal.Length a des trous is.na(Sepal.Length) 20 130 86.67
Petal.Length trop bas Petal.Length < 4.5 65 65 43.33
Sepal.Width élevée Sepal.Width > 3.2 10 55 36.67
======================== ====================== ====================== ======== ================ Le package flowchart de générer facilement des flowcharts directement depuis vos données. Que vous travailliez en recherche clinique, en biostatistique, en data science, en industrie ou en énergie, cette approche vous permet de documenter clairement les étapes de sélection et d’exclusion des données.
Si l’article vous a été utile, je vous invite à laisser un commentaire ci-dessous : vos retours, questions ou suggestions permettront d’améliorer encore les prochains contenus du blog.
Et si vous travaillez dans les sciences du vivant, la santé, les dispositifs médicaux ou l’industrie, sachez que j’accompagne régulièrement des équipes (chercheurs, cliniciens, ingénieurs, data scientists) dans : l’analyse statistique, la méthodologie d’études, la construction de pipelines R reproductibles, et la visualisation de données, y compris la création de graphiques destinés à des publications scientifiques.
N’hésitez pas à me contacter si vous souhaitez être accompagné sur un projet.(claire@delladata.fr)
Vous pouvez retrouver mes tutoriels vidéo sur la chaine youtube du blog

Enregistrez vous pour recevoir gratuitement mes fiches « aide mémoire » (ou cheat sheets) qui vous permettront de réaliser facilement les principales analyses biostatistiques avec le logiciel R et pour être informés des mises à jour du site.
2 réponses
Bonjour madame !
Je suis vraiment ravie par vos articles, j’aimerais participer à vos formations, hélas les conditions ne sont pas réunies.je vais essayer d’être parmi vos fruits, puisque ce mon domaine.merci infiniment
Je vous remercie, j’avais aucune notion sur les flowcharts.
Grand merci à vous