
Double programmation en R : valider un jeu ADaM avec diffdf
Le schéma de vérification indépendante qui contrôle (QC) chaque jeu de données clinique dérivé avant soumission — re-dériver une variable d’une seconde manière indépendante, la comparer avec diffdf et itérer jusqu’à zéro différence
Un tutoriel complet et exécutable sur la double programmation — la méthode de contrôle qualité standard de l’industrie pour valider un jeu de données clinique dérivé. Découvrez ce qu’est la double programmation (un programmeur de production et un programmeur QC indépendant travaillent à partir de la même spécification mais écrivent un code séparé ; les deux sorties doivent concorder exactement), puis validez une véritable variable ADaM dérivée en R : re-dériver de façon indépendante un groupe d’âge, comparer les deux jeux de données avec diffdf::diffdf() sur une clé sujet, lire le rapport de différences, détecter un écart introduit, le corriger et relancer jusqu’à zéro différence — la boucle qui transforme « le code s’est exécuté » en « le jeu de données est validé ». Sur des données pharmaverse publiques, pour que chaque ligne s’exécute.
- La double programmation valide un jeu de données en le reproduisant de manière indépendante. Un programmeur de production construit le jeu de données dérivé ; un programmeur QC (contrôle qualité) distinct le re-dérive à partir de la même spécification avec un code séparé. Si les deux concordent exactement, le jeu de données est validé.
diffdf()est le moteur de comparaison de jeux de données de R — l’équivalent duPROC COMPAREde SAS. Il rapporte chaque différence de valeur, ligne supplémentaire, colonne supplémentaire et incohérence de type entre deux data frames.- Comparez sur une clé, pas sur l’ordre des lignes. Passez
keys = "USUBJID"pour quediffdf()associe les sujets par identité ; l’ordre des lignes n’a alors plus d’importance, mais la clé doit être unique dans les deux jeux de données. - « No issues were found! » signifie validé. Un rapport propre est l’objectif ; tout le reste est un écart à diagnostiquer — un mauvais seuil, une frontière
<vs<=, un sujet manquant, une incohérence de type. - La boucle est : comparer → diagnostiquer → corriger → relancer → zéro. Vous itérez jusqu’à ce que
diffdf()ne trouve rien. Ce zéro — et non « il s’est exécuté sans erreur » — est ce qui rend un jeu de données dérivé apte à la soumission.
Introduction
Un régulateur ne fait pas confiance à un jeu de données clinique dérivé parce que le programme qui l’a construit s’est exécuté sans erreur. Un code s’exécute sans erreur à tout moment et calcule quand même le mauvais chiffre. Un régulateur fait confiance au jeu de données parce qu’un second programmeur, indépendant, l’a reproduit — a écrit un code séparé à partir de la même spécification — et que les deux résultats ont concordé à la valeur près. Cette pratique s’appelle la double programmation (ou programmation indépendante), et c’est la façon standard de l’industrie, établie de longue date, de contrôler la qualité des jeux de données d’analyse qui entrent dans une soumission réglementaire.
Cette leçon enseigne le schéma de bout en bout en R. Nous prenons une variable ADaM (Analysis Data Model) dérivée d’un véritable jeu de données ADSL — le jeu de données d’analyse au niveau du sujet, une ligne par sujet — la re-dérivons de manière indépendante à la façon QC, et comparons les deux avec diffdf, le package R qui vous donne le PROC COMPARE de SAS (sa procédure de comparaison de jeux de données) au sein de R. Puis nous faisons ce qui compte : introduire un bug réaliste, regarder diffdf() l’attraper, le corriger et relancer jusqu’à zéro. Nous travaillons sur des données pharmaverse publiques, pour que chaque ligne s’exécute telle quelle.
Voici la forme de l’ensemble du travail — le nombre de différences que diffdf() trouve, avant et après la correction :
Ce zéro azur est le livrable. Tout ce qui suit explique comment l’atteindre.
Ce qu’est la double programmation
La double programmation est une vérification indépendante. Deux programmeurs, une spécification, deux programmes séparés :
- Le programmeur de production écrit le code qui construit le jeu de données dérivé qui sera réellement soumis.
- Le programmeur QC (validation) — une personne différente, qui ne lit pas le code de production — écrit son propre programme à partir de la même spécification pour reproduire la même sortie.
Les deux jeux de données sont ensuite comparés. S’ils concordent exactement, la dérivation est validée : deux chemins de code indépendants atteignant le résultat identique constituent une preuve solide que la spécification a été implémentée correctement. S’ils diffèrent, au moins un programme est erroné, et la différence vous indique où. La recommandation PHUSE sur le QC indépendant est explicite : le rôle QC doit être assuré par une autre personne que le programmeur de production — l’indépendance est tout l’enjeu.
| Programmeur de production | Programmeur QC (validation) | |
|---|---|---|
| Lit | la spécification | la même spécification (pas le code de production) |
| Écrit | le code qui construit le jeu de données soumis | un code séparé et indépendant pour la même sortie |
| Objectif | le jeu de données dérivé | le reproduire pour confirmer qu’il est correct |
| Réussite | — | diffdf() ne trouve aucune différence |
C’est pourquoi on parle de double programmation : la dérivation est faite deux fois, indépendamment, et les deux résultats sont comparés (« diffés »). La même discipline valide chaque niveau du pipeline — les domaines SDTM (Study Data Tabulation Model), les jeux de données ADaM et les TLF (tableaux, listings et figures) qui vont à la FDA (Food and Drug Administration). Ici, nous validons une seule variable dérivée, qui est l’ unité à partir de laquelle toute la méthode est construite.
Une réserve honnête : la double programmation est une pratique industrielle largement utilisée, et non un mandat réglementaire fixe — les promoteurs peuvent valider avec d’autres méthodes — mais la re-dérivation indépendante reste le QC par défaut, offrant la plus haute assurance pour les données d’analyse dérivées, et c’est le schéma à connaître en premier.
Le jeu de données de production
Nous utilisons l’ADSL pilote CDISC (Clinical Data Interchange Standards Consortium) livré dans pharmaverseadam comme sortie de production — le jeu de données qu’un programmeur de production a livré. La variable que nous allons QC est AGEGR1, le groupe d’âge regroupé : une bande catégorielle dérivée de l’AGE numérique du sujet. Presque toutes les tables démographiques et d’efficacité sont ventilées par bande d’âge, donc AGEGR1 est une petite variable au large rayon d’impact — une bonne candidate pour la calculer correctement de manière indépendante.
library(dplyr, warn.conflicts = FALSE)
library(pharmaverseadam)
library(diffdf)
# The production ADSL, as delivered. Keep the subject key, the source AGE,
# and the derived variable we are validating.
adsl_prod <- pharmaverseadam::adsl %>%
select(STUDYID, USUBJID, AGE, AGEGR1)
# diffdf compares values; drop the source variable label so it does not
# register as an attribute difference (we validate the derivation, not the label)
attr(adsl_prod$AGEGR1, "label") <- NULL
adsl_prod %>% head(5)# A tibble: 5 × 4
STUDYID USUBJID AGE AGEGR1
<chr> <chr> <dbl> <chr>
1 CDISCPILOT01 01-701-1015 63 18-64
2 CDISCPILOT01 01-701-1023 64 18-64
3 CDISCPILOT01 01-701-1028 71 >64
4 CDISCPILOT01 01-701-1033 74 >64
5 CDISCPILOT01 01-701-1034 77 >64
La spécification d’AGEGR1 dans cet essai est le découpage adulte standard : <18, 18-64 et >64. Le jeu de données de production porte déjà la colonne dérivée ; notre tâche est de la reproduire.
adsl_prod %>% count(AGEGR1)# A tibble: 2 × 2
AGEGR1 n
<chr> <int>
1 18-64 42
2 >64 264
Chaque sujet est un adulte, donc la production les place dans 18-64 ou >64 — aucun mineur de 18 ans. Ce sont les chiffres que notre dérivation indépendante doit reproduire.
Re-dériver à la façon QC
Jouez maintenant le programmeur QC. En travaillant uniquement à partir de la spécification — <18, 18-64, >64 — nous écrivons notre propre code pour dériver la bande d’âge à partir d’AGE. Nous ne copions délibérément pas la logique de production ; nous réexprimons la spécification de manière indépendante avec case_when().
# Independent QC derivation of AGEGR1 from the source AGE, per the same spec.
adsl_qc <- pharmaverseadam::adsl %>%
select(STUDYID, USUBJID, AGE) %>%
mutate(
AGEGR1 = case_when(
AGE < 18 ~ "<18",
AGE >= 18 & AGE <= 64 ~ "18-64",
AGE > 64 ~ ">64"
)
)
adsl_qc %>% head(5)# A tibble: 5 × 4
STUDYID USUBJID AGE AGEGR1
<chr> <chr> <dbl> <chr>
1 CDISCPILOT01 01-701-1015 63 18-64
2 CDISCPILOT01 01-701-1023 64 18-64
3 CDISCPILOT01 01-701-1028 71 >64
4 CDISCPILOT01 01-701-1033 74 >64
5 CDISCPILOT01 01-701-1034 77 >64
Nous avons maintenant deux jeux de données qui devraient être identiques sur STUDYID, USUBJID, AGE et AGEGR1. La question que pose la double programmation est simple : concordent-ils ?
Comparer avec diffdf
diffdf() prend les deux data frames et un ensemble de clés — les colonnes qui identifient une ligne de façon unique. Ici, la clé est le sujet : USUBJID. Comparer sur la clé signifie que diffdf() aligne les sujets par identité, et non par position, de sorte que les deux jeux de données peuvent être triés différemment et se comparer quand même correctement.
diffdf(adsl_prod, adsl_qc, keys = "USUBJID")No issues were found!
No issues were found! — les deux dérivations indépendantes concordent pour chaque sujet. Cette seule ligne est le résultat de la validation : AGEGR1 est confirmée. Dans un vrai dossier QC, c’est le résultat que vous approuvez, et c’est la réponse à « pourquoi devrait-on faire confiance à cette variable dérivée ? » — parce que deux personnes l’ont calculée séparément et ont obtenu la même chose.
Parce que la comparaison est fondée sur une clé, l’ordre des lignes est sans importance. Les mêmes sujets dans un ordre mélangé se valident quand même proprement — une propriété utile, puisque les programmes de production et de QC trient rarement leur sortie de la même façon :
set.seed(1)
adsl_qc_shuffled <- adsl_qc[sample(nrow(adsl_qc)), ] # scramble the row order
diffdf(adsl_prod, adsl_qc_shuffled, keys = "USUBJID")No issues were found!
Toujours propre. La clé fait la correspondance ; l’ordre non.
Lire le rapport de différences
Un rapport propre tient en une ligne. Un rapport avec des constats a une anatomie fixe — apprenez à le lire une fois et chaque résultat de diffdf() devient lisible. Quand les jeux de données divergent, diffdf() imprime des sections comme celles-ci :
| Section du rapport | Signification |
|---|---|
| Summary of BASE and COMPARE | Noms, classes et nombres de lignes/colonnes des deux jeux de données — la première vérification de bon sens (avez-vous même passé les bons data frames ?). |
| Not all Values Compared Equal | Le constat central : pour chaque variable, le nombre de valeurs qui diffèrent, puis une table de VARIABLE, la clé, la valeur BASE et la valeur COMPARE pour chaque non-concordance. |
| Rows in BASE not in COMPARE (et inversement) | Lignes supplémentaires — des sujets présents dans un jeu de données mais absents de l’autre. Une différence de nombre, pas de valeur. |
| Columns / classes / attributes differ | Différences structurelles : une variable présente dans l’un et pas dans l’autre, ou la même variable typée différemment (integer vs numeric, factor vs character). |
Pour voir les lignes supplémentaires à l’œuvre, retirez deux sujets de la copie QC et recomparez :
adsl_qc_missing <- adsl_qc[1:(nrow(adsl_qc) - 2), ] # QC lost 2 subjects
diffdf(adsl_prod, adsl_qc_missing, keys = "USUBJID", suppress_warnings = TRUE)Differences found between the objects!
Summary of BASE and COMPARE
==================================================================
PROPERTY BASE COMP
------------------------------------------------------------------
Name adsl_prod adsl_qc_missing
Class "tbl_df, tbl, data.frame" "tbl_df, tbl, data.frame"
Rows(#) 306 304
Columns(#) 4 4
------------------------------------------------------------------
There are rows in BASE that are not in COMPARE !!
=============
USUBJID
-------------
01-718-1371
01-718-1427
-------------
diffdf() rapporte There are rows in BASE that are not in COMPARE et liste les deux USUBJID qui manquent au programme QC. C’est un échec différent d’une mauvaise valeur — les décomptes divergent — et le rapport vous dit exactement quels sujets traquer. (suppress_warnings = TRUE empêche simplement la différence d’être aussi levée comme un avertissement R ; laissez-le désactivé dans le code de production pour qu’un écart soit impossible à manquer.)
Attraper un écart : introduire un bug réaliste
Une validation qui ne fait que passer ne vous apprend rien. Le vrai savoir-faire est de lire un rapport en échec et d’en trouver la cause. Introduisons donc le bug de découpage le plus courant : un décalage d’une unité à la frontière — le programmeur QC écrit le seuil comme <= 65 au lieu de <= 64, déplaçant discrètement les personnes de 65 ans dans la mauvaise bande.
# QC derivation with a boundary bug: 65 wrongly grouped into "18-64"
adsl_qc_bug <- pharmaverseadam::adsl %>%
select(STUDYID, USUBJID, AGE) %>%
mutate(
AGEGR1 = case_when(
AGE < 18 ~ "<18",
AGE >= 18 & AGE <= 65 ~ "18-64", # BUG: should be <= 64
AGE > 65 ~ ">64" # BUG: should be > 64
)
)
diff <- diffdf(adsl_prod, adsl_qc_bug, keys = "USUBJID", suppress_warnings = TRUE)
diffDifferences found between the objects!
Summary of BASE and COMPARE
==================================================================
PROPERTY BASE COMP
------------------------------------------------------------------
Name adsl_prod adsl_qc_bug
Class "tbl_df, tbl, data.frame" "tbl_df, tbl, data.frame"
Rows(#) 306 306
Columns(#) 4 4
------------------------------------------------------------------
Not all Values Compared Equal
=============================
Variable No of Differences
-----------------------------
AGEGR1 4
-----------------------------
======================================
VARIABLE USUBJID BASE COMPARE
--------------------------------------
AGEGR1 01-707-1206 >64 18-64
AGEGR1 01-709-1309 >64 18-64
AGEGR1 01-715-1107 >64 18-64
AGEGR1 01-715-1319 >64 18-64
--------------------------------------
Le rapport est maintenant précis : Not all Values Compared Equal, AGEGR1 a 4 différences, et la table les nomme — quatre sujets où BASE (la production) dit >64 mais COMPARE (notre QC) dit 18-64. diffdf() ne nous a pas seulement dit que la dérivation est erronée ; il nous a dit quels sujets et comment.
Vous pouvez transformer cela en un verdict oui/non avec diffdf_has_issues(), et extraire les lignes fautives de la table avec diffdf_issuerows() :
diffdf_has_issues(diff) # TRUE -> not validated[1] TRUE
diffdf_issuerows(adsl_qc_bug, diff) # the exact rows flagged USUBJID STUDYID AGE AGEGR1
1 01-707-1206 CDISCPILOT01 65 18-64
2 01-709-1309 CDISCPILOT01 65 18-64
3 01-715-1107 CDISCPILOT01 65 18-64
4 01-715-1319 CDISCPILOT01 65 18-64
Chaque sujet signalé a 65 ans — la signature d’un bug de frontière. Le diagnostic est immédiat : le seuil est décalé d’une unité. >64 signifie « plus de 64 ans », donc 65 appartient à >64, mais <= 65 l’a balayé dans 18-64. C’est exactement le type d’erreur que la double programmation existe pour attraper : les programmeurs de production et de QC ont fait des choix de frontière différents, et le désaccord l’a fait surgir. Si les deux avaient copié le même code, tous deux porteraient le même bug et le diff serait faussement propre.
Corriger, relancer et atteindre zéro
Corrigez la frontière — <= 64 et > 64 — et relancez la comparaison.
adsl_qc_fixed <- pharmaverseadam::adsl %>%
select(STUDYID, USUBJID, AGE) %>%
mutate(
AGEGR1 = case_when(
AGE < 18 ~ "<18",
AGE >= 18 & AGE <= 64 ~ "18-64", # fixed
AGE > 64 ~ ">64" # fixed
)
)
diffdf(adsl_prod, adsl_qc_fixed, keys = "USUBJID")No issues were found!
No issues were found! — retour à zéro. C’est toute la boucle du QC par double programmation : comparer → diagnostiquer → corriger → relancer → zéro. Vous la répétez jusqu’à ce que diffdf() ne trouve rien, et c’est seulement alors que la variable dérivée est validée. La discipline n’est pas « l’exécuter une fois et espérer » ; c’est « itérer jusqu’à ce que la vérification indépendante soit silencieuse ».
Affiner la comparaison : clés, tolérance et types
Trois arguments gèrent les situations qui font trébucher un premier diff.
keys doit être unique. La comparaison par clé ne fonctionne que si la clé identifie une seule ligne. Pointez diffdf() vers une colonne non unique et il s’arrête plutôt que de deviner :
diffdf(adsl_prod, adsl_qc_fixed, keys = "AGE") # AGE is NOT unique per subjectError:
! BY variables in BASE do not result in unique observations
L’erreur — BY variables … do not result in unique observations — signifie que votre clé est erronée, pas vos données. Pour ADaM, la clé est ce qui rend une ligne unique : USUBJID pour un jeu de données au niveau du sujet comme ADSL, et USUBJID + PARAMCD + AVISIT (et souvent plus) pour un jeu de données de type Basic Data Structure (BDS) avec plusieurs lignes par sujet.
tolerance gère la virgule flottante. Les dérivations numériques concordent rarement au dernier bit — la même formule calculée de deux façons peut différer d’un epsilon machine. Laissé strict, diffdf() signale une différence si petite que les valeurs imprimées semblent identiques ; une petite tolerance l’absorbe :
base_num <- data.frame(USUBJID = c("X1", "X2"), BMI = c(24.500000, 27.2))
comp_num <- data.frame(USUBJID = c("X1", "X2"), BMI = c(24.500001, 27.2))
diffdf(base_num, comp_num, keys = "USUBJID", suppress_warnings = TRUE) # strict: flags X1Differences found between the objects!
Summary of BASE and COMPARE
====================================
PROPERTY BASE COMP
------------------------------------
Name base_num comp_num
Class data.frame data.frame
Rows(#) 2 2
Columns(#) 2 2
------------------------------------
Not all Values Compared Equal
=============================
Variable No of Differences
-----------------------------
BMI 1
-----------------------------
==================================
VARIABLE USUBJID BASE COMPARE
----------------------------------
BMI X1 24.5 24.5
----------------------------------
diffdf(base_num, comp_num, keys = "USUBJID", tolerance = 1e-4) # tolerant: cleanNo issues were found!
Réglez tolerance sur la précision que l’analyse exige réellement — assez serrée pour attraper une vraie erreur numérique, assez lâche pour ignorer le bruit d’arrondi.
Le typage strict attrape les dérives de type silencieuses. Par défaut, diffdf() traite une colonne integer et une colonne numeric comme différentes, même quand les valeurs concordent — parce qu’un changement de type dans une variable ADaM est lui-même un constat. Quand une conversion est réellement acceptable, relâchez-la avec strict_numeric = FALSE (ou strict_factor = FALSE pour factor-vs-character) :
base_typ <- data.frame(USUBJID = c("X1", "X2"), AVAL = as.integer(c(1, 2)))
comp_typ <- data.frame(USUBJID = c("X1", "X2"), AVAL = as.numeric(c(1, 2)))
diffdf(base_typ, comp_typ, keys = "USUBJID", suppress_warnings = TRUE) # flags class mismatchDifferences found between the objects!
Summary of BASE and COMPARE
====================================
PROPERTY BASE COMP
------------------------------------
Name base_typ comp_typ
Class data.frame data.frame
Rows(#) 2 2
Columns(#) 2 2
------------------------------------
There are columns in BASE and COMPARE with different classes !!
==================================
VARIABLE CLASS.BASE CLASS.COMP
----------------------------------
AVAL integer numeric
----------------------------------
diffdf(base_typ, comp_typ, keys = "USUBJID", strict_numeric = FALSE) # allows the castNo issues were found!
Par défaut, restez strict. Ne relâchez une vérification que lorsque vous avez décidé que la différence est acceptable — jamais pour faire taire un diff tenace.
Demandez à Prova « comment QC mon ADSL par rapport à une copie de production avec diffdf, avec USUBJID comme clé et en tolérant de minuscules différences numériques ? » — elle répond en s’appuyant sur les leçons de ce pilier, avec du code diffdf exécutable que vous pouvez essayer sur les données pharmaverse d’exemple. The runtime is the judge. Demander à Prova →
Problèmes fréquents
diffdf() signale chaque ligne comme différente. Vous avez comparé sans clés, donc diffdf() s’est rabattu sur la correspondance par numéro de ligne, et les deux jeux de données sont triés différemment. Passez toujours keys = les colonnes qui rendent une ligne unique (USUBJID, ou USUBJID + PARAMCD + AVISIT pour un jeu de données BDS). L’ordre des lignes cesse alors d’importer — la clé fait la correspondance.
« BY variables … do not result in unique observations. » Votre clé n’est pas unique dans l’un des jeux de données. Sur un jeu de données au niveau du paramètre, USUBJID seul ne suffit pas — un sujet a plusieurs lignes. Ajoutez les variables qui les distinguent (PARAMCD, AVISIT, ATPT) jusqu’à ce que la clé identifie exactement une ligne, ou vérifiez le doublon que la clé vient d’exposer.
Une variable numérique qui devrait concorder est signalée en continu. Arrondi en virgule flottante. Les valeurs imprimées semblent souvent identiques tout en différant bien en dessous de la décimale affichée. Réglez tolerance sur la précision dont l’analyse a besoin (par exemple tolerance = 1e-6) plutôt que de courir après une différence d’epsilon machine.
Une variable est signalée comme une différence de classe, pas de valeur. Un jeu de données l’a en integer ou factor, l’autre en numeric ou character. C’est réel — un changement de type est un constat — mais si la conversion est intentionnelle, autorisez-la avec strict_numeric = FALSE ou strict_factor = FALSE. Décidez d’abord qu’elle est acceptable ; ne relâchez pas la rigueur juste pour faire taire le rapport.
Questions fréquentes
La double programmation (programmation indépendante) est une méthode de contrôle qualité où deux programmeurs produisent indépendamment la même sortie — un jeu de données dérivé ou une table — à partir de la même spécification mais avec un code séparé, et les deux résultats sont comparés. S’ils concordent exactement, la sortie est validée, car deux implémentations indépendantes qui concordent constituent une preuve solide que la spécification a été implémentée correctement. Le programmeur QC doit être une personne différente du programmeur de production.
Utilisez le package diffdf : diffdf(base, compare, keys = "USUBJID"). Il rapporte chaque différence de valeur, ligne supplémentaire, colonne supplémentaire et incohérence de type entre les deux data frames — l’équivalent R du PROC COMPARE de SAS. Passez keys = les colonnes qui identifient une ligne de façon unique pour que la comparaison associe les enregistrements par identité plutôt que par position.
Cela signifie que les deux jeux de données sont identiques sur chaque ligne et colonne fondées sur la clé, dans les limites de la tolérance et de la rigueur configurées — aucune différence de valeur, aucune ligne supplémentaire, aucune incohérence de type. Dans un QC par double programmation, c’est la condition de réussite : la re-dérivation indépendante a reproduit exactement le jeu de données de production, donc la dérivation est validée.
Trois causes habituelles. (1) Pas de keys, donc les lignes sont associées par position et un ordre de tri différent signale tout — passez keys =. (2) L’arrondi en virgule flottante fait différer les valeurs numériques en dessous des décimales imprimées — réglez une tolerance. (3) Une variable a la même valeur mais un type différent (integer vs numeric, factor vs character) — diffdf() le signale par conception ; relâchez avec strict_numeric = FALSE / strict_factor = FALSE seulement si la conversion est acceptable.
Ce n’est pas un mandat réglementaire fixe — les promoteurs peuvent valider les jeux de données dérivés avec d’autres méthodes de QC documentées. Mais la double programmation indépendante est la pratique industrielle établie de longue date, offrant la plus haute assurance pour valider les jeux de données d’analyse et les sorties qui entrent dans une soumission, c’est pourquoi elle reste le choix par défaut et le schéma à apprendre en premier.
Testez vos connaissances
L’ADSL de production porte TRTDURD (durée de traitement en jours). Supposons que la spécification demande aussi un indicateur TRTDURFL = "Y" quand un sujet a été traité pendant au moins 28 jours, sinon "N". Re-dérivez TRTDURFL de manière indépendante à partir de pharmaverseadam::adsl$TRTDURD, puis utilisez diffdf() pour le QC par rapport à une version de production délibérément boguée qui a utilisé > 28 au lieu de >= 28. Quels sujets diffdf() signale-t-il, et pourquoi ?
Construisez l’indicateur QC correct avec mutate(TRTDURFL = if_else(TRTDURD >= 28, "Y", "N")), en clé sur la comparaison sur "USUBJID". Le bug de frontière (> 28) ne diverge de la règle correcte (>= 28) que pour les sujets dont la durée est exactement de 28 jours — ceux-là basculent de "Y" à "N". diffdf() les nommera.
library(dplyr)
library(pharmaverseadam)
library(diffdf)
# Correct QC derivation: at least 28 days
qc <- pharmaverseadam::adsl %>%
select(STUDYID, USUBJID, TRTDURD) %>%
mutate(TRTDURFL = if_else(TRTDURD >= 28, "Y", "N"))
# Buggy "production" version: strictly greater than 28
prod <- pharmaverseadam::adsl %>%
select(STUDYID, USUBJID, TRTDURD) %>%
mutate(TRTDURFL = if_else(TRTDURD > 28, "Y", "N"))
diffdf(prod, qc, keys = "USUBJID")diffdf() rapporte des différences de TRTDURFL uniquement pour les sujets avec TRTDURD == 28 : la règle correcte les signale "Y" (au moins 28 jours inclut 28), la règle boguée > 28 les signale "N". C’est la même erreur de frontière < vs <= que le bug de bande d’âge — et exactement pourquoi une seconde dérivation indépendante, avec sa propre lecture de « au moins », attrape ce qu’un programme copié ne ferait pas.
keys = "USUBJID" à diffdf() ?
A. Il trie les deux jeux de données par USUBJID avant de comparer les valeurs ligne par ligne. B. Il associe les lignes entre les jeux de données par identité de sujet, de sorte que l’ordre des lignes n’a plus d’importance. C. Il compare seulement la colonne USUBJID et ignore les autres.
B. keys indique à diffdf() quelles colonnes identifient une ligne de façon unique, et il associe les enregistrements des deux jeux de données par cette identité. L’ordre des lignes devient sans importance (la comparaison en ordre mélangé ci-dessus s’est quand même validée proprement), mais la clé doit être unique dans les deux jeux de données — une clé non unique lève l’erreur « do not result in unique observations ». A est faux car il s’agit d’une association par identité, et non d’un tri positionnel ; C est faux car toutes les colonnes partagées sont quand même comparées — la clé contrôle seulement comment les lignes sont alignées.
Conclusion
La double programmation transforme un « le code semble correct » subjectif en un test objectif et reproductible : construire le jeu de données dérivé une seconde fois, indépendamment, et laisser diffdf() décider si les deux concordent. La mécanique est modeste — diffdf(base, compare, keys = ...), lire le rapport, régler tolerance et la rigueur pour correspondre à l’analyse — mais la discipline est l’essentiel. Vous re-dérivez à partir de la spécification plutôt que de copier le code de production (pour que les deux programmes puissent diverger et exposer un bug), vous comparez sur une vraie clé (pour que l’ordre des lignes ne puisse pas vous tromper), et vous itérez comparer → diagnostiquer → corriger → relancer jusqu’à ce que le rapport soit silencieux. Ce rapport silencieux, à zéro différence, est ce qui permet à un jeu de données clinique dérivé d’être digne de confiance jusqu’à la soumission — non parce qu’il s’est exécuté, mais parce qu’il a été prouvé correct de manière indépendante.
Cette leçon est reproductible : chaque résultat de cette page a été produit par le code montré — copiez n’importe quel bloc et exécutez-le pour les reproduire. The runtime is the judge.
Leçons connexes
- Créer un ADSL en R avec admiral — construire le jeu de données au niveau du sujet même que cette leçon valide, y compris la dérivation d’
AGEGR1que nous QC ici. · Construire le jeu de données d’analyse de laboratoire (ADLB) et la structure BDS — un jeu de données au niveau du paramètre dont la clédiffdf()estUSUBJID+PARAMCD+AVISIT, et nonUSUBJIDseul. · Les standards CDISC : CDASH, SDTM, ADaM, Define-XML — où les jeux de données ADaM que vous QC se situent dans le pipeline de soumission. · Le passage de SAS à R pour la programmation clinique — pourquoidiffdf()est la réponse R auPROC COMPAREde SAS. - Où cela s’inscrit : construire l’ADaM avec admiral → le valider (double programmation avec diffdf — vous êtes ici) → assertions défensives, reproductibilité et qualification des packages. La validation est l’étape qui se tient entre un jeu de données dérivé et la soumission.
Réutilisation
Citation
@online{2026,
author = {},
title = {Double programmation en R : valider un jeu ADaM avec diffdf},
date = {2026-07-01},
url = {https://www.datanovia.com/learn/pharma-clinical/05-validation-qc/qc-double-programming-diffdf},
langid = {fr}
}