Des FP32 pas standard

Vous avez peut-être l’habitude des nombres à virgule flottantes 32 bits IEEE-754 avec 23 bits de mantisse (24 bits effectif grace à la normalisation), 8 bits d’exposant et 1 bit de signe. La mantisse est un peu courte, mais il y a beaucoup d’usages pour lesquels ça reste tolérable.

Mais, savez-vous que le logiciel statistique SAS n’utilise pas cette représentation lorsqu’on stocke une valeur numérique de 4 octets dans un dataset, sous Windows ou Linux, sur PC ? Il utilise une mantisse 20 bits (21 bits effectif), 11 bits d’exposant et 1 bit de signe. Cela lui permet d’avoir la même étendue d’exposant qu’avec un FP64 IEEE-754.

En conséquence, dès qu’on dépasse les deux millions environ, on ne peut plus représenter un nombre entier de manière exacte:

data test;                                                                                                                              
        length x1 4 x2 4 x3 4;                                                                                                          
        x1=2097151; /* 2 puissance 21 moins 1 */                                                                                        
        x2=x1+1;                                                                                                                        
        x3=x2+1;                                                                                                                        
run;                                                                                                                                    
proc print data=test;run;

Va afficher le tableau:

Obsx1x2x3
1209715120971522097152

SAS gère aussi des FP24 avec seulement 12 bits de mantisse (13 effectifs), ce qui limite la représentation exacte des nombres entiers à 8192.

En pratique, la représentation dépend de la plateforme. Comme SAS a un format de fichier interne incompatible entre les plateformes, incapable même de communiquer entre la plateforme PC 32 bits et 64 bits, les fichiers d’une plateforme ne peuvent de toute façon pas être ouverts sur une autre plateforme; il n’y a pas non plus d’utilitaire de conversion avec SAS.

Comme SAS ne gère pas les nombres entiers, mais seulement les nombres à virgule flottante, je vous conseille d’avoir bien conscience de ces limites avant de décider du nombre d’octets à allouer aux valeurs. Si vous ne voulez pas avoir à réfléchir, utilisez toujours les 8 octets de la représentation par défaut.

Qualité des données

Pour faire suite au billet sur l’absence de définition consensuelle d’étude prospective. Le débat sémantique sur le terme cache les vrais problèmes de qualité de données.

Ainsi, je vous propose de vous poser les questions suivantes lorsque vous évaluez la qualité d’une donnée :

  1. La donnée a-t-elle été renseignée de manière systématique (faible nombre de données manquantes) ; sinon, le manquement des données est-il fortement corrélé à la valeur de la donnée.
  2. La donnée a-t-elle été évaluée de manière standardisée ou selon la pratique courante ; en cas d’évaluation subjective, les deux options peuvent chacune avoir un intérêt (meilleure validité interne ou meilleure validité externe)
  3. À quel point la donnée est-elle évaluateur-dépendant ?
  4. La donnée est-elle soumise à un biais de mémorisation ?
  5. La donnée est-elle soumise à des biais d’évaluation par absence d’aveugle d’une exposition ou d’une intervention
  6. De bonnes pratiques de saisie, monitoring et gestion des données ont-elles été mises en place afin de garantir l’absence de l’erreur de saisie ou de recopie entre la donnée source et la donnée finale qui a été présentée dans l’article publié
  7. Le monitoring et la gestion de données ont-ils montré des erreurs fréquentes : écarts entre données saisies et données sources, incohérences des données saisies.

L’élément N°7 est en pratique impossible à retrouver dans les articles publiés, parce qu’il n’y a pas de paragraphe « gestion de données » dans les articles biomédicaux. Il est pourtant important de limiter au minimum le nombre de recopies, faire double saisie aux étapes clés ; il faudrait toujours garder un audit trail (historique des modifications) de la base de données ou des bases de données. Il faut savoir que la durée d’hospitalisation, qui est une information médico-administrative que l’on peut extraire de manière fiable à partir des données du département d’information médicale, est parfois recopiée depuis des comptes-rendus, avec des erreurs et des données manquantes !

J’ai vu des base de données Excel évolutives et immondes : tout ce qui pouvait être incohérent l’était de manière massive même pour des choses considérées comme « fiables », comme l’anatomie pathologique. C’est-à-dire, que la moitié des « Adénocarcinomes in situ » étaient de stade T1a (alors que ça devrait être Tis), avec le détail du T, du N et du M qui n’était pas en cohérence avec le stade TNM synthétique ; par ailleurs, selon que l’on analysait la « version finale N°1 » ou la « version finale N°2 » du fichier Excel, on avait des cellules qui se vidaient (apparition d’une donnée manquante), d’autres qui se remplissaient, des incohérences qui apparaissaient, d’autres qui disparaissaient. Sans compter les problèmes de décrépitude des fichiers informatiques : les caractères accentués qui passent à du double ou triple UTF-8 (Modéré qui devient Modéré puis Modéré) ou qui au contraire, passe de l’UTF-8 à l’ISO-8859-1 de manière répétée, conduisant à la disparition des caractères accentués ou à leur remplacement par des points d’interrogation.

Et puis il y a les grands classiques d’Excel et des mauvaises pratiques de gestion des données associées :

  1. Les données qui sont saisies dans la colonne voisine à la colonne dans lesquelles elles sont censées être saisies ;
  2. Les colonnes qui sont triées mais en oubliant d’étendre la sélection aux autres colonnes, de telle sorte qu’il y a une perte totale de la correspondance des lignes entre les colonnes, ce qui décorrèle complètement les données ;
  3. Les données qui sont interprétées à tort comme des dates alors qu’elles n’ont rien à voir
  4. Les dates qui se décalent de quatre ans à cause d’un copier-coller depuis un tableur créé sur Macintosh ;
  5. Les inversions mois/jours des dates à cause du mélange des dates américaines (MM/JJ/AAAA) et françaises (JJ/MM/AAAA) ;
  6. Les blocs de cellules qui sont copiées depuis un autre tableur Excel, mais les lignes n’étant pas dans le même ordre, les données sont interverties entre les lignes ;
  7. Le suppression des lignes des patients exclus qui rend impossible la création d’un flow chart ; à la fin, on ne sait même plus pourquoi certains patients avaient été exclus ;
  8. Les multiples corrections de données (p.e. erreurs de saisies corrigées), mais avec une perte de la trace des modifications et de leur raison avec un travail en parallèle sur plusieurs fichiers : je crois que le tableur N°1 est « bon » pour les colonnes 1 à 5 et le tableur N°2 est « bon » pour les colonnes 6 à 8… à moins que ce ne soit l’inverse ;
  9. Les corrections qui portent sur certaines données mais pas d’autres : « j’ai corrigé l’IMC mais j’ai gardé le mauvais poids… C’est l’IMC qu’il faut regarder… sauf pour le patient N°8 dont j’ai corrigé le poids mais pas l’IMC… » ;
  10. Les données redondantes incohérentes telles que : « ah, la colonne groupe ne doit pas être prise en compte parce que j’ai arrêté de la remplir au bout d’un moment : il faut juste regarder la couleur des lignes : en vert ce sont les malades suivis jusqu’au bout, en violet, ce sont les malades perdus de vue et en orange ce sont les sujets sains ».

Et puis, il y a ce qui se passe durant la rédaction du rapport d’analyse statistique : cette fois-ci, le statisticien est à blâmer. Le logiciel SAS, quand on lui demande de sortir le pourcentage de A (variable binaire) parmi B (variable binaire), en affiche 27 différents, parce qu’il affiche un tableau de contingence 3×3 (OUI, NON, TOTAL) avec 3 pourcentages (en ligne, en colonne, sur total général) par cellule. Pour ceux qui recopient à la main le pourcentage dans le rapport d’analyse, il faut choisir le seul bon pourcentage sur les 27 présentés. Ensuite, il y a la pratique de remplissage des tableaux cellule par cellule, copiant un à un les résultats des sorties logicielles dans le tableau. Il y a même la pratique d’inversion des risques relatifs ou odds ratio, présentés à l’envers par le logiciel, à la calculatrice. Enfin, lors d’une mise à jours de la base de données, il est nécessaire de recalculer toutes les statistiques du rapport. L’idéal est de générer le rapport d’analyse entièrement à partir du script (p.e. avec R markdown), mais ce genre de pratique n’est pas rapporté dans les articles.

Ensuite, il y a les erreurs de recopie du rapport d’analyse dans l’article : avec les mauvais doigts, il est même possible de copier-coller un tableau complet en rajoutant des erreurs en plein milieu du tableau. Bien sûr, le texte est l’endroit où il y a le plus d’erreurs de recopie possible, notamment sur le contexte entourant un chiffre. Par exemple, la phrase « le pourcentage de diabétiques parmi les insuffisants rénaux de stade III qui ont survécu au-delà de deux ans de suivi était estimé à X% », deviendra « le pourcentage d’insuffisants rénaux parmi les diabétiques était de X% ».

Il est dommage que le travail des vrais gestionnaires de données, techniciens d’étude clinique et attachés de recherche clinique ne soit pas valorisé dans les articles ; il faudrait aussi publier les rapports d’analyse statistique.

Facteurs croisés et modèles à effets mixtes

Un billet pour vous décrire un joli problème statistique auquel les modèles à effets mixtes sont censés répondre, mais qui malheureusement, n’aident pas trop ; sauf si on les utilise vraiment très bien.

Il s’agit d’une étude portant sur l’évaluation d’une intelligence artificielle (https://pubs.rsna.org/doi/10.1148/radiol.2021203886) assistant le travail de lecture radiographique. L’intelligence artificielle détecte de potentielles fractures osseuses sur des radiographies conventionnelles et les surligne avec un carré blanc, aidant ainsi le lecteur à les identifier. L’objectif de l’étude était de montrer que les lecteurs (radiologues et urgentistes de divers niveaux d’expérience) gagnaient en performance diagnostique grace à l’intelligence artificielle. L’analyse principale avait pour objectif de montrer une supériorité sur la sensibilité et une non-infériorité sur la spécificité (au seuil -3%). Pour être complètement transparent, il existe un conflit d’intérêt puisque la plupart des auteurs appartiennent à l’entreprise qui commercialise l’intelligence artificielle. En tant que statisticien j’étais néanmoins indépendant et n’ai reçu aucune rémunération.

La méthodologie a consisté à sélectionner 600 examens, dont 300 avec fracture et 300 sans fracture. Le gold standard a été créé par la lecture de trois experts du domaine. Les 600 examens ont été subdivisés aléatoirement en deux jeux de données (A et B) équilibrés de 300 examens, dont 150 avec et 150 sans fracture. Ensuite, 12 lecteurs de différents niveaux d’expérience (indépendants des experts du gold standard), ont lu les deux jeux de données, dont un des deux avec aide de l’IA et l’autre sans l’aide de l’IA. Sur les 12 lecteurs, 6 ont lu le jeu A avec l’aide de l’IA et le jeu B sans aide et les 6 autres ont lu le jeu B avec l’aide de l’IA et le jeu A sans aide.

Une nouvelle étude, avec la version suivante de l’IA (en cours de reviewing) utilise une méthodologie similaire, mais cette fois-ci avec 24 lecteurs et 480 examens. Cette fois-ci les 480 examens sont tous lus deux fois, avec et sans IA, avec une période de wash-out évitant une mémorisation des images. C’est plutôt de cette étude dont nous parlerons car certains phénomènes sont plus visibles avec celle-ci. Il n’y avait aucune donnée manquante. Tout était parfaitement équilibré (480 examens pour chacun des 24 lecteurs et 24 lecteurs pour chacun des examens).

On souhaite généraliser les résultats à l’ensemble des examens possibles, en supposant que l’échantillon d’examens radiologiques est représentatif, ainsi qu’à l’ensemble des médecins susceptibles de lire ces examens. En considérant qu’on s’intéresse aux moyennes et différences de moyennes des sensibilités et spécificités, on s’aperçoit qu’il est extrêmement simple de faire une estimation ponctuelle de l’effet de l’aide par l’IA. Considérons la sensibilité, égale à la moyenne de la variable binaire « vrai positif » (qui vaut 0 ou 1) dans les 240 examens avec fracture. Toutes les manières intuitives de calculer l’effet conduisaient à la même estimation ponctuelle, du fait de l’équilibre de taille des groupes:

  1. Calculer l’effet de l’IA pour chacun des lecteurs, puis faire la moyenne des 24 effets obtenus
  2. Calculer l’effet de l’IA pour chacun des examens, puis faire la moyenne des 240 effets obtenus
  3. Calculer la sensibilité avec l’IA sur les 240×24 observations sans aide de l’IA, puis calculer la sensibilité sur les 240×24 observations avec aide de l’IA, puis faire la différence de ces deux sensibilités, en ignorant complètement les colonnes « numéro d’examen » et « numéro de lecteur »

Ces trois estimateurs conduisent à la même estimation, parce que tous les lecteurs lisent le même nombre d’examens et tous les examens sont lus le même nombre de fois. Par ailleurs, ils ont tous des propriétés de pondération uniforme souhaitables. On n’a pas vraiment raison de donner plus de poids à certains lecteurs qu’à d’autres (même si après réflexion, on pourrait imaginer donner plus de poids à ceux qui ont un plus grand volume d’activité, du fait d’un impact de santé publique plus important). On n’a pas non plus raison de donner un plus grand poids à certains examens qu’à d’autres. Le design équilibré est intéressant parce qu’il fournit une précision statistique optimale pour cette moyenne non pondérée, en négligeant les problèmes d’hétéroscédasticité. En réalité, il existe des modèles foireux (modèles linéaires généralisés à effets mixtes) qui vont exploiter cette hétéroscédasticité, mais vu qu’ils ne convergent pas, je ne m’étendrai pas trop dessus.

Donc, la question ne porte pas vraiment sur la statistique en elle même, mais seulement sur la manière d’estimer son incertitude.

Description des fluctuations : modèle N°1

Il existe alors deux sources de fluctuations d’échantillonnage : les examens (n=480) et les lecteurs (n=24). Ces deux sources sont croisées, avec les problèmes suivants à prendre en compte:

  1. Il existe une variance de la difficulté d’un examen modélisable comme la sensibilité moyenne pour cet examen sur un nombre virtuellement infini de lecteurs. Notation (1|examen) dans le package R ‘lme4’.
  2. Il existe une variance de la compétence de base (sans IA) d’un lecteur, modélisable comme la sensibilité moyenne de ce lecteur sur un nombre virtuellement infini d’examens. Notation (1|lecteur)
  3. Il existe une interaction statistique entre les deux, c’est-à-dire, qu’un examen facile pour un lecteur peut être difficile pour un autre et vice versa, parce que différents lecteurs ne tombent pas dans les mêmes « pièges » et qu’un examen est difficile parce qu’il est piège. Notation (1|paste(lecteur,examen))
  4. Il existe une variance de l’effet de l’IA selon le lecteur ; c’est-à-dire, que l’IA peut faire gagner beaucoup de sensibilité à certains lecteurs alors qu’elle n’apportera peu, voire rien à d’autres. Notamment, un lecteur dont la sensibilité est très élevée ne pourra presque rien gagner avec l’IA alors qu’un lecteur qui a une sensibilité mauvaise pourra progresser beaucoup, ou pas. Notation (aide|lecteur).
  5. Il existe une interaction entre l’effet moyen (sur une infinité de lecteurs) de l’IA et l’examen ; notamment, il est évident qu’une IA n’apportera rien du tout à un examen sur lequel elle ne détecte aucune fracture réelle alors qu’elle pourra apporter beaucoup sur un examen pour lequel elle détecte une fracture très difficile à voir. Notation (aide|examen).
  6. Il existe bien sûr, une variance résiduelle, c’est-à-dire, que le même examen, lu par le même lecteur, deux fois à un mois d’intervalle, aura un résultat différent. Il y a des effets contextuels dans ce résidu.
  7. Il existe une interaction entre l’effet de l’IA, le lecteur et l’examen ; c’est-à-dire que l’effet de l’IA (virtuellement moyenné sur des centaines de lectures à plusieurs mois d’intervalles pour neutraliser la variance résiduelle) pourra être particulièrement élevé pour un lecteur donné sur un examen donné ; notamment on peut imaginer que l’IA aura un effet maximal si, à la fois, le lecteur était fortement susceptible de rater la fracture sur l’examen, que l’IA surligne la fracture considérée et qu’en plus, le lecteur est fortement susceptible de faire confiance à l’IA ; en effet, beaucoup de lecteurs (notamment pour les fractures vertébrales) ne font pas confiance à l’IA quand bien même elle a raison sur la fracture qu’elle surligne. Notation (aide|paste(lecteur,examen))
  8. Il existe une covariance entre le N°2 et le N°4, a priori, fortement négative. C’est-à-dire que ceux qui sont les moins bons vont s’améliorer plus fortement en moyenne ; à l’opposé, un lecteur dont la sensibilité est de 100% ne pourra voir qu’une stabilité ou une dégradation avec l’aide de l’IA. Implicitement inclus dans la notation (aide|lecteur).
  9. Il existe une covariance entre le N°1 et le N°5 (même concept que pour la covariance entre le N°2 et le N°4 mais s’appliquant aux examens plutôt qu’aux lecteurs). Implicitement inclus dans la notation (examen|lecteur).
  10. Il existe une covariance entre le N°3 et le N°7 (même concept que la covariance entre le N°1 et le N°5 mais conditionnel au lecteur). Implicitement inclus dans la notation (aide|paste(lecteur,examen))

Toutes ces sources de fluctuations d’échantillonnage ne sont pas pure spéculation de ma part : j’ai même pu évaluer, de manière directe ou indirecte les huits premiers éléments que je décris, notamment l’existence de l’interaction IA×lecteur×examen (effet N°7).

Avant de parler des modèles à effets mixtes, je vais décrire des estimateurs d’intervalles de confiance beaucoup plus simples et génériques et, fortement ou faiblement biaisés.

Simplification : modélisation des deltas (modèle N°2)

Pour le sous-groupes des examens avec fracture, le jeu de données contient 240 examens × 24 lecteurs × 2 observations (avec et sans IA) soit 11520 observations. Pour chaque observation, on a une variable binaire « fracture détectée par le lecteur » 1 ou 0. Comme on s’intéresse à la différence de moyennes de cette variable avec IA versus sans IA, on peut commencer par calculer les différences appariées pour chacune des 240×24 combinaisons (examen, lecteur), conduisant ainsi à un échantillon de 240×24=5760 observations. Cette différence appariée (delta(IA)) aura trois valeurs possibles : -1, 0 ou 1. La moyenne de la variable delta(IA) sur ces 5760 observations est alors l’estimateur de l’effet moyen de l’IA. Ces différences appariées vont simplifier beaucoup les choses, puisqu’au lieu de prendre en compte à chaque fois trois effets (la sensibilité sans IA, le gain de sensibilité par l’IA et la covariance entre les deux) pour trois facteurs (le lecteur, l’examen et la combinaison lecteur×examen) on n’a plus qu’à prendre en compte un seul effet, toujours pour les trois facteurs considérés.

Il reste alors à prendre en compte:

  • A : Une variance inter-lecteur du delta(IA) (prenant implicitement en compte les effets N°2, N°4 et N°8 du modèle précédent)
  • B : Une variance inter-examen du delta(IA) (effets N°1, N°5 et N°9 du modèle précédent)
  • C : Une interaction lecteur-examen sur le delta(IA) (effets N°3, N°7 et N°10 du modèle précédent)
  • D : Un résidu

En réalité, seuls les effets N°4, N°5 et N°7 du modèle précédent ont réellement à être modélisés, puisque les autres n’ont aucun impact sur la statistique.

Méthode N°1 : Student

Pour commencer, on peut calculer, pour chacun des 24 lecteurs, le gain de sensibilité attribuable à l’intelligence artificielle, comme la différence de sensibilité avec vs sans IA. On peut ensuite utiliser la méthode de Student à un seul échantillon, pour en estimer la moyenne. C’est équivalent à un test de Student sur séries appariées, avec n=24 paires d’observations avec vs sans IA. Cette méthode néglige la covariance entre les gains des différents lecteurs attribuable au fait que les mêmes examens sont lus. Néanmoins, cette méthode est acceptable si on considère que l’échantillon de 480 examens est suffisamment grand pour que l’erreur d’estimation du gain par l’IA de chacun des lecteurs (erreur type sur le delta(IA)), est négligeable en comparaison à la variance inter-lecteur de ce gain. On observait alors un effet moyen de l’IA sur la sensibilité à +10,4% (IC 95%: 7,7% à 13,1%), avec donc, une largeur d’intervalle de confiance à 5,4%.

Ce test de Student est nettement moins mauvais qu’il n’y paraît naïvement. Déjà, il bénéficie des différences appariées, et donc a tous les bénéfices du modèle N°2. Il prend évidemment en compte le A du modèle simplifié (variance inter-lecteur des deltas), mais aussi le B (variance inter-examen des deltas) car cette variance va être incluse dans la variance observée par le Student. En effet, pour un lecteur donné, avec n=240 deltas, cette variance inter-lecteur conduira à une légère fluctuation d’échantillonnage de la moyenne de ces 240 deltas, égale à la variance inter-examen divisée par 240, car les examens sont indépendants. Cela va indirectement se ressentir dans la variance du Student avec n=24 lecteurs, car le Student ne fait pas de shrinkage : la variance empirique du Student va à la fois contenir la vraie variance inter-lecteur mais aussi l’erreur de mesure de l’effet lecteur attribuable aux variances inter-examens qui va s’y additionner. Le Student prendra aussi en compte la covariance entre les lecteurs, attribuable au fait qu’ils ont observé les mêmes examens. En effet, cette covariance positive va se traduire par une diminution de la variance empirique du Student. Le problème, c’est que la variance empirique du Student va TROP fortement diminuer à cause de cette covariance. Pour caricaturer, si les lecteurs étaient parfaitement stéréotypés (variance inter-lecteur nulle et résidu nul), alors avec n=5 examens et n=100 lecteurs, la variance empirique de Student serait nulle bien qu’en réalité il existe de véritables fluctuations d’échantillonnage sur les n=5 examens. Enfin, l’effet C du modèle simplifié va tendre à diminuer la covariance entre les lecteurs attribuables au fait qu’ils évaluent les mêmes examens, et donc, va atténuer le problème de surcorrection de la covariance dont je viens de parler.

Si on se réfère au premier modèle, non simplifié, le Student fait quand même totalement disparaître les effets N°1, N°2, N°3, N°8, N°9 et N°10) grâce à l’usage des différences appariées du modèle simplifié. Ça prend en compte les effets N°4 et N°5 (A et B du modèle simplifié) mais pas bien la covariance qu’elles engendrent, même si, l’effet N°7 tendrait à atténuer le problème.

Méthode N°2 : Double bootstrap

Globalement, quand je ne sais pas comment estimer une incertitude, j’ai le réflexe de penser au boostrap. Néanmoins, le schéma du boostrap doit suivre le schéma d’échantillonnage ! Ceux qui m’avaient commandé les analyses avaient boostrappé les lignes d’observation (n=480×24×2 = 23040 observations) en ignorant complètement que les lecteurs étaient soumis aux fluctuations d’échantillonnage et que les 48 observations d’un même examen étaient corrélées ! Cela conduit à un intervalle de confiance nettement trop étroit : (IC95% : 8,8% à 12,0%) d’une largeur de 3,3%. L’intervalle n’est pas aussi étroit qu’on pourrait craindre parce qu’en ignorant la covariance intra-lecteur et intra-examen, ça surestime aussi la variance sur certains aspects ; en bref, ça biaise fortement dans des directions opposées.

Ici, on peut proposer un boostrap respectant les règles d’échantillonnage : on tire au hasard avec remise 240 examens parmi 240 (on se limite aux examens avec fracture pour le calcul de la sensibilité), puis on les renumérote de N°1 à N°240 (puisque les « doublons » artificiellement créés par la méthode de rééchantillonnage sont censés représenter des examens différents si on tirait au hasard depuis la population). Ensuite, dans cet échantillon, on tire au hasard 24 lecteurs parmi 24 avec remise, et on les renumérote de 1 à 24. On peut alors calculer la statistique de gain moyen de sensibilité attribuable à l’IA.

On peut faire ce double boostrap sur le jeu des 240×24×2 observations ou sur le jeu des différences appariées de 240×24 observations, proposé dans la section « Simplification : modélisation des deltas », mais les deux sont strictement équivalents. En effet, les 2 observations d’un même couple (lecteur,examen) resteront toujours soudés dans un même bloc durant les deux rééchantillonnages.

Le double bootstrap (le terme correct serait plutôt bootstrap de 2 facteurs croisés, mais j’aime bien double bootstrap) est particulièrement simple et rapide à réaliser informatiquement sous R sur 240×24 différences appariées. Il suffit de faire une matrice de 24 lignes et 240 colonnes (ou 240 lignes et 24 colonnes, peu importe). On peut rééchantillonner simultanément les lignes et les colonnes avec la syntaxe du logiciel R mat[indices_lignes, indices_colonnes]. Ce rééchantillonnage simultané est équivalent à un rééchantillonnage successif des lignes puis des colonnes (ou des colonnes puis des lignes). Enfin, en appliquant la fonction mean() à la matrice entière, on calcule la moyenne de toutes les cellules de la matrice : c’est l’estimation ponctuelle de l’effet de l’IA.

Le rôle des lecteurs et des examens est parfaitement symétrique dans cette méthodologie de double bootstrap. Le bootstrap va prendre empiriquement en compte la structure de corrélation dans toute sa complexité. Cela semble simple intuitif et non biaisé au premier abord, mais en réalité, c’est un plus biaisé qu’il n’y paraît. Il y a une tendance à surestimer les fluctuations d’échantillonnage. Même si le rôle des examens et des lecteurs est symétrique, considérons la matrice de 24 lignes × 240 colonnes, contenant dans chaque cellule, un delta (gain de sensibilité sur cet examen pour ce lecteur par l’IA) et considérons des scenarii virtuels qui vont beaucoup nous aider à comprendre ce qu’il se passe.

Comme précisé précédemment, les 24 moyennes des 24 lignes sont corrélées parce que les 24 lecteurs observent les mêmes examens. Supposons dans un premier temps que cette corrélation soit nulle parce que tous les examens auraient la même difficulté et l’IA fournirait le même gain pour tous les examens : il y aurait juste un effet lecteur et un résidu mais pas d’effet examen. Même si c’est probablement faux, on peut imaginer qu’on puisse être plus ou moins proche de cette situation. Dans ce contexte d’absence d’effet examen, le simple bootstrap des lignes (ou l’intervalle de confiance de Student sur les n=24 lignes) ne sera pas biaisé du tout : il prendra à la fois en compte la variance inter-lecteur (varL) et la variance résiduelle déjà atténuée par n=240 mesures (varR/240) : ces deux variances (varL et varR/240) s’additionneront sous forme de la variance empirique des 24 moyennes des 24 lignes. Comme il n’y a aucune covariance entre ces lignes, l’estimateur de la variance n’est pas biaisé. Si on double boostrappe, on va surestimer la variance, parce que la variance résiduelle (divisée par 240) sera ajoutée une seconde fois lors du rééchantillonnage des colonnes ! La cause profonde de ce double comptage de la variance résiduelle est explicable par l’absence de shrinkage fait par le boostrap des lignes, c’est-à-dire qu’elle n’enlève pas la varR/240. Si on échantillonnait les 24 lignes à partir de la population plutôt que de faire un rééchantillonnage, on aurait la variance inter-lecteur varL, mais il ne s’y ajouterait pas varR/240 ; c’est alors que le rééchantillonnage des colonnes ajouterait de manière tout à fait juste ce varR/240 qui participe quand même aux fluctuations d’échantillonnage. De la même manière, comme il n’y a pas de shrinkage de la variance inter-examen, la varR/24 lui est artificiellement additionnée et cette varR/24 est comptée deux fois.

L’intérêt principal du double bootstrap réside néanmoins dans le scenarii où on aurait une forte variance de l’effet de l’IA selon l’examen, conduisant à une covariance non négligeable des 24 moyennes correspondant aux 24 lignes. Le double bootstrap prend en compte les effets les plus fins générant cette covariance, y compris l’interaction entre l’effet de l’IA, l’examen et le lecteur (effet N°7 du premier modèle et effet C du modèle simplifié).

Le double boostrap percentile conduisait à l’intervalle de confiance 10,4% (IC 95%: 6,8 à 14,1%) alors que le double bootstrap normal avec approximation à la loi de Student à 23 degrés de liberté conduisait à l’intervalle de confiance 10,4% (6,6% à 14,2%) avec une largeur d’intervalle à 7,57%, plus élevée que celle du Student naïf mais peut-être trop élevée.

Méthode N°3 : hybride Student/boostrap

La méthode de Student est biaisée parce qu’elle ne prend pas en compte la covariance entre les moyennes des lignes de la matrice de 24 lignes et 240 colonnes. Peut-on estimer et prendre en compte cette covariance ? Oui ! On peut estimer cette covariance empiriquement en rééchantillonnant les colonnes, sans toucher aux lignes. Une opération de boostrap correspond à un rééchantillonnage des 240 colonnes, suivi d’un calcul de 24 moyennes (de chacune 240 observations). Avec dix mille boostrap, on calcule la matrice de variance-covariance des 24 moyennes, c’est-à-dire, une matrice 24×24, qui donne la covariance de chaque paire de lecteurs. Il existe ensuite deux stratégies totalement équivalentes. Une première stratégie qui consiste à estimer la covariance entre deux lecteurs comme la moyenne de toutes les cellules de la matrice 24×24 après exclusion de la diagonale. C’est cette fameuse diagonale qu’il ne faut pas additionner à la variance du Student sur les 24 lignes pour ne pas doublement compter varR/240. Une fois qu’on a cet estimateur de la covariance, en appliquant les formules VAR(X+Y)=VAR(X)+VAR(Y)+2×COV(X,Y) aux 24 lecteurs, on s’aperçoit que l’erreur type de la moyenne sur les 24 lecteurs se calcule comme sqrt(n×V+C*(n^2-n))/n où n=24 est le nombre de lecteurs, V est la variance empirique inter-lecteur (qui est une estimation de varL+varR/240), et C est la covariance empirique qu’on a calculé par boostrap. On s’aperçoit aussi que le calcul est identique si on considère que les covariances de toutes les paires de lecteurs sont toutes différentes et qu’on n’en fait pas la moyenne. Cette méthode d’addition de la covariance est donc valable, quand bien même les lecteurs ne seraient pas indépendants les uns des autres. En pratique, on repose quand même sur l’hypothèse d’indépendance des lecteurs pour le calcul de varL. De manière intéressante, on obtient exactement la même erreur type si on inverse le rôle des lignes et des colonnes, c’est-à-dire si on fait un Student avec les 240 examinateurs et qu’on corrige le Student par la covariance empiriquement obtenue en rééchantillonnant les 24 lecteurs. Il existera éventuellement une différence mineure entre les deux approches : le nombre de degrés de liberté de la procédure de Student. Dans un cas, il paraît logique d’utiliser 23 ddl, en se disant que la covariance est assez faible et donc, devrait peu altérer la loi de Student, alors que dans l’autre cas, on est tenté d’utiliser 239 ddl. Si on veut un intervalle de confiance qui ne soit ni conservatif ni libéral, il faudrait un nombre de ddl intermédiaire qu’on devrait pouvoir calculer par des approximations de la somme de plusieurs lois de Student de variance différente et qui covarient. En l’absence de covariance, il y a l’approximation de Satterthwaite. En présence de covariance (modeste), on doit pouvoir généraliser cette approximation, mais c’est le cadet de nos soucis. Avec 23 ddl, la loi de Student est déjà proche de la loi normale (quantile 0,975 à 2,07 vs 1,96). On a des problèmes plus sévères que le calcul du nombre de degrés de libertés pour l’approximation à une statistique t de Student, tels qu’une imperfection de l’approximation normale avec n=24 lecteurs dont l’un d’entre eux s’avère d’ailleurs être un outlier.

Avec cette méthode, on trouve un intervalle de confiance à 10,4% (IC 95%: 6,9 à 13.9%), avec une largeur de 7,0%, soit un petit peu moins que le double bootstrap, parce qu’on n’a pas compté doublement les variances.

Méthode N°4 : double boostrap rétréci (shrinked)

On pouvait aussi se baser sur du double boostrap, mais enlever à la variance double-bootstrappée, les variances qui sont comptées doublement. Avec cette stratégie, on trouve une erreur type très proche de celle de la méthode hybride Student/boostrap soit un intervalle de confiance à 10,4% (IC 95% : 6,9 à 13,9%) ; pour lequel j’ai utilisé la loi de Student à 23 ddl, afin d’avoir le moins de différences possibles pour comparer aux autres méthodes. L’intervalle de confiance est de largeur équivalente avec ce double bootstrap rétréci (6,95%) et avec la méthode hybride (7,04%) ; la différence étant principalement explicable par la correction du biais de la variance (24/23) dans la méthode hybride ; la méthode hybride sans le facteur correcteur de la variance fournit une largeur d’intervalle de confiance à 6,96%. Les deux stratégies semblent donc très proches.

Méthode N°5 : modèle à effets mixtes sur le jeu de données simplifié

On va essayer de modéliser ce système de deux facteurs croisés, indépendamment échantillonnés, avec un modèle linéaire à effets mixtes (linear mixed effect model ou LMM). On va commencer à partir du modèle simplifié (les deltas), qui efface quand même la majeure partie de la complexité du problème. A priori, on peut modéliser les effets A, B et D du modèle simplifié, mais l’effet C (interaction examen×lecteur) est impossible à estimer car il n’y aura qu’une seule observation pour estimer chacun des effets, alors qu’on doit en même temps évaluer une variance résiduelle. Ce modèle aurait autant d’effets aléatoires que d’observations, en plus d’un effet fixe (intercept) ; c’est impossible à estimer. On doit alors faire un modèle ignorant l’interaction examen×lecteur. Le modèle suivant (syntaxe lme4) a été estimé :

deltaIA ~ (1|lecteur)+(1|examen)

Cela a permis d’estimer un intervalle de confiance (IC95% : 7,0% à 13,8%) d’une largeur de 6,790% selon la méthode du rapport de vraisemblance ; avec Wald, on a une largeur à 6,739%. Il semble que le modèle fournisse des résultats à peu près cohérents avec les méthodes de double bootsrap rétréci et hybride. En creusant il semblerait que la différence soit principalement explicable par une différence du calcul du nombre de degrés de liberté.

Méthode N°6 : modèle à effet mixte sur le jeu de données complet

Malheureusement, je crains que la plupart des usagers du modèle à effets mixtes ne passent pas par l’étape de simplification modélisatoire engendré par l’usage des deltas sur chaque couple (examen,lecteur).

On arrive à modéliser un maximum d’effets tout en conservant un modèle qui converge, on arrive à:

vrai_positif ~ IA+(IA|examen)+(IA|lecteur)+(1|couple_examen_lecteur)

On arrive alors à modéliser les effets N°1 à N°9 sauf l’effet N°7. On s’en sort donc aussi bien que pour le modèle simplifié. Les largeurs des intervalles de confiance sont presque les mêmes : 6,739% pour Wald et 6,788% pour le rapport de vraisemblance. On a juste des problèmes de performances computationnelles, qui restent encore tolérables (39 secondes pour calculer l’IC95% du rapport de vraisemblance).

Méthode N°7 : modèle à effets mixtes naïf

Tout le monde ne passe pas par la phase de réflexion de modélisation des 10 effets du modèle le plus complexe que j’ai décrit. Souvent, un modèle à effets mixtes est juste proposé avec un ou deux intercepts aléatoires. Qu’est-ce que ça donne ?

vrai_positif ~ IA+(1|examen)+(1|lecteur)

La largeur de l’intervalle de confiance est de 2,20%, ridiculement petit. Il faut dire qu’on fait l’hypothèse que les gains attribuables à l’IA sont strictement identiques pour tous les examens, tous les lecteurs et toutes les combinaisons examen×lecteur. Par rapport au modèle simplifié, il ne modélise, ni l’effet A, ni l’effet B, ni l’effet C. Ça modélise peut-être un peu la sensibilité sans IA, mais très mal l’effet de l’IA. C’est tellement mauvais que même le bootstrap des 23040 observations, ignorant totalement toute structure de corrélation est plus proche de la réalité (erreur type 3,3%).

Méthode N°8 : modèle identité-binomial à effets mixtes

Pour des raisons qui m’échappent, les modèles linéaires gaussiens sont mal vus sur les variables binaires : les modèles linéaires généralisés leurs sont souvent préférés, même si ça ajoute plus de biais que ça n’en enlève. Que donne le modèle linéaire généralisé à effets mixtes identité-binomial ? Ça ne converge pas, même avec le modèle très simplifié :

vrai_positif ~ IA+(1|examen)+(1|lecteur)

Ça ne converge toujours pas quand on enlève l’effet lecteur. Il faut enlever l’effet examen si on veut le faire converger. Le modèle le plus élaboré qui converge est:

vrai_positif ~ IA+(IA|lecteur)

Qui n’apporte alors rien par rapport au Student (methode N°1). Avec ça, on obtient un intervalle de confiance d’une largeur de 5,1% ; un peu plus étroit que celui du Student (méthode N°1).

Méthode N°9 : modèle logistique à effets mixtes

Pour le modèle logistique à effets mixtes, on arrivait à modéliser plus de termes :

vrai_positif ~ IA+(IA|examen)+(IA|lecteur)

C’est-à-dire qu’on est arrivé à mettre tous les effets les importants. Sauf que les coefficients ressortis par le modèle ne sont pas interprétables car conditionnels aux effets aléatoires inconnus. Ici, on observe un odds ratio conditionnel (dans le modèle logistique à effets mixtes) à 6,78 (IC95% : 4,41 à 10,44) alors que l’estimation ponctuelle de l’odds ratio marginal est à 1,65. En bref, c’est complètement bidon.

Si on veut comparer l’incertitude à celle d’autres méthodes, on va calculer le petit z d’un test de Wald de comparaison à l’effet nul (ici, odds ratio à 1). Le petit z était égal à 8,7 contre 6,1 pour la méthode hybride N°3.

Méthode N°10 : GEE

Les équations d’estimation généralisées (GEE) servent à estimer des effets marginaux dans des modèles linéaires généralisés en prenant en compte une structure de corrélation. Malheureusement, ils ne gèrent pas les facteurs croisés : il faut donc choisir si le cluster est défini par l’examen ou le lecteur. Le reviewer qui proposa les GEE suggérait l’usage de l’examen radiologique comme cluster, ignorant totalement les variances attribuables au lecteur. Ainsi, avec un modèle GEE identité-gaussien, on observait un intervalle de confiance d’une largeur 4,72%, avec l’estimateur sandwich de la variance ; la structure de corrélation (exchangeable, unstructured) n’a pas d’importance dans un design équilibré à partir du moment où on utilise l’estimateur sandwich, à ceci près que certains ont du mal à converger. En creusant un peu, on s’aperçoit qu’en cas de design équilibré et avec l’usage de la variance sandwich le modèle est équivalent à faire la moyenne de l’effet brut dans chaque cluster, puis faire un intervalle de confiance de Student grace à la variance empirique inter-cluster ; la seule différence porte sur les ddl ; la variance du GEE/sandwich n’est pas débiaisée par la multiplication par 24/23 et l’intervalle de confiance est calculé avec une loi normale plutôt qu’un Student à 23 ddl ; autrement, c’est exactement la même chose. À partir du moment où on utilise la variance sandwich, le GEE est invariant à la transformation : que l’on utilise un modèle identité-gaussien, logit-binomial, identité-binomial, on arrive au même intervalle de confiance, après usage de la méthode de delta pour tout exprimer sur l’échelle de la différence absolue de pourcentages. Attention, ce n’est pas une vérité générale sur les GEE, mais une vérité contextuelle au fait que (1) le design est équilibré et (2) les modèles fournissent exactement les mêmes prédictions parce qu’il n’y a a qu’un seul effet binaire ; ce serait différent si on intégrait plusieurs effets fixes, avec hypothèse d’absence d’interaction qui conduiraient à des contraintes différentes selon la fonction de lien utilisée. Donc, au final, un GEE ne fait pas mieux qu’un simple Student (méthode N°1).

Synthèse des résultats et discussion

Pour commencer, il faut oublier les GLMM. Ça converge rarement et dès qu’on met une fonction de lien différente de l’identité, ça fournit des résultats conditionnels ininterprétables et complètement gonflés.

Pour le problème des deux facteurs croisés équilibrés, les GEE n’apportent rien par rapport aux méthodes les plus naïves : le Student tout bête après avoir calculé la moyenne dans chaque cluster.

Les modèles linéaires à effets mixtes (LMM) fonctionnent probablement correctement s’ils sont bien spécifiés ; ils peuvent modéliser les principaux effets sans trop de problème de convergence (ils en avaient un peu plus sur l’étude sur 600 radiographies × 12 lecteurs × 1 lecture). Néanmoins, je ne suis vraiment pas à l’aise avec ces modèles, parce que je ne vois pas en détail leur mécanique s’exécuter, et donc je ne peux pas de manière complètement intuitive percevoir leurs conditions de validité ; la relative fiabilité de l’estimateur hybride (méthode N°2) me paraît bien plus facile à percevoir. Sans Monte Carlo de validation, je n’arrive pas à donner du crédit au LMM même dans des conditions aussi bonnes que celle de deux facteurs croisés équilibrés.

Le bootstrap est très intuitif et compréhensible, mais il faut respecter le schéma d’échantillonnage dans le rééchantillonnage et bien concevoir son fonctionnement pour percevoir les inflations de variance dès qu’il y a des schémas non triviaux ; il aide aussi beaucoup à percevoir les limites dues aux tailles d’échantillon : même si on a 23040 observations, on a que 24 lecteurs.

Les bidouilles comme l’estimateur hybride ne sont probablement pas parfaites, mais sont très compréhensibles et transparentes dans leur fonctionnement ; on se doute que ce qu’on fait n’est pas parfait, mais on sait que c’est largement suffisant et on comprend ce qu’on fait. Plutôt que de chercher à modéliser très finement les paramètres d’un modèle, on peut se contenter d’estimer et corriger des biais, en allant toujours du plus gros au plus fin. On peut aussi estimer et corriger un biais sans forcément le décomposer complètement dans un modèle.

Les effets aléatoires sont généralement nombreux si on veut une modélisation à peu près correcte. L’intercept aléatoire seul, peut être une solution catastrophique à un problème qui nécessite une ou plusieurs pentes aléatoires. L’usage d’un modèle à effets mixtes ne doit pas non plus vous empêcher de pré-traiter les données avant le calcul, avec la méthode des deltas, par exemple.

L’usage de moyennes et modèles linéaires estimés par les moindres carrés simplifie énormément la vie, surtout avec un design équilibré : tout est beaucoup plus cohérent parce que la plupart des manières d’attaquer le problème sont équivalents. En bonus, ça fournit des informations interprétables en absolu (différences absolues de pourcentages dont l’inverse est le Number Needed to Treat). C’est simple, compréhensible, efficace. Dès qu’on utilise des odds ratio, on s’aperçoit que c’est bien plus difficile de comprendre ce qui se passe en présence d’interactions ou d’ajustements. D’ailleurs, l’estimateur des moindres carrés offre de nombreuses propriétés extraordinaires, telles qu’un résidu moyen nul, même en cas d’écart aux conditions de validité (interactions, effets non linéaires), comme décrit dans un billet précédent.

Ai-je insisté sur le fait qu’il fallait comprendre ce qu’on faisait ? Je suis effrayé par les usages délirants des modèles à effets mixtes que je vois : résidus ultra-corrélés considérés comme indépendants, moyennes faites entre des objets de nature différente, confusion entre la position de variable réponse et de covariable, absence totale de questionnement sur la nature des effets qu’on mesure : on parle « d’effet » plutôt que de différences de moyennes pondérées ; on ne sait même plus quels sont les poids. On a aussi un gros souci avec la notion d’hypothèse. On fait des tonnes « d’hypothèses » manifestement fausses sans préciser, ni savoir, ce qu’il se passe lorsque ces hypothèses sont violées. Je suis bien plus à l’aise quand je définis précisément LA statistique qui m’intéresse avec une formule simple (p.e. différences de moyennes arithmétiques) et que je ne parle pas d’hypothèse mais de biais d’estimateur ; ces biais étant en partie évaluables et contrôlés comme ne dépassant pas un seuil déraisonnable. Même si ça peut être plus fortement biaisé qu’un modèle avec hypothèses, au moins, on sait à quoi s’attendre au pire des cas.

Interprétation combinée des analyses ajustées et non ajustées

Pourquoi ce billet de blog

Tout vient de l’interprétation, avec des collègues cliniciens de l’article suivant BMJ 2021;372:m4948. Dans le modèle de base (seulement ajusté sur le sexe, l’âge et l’effet centre), l’effet (hazard ratio) d’une forte consommation de céréales raffinées (>= 350 g/jour) par rapport à une faible consommation (< 50 g/jour) sur le critère composite de mortalité et événement cardiovasculaire majeur était estimé à 1.12 (IC95% : 1.03 à 1.21, p=0.05). Après des ajustements successifs sur des facteurs socio-économiques et de comportements à risque ou protecteurs (tabac, alcool, obésité, consommation de fruits/légumes, etc.), l’effet n’a cessé de croitre pour atteindre 1.29 (IC95% : 1.16 à 1.43, p<0.0001) dans le modèle complètement ajusté. De mon point de vue, cette amplification de l’effet lors des ajustements était un argument pour penser à l’existence d’une relation causale, puisqu’en cas de sous-ajustement (très probable ici), cela montrait que l’effet réel était plus grand encore que l’effet ajusté observé. J’aurais été beaucoup plus réservé dans mes conclusions si l’effet non ajusté avait été estimé à 3.00 et qu’il était descendu à 1.29 (IC95% : 1.16 à 1.43) après ajustement ; en fait, connaissant la grande difficulté à mesurer les facteurs comportementaux et socio-économiques, j’aurais probablement considéré que cette étude ne fournissait aucun argument pour penser à une augmentation causale du risque de morbi-mortalité. J’ai alors constaté que mes collègues ne faisaient pas du tout cette interprétation combinée des analyases brutes et ajustée. Je me suis alors dit que l’analyse que je faisais intuitivement devait être formalisée pour pouvoir être communiquée ; c’est l’objet de ce billet de blog.

Définition

Il est fréquent que l’on s’intéresse à l’effet d’une exposition contrôlable sur un état de santé futur. L’exposition peut être une intervention (p.e. administration d’un médicament) ou peut-être une exposition subie (p.e. pollution atmosphérique). Dans les deux cas, il y a, dans une certaine mesure, une possibilité de contrôler directement ou indirectement l’exposition. Se pose alors la question : en imposant ou réduisant l’exposition, peut-on améliorer l’état de santé moyen de la population ? C’est ce que nous appelerons une causalité contrafactuelle probabiliste. Il existe d’autres définitions de la causalité, mais nous ne nous y intéresserons pas.

Notion de facteur de confusion

Afin de rechercher cette causalité, il est habituellement cherché une corrélation entre l’exposition et l’état de santé futur (outcome). Mais des corrélations non causales sont possibles, notamment en raison de facteurs de confusion. Un facteur de confusion est une cause commune de l’exposition et de l’outcome, ne permettant plus de considérer que la corrélation entre l’exposition et l’outcome est entièrement causale. Un ou plusieurs facteurs de confusion peuvent faire complètement apparaître une corrélation entre l’exposition et l’outcome alors qu’aucune relation causale n’existe. Ils peuvent aussi aléter une relation causale existante, en amplifiant, réduisant, annulant ou inversant une corrélation causale existante. En cas d’inversion de la relation, on parle de paradoxe de Simpson.

Exemple caricatural

Un exemple très caricatural, qui fera transparaître ce paradoxe de manière évidente, c’est la ventilation mécanique et la COVID-19. Les patients mécaniquement ventilés ont une mortalité bien plus élevée que les autres ; on ne peut pas en déduire qu’il faudrait arrêter de les ventiler. Le facteur de confusion est la sévérité de la détresse respiratoire, qui est à la fois la cause de leur mortalité élevée et de l’instauration d’une ventilation mécanique. Si on sélectionnait un sous-groupe de patients avec une détresse respiratoire pour lesquels il y avait une indication à la ventilation mécanique, et qu’on leur attribuait aléatoirement une ventilation mécanique, on constaterait très certainement une meilleure survie dans le groupe bénéficiant de la ventilation mécanique.

Il est à noter que, selon le point de vue, thérapeutique ou pronostic, l’interprétation diffère nettement. Si on apprend qu’un de nos proches est hospitalisé en réanimation avec une ventilation mécanique, on a toutes les raisons de craindre le pire plutôt que de se rassurer faussement en se disant qu’il est entre de bonnes mais, avec la meilleure thérapeutique qu’on puisse lui donner. En effet, les traitements mais aussi du contexte ayant conduit à la prescription de ces traitements, doivent être pris en compte dans l’évaluation d’un pronostic absolu, sous la forme d’un simple pourcentage, tel que : ses chances de survie sont de 50%.

Notion d’ajustement statistique

Une manière classique d’éliminer un facteur de confusion dans l’évaluation d’une corrélation causale, est l’ajustement statistique. Il existe de très nombreuses manières d’ajuster, mais les différentes méthodes sont plus ou moins équivalentes et reviennent toujours approximativement à faire quelque chose d’assez intuitif : une moyenne d’analyses en sous-groupes.

Sur l’exemple de la ventilation mécanique, on pourra s’intéresser aux patients admis aux urgences pour COVID-19. En supposant que la SpO2 (oxymétrie de pouls) soit évaluée chez tous les patients à l’admission, on pourra définir deux groupes : les patients avec désaturation en air ambiant au repos (SpO2 < 92%) et les patients sans désaturation (SpO2 >= 92%). On pourra alors observer l’effet de la ventilation mécanique chez les patients avec désaturation, c’est-à-dire, la différence de pronostic (p.e. mortalité) entre les patients avec désaturation et ventilation mécanique et les patients avec désaturation mais sans ventilation mécanique. De même, on pourra observer l’effet de la ventilation mécanique chez les patients sans désaturation, à condition de trouver des patients sans désaturation mais ayant quand même eu une ventilation mécanique. En bref, on évaluera l’effet de la ventilation mécanique dans des sous-groupes « homogènes » en termes d’oxymétrie. On pourra ensuite faire la moyenne pondérée de ces effets de la ventilation mécanique en sous-groupes. Il existe plusieurs pondération possibles de ces effets en sous-groupes, mais la plupart des méthodes d’ajustement seront équivalentes à une pondération proportionnelle à la quantité d’information (inverse de la variance ou différence de log-vraisemblance) dans chacun des sous-groupes. Cette méthode de pondération par l’inverse de la variance est l’estimateur le plus efficace (moindre variance) sous l’hypothèse que les effets de la ventilation mécanique sont identiques dans tous les sous-groupes d’oxymétrie, de telle sorte que la seule différence que l’on observe dans les effets en sous-groupes est due aux fluctuations d’échantillonnage. Le détail technique de la méthode de pondération doit être connu lorsqu’on craint une interaction statistique, c’est-à-dire, que les effets du traitement diffèrent selon le sous-groupes.

Pourquoi un ajustement permet-il de s’abstraire d’un facteur de confusion ? Parce que dans chacun des sous-groupes, le facteur de confusion est « constant », il ne peut plus être à l’origine d’une corrélation entre l’exposition et l’outcome. On retrouve alors la corrélation causale entre l’exposition et l’outcome, ou la corrélation explicable par les autres facteurs de confusion.

Notion de sous-ajustement

Un ajustement permet-il vraiment de s’abstraire d’un facteur de confusion ? C’est la théorie, la pratique est assez différente… En reprenant l’exemple de l’oxymétrie et la ventilation mécanique, de nombreux problèmes persistent, même après ajustement.

  1. Le découpage de l’oxymétrie en deux catégories (< 92% vs >= 92%) ne garantit pas son homogénéité dans chacune des deux catégories.
  2. La mesure de l’oxymétrie est ponctuelle et unique ; première mesure à l’admission aux urgences, elle ne prend pas en compte l’évolution dans les heures et jours qui suivent.
  3. La SpO2 (oxymétrie colorimétrique) est une mesure imparfaite de la SaO2 (saturation mesurée précisément sur un prélèvement de sang artériel), avec une erreur de mesure non négligeable.
  4. L’oxymétrie ne reflète que partiellement la détresse respiratoire ; elle ne prend pas en compte la capnie, ni le tirage, ni la réponse aux autres thérapeutiques telles que l’oxygénothérapie non invasive.
  5. La détresse respiratoire ne résume pas complètement l’indication de la ventilation mécanique ; une décompensation aigüe d’une insuffisance cardiaque chronique ou une anémie pourraient aggraver le tableau clinique et conduire plus facilement à l’indication.

Le problème N°1, peut être résolu par un affinement de l’ajustement, en découpant la SpO2 en un plus grand nombre de catégories, ou par appariement sur la SpO2, ou par une modélisation de l’effet de la SpO2 sur le pronostic par une spline décrivant la relation de manière non-linéaire mais continue dans un modèle additif généralisé. Cela arrive néanmoins que les auteurs d’un article fassent un ajustement très grossier, sans qu’on ait accès aux données pour refaire l’analyse de manière plus fine.

Néanmoins, les problèmes N°1 à N°4 peuvent se résumer en une erreur de mesure par rapport à la notion de « détresse respiratoire conduisant à une indication de la ventilation mécanique ». Le problème N°5, c’est l’existence d’autres facteurs de confusion. La conséquence pratique de tous ces problèmes est la persistance d’un biais de confusion résiduel ; l’ajustement est insuffisant ; on sous-ajuste.

En effet, à l’intérieur d’une catégorie soi-disant homogène de détresse respiratoire (SpO2 < 92%), les patients avec ventilation mécanique seront plus souvent ceux qui ont une SpO2 particulièrement basse (p.e. 85% plutôt que 91%), ou auront mal répondu à l’oxygénothérapie non invasive, ou auront eu une hypercapnie. Il y aura donc un biais de confusion dans chacun des sous-groupes, et dans l’analyse ajustée globale.

Notion de proportion d’ajustement

Le sous-ajustement est la règle plutôt que l’exception. Les variables mesurées manquent toujours de finesse, sont souvent mal mesurées, et il existe alors toujours un biais de confusion résiduel. Néanmoins, on peut évaluer de manière informelle et approximative les conséquences de ce biais de confusion résiduel, si on dispose à la fois de l’effet ajusté et non ajusté de l’intervention. C’est pourquoi beaucoup d’articles présentent les deux à la fois !

On peut formaliser la notion de sous-ajustement de manière quantitative. Sur un échantillon, on observera toujours un effet imparfaitement ajusté, en raison des erreurs de mesures et approximations des concepts (p.e. détresse respiratoire au moment de l’introduction de la ventilation mécanique approximé à la SpO2 à l’admission). On imagine alors qu’il existe un effet parfaitement ajusté, que l’on obtiendrait si on mesurait le concept sous-jacent de manière parfaite et qu’on ajustait de manière infiniment fine dessus. On va considérer trois effets : effet brut non ajusté (Ebrut), effet imparfaitement ajusté (EIA) et effet parfaitement ajusté (EPA). Plaçons nous dans un contexte où les échantillons sont suffisamment grands pour qu’on n’aie plus à distinguer les effets observés des effets réels.

Alors, dans un modèle additif, nous allons définir la proportion d’ajustement, comme le rapport (EIA-Ebrut)/(EPA-Ebrut). Par exemple, si l’effet brut s’exprime comme une surmortalité de +20%, que l’effet imparfaitement ajusté est une surmortalité de 10% et que l’effet parfaitement ajusté est une sous-mortalité de 10%, alors Ebrut=+20%, EIA=+10% et EPA=−10% et la proportion d’ajustement (PA) est d’un tiers. On peut alors dire qu’on a ajusté 33% du biais de confusion attribuable au concept sous-jacent à la variable d’ajustement d’intérêt. Connaissant PA, Ebrut et EIA, on peut calculer EPA comme Ebrut+(EIA-Ebrut)/PA.

Évidemment, en pratique, on peut seulement estimer Ebrut et EIA, la valeur de EPA restant inconnue ; pourtant c’est bien EPA qui nous intéresse. Cependant, on peut faire des spéculations sur la proportion d’ajustement PA afin de se faire une idée du domaine dans lequel EPA se situe. Cette proportion est, a priori, comprise entre 0 et 1, si on considère qu’on aura tendance à sous-ajuster, comme mentionné précédemment. L’inverse de la proportion d’ajustement sera alors comprise entre 1 et l’infini. Il est très difficile de connaître précisément la proportion d’ajustement, mais on peut imaginer qu’avec une mesure à peu près correcte du facteur de confusion, on aura rarement un PA < 0,10 et donc, un inverse de PA supérieur à 10. De mon expérience subjective sur des analyses statistiques avec des variables mal mesurées par rapport à des analyses sur des variables très finement mesurées ou par rapport aux résultats des essais cliniques randomisés, le PA sera le plus souvent supérieur à 0,50 et pourra assez souvent atteindre voire dépasser 0,80. Néanmoins, il n’y a strictement aucun seuil consensuel. En fait, en 2021, je ne pense pas que le concept de PA ait été formalisé et décrit aussi bien que je ne l’ai fait ici.

Exemples d’interprétation

Supposons encore que l’on néglige les incertitudes statististiques dus à des tailles d’échantillon limitées et que l’on s’intéresse à évaluer l’effet d’un traitement sur le risque de mortalité. Un effet négatif (p.e. -10%) correspondrait à un traitement efficace, car protecteur, alors qu’un effet positif correspondrait à un effet nocif. Considérons une étude observationnelle sur ce traitement, avec un potentiel biais d’indication. Considérons que la proportion d’ajustement est raisonnablement comprise entre 1/3 et 100% ; c’est-à-dire que l’ajustement est peut-être de mauvaise qualité (1/3), ou de bonne qualité, voire parfait.

Effet brut
Ebrut
Effet imparfaitement ajusté
EIA
Pari sur la proportion d’ajustement (PA)Pari sur l’effet parfaitement ajusté EPAConclusion
+20%+10%1/3 à 1+10% à -10%Efficacité inconnue
+10%+10%1/3 à 1Environ +10%Traitement efficace
+0%+10%1/3 à 1+10% à +30%Traitement efficace
-10%+10%1/3 à 1+10% à +50%Traitement efficace
+10%0%1/3 à 10% à -20%Traitement inefficace, voire nocif
-10%0%1/3 à 10% à +20%Traitement possiblement efficace

L’intervalle de pari de l’effet parfaitement ajusté est calculé à partir de la formule EPA=Ebrut+(EIA-Ebrut)/PA. On constate que bien qu’il y ait une grande incertitude, il existe plusieurs scenarii dans lesquels la conclusion reste quand même univoque. Par exemple, si l’effet brut du traitement passe de 0% à +10%, on peut se dire que le traitement était désavantagé par un biais d’indication tendant à fournir le traitement aux patients les plus fragiles et que malgré cela, l’efficacité réelle du traitement arrivait à compenser complètement se désavantage. En ajustant imparfaitement, on sous-estime encore le bénéfice du traitement. À l’opposé, si l’effet du traitement passe de +20% à +10% lors d’un ajustement de piètre qualité, on peut se dire que ce traitement était avantagé par le biais d’indication et qu’une partie de l’avantage persiste peut-être encore : on est en droit de douter du bénéfice du traitement.

Tout ceci montre qu’il faut à la fois avoir les effets bruts et les effets ajustés pour interpréter l’effet causal d’une exposition. Les analyses de sensibilité sur le possible biais de confusion résiduel devraient se faire sous la forme d’une proportion d’ajustement.

Sur certaines variables, telle que la catégorie socio-économique ou des facteurs de style de vie (p.e. alimentation) les erreurs de mesure sont très fortes et il est possible qu’on sous-ajuste avec une proportion d’ajustement inférieure à 30% (https://www.bmj.com/content/373/bmj.n604).

Variante du concept

Sur des odds ratio, risques relatifs ou hazard ratio, on peut conceptualiser la proportion d’ajustement sur une échelle logarithmique ou pas. Supposant, par exemple, que l’odds ratio brut soit à 4,0, l’odds ratio imparfaitement ajusté soit à 2,0 et l’odds ratio parfaitement ajusté soit à 1,0, alors on peut considérer que la proportion d’ajustement est à 50% sur l’échelle logarithmique plutôt que 3/4 sur une échelle non modifiée. Comme la proportion d’ajustement est très incertaine, la nuance n’a pas d’importance la plupart du temps.

Limites du raisonnement

La première limite d’interprétation, c’est la grande inconnue et la grande subjectivité concernant les proportions d’ajustement que l’on peut considérer comme plausible. Cela n’empêche pas forcément de conclure, comme mentionné auparavant. Il est très fréquent, malheureusement, de considérer que la proprotion d’ajustement est égale à 1, sans aucune incertitude.

La deuxième limite à cette stratégie d’interprétation, c’est le problème N°5 listé dans la section « tendance à sous-ajuster » : la présence d’autres facteurs de confusion que ceux sur lesquels on a ajusté. Si les vrais facteurs de confusion ne sont pas mesurés du tout, on peut avoir l’illusion d’un bon ajustement alors qu’il est mauvais. Par exemple, si on s’intéresse à l’effet d’un programme d’éducation thérapeutique sur l’observance à un traitement et qu’on ajuste sur la sévérité de la maladie, il est très probable qu’on observe un effet ajusté et un effet brut presque identiques et qu’on en déduise qu’on a des résultats robustes à un sous-ajustement. Pourtant on a oublié d’ajuster sur des facteurs psycho-sociaux, généralement absents du dossier médical. Il est probable qu’on ne propose le programme d’éducation thérapeutique qu’à des patients qui sont déjà assez impliqués dans les soins, mais qu’en plus, seuls les plus observants accepte le programme. Ces facteurs psycho-sociaux sont des facteurs de confusion majeurs non mesurés.

La troisième limite, qui se résout néanmoins si on est méthodique, apparait dans la situation où il y a plusieurs variables d’ajustement conduisant à des biais de confusion opposés. Par exemple, on peut constater qu’un effet passe de +20% à +10% après l’ajustement sur une première variable, puisse repasse à +20% lors d’un second ajustement. Si on considère les deux ajustements de manière synthétique, on aura l’impression que le résultat est robuste, (effet brut = +20% et effet ajusté = +20%) alors qu’en réalité, tout dépend des deux proportions d’ajustement associées aux deux variables. Si le premier ajustement est de mauvaise qualité (proportion d’ajustement = 1/4) alors que le second ajustement est parfait (proportion d’ajustement = 1), alors l’effet parfaitement ajusté pourrait être négatif (-10% dans l’exemple). Ce raisonnement ne peut être fait qu’en détaillant les effets dans des modèles successivement ajustés sur un nombre croissant de variables. Il est alors possible de considérer une faible proportion d’ajustement pour toutes les variables biaisant les résultats dans une direction et une forte proportion d’ajustement pour les variables biaisant dans l’autre direction.

Un point qui n’a pas été discuté, c’est l’interprétation combinée de l’incertitude attribuable aux ajustements imparfaits avec l’incertitude, plus classique, attribuable aux fluctuations d’échantillonnage et généralement représentée par des intervalles de confiance. Les deux sources d’incertitude sont indépendantes, puisque les fluctuations d’échantillonnage (fréquentistes et liées à l’expérience) sont sans aucun rapport avec l’incertitude qu’on puisse avoir sur la qualité des ajustements (bayésienne et subjective), du moins lorsqu’on choisit l’intervalle de crédibilité de la proportion d’ajustement en aveugle des résultats. On pourrait donc, en théorie, additionner formellement les deux sources de variance ; cela nécessiterait d’avoir la covariance de l’effet de l’exposition dans l’analyse ajustée avec l’effet de l’exposition dans l’analyse brute ; cette covariance n’est pratiquement jamais donnée mais elle est généralement très forte. On doit pouvoir la calculer de manière indirecte par l’élargissement de l’intervalle de confiance entre l’analyse brute et l’analyse ajustée ; du moins dans les modèles linéaires. Néanmoins, l’incertitude sur la proportion d’ajustement étant énorme, cette considération sera futile la plupart du temps ; par ailleurs, ces calculs seront rarement faits de manière formelle, mais plutôt faits de tête, de manière très approximative.

Bon usage des nombres à virgule flottante : exemples

Calculer une variance peut paraître simple, mais il y a de très mauvaises formules qui circulent.

Considérons une variance portant sur des nombres énormes avec une très faible variance. Un cas fréquent, ce sont les dates ou paires dates/heures ! Ces valeurs sont typiquement représentées par un nombre de jours, secondes ou millisecondes depuis une certaine époque (1er janvier 1970, 1er janvier 1900 ou 1er janvier 1601). Le plus extrême est obtenu par les dates de création/modification des fichiers Windows qui sont enregistrés comme le nombre d’intervalles de 100 nanosecondes depuis le 1er janvier 1601. Ainsi, pour les dates récentes, les valeurs dépassent les 10 puissance 17.

Même si les dates sont très grandes, on peut vouloir les manipuler sur un intervalle de temps très court, tel que les quelques secondes d’une expérience de marche, comme j’ai pu voir dans un projet de recherche.

Considérons donc le code R suivant (exécuté sur R 4.0.3 sous Windows 10), basé sur deux dates/temps, exprimées en nombre de secondes depuis le 1er janvier 1970:

> x1=strptime("2021-01-01 00:00:01", format="%Y-%m-%d %H:%M:%S")
> x2=strptime("2021-01-01 00:00:02", format="%Y-%m-%d %H:%M:%S")
> var(as.numeric(c(x1,x2)))
[1] 0.5

Tout semble fonctionner bien avec la fonction var de base. Cependant si on réimplemente cette fonction avec la formule que l’on retrouve dans tous les livres, on obtient un résultat aberrant:

> badvar=function(x) (sum(x^2)-sum(x)^2/length(x))/(length(x)-1)
> badvar(as.numeric(c(x1,x2)))
[1] 0

Cela est explicable par le fait qu’on fait la soustraction de deux nombres très grand (sum(x^2) et sum(x)^2), dont la différence devrait être petite.

> sum(x^2) # représentation inexacte (dépasse 2^53, soit 9.0e15)
[1] 5.180695e+18
> sum(x)^2/(length(x))
[1] 5.180695e+18
> sum(x^2) - sum(x)^2/(length(x))
[1] 0

Une meilleure fonction commence par enlever la moyenne à toutes les valeurs, en utilisant la formule la plus naïve de la variance:

> bettervar=function(x) {sum((x-mean(x))^2)/(length(x)-1)}
> bettervar(x)
[1] 0.5

Sur de très grands vecteurs cette fonction peut quand même avoir une perte de précision parce que sa manière de calculer la moyenne des carrés, passe par une somme très grande.

Pour mieux comprendre ça, comparons l’implémentation de R de la fonction mean, à une implémentation naïve:

> badmean=function(v) {sum(v)/length(v)}
> v=rep(c(0,2^50 + 100), 2^24)
> mean(v[1:2])-2^49 # correct
[1] 50
> mean(v)-2^49 # correct aussi
[1] 50
> badmean(v)-2^49 # incorrect
[1] 7.5
> rowMeans(matrix(nrow=1,v))-2^49 # incorrect
[1] 7.5

En effet, la fonction sum() va conduire à un nombre aux alentours de 2^74, avec une représentation intermédiaire inexacte. On peut corriger ça, en rectifiant le biais, par deux appels à badmean.

> bettermean=function(v) {m = badmean(v); bias = badmean(v - m) ; m+bias}
> bettermean(v)-2^49
[1] 50

En effet, on commence par calculer une moyenne approximative avec badmean, puis on calcule la moyenne des écarts à cette moyenne, qui devrait correspondre à l’opposé du biais de la moyenne. Ce biais est estimé avec des sommes qui oscillent autour de zéro et ne vont donc pas dans des ordres de grandeur trop grands.

Sauf qu’il y a quand même un problème… Ces oscillations de la somme autour de zéro disparaissent si les valeurs sont plus ou moins ordonnées:

> w=sort(v)
> mean(w)-2^49 # incorrect
[1] 20.375
> bettermean(w)-2^49 # incorrect
[1] 20.375
> mean(w) - mean(v) # ouille
[1] -29.625

Ainsi, la fonction bettermean, comme la fonction mean de R, dépendent de l’ordre des valeurs dans le vecteur ! Heureusement sur plateforme PC, les FP80 utilisés par sum() fournissent une marge de sécurité durant le processus de sommation des nombres positifs et négatifs, de telle sorte que ce problème n’arrive que pour les grands vecteurs.

Par ailleurs la fonction badmean() dépend aussi de l’ordre des valeurs, comme dans l’exemple ci-dessous :

> v=rep(c(-2^52-1, 2^52), 1e6)
> badmean(v)
[1] -0.5
> badmean(sort(v))
[1] -0.002048

On peut alors implémenter encore une variance plus robuste en utilisant la fonction mean() deux fois:

> badvar=function(x) (sum(x^2) - sum(x)^2/length(x))/(length(x)-1)
> bettervar=function(x) {sum((x-mean(x))^2)/(length(x)-1)}
> bestvar = function(x) {mean((x-mean(x))^2)*(length(x)/(length(x)-1))}
> cst=2^25+10
> off=2^49
> v=rep(off+c(-cst, cst), each=1e7)
> vartheo = cst^2*(length(v)/(length(v)-1))
> bestvar(v) - vartheo # perfect
[1] 0
> bettervar(v) - vartheo # small error
[1] -87.5
> var(v) - vartheo # small error
[1] -87.5
> badvar(v) - vartheo # huge error
[1] -1.480078e+15
> badvar(v) # negative variance!
[1] -3.541775e+14

x87 Intel vs AMD

En développant divers tests de performances sur SSE2, AVX2 et les vieilles instructions x87, j’ai découvert un problème de performance avec les processeurs Intel. En présence de représentations spéciales (NaN, +Inf, -Inf), les processeurs Intel (Core i5-4460 de microarchitecture Haswell et Celeron J1900 de microarchitecture Atom/Bay Trail) verront un ralentissement de leurs calculs par un facteur 300 environ, alors que les processeurs AMD récents (Ryzen 1700X, Ryzen 3600) ne verront aucune dégradation de performances par rapport aux mêmes calculs sur des représentations non spéciales. Même si cela se voit très bien dans du code C, on peut aussi en voir l’impact avec du code R, notamment avec des NA.

> v=double(1e5) # sur Ryzen 1700 (Windows 10)
> system.time(for(i in 1:1e4)sum(v))
   user  system elapsed 
   1.51    0.00    1.51
> v[1]=NA;system.time(for(i in 1:1e4)sum(v))
   user  system elapsed 
    1.5     0.0     1.5 

Ainsi, il faut 1.5 secondes pour qu’un Ryzen 1700 fasse les 1e5*1e4 = 1 milliard d’additions ; peu importe la présence de NA. Cela ne montre pas le pic de performance FP80, en raison du vecteur de grande taille (800 Ko) qui dépasse de la cache L1 et L2 des processeurs modernes.

Le même code sur Haswell (Windows 10) montre des résultats très différents:

> v=double(1e5) # sur core i5-4460 (Haswell)
> system.time(for(i in 1:1e4)sum(v))
   user  system elapsed 
   0.93    0.00    0.95 
> v[1]=NA;system.time(for(i in 1:1e4)sum(v))
   user  system elapsed 
 134.04    0.17  134.33

Ainsi, les performances sont environ 144 fois plus basses dès qu’il y a un NA au début du vecteur ! Le même problème est observé sur Celeron J1900 (Ubuntu 64 bits).

> v=double(1e5) # sur Celeron J1900 (Atom/Bay Trail)
> system.time(for(i in 1:1e4)sum(v))
utilisateur     système      écoulé
      2.908       0.000       2.901
> v[1]=NA;system.time(for(i in 1:1e4)sum(v))
utilisateur     système      écoulé
    135.600       0.012     135.416
>

Ce problème de performance avec les NA est observé pour sum(), mean(), rowSums(), rowMeans(), prod(), mais pas pour var() et sd() qui doivent détecter les NA avant de lancer les calculs. Ces problèmes disparaissent dès qu’on utilise le paramètre na.rm=TRUE.

Je n’ai pas accès à un CPU Intel vraiment récent, mais je crains que ce problème soit toujours présent (si vous pouvez tester et répondre en commentaire, je serais très intéressé de savoir). On peut se demander si ça pourrait venir d’une mauvaise gestion des Exception Flags x87 par Intel.

Je ne suis pas le seul à avoir remarqué ça.

Piège des nombres à virgules flottante

J’ai l’habitude d’être très prudent avec la gestion des nombres à virgule flottante, notamment lorsqu’il y a des allers-retours avec des nombres entiers, mais je me suis récemment fait avoir comme un bleu sous R, probablement parce qu’une interruption de plusieurs jours dans mon écriture du script m’a fait oublier l’origine des données, ainsi qu’une autre problématique ayant détourné mon attention à un moment critique.

Afin de bien présenter cette problématique, je vais exposer tous les éléments en jeu en essayant de ne pas demander trop de pré-requis.

Représentation binaire d’un nombre entier

Commençons par décrire la représentation décimale des nombres entiers qu’on apprend à l’école primaire. Un nombre comme 532 comporte les trois chiffres « cinq », « trois » et « deux » dans le système décimal. Le nombre 532 est simplement égal à 5×100 + 3×10 + 2×1 = 5×10^2 + 3×10^1 + 2×10^0. De droite à gauche, les trois chiffres ont un « poids » croissant, selon les puissances de dix. Le chiffre 2 a un poids d’une unité, alors que le chiffre 3 représente les dizaines et le chiffre 5 représente les centaines. On peut représenter n’importe quel nombre entier positif ainsi. Le choix d’avoir dix chiffres (les chiffres de 0 à 9) plutôt que douze ou n’importe quel autre nombre est arbitraire. Une représentation bien plus simple pour les ordinateurs est la représentation binaire dans laquelle il n’y a que deux chiffres (0 et 1) et dans laquelle le poids des différents chiffres d’une représentation numérique correspondant à des puissances de deux. Ainsi, la représentation numérique binaire 11010 sera interprétée comme 1×2^4 + 1×2^3 + 0×2^2 + 1×2^1 + 0×2^0 = 1×16 + 1×8 + 0×4 + 1×2 + 0×0 = 16+8+2 = 26. Une autre représentation, assez populaire en informatique, est la représentation hexadécimale dans laquelle on a seize chiffres : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Par exemple, la représentation numérique hexadécimale B16 correspondra au nombre entier 11×16^2 + 1×16^1 + 6×16^0 = 11×256 + 1×16 + 6×1 = 2838.

Pour la plupart de leurs calculs, les ordinateurs utiliseront, en interne, un format binaire avec un nombre fixé de chiffres. Par exemple, un ordinateur 32 bits manipulera majoritairement des nombres avec exactement 32 chiffres binaires, pas un de plus, ni un de moins. Le terme de bit est ici synonyme de chiffre binaire. Pour représenter les petits nombres, on utilisera des zéros comme préfixe. Par exemple, la valeur trois sera représentée par la séquence de 32 chiffres binaires suivante:

0000 0000 0000 0000 0000 0000 0000 0011

(les espaces ici sont seulement présentés pour faciliter la lecture mais ne sont pas signifiants)

En lisant cette représentation de droite à gauche, on peut calculer qu’elle représente le nombre 1×2^0 + 1×2^1 + 0×2^2 + 0×2^3 + … + 0×2^31 = 1 + 2 + 0 + 0 + … + 0 = 3

Le plus grand nombre que l’on puisse représenter ainsi ne contient que des 1 et est égal à la somme de toutes les puissances entières de deux, de 2^0 jusqu’à 2^31. On peut aisément montrer par récurrence que ce nombre est égal à 2^32 – 1, soit 4294967295 ; ce qui est suffisamment grand pour les décomptes de tous les jours. D’une manière générale, avec N bits, on peut représenter tous les nombre entiers de 0 jusqu’à 2^N-1, pour un total de 2^N nombres distincts. Il faut dire qu’une séquence ordonnée de N chiffres binaires dispose d’exactement 2^N combinaisons possibles (cela se démontre aisément par récurrence) si on considère que l’ordre a de l’importance, c’est-à-dire que 0011 n’est pas équivalent à 1100 ni à 1010. Pour les nombres entiers relatifs (positifs ou négatifs), il existe une représentation particulièrement astucieuse (représentation en complément à deux) si on connaît ℤ/nℤ, que je ne décrirai pas ici, car ce n’est pas nécessaire à mon exposé.

Un ordinateur moderne 64 bits, va principalement manipuler des nombres entiers 8 bits, 32 bits et 64 bits, selon le contexte.

Nombres à virgule flottante

Représentation

R, comme à peu près tous les logiciels et langages de programmation gèrent principalement les nombres réels de manière approximative sous forme de nombres à virgule flottante, communément appelés floating point values, ou FP. Cela permet de représenter approximativement des nombres comme 0.537877 ou 8.11215×10^56.

Les FP sont omniprésents en informatique : que ce soit sur une calculatrice, le processeur graphique (GPU) qui affiche un jeu vidéo sur mobile, PC, Mac ou console, n’importe quelle vidéo sur n’importe quelle plateforme postérieure à l’an 2000, ainsi que de nombreux formats de compression de sons ou d’image avec perte ; enfin, c’est encore utilisé par les réseaux neuronaux convolutionnels. C’est pourquoi, sur une vie entière au XXIème siècle, on est forcément confronté à un moment ou à un autre aux problèmes de ces représentations numériques. Enfin, un statisticien est amené à manipuler des FP dès qu’il utilise des variables quantitatives continues ou qu’il calcule des moyennes, pourcentages, odds ratio, hazard ratio ou toute autre statistique qui ne soit pas structurellement un nombre entier. Même si on peut utiliser des FP pendant des années sans en connaître les problèmes, on se cassera forcément une dent dessus à un moment où à un autre de sa carrière, parce que les FP sont structurellement imparfaits : ils tentent de résoudre au moins mal le problème insoluble de la représentation des nombres réels, appartenant à un ensemble infini indénombrable, dans un espace numérique fini et très borné.

Comme pour les nombres entiers, les représentations utiliseront typiquement des formats avec un nombre de bits bien prédéfini. R utilisera majoritairement des nombres à virgule flottante à double précision sur 64 bits, connus sous le nom de FP64 (conformes à la norme IEEE-754). Ces 64 bits sont divisés en trois partie : la mantisse, sur 52 bits, l’exposant sur 11 bits et le signe, sur 1 bit, pour un total de 52+11+1 = 64 bits. Avec ces trois composants, on représentera le nombre : signe × mantisse × 2^exposant. Le signe vaudra -1 ou +1. La mantisse sera un nombre entier positif. L’exposant sera un nombre entier positif ou négatif. Par exemple on pourra représenter -0,75 comme -1×3×2^-2. On constate, de prime abord, qu’il y a plusieurs représentations possibles pour le même nombre. On pourra représenter 16 avec une mantisse à 16 et un exposant à 0 (16×2^0), ou avec une mantisse à 8 et un exposant à 1 (16×2^1), ou encore, avec une mantisse à 4 et un exposant à 2 (4×2^2). Il existe un processus de normalisation, qui force l’usage d’une seule représentation avec une mantisse qui débute par le chiffre binaire 1 (sauf valeur trop petite, appelée dénormale). Ce bit à 1 est implicite dans la représentation binaire, ce qui permet d’avoir une précision de 53 bits sur la mantisse quand bien même elle n’est représentée que sur 52 bits. En bref, en dédoublonnant les représentations, on gagne en espace de représentation. C’est assez technique, mais ça ne change pas le fond du problème.

Problèmes (bugs insolubles) des nombres à virgules flottante

Cette représentation de nombre à virgule flottante souffre des mêmes forces et défaut que la représentation « scientifique » à quatre chiffres significatifs que vous avez peut-être apprise au lycée. Dans cette représentation, on va représenter n’importe quel nombre sous la forme A,BCD×10^EF où A, B, C, D, E et F représentent des chiffres de 0 à 9, sauf A qui vaudra au moins 1 ; sauf pour représenter zéro. Cette représentation est très précise, en absolu, pour les tous petits nombres. Par exemple, on pourra exprimer un volume de 1,567 microlitre comme 1,567×10^-6 litre ; ici, on a la précision du nanolitre. Cette même représentation sera beaucoup moins précise pour les gros nombres. Par exemple, mille deux-cent trois mètres cubes seront exprimés comme 1,203×10^6 litres, avec une précision du mètre cube. Si on veut ajouter quinze litres aux mille deux-cent trois mètres cubes, on va obtenir 1,203015×10^6 litres qui seront arrondis à 1,203×10^6 litres pour ne garder que les quatre chiffres significatifs selon le principe d’arrondi à la plus proche valeur représentable. Au final, le nombre est inchangé par l’addition des quinze litres, en raison de l’erreur d’arrondi finale. Ce problème d’arrondi touche aussi bien les nombres binaires à virgule flottante tels que les FP64. Même s’ils sont capables de représenter des nombres très proches de zéro, tels que 1/(2^1000), soit 2^-1000, et des nombres très grands, tels que 2^1000, l’addition de chiffres qui sont d’ordre de grandeur très différent, conduit à une perte de précision dommageable. Notamment, si on ajoute un nombre très grand à un nombre très petit, puis qu’on retranche le nombre très grand, alors le nombre très petit peu perdre énormément en précision, voire être annulé.

Ci-dessous, des exemples sous R:

> 1e9 + 42 - 1e9 # ça reste tolérable
[1] 42
> 1e17 + 42 - 1e17 # on commence à perdre
[1] 48
> 5e17 + 42 - 5e17 # ça fait mal
[1] 64
> 1e20 + 42 - 1e20 # on perd tout
[1] 0

L’ordre dans lequel on fait les calculs se met à avoir une importance !

> 5e20 - 5e20 + 42
[1] 42
> 1e20 + 42 - 1e20
[1] 0

En effet, la première ligne commence par le calcul (5e20 – 5e20) qui fait zéro, qui est ensuite additionné à 42, alors que la seconde ligne commence par 1e20+42 qui fait 1e20, puis retranche 1e20, ce qui conduit à la valeur zéro.

Un autre problème, c’est l’impossibilité de représenter de manière exacte certaines fractions pourtant simples, telles qu’un tiers. En effet, un tiers est approximé, en décimal, par 0.3333333333. Pour une représentation exacte, il faudrait une infinité de chiffres après la virgule. Même si 1/5 peut être représenté en décimal, il ne peut pas être représenté en binaire, parce que 5 est premier avec 2.

C’est ainsi, que des calculs très simples peuvent conduire à des résultats légèrement erronés:

> (1/5-1+1)*5 - 1
[1] -2.220446e-16

On peut se dire que le résultat est « suffisamment » proche de la bonne réponse (zéro) pour que ce soit tolérable, mais ça peut, en réalité, conduire à des réponses totalement erronées dès qu’on utilise l’opérateur d’égalité:

> (1/5-1+1)*5 == 1
[1] FALSE

Si R disposait d’un moteur de calcul exact, il afficherait TRUE ici. Sa réponse, d’une certaine manière, est complètement erronée.

Pour rendre les choses plus compliquées, R n’est pas honnête sur le vrai contenue des valeurs numériques, les arrondissant à quelques chiffres après la virgule (7 chiffres significatifs par défaut) pour l’affichage comme le montre le code ci-dessous:

> x=(1/5-1+1)*5
> x # on dirait qu'on a bien la valeur 1
[1] 1
> 1 == x # mais ce n'est pas vraiment 1
[1] FALSE
> x-1 # en fait, c'est un poil inférieur à 1
[1] -2.220446e-16
> 1-1 # alors que le "vrai" 1 est bien égal à 1
[1] 0

Ce comportement peut être corrigé par l’option digits, comme décrit ci-dessous:

> options(digits=22)
> (1/5-1+1)*5
[1] 0.99999999999999978
> 1/5
[1] 0.20000000000000001
> 1/5-1+1
[1] 0.19999999999999996

Là, on comprend beaucoup mieux les erreurs d’arrondi !

Il faut donc considérer que TOUT calcul avec nombre à virgule flottante est approximatif et que deux manières très légèrement différentes d’arriver au même calcul conduiront à deux approximations légèrement différentes. En fait, dans certains langages de programmation (p.e. le langage C), on n’a même pas la garantie qu’en réalisant deux fois exactement le même calcul on arrive au même résultat, parce que le compilateur peut traduire le code de manière différente.

Pour « tester » l’égalité, il faut toujours se donner des marges de manoeuvre, en comparant la valeur absolue de la différence à une valeur seuil correspondant à l’erreur maximale attendue.

Par exemple, plutôt que de tester (1/5-1+1)*5 == 1, on testera abs((1/5-1+1)*5 – 1)<1e-14.

On ne peut pas donner de valeur seuil universelle, parce que la bonne constante dépend de l’échelle sur laquelle on se trouve et de l’erreur cumulée des calculs. C’est-à-dire, que quand on manipule des nombres de l’ordre de 10^20, une erreur de plusieurs unités, voire plusieurs dizaines est rapidement arrivée, alors que quand on manipule correctement des nombres de l’ordre de 10^-20, alors on s’attend à avoir des erreurs inférieures à 10^-30. Parfois, on peut comparer le rapport entre les deux nombres à une valeur précise, mais ça ne marche que si les nombres restent écartés, d’une certaine manière, de la valeur zéro. Ce problème de connaissance de la bonne échelle se retrouve dans des problématiques apparentées, telles que la détection de convergence par stabilisation d’un paramètre (cf https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17885).

Il faut aussi savoir que même lorsqu’on manipule des nombres du même ordre de grandeur, les erreurs d’arrondi ont tendance à se cumuler. Même si les erreurs positives compensent en partie les erreurs négatives, grace à la méthode d’arrondi au plus proche, plus on fera d’opérations, plus l’erreur sur le résultat final sera importante.

Il existe une seule situation, fréquente dans R, pour laquelle les calculs peuvent être considérés comme exacts : lorsqu’on manipule des nombres entiers suffisamment petits (< 2^52) sous forme de nombre à virgule flottante, en veillant à ne faire aucune opération qui les fasse passer en nombre non entiers. En effet, dans R, si on oublie de préciser un suffixe ‘L’ derrière une constante numérique, elle représente un nombre à virgule flottante.

> storage.mode(42L) # vrai nombre entier
[1] "integer"
> storage.mode(42) # nombre à virgule flottante
[1] "double"

La manipulation du nombre à virgule flottante 42 est relativement sûre. J’appelerai ces nombres à virgule flottante représentant des entiers, des pseudo-entiers.

> 42*2 - 42 == 42 # ça reste sûr
[1] TRUE

Les opérations acceptables avec les pseudo-entiers sont : l’addition, la soustraction, la multiplication. L’exponentiation par l’opérateur ^ est acceptable si on reste dans des valeurs entières pas trop grandes (en restant en dessous de 2^52 pour le résultat final) et qu’on reste sur plateforme PC. En effet, elle repose sur la fonction pow() en langage C dont je crains qu’elle soit fortement approximative sur beaucoup de plateformes, en passant par des fonctions logarithme et exponentielles qui n’aient pas toute la précision des FP64. Cet opérateur ^ est toujours risqué car il n’existe pas en version vraiment entière mais seulement sur les nombres à virgule flottante, comme le montre le code suivant:

> storage.mode(3L^3L)
[1] "double"

Les pseudo-entiers ne marchent plus correctement à partir de 2^54:

> x=18014398509481984
> x+1 == x
[1] TRUE

R fournit quelques points de repères sur la précision des nombres à virgule flottantes dans l’objet .Machine.

Par exemple la constante .Machine$double.eps environ égale à 2.22e-16 est la plus petite valeur x telle que 1 + x != 1 selon la documentation. En réalité, ce n’est pas tout à fait vrai, il s’agirait plutôt de la plus petite valeur telle que 1 + x -1 == x.

Représentations spéciales

Outre les problèmes d’arrondi, les FP ont aussi des problèmes d’overflow et underflow parce que l’exposant est limité (variant de -1023 à +1024). La valeur 1,8×10^308 est le plus grand nombre représentable sur FP64 alors que 5×10^-324 est le nombre le plus petit (proche de zéro) représentable. Si on dépasse le plus grand nombre, on fait un « overflow » qui conduit à une valeur spéciale Inf représentant l’infini. Si on passe en dessous du plus petit nombre représentable, alors on atteint la valeur zéro par un « underflow ».

> 1e308*100
[1] Inf
> 1e-324/100 == 0
[1] TRUE

Il existe deux zéros différents et deux infinis différents : positifs et négatifs, avec des relations assez attendues entre les quatre:

> 1/0
[1] Inf
> 1/-0
[1] -Inf
> 1/Inf
[1] 0
> 1/(1/Inf)
[1] Inf
> 1/-Inf
[1] 0
> 1/(1/-Inf)
[1] -Inf

Néanmoins, les deux zéros sont égaux sans que leurs inverses le soient !

> pos=0
> neg=-0
> pos == neg
[1] TRUE
> 1/pos == 1/neg
[1] FALSE
> sign(pos)
[1] 0
> sign(neg)
[1] 0

La valeur Inf représente donc un nombre supérieur à tout ce qui est représentable mais inconnu. Alors que le +0 représente un nombre positif inférieur à tout ce qui est représentable mais inconnu. Certains calculs, tels que Inf – Inf conduisent à un résultat totalement inconnu qui est représenté par une valeur spéciale NaN. Cette valeur NaN se lit « Not a Number ».

> Inf - Inf
[1] NaN

Ce NaN se propage un peu de la même manière que les données manquantes (NA). La fonction is.na reconnaît d’ailleurs ces NaN comme des données manquantes. La fonction is.nan, par contre est spécifique des NaN.

Cas rapporté

Revenons à mon cas, qui illustre bien la perversité des FP si on n’est pas attentif. J’avais plusieurs fichiers de données correspondant à la capture de mouvement de plusieurs mires sur un mouvement de marche de plusieurs sujets. La capture de mouvement était faite cent fois par seconde par des caméras. Le timing de certains événements était enregistré en secondes, avec deux chiffres après la virgule. Ainsi, le temps 1,75 seconde correspondait à la frame numéro 175 de l’enregistrement. Une simple multiplication par 100 permettait de passer du timing enregistré au numéro de frame.

Pour un des calculs, j’avais besoin de calculer, sur un intervalle temporelle, la différence entre la coordonnée Z (hauteur) minimale observée sur l’intervalle [T1 ; T2] et la coordonnée Z observée à la fin de l’intervalle, c’est-à-dire au temps T2. Étant donné que T2 fait partie de l’intervalle, cette différence était obligatoirement positive ou nulle. J’observais pourtant des valeurs négatives ! Cela me conduit à un bout de code ressemblant à peu près à :

> min(Z[T1:T2])
[1] 23.78
> Z[T2]
[1] 21.14

(j’ai changé les noms des variables et les valeurs numériques, mais l’idée reste là)

Voilà à peu près ce qu’ont donné les investigations:

> T1 # ok
[1] 10
> T2 # ok
[1] 15
> T1:T2 # semble ok
[1] 10 11 12 13 14 15
> Z=1:15 # ça va simplifier l'interprétation
> Z[T2] # toujours ok
[1] 15
> Z[T1:T2] # tout est décalé vers le bas !
[1]  9 10 11 12 13 14
> Z[10:15]# mais là ça marche 
[1] 10 11 12 13 14 15

Pouvez-vous deviner le problème ?

Il vient de T1 et T2 qui ne sont pas vraiment entiers mais flottant, très proches de nombres entiers, d’une manière particulièrement perverse:

> T1-10
[1] -1.065814e-14
> T2-15
[1] 1.065814e-14

Et voilà !

Avec options(digits=22) on comprend encore mieux ce qui se passe:

> options(digits=22)
> T1
[1] 9.9999999999999893
> T2
[1] 15.000000000000011
> T1:T2
[1]  9.9999999999999893 10.9999999999999893 11.9999999999999893
[4] 12.9999999999999893 13.9999999999999893 14.9999999999999893
> as.integer(T2)
[1] 15
> as.integer(T1:T2)
[1]  9 10 11 12 13 14

Il faut comprendre que l’opérateur deux-points est un raccourci pour la fonction seq(from,to,by) qui marche avec des nombres non entiers. C’est ainsi, qu’on peut générer des séquences de nombres, en additionnant répététivement by à from, sans dépasser to. Ainsi, seq(0, 1, 0.40) va générer la sequence c(0, 0.40, 0.80). Eh bien, A:B est équivalent à seq(A, B, 1). Ici, le décalage vers le bas de T1 est répercuté sur la séquence entière. Ensuite, lorsqu’on indexe un vecteur par des indices non entiers, ils sont implicitement convertis par la fonction as.integer() qui tronque la virgule, c’est-à-dire, qui arrondit vers le bas pour les nombres positifs et vers le haut pour les nombres négatifs.

Le problème provenait des timings T1 et T2 que j’avais oublié d’arrondir au plus proche après la multiplication par 100 pour passer des secondes aux centisecondes.

Une fois détecté, le problème est trivial à résoudre, avec un simple appel à la fonction round().

Conséquences

Cette erreur a eu des conséquences non négligeables. J’ai rendu un rapport d’analyse statistique préliminaire dont les résultats étaient très légèrement erronés, car ils étaient fondés sur des valeurs qui avaient été calculés avec des décalages accidentels d’un centième de seconde. Ce n’est que lorsque j’ai voulu calculer des statistiques post hoc, que j’ai découvert l’incohérence ; celle-ci m’a ensuite conduit à découvrir le bug. Heureusement, une fois l’incohérence découverte l’investigation du bug n’a demandé que quelques minutes.

Ce qu’il faut en retenir

Il faut bien connaître les FP, parce qu’on va forcément se les prendre dans la face un jour ou l’autre. Je conseille aussi d’avoir une approche très dichotomique et systématique au débogage. Non seulement, cela permet de corriger son bug, mais parfois aussi d’en découvrir dans le logiciel et de faire un beau bug report !

Est-ce un bug de R ? Certains diraient que it works as designed ce à que je répondrais it is badly designed. Plutôt qu’un bug report, cela mériterait une feature request, pour demander au beau message d’erreur pour des indiçages aussi foireux.

Pour aller plus loin

Je conseille le site Web suivant: https://randomascii.wordpress.com/2012/04/21/exceptional-floating-point/

Update (14/05/2021)

Le logiciel R utilise pour certaines routines, sur plateforme PC (i386 ou x86_64), les FP80 de précision étendue offerts par l’interface x87 des microprocesseurs des PC plutôt que l’interface SSE2 qui implémente des FP64. C’est ainsi, que certaines routines fonctionnent avec une précision excédentaire.

> x=0;for(i in 1:1e9) {x=x+1/3};print(x); # FP64
[1] 333333332.6651181
> sum(rep(1/3,1e9)) # FP80 en interne
[1] 333333333.33365959
> 1/3*1e9 # pas de cumul d'erreur (en relatif)
[1] 333333333.33333331

Avec les FP80, l’erreur cumulée est 2048 fois plus faible, révélant une mantisse 63 bits plutôt que 52 bits, soit 11 bits supplémentaires, conduisant à une erreur 2^11 = 2048 fois plus faible.

Les fonctions rowSums, colSums, rowMeans, colMeans fonctionnent aussi en FP80 sur plateforme PC. On peut imaginer qu’on gagnerait nettement en performance si R utilisait des FP64 avec du SSE2 (SIMD 2×FP64) ou de l’AVX2 (SIMD 4×FP64), voire de l’AVX-512 (SIMD 8×FP64), mais en réalité, même pour un logiciel numérique comme R, le gain n’en vaut pas la chandelle par rapport au gain de précision fourni par les vieux FP80 des années 80 qui ont fait la renommée du Pentium. Pour ceux qui voudraient absolument booster les performances au détriment de la précision, il existe rowsums dans le package Rfast.

> Rfast::rowsums(matrix(nrow=1,rep(1/3,1e9)))
[1] 333333332.6651181

Mais le gain de performances est loin d’être extraordinaire sur des énormes vecteurs qui débordent largement de la cache L3, même lorsque la préparation de la matrice est minimaliste (testé sur AMD Ryzen 1700 qui dispose de 2 unités 128 bits capables de faire des additions 2×FP64):

> system.time(Rfast::colsums(matrix(nrow=1e9,ncol=1,1/3)))
   user  system elapsed 
   1.58    0.41    1.99 
> system.time(colSums(matrix(nrow=1e9,ncol=1,1/3)))
   user  system elapsed 
   2.56    0.49    3.05

Sur des vecteurs de taille modeste, tenant en cache L1, on peut perdre en performance:

> system.time(replicate(1e5,Rfast::colsums(matrix(nrow=1e3,ncol=1,1/3))))
   user  system elapsed 
   1.53    0.00    1.54 
> system.time(replicate(1e5,colSums(matrix(nrow=1e3,ncol=1,1/3))))
   user  system elapsed 
   1.13    0.00    1.13 

C’est probablement dû à l’inertie de lancement de la fonction Rfast::colsums. Cela nous montre que même sur des microbenchmarks, le gain SIMD est loin d’être extraordinaire.

Si on veut gagner un facteur deux en performances, on est obligé de créer un scenario spécifiquement optimisé pour ça:

> system.time(replicate(1e4,Rfast::colsums(matrix(nrow=1e5,ncol=1,1/3))))
   user  system elapsed 
   1.26    0.00    1.27 
> system.time(replicate(1e4,colSums(matrix(nrow=1e5,ncol=1,1/3))))
   user  system elapsed 
   2.33    0.00    2.33

Sur microarchitecture Intel, le gain devrait être nettement supérieur (https://www.agner.org/optimize/blog/read.php?i=838) depuis Haswell grace aux deux units 256 bits capables toutes deux de faire des additions de 4 FP64.

Update 2 (14/05/2021)

La fonction Rfast::colsums semble ne pas utiliser AVX2. Afin de vraiment évaluer les performances d’AVX2, j’ai dû développer un petit package:

#include <Rinternals.h>

#define NBYTES 32
#define NFP64 (NBYTES/8)
typedef double multifp __attribute__((vector_size (NBYTES)));

static double fpaddpar(double  *v, R_xlen_t n) {
        multifp psum1={0};
        multifp psum2={0};
        R_xlen_t i=0;
        double asum=0;
        
        /* ensure alignment */
        size_t uv = (size_t)v;
        while ((uv % NBYTES)!=0) {
                asum += *v++;
                uv = (size_t)v;
                n--;
        }

        multifp * restrict w=(multifp * restrict)v;
        R_xlen_t n2 = n/NFP64;
        for (i=0; i <= n2-2; i+=2) {
                psum1 += w[i];
                psum2 += w[i+1];
        }
                
        psum1 += psum2;
        #if NBYTES == 16
        	asum += psum1[0] + psum1[1];
        #elif NBYTES == 32
        	asum += psum1[0] + psum1[1] + psum1[2] + psum1[3];
        #else
        	#error number of bytes unsupported
        #endif
        
        for(i=i*NFP64; i < n; i++) {
                asum += v[i];
        }

        return asum;
}

static double slow_fpaddpar(double  *v, R_xlen_t n) {
  long double out=0;
  for(R_xlen_t i=0;i<n;i++) {
   out += v[i];
  }
  return out;
}

static SEXP fastsum(SEXP v) {
  SEXP result = PROTECT(allocVector(REALSXP, 1));
  REAL(result)[0] = fpaddpar(REAL(v), XLENGTH(v));

  UNPROTECT(1);
  return result;
}
static SEXP slowsum(SEXP v) {
  SEXP result = PROTECT(allocVector(REALSXP, 1));
  REAL(result)[0] = slow_fpaddpar(REAL(v), XLENGTH(v));

  UNPROTECT(1);
  return result;
}
static const R_CallMethodDef callMethods[]  = {
  {"fastsum", (DL_FUNC) &fastsum, 1},
  {"slowsum", (DL_FUNC) &slowsum, 1},
  {NULL, NULL, 0}
};

__declspec(dllexport) void R_init_fastsum(DllInfo *info) {
  R_registerRoutines(info, NULL, callMethods, NULL, NULL);
}

Le coeur de l’algorithme peut être retrouvé avec objdump -S pour avoir le code assembleur (usage de GCC 8.3.0 64 bits avec les options -O2 -march=haswell -mtune=haswell) :

 120:   48 83 c2 02             add    $0x2,%rdx
 124:   c4 c1 7d 58 00          vaddpd (%r8),%ymm0,%ymm0
 129:   c4 c1 75 58 48 20       vaddpd 0x20(%r8),%ymm1,%ymm1
 12f:   49 83 c0 40             add    $0x40,%r8
 133:   48 39 d1                cmp    %rdx,%rcx
 136:   7f e8                   jg     120 <fastsum+0x90>

Ainsi, AVX2 est bien utilisé, avec une parallélisation qui permet d’exploiter à fond les deux unités 256 bits du Haswell.

En créant des conditions artificielles absolument idéales, on voit effectivement un gain:

> v=rep(1/3,1e5) # AMD Ryzen 1700 à 3.00 Ghz
> system.time(replicate(1e4,.Call(C_fastsum,v)))
   user  system elapsed 
   0.14    0.00    0.14 
> system.time(replicate(1e4,sum(v)))
   user  system elapsed 
   1.61    0.00    1.61 

Mais dès qu’on prend en compte le temps de génération du vecteur, même simple, ce bénéfice s’estompe énormément:

> system.time(replicate(1e4,.Call(C_fastsum,rep(1/3, 1e5))))
   user  system elapsed 
   2.53    0.94    3.47 
> system.time(replicate(1e4,sum(rep(1/3, 1e5))))
   user  system elapsed 
   3.67    0.89    4.56

De même sur Haswell (core i5-4460 à 3.20 Ghz)

> v=rep(1/3,1e5)
> system.time(replicate(1e4,.Call(C_fastsum,v)))
   user  system elapsed 
   0.21    0.00    0.21 
> system.time(replicate(1e4,sum(v)))
   user  system elapsed 
   0.91    0.00    0.91 
> system.time(replicate(1e4,.Call(C_fastsum,rep(1/3,1e5))))
   user  system elapsed 
   2.28    1.30    3.58 
> system.time(replicate(1e4,sum(rep(1/3,1e5))))
   user  system elapsed 
   2.93    1.26    4.22

Étonnamment, le vieux Haswell core i5-4460 de 2014 est plus performant en FP80 que le Ryzen de 2017. En tout cas, les gains du SIMD se sentent toujours sur les microbenchmarks mais sont peu susceptibles d’avoir une influence dans du code R réel.

Update 3 (14/05/2021)

Après un déroulage de la boucle des FP80, la différence de performance s’amenuise:


#define NBYTES 32
#define NFP64 (NBYTES/8)
typedef double multifp __attribute__((vector_size (NBYTES)));

static double fpaddpar(double  *v, R_xlen_t n) {
        multifp psum1={0};
        multifp psum2={0};
        multifp psum3={0};
        multifp psum4={0};
        R_xlen_t i=0;
        double asum=0;
        
        /* ensure alignment */
        size_t uv = (size_t)v;
        while ((uv % NBYTES)!=0) {
                asum += *v++;
                uv = (size_t)v;
                n--;
        }

        multifp * restrict w=(multifp * restrict)v;
        R_xlen_t n2 = n/NFP64;
        for (i=0; i <= n2-4; i+=4) {
                psum1 += w[i];
                psum2 += w[i+1];
                psum3 += w[i+2];
                psum4 += w[i+3];
        }
                
        psum1 += psum2 + psum3 + psum4;
        #if NBYTES == 16
        	asum += psum1[0] + psum1[1];
        #elif NBYTES == 32
        	asum += psum1[0] + psum1[1] + psum1[2] + psum1[3];
        #else
        	#error number of bytes unsupported
        #endif
        
        for(i=i*NFP64; i < n; i++) {
                asum += v[i];
        }

        return asum;
}

static double slow_fpaddpar(double  *v, R_xlen_t n) {
  long double out1=0;
  long double out2=0;
  long double out3=0;
  long double out4=0;
  long double out5=0;
  long double out6=0;

  R_xlen_t i=0;
  for(;i<n;i+=6) {
   out1 += v[i];
   out2 += v[i+1];
   out3 += v[i+2];
   out4 += v[i+3];
   out5 += v[i+4];
   out6 += v[i+5];
  }
  out1+=out2+out3+out4+out5+out6;
  for(;i<n;i++) {
    out1+=v[i];
  }
  return out1;
}

Sur le core i5-4460 à 3.2 Ghz:

> v=rep(1/3, 1e5)
> system.time(replicate(1e4,.Call(C_fastsum,v)))
   user  system elapsed 
   0.22    0.00    0.22 
> system.time(replicate(1e4,.Call(C_slowsum,v)))
   user  system elapsed 
   0.42    0.02    0.44

Et sur le Ryzen 1700 à 3.0 Ghz (qui monte en réalité à 3.2 Ghz bien ventilé) :

> v=rep(1/3, 1e5)
> system.time(replicate(1e4,.Call(C_fastsum,v)))
   user  system elapsed 
   0.14    0.00    0.14 
> system.time(replicate(1e4,.Call(C_slowsum,v)))
   user  system elapsed 
   0.33    0.00    0.33 

Après analyse plus fine, les unités SIMD 128 bits (Ryzen) et 256 bits (Haswell) n’arrivent pas à saturation à cause du temps de load/store dans la cache L1/L2. Si on veut artificiellement montrer la supériorité du SIMD, il faut bidouiller le micro-benchmark pour que tout tienne de justesse en cache L1 et minimiser strictement les overheads d’appels en s’aidant de l’ALTREP de R 4.0 avec la boucle for.

> v=rep(1/3, 3e3) # Ryzen 1700
> A_fastsum=C_fastsum$address
> A_slowsum = C_slowsum$address
> system.time(for(i in 1:1e6) .Call(A_fastsum,v))
   user  system elapsed 
   0.33    0.00    0.33 
> system.time(for(i in 1:1e6) .Call(A_slowsum,v))
   user  system elapsed 
   1.08    0.00    1.08 

Même en trichant au maximum, on voit que c’est difficile de montrer un gros bénéfice du SIMD. L’architecture Zen étant limitée à 2 loads 128 bits/cycle depuis la cache L1 vers un registre.

Conditions de validité des estimateurs

J’avais vivement critiqué l’usage des tests de normalité dans un billet précédent, mais je n’avais pas insisté sur les bonnes manières de faire du cas de mésusage le plus fréquent des tests de normalité : s’assurer de la validité des estimateurs statistiques reposant sur la normalité asymptotique, tel que le test de Student sur séries appariées. Le test de normalité, dans ce contexte, fait exactement le contraire de ce qu’il faut, puisqu’il a une chance d’autant plus grande de conclure à la non normalité que l’échantillon est grand alors que la moyenne s’approche d’autant plus d’une loi normale. Ici, ce qui nous intéresse, c’est le biais d’estimation, exprimable par :

  1. Les risques de sous-estimation et surestimation de la statistique d’intérêt par l’intervalle de confiance
  2. Le risque alpha réel, qui doit s’approcher autant que possible du risque alpha nominal (généralement 5%)

Enjeu du choix d’un estimateur

Un point important est de bien distinguer l’estimateur du paramètre de la population qu’il estime. L’estimateur est une procédure qui, à partir d’un échantillon, fournit une estimation du paramètre, plus ou moins proche de cette cible. Dans les estimateurs, on distingue aussi:

  1. Les estimateurs ponctuels, qui fournissent une seule valeur, que l’on espère être aussi proche que possible du paramètre
  2. Les estimateurs d’intervalles de confiance, qui fournissent un intervalle qui doit avoir une chance élevée (p.e. 95%) de contenir le paramètre

Dans un projet de recherche on voudra généralement estimer un paramètre précis, mais le statisticien aura la liberté de choisir les estimateurs (p.e. ponctuel et intervalle de confiance) qui lui conviennent dans le paragraphe statistique du protocole de recherche. Par exemple, on voudra estimer la différence de moyennes entre deux groupes, mais le statisticien pourra calculer l’intervalle de confiance de cette différence de moyennes par:

  1. Test de Student sur séries indépendantes, avec hypothèse de variances égales
  2. Test d’Aspin-Welch qui est la variante à « variances inégales » du test de Student sur séries indépendantes
  3. Bootstrap non paramétrique, avec ses variantes : percentile, BCa, avec approximation normale, etc.
  4. Encore d’autres méthodes sont possibles, notamment dans des situations où on a des informations supplémentaires sur les distributions de ces moyennes, telles que le fait qu’elles suivent des lois binomiales ou de Poisson

Tous ces estimateurs estiment la même chose : différence de deux moyennes. Ils répondent à la même question de recherche et leur choix doit être guidé principalement par deux choses : les biais et l’efficacité. L’estimateur d’intervalle de confiance à 95% idéal conduira à une faible largeur d’intervalle de confiance tout en contrôlant parfaitement les risques de sous-estimation et surestimation à 2,5% chacun.

Choisir un estimateur ou un paramètre ?

Il faut bien distinguer le choix d’estimateur du choix du paramètre de la population à estimer. Le choix du paramètre de la population à estimer doit se faire en premier et il doit être guidé par la question de recherche. Le choix de l’estimateur doit se faire sur les biais et l’efficacité. Dans certaines situtations, le paramètre d’intérêt n’est tout simplement pas estimable car l’échantillon est trop petit (ou pour d’autres raisons). Au pire, il faut complètement remettre en cause la faisabilité de l’étude ; changer complètement de méthodologie, voire renoncer complètement à répondre à la question de recherche plutôt que de répondre de manière correcte à une question hors sujet.

Je vais donner un exemple caricatural qui permet de comprendre l’enjeu. Considérons une assurance souhaitant calculer la rentabilité d’une assurance portant sur des satellites artificiels face au risque de défaillance du lanceur. La rentabilité de l’assurance repose sur le fait que les primes, en moyenne, sont plus grandes que le coût des sinistres assurés plus les frais de fonctionnement. L’incertitude principale concerne la fréquence des sinistres. Si l’échantillon ne contient qu’une centaine d’événements assurés et aucun sinistre, il est impossible d’évaluer la rentabilité de l’assurance de manière empirique directe. Le paramètre à estimer est une différence de moyennes. Certains pourraient être tentés d’estimer une rentabilité médiane plutôt que moyenne, mais celle-ci serait hors sujet. Il est certain que moins de 50% des assurés auront un sinistre. Une prime d’assurance égale aux frais de gestion du dossier plus trois dollars aura l’air alors rentable.

Il va falloir parfois estimer la fréquence et le coût des sinistres de manière indirecte, en se basant sur des statistiques générales de taux de défaillance des lanceurs.

Validité a posteriori ou a priori

Pour commencer, il faut distinguer deux approches distinctes à la validité : approche a priori et approche a posteriori. L’approche bien trop souvent choisie est purement a posteriori dans laquelle les données observées sur un échantillon servent à calculer une ou plusieurs statistiques de validité. Si toutes les statistiques de validité sont satisfaisantes, selon des seuils prédéfinis ou selon une évaluation subjective, alors, l’estimateur statistique (p.e. Student) est considéré comme valide. Sinon, l’estimateur est considéré comme invalide et un autre estimateur statistique est utilisé, voire dans le pire des cas, le statisticien renonce à fournir une estimation.

Cette approche a posteriori est particulièrement problématique par rapport à la théorie fréquentiste qui doit juger de la procédure statistique dans son ensemble. Considérons par exemple, la procédure statistique suivante:

  1. Réaliser un test d’égalité des variances entre deux groupes
  2. Si les variances sont significativement différentes, alors réaliser estimer la différence de moyennes par Aspin-Welch
  3. Sinon, estimer la différence de moyennes par Student

Cette procédure statistique devient un nouvel estimateur dont on doit étudier les propriétés statistiques dans l’ensemble. Si vous étudiez cet estimateur sur des simulations, vous devriez constater qu’il est fortement biaisé en cas d’hétéroscédasticité forte sur de petits échantillons de taille déséquilibrée, parce que le test d’égalité des variances sera de puissance insuffisante, conduisant à une procédure proche d’un Student systématique.

Renoncer à fournir une statistique en raison d’une invalidité statistique fait aussi partie de la procédure dans son ensemble et engendre un selective reporting biais. Je pense notamment aux odds ratio dans une étude cas-témoin dont l’estimateur semble d’autant moins valide que l’odds ratio observé est éloigné de un. Ainsi, il existera une covariance entre l’odds ratio observé et ses chances de publication. Par exemple, considérons une étude cas-témoin avec 100 cas, 100 témoins et 20 exposés. Si on observe 10 exposés chez les cas et 10 exposés chez les témoins (odds ratio = 1), beaucoup de procédures considérerons que l’estimation de l’odds ratio est valide. Si on observe les 19 exposés chez les cas et 1 exposé chez les témoins, on peut craindre une mauvaise fiabilité de l’estimateur de Wald de l’intervalle de confiance dans une régression logistique. Si on rennonce à fournir cet odds ratio, cela est alors entièrement dû à la valeur observée trop forte de cet odds ratio ; en faisant ainsi, on biaise la méta-analyse, vers une sous-estimation de l’odds ratio réel.

On peut limiter ce problème en particulier, en basant le seuil décisionnel sur le nombre total de cas exposés (ici 20 exposés) plutôt que sur le nombre d’exposés dans les groupes de cas (n=19) et de témoins (n=1) ; cela évite que le selective reporting ait une covariance avec l’odds ratio observé et engendre un biais important dans la méta-analyse. Néanmoins, ça ne résoud pas complètement le problème de validité si jamais on obtient 19 exposés dans un groupe et 1 dans l’autre. Pour cela, la solution est d’anticiper autant que possible et de reposer au maximum sur la validité a priori. C’est-à-dire, que si on pense que l’odds ratio réel est très fort, alors on aura anticipé ce problème et on calculera le nombre de sujets nécessaires afin d’avoir suffisamment d’exposés dans les deux groupes.

Approche combinée

L’approche que je conseille combine et hiérarchise les deux méthodes : a priori et a posteriori. Idéalement, il faut anticiper au maximum la validité statistique, en se mettant de bonnes marges de sécurité. On n’est néanmoins jamais à l’abri de résultats très écartés de ce qu’on avait anticipé. La vérification a posteriori de la validité est avant tout un filet de sécurité : sa présence est importante, mais il doit servir le plus rarement possible. C’est ce que les anglo-saxons appelleraient un sanity check. En cas de chute dans la filet de sécurité, je ne donnerai pas de règle quant aux analyses à réaliser. Une ou plusieurs choses se sont complètement écartées de ce qu’on anticipait. D’un côté, toute analyse que l’on réalisera sera biaisée puisque le schéma fréquentiste global, supposant que la procédure de l’analyse est parfaitement prédéfinie, sera violé. D’un autre côté, c’est aussi une opportunité d’avancer vraiment dans la connaissance sur le sujet. Les échecs sont au moins aussi importants que les succès en recherche. Le processus de publication, obligeant tous les articles à être les plus lisses et prévisibles possibles ne facilite malheureusement pas la publication de ces résultats pourtant nécessaires à l’avancée de la recherche.

On remarquera que l’anticipation, bien que parfois délicate, facilite beaucoup l’identification des situations où les résultats sont écartés de ce qu’on avait anticipé. Cela limite le HARKing (Hypothesis After Results are Known) et permet de vraiment apprendre des résultats.

Méthode générale a posteriori

Les dysfonctionnements pouvant conduire à une invalidité statistique détectable a posteriori sont:

  1. Hypothèses erronées sur la population : prévalences, incidences, distributions ou paramètres fortement différents de ce qui avait été anticipé
  2. Mauvaise connaissance de l’estimateur, qui, même dans des conditions a priori bonnes, s’avère être biaisé

Il existe une méthode très générale de validation a posteriori qui permet de limiter beaucoup ce second risque : le bootstrap.

Par exemple, pour évaluer la validité de l’intervalle de confiance de Student, on crée des rééchantillonnages de l’échantillon de taille N, par tirage au sort de N observations avec remise parmi les N observations. On calcule l’intervalle de confiance de Student dans chacun des N rééchantillonnages puis on estime la proportion des intervalles de confiance entièrement en dessous et au-dessus de la différence de moyennes observée sur l’échantillon initial. On compare ces proportions empiriques aux proportions nominales : généralement 2,5% de risque de sous-estimation et 2,5% de risque de surestimation.

Cette méthode est aussi adaptée aux estimateurs plus complexes, par exemple, dans des situations où un score de propension ou un modèle multivarié est impliqué. Dans ces situations il est important de bootstrapper la procédure dans son ensemble : par exemple, pour un ajustement sur le score de propension, il faut recalculer le score de propension sur chaque échantillon.

Un bénéfice secondaire majeur de cette stratégie est la progressive est l’acquisition de la connaissance du comportement des estimateurs que vous acquerrez progresivement durant votre pratique statistique. Cela facilitera l’anticipation de la validité. Il est aussi possible de faire des variantes, par exemple, en changeant les tailles des rééchantillonnages tout en rééchantillonnant toujours depuis le même échantillon initial. Cela vous permettra d’identifier des « limites » de validité. Il est aussi possible de déformer les distributions.

C’est ainsi que vous verrez que les tests de normalité n’ont aucune pertinence pour la méthode de Student : (1) si les deux groupes sont de taille peu différentes, la méthode est particulièrement robuste (2) la méthode est très résistante aux distributions discrètes, platykurtiques ou même leptokurtiques tant que ces distributions restent symétriques et bornées (3) en présence d’outliers, c’est cette fréquence d’outlier qui est importante à connaître : on peut considérer que la validité devient superposable à celle d’un estimateur de différences de pourcentages d’outliers. Nécessité d’une quarantaine d’outliers s’ils sont tous situés dans le même groupe, mais d’un bien plus petit nombre si les deux groupes sont de taille proche et la fréquence est peu différente entre les deux groupes.

Il est aussi possible, pour sa culture personnelle, de comparer plusieurs estimateurs afin de mieux les connaître.

Limites de la méthode

Cette méthode d’estimation empirique de la validité de l’estimateur par rééchantillonnage souffre d’une limite principale : le rééchantillonnage est réalisé à partir de l’échantillon plutôt que de la population. Si on est largement dans les conditions de validité, l’échantillon devrait être suffisament grand pour que les différences population-échantillon n’aient pas une influence majeure sur les résultats, et confirment alors la validité. Si on est largement en dehors des conditions de validité, l’échantillon pourra différer de la population, mais il est probable que des problèmes de validité restent manifestement visibles par les rééchantillonnages, même s’il existe des exceptions dont nous discuterons. Le problème surviendra généralement lorsqu’on est proches des limites de validité. On pourra alors surestimer ou sous-estimer la validité de l’estimateur selon que l’échantillon est malchanceux ou chanceux. Si on applique le principe « better safe than sorry« , il faut donc se mettre une certaine marge de sécurité, en ayant un niveau d’exigence assez élevé sur l’inflation ou la déflation des risques de sous-estimation et surestimation par intervalles de confiance. On peut aussi artificiellement baisser la taille des rééchantillonnages pour se mettre en situation défavorable.

Nous allons maintenant décrire les situations d’incapacité totale de la méthode du bootstrap à identifier un problème majeur de validité. La première situation est celle des outliers cachés. En reprenant l’exemple de l’assurance des satellites, si la base de données ne contient aucune défaillance de lanceur, on n’observera aucun problème de validité et on conclura, à tort, que la méthode de Student est tout à fait valide. C’est pourquoi, ce ne sera que par connaissance a priori du problème qu’on saura qu’on est largement en dehors des conditions de validité. Par contre, si on observe un ou quelques outliers, on devrait prendre conscience du problème même si on aura du mal à l’évaluer précisément. En effet, considérant l’indépendance entre les observations, si on observe trois outliers, on pourra considérer que l’espérance du nombre d’outliers (paramètre lambda d’une loi de Poisson) est compris entre 0,24 et 7,2 (intervalle de confiance de Garwood, cas limite d’un Clopper-Pearson avec dénominateur infini). Dans les deux scenarii extrêmes, on reste en dehors des conditions de validité.

La seconde situation est la présence de discontinuités dans l’estimateur. Par exemple, un estimateur d’effet avec des ajustements réalisés ou pas selon leur degré de significativité (p.e. inclusion des variables pas à pas) aura des fluctuations d’échantillonnage complètement différentes sur des rééchantillonnages que si on répétait l’expérience depuis la population, en raison de la non reproductibilité des petits p. Par exemple, si la puissance statistique pour une covariable est à 30%, et que cette covariable est fortement liée à l’exposition d’intérêt, il est possible que les fluctuations soient cahotiques et les approximations normales complètement invalides. Sur l’échantillon, cette covariable a 20% de risque de faire un petit p bilatéral supérieur ou égal à 0.78 (calcul : pnorm(-(qnorm(0.975)+qnorm(0.20)+ qnorm(0.20)))*2) ce qui conduirait à une fréquence d’inclusion de 5,9% (calcul : pnorm(qnorm(0.20)+ qnorm(0.20)) + 1-pnorm(qnorm(0.20)+ qnorm(0.20)+2*qnorm(0.975))) ce qui va très fortement sous-estimer les fluctuations discrètes engendrées par l’inclusion aléatoire de cette covariable dans le modèle.

De toute façon, il est préférable d’utiliser des estimateurs ne présentant que des fluctuations continues, mais ce n’est pas toujours évitable à 100%, notamment lorsqu’on manipule des lois binomiales ou de Poisson dans des cas limites.

La réalité des choses : pressions extérieures

Au final, on peut être obligé de calculer des statistiques sur des résultats secondaires, en sachant, dès l’élaboration du protocole, qu’on fonce droit dans le mur, parce que les investigateurs ou évaluateurs du projet insistent sur la nécessité d’utiliser un critère de jugement. Par exemple, on m’a imposé de comparer un incidence ratio à la valeur 1, sachant, qu’en étant optimiste, il était tout au plus à 1,20. Le nombre d’événements attendu, les deux groupes réunis, était inférieur à 2. Il faudrait des centaines d’événements si on voulait une puissance acceptable. Ma stratégie, dans ces situations, est de planifier l’échec, en imposant un critère statistique conditionnant le calcul à une statistique sur l’échantillon. Par exemple, je rédigerai « en dessous de 20 événements au total, aucune comparaison ne sera faite. » dans un contexte ou je sais très bien qu’on n’atteindra pas les 20 événements. Si j’avais tort, cela me placerait dans la même situation qu’un échec inattendu : celle d’un résultat extraordinaire qui mérite publication.

Apprentissage

J’ai déjà mentionné qu’en évaluant a posteriori la validité des estimateurs on apprenait à les connaître. J’insiste là-dessus. De mon point de vue un bon statisticien connaît les outils qu’il utilise. La formation continue, à partir de ses propres analyses lui permet d’acquérir cette connaissance, concernant à la fois les estimateurs, mais aussi les distributions univariées et multivariées du domaine qu’il analyse. C’est ainsi qu’il pourra anticiper un maximum de choses lors de l’élaboration du protocole. Contrairement à la plupart des formations initiales et continues qui restent trop générales, cette formation sur le tas permet l’acquisition d’une quantification fine et intuitive des distributions les plus courantes ainsi que du comportements des estimateurs.

Erreur fréquente : ajustement sur baseline

Un billet pour vous présenter une erreur d’analyse tellement fréquente que c’est l’analyse correcte qui devient une exception. C’est le cas de la comparaison de scores quantitatifs (p.e. scores de qualité de vie, scores fonctionnels) après une intervention (p.e. 3 mois après) entre deux groupes de sujets susceptibles de différer sur le score avant l’intervention (baseline). Le cas le plus problématique est celui où les deux groupes ne sont pas randomisés. Cela peut correspondre à l’analyse de facteurs pronostiques sous traitement, les groupes étant définis par ces facteurs pronostiques, ou à la comparaison non randomisée de deux interventions.

Même si une partie des réflexions s’appliquent aussi bien à la recherche de facteurs pronostiques sous traitement qu’à la comparaison thérapeutique, je me focaliserai sur ce second scenario car il est difficile de définir si un pronostic (évolution favorable ou défavorable) est meilleur qu’un autre lorsque le point de départ (état initial) diffère. Pour les facteurs pronostiques, les différences à baseline ne sont pas forcément accidentelles mais sont souvent réelles, devant être prises en compte pour l’interprétation, alors que pour les comparaisons thérapeutiques, ces différences sont typiquement dues à des biais d’indication et pourraient être rectifiées par un essai clinique randomisé.

Les trois manières les plus fréquentes de comparer les deux groupes sur leur score post-intervention sont:

  1. La comparaison brute des scores post-intervention, sans ajustement (p.e. test de Student sur séries indépendantes)
  2. La comparaison des changements (aussi appelées deltas ou différences appariées) de score entre les deux groupes. Ça peut être fait par un test de Student sur séries indépendantes sur les deltas, ou par un test d’interaction temps×groupe dans un modèle linéaire à effets mixtes.
  3. Comparaison des scores post-intervention entre les deux groupes avec ajustement sur le score pré-intervention (modèle linéaire général).

La méthode N°3 est la seule a être correcte dans les études comparant deux thérapeutiques. La méthode N°1 ignore les différences pré-intervention et est donc sujette aux facteurs de confusion tels que le biais d’indication qui tendrait, par exemple, à faire donner le traitement innovant aux patients dont le pronostic est meilleur, ou pire, que les autres. La méthode N°2 donne l’illusion de corriger les différences pré-existantes au temps pré-intervention mais en réalité, sur-corrige systématiquement la différence. Le groupe avantagé à baseline se trouvera alors fortement désavantagé. Un biais d’indication positif deviendra un biais d’indication négatif.

Pour comprendre ça, il faut comprendre comment fonctionne la méthode N°3, pourquoi elle est correcte, et comment les méthodes N°1 et N°2 se comportent par rapport à cette méthode de référence.

Régression vers la moyenne

Il faut comprendre que même pour une maladie chronique « stable », deux mesures répétées ne sont jamais parfaitement corrélées, ne serait-ce qu’à cause des erreurs de mesure et fluctuations d’échantillonnage de chacune des deux mesures. Si la maladie est stable, sans la moindre intervention, alors une régression linéaire d’une mesure faite à T1 sur une mesure faite à T2 (p.e. trois mois plus tard), va trouver une pente systématiquement inférieure à 1.

Ce petit exemple de code R illustre mon propos:

vraie_valeur_T1=rnorm(100000) # valeur réelle de l'état du sujet
vraie_valeur_T2=vraie_valeur_T1

mesure_T1 = vraie_valeur_T1 + rnorm(100000)
mesure_T2 = vraie_valeur_T2 + rnorm(100000)

lm(mesure_T2 ~ mesure_T1) # pente à 0.50

Dans l’exemple ci-dessus, je distingue deux concepts:

  1. la valeur du score du sujet, qui reflète son état général de base, tel qu’on pourrait l’obtenir en faisant la moyenne d’un grand nombre de mesures étalées sur une période de temps suffisante
  2. la mesure du score du sujet, qui reflète la valeur du score à laquelle s’ajoutent la fluctuation d’échantillonnage attribuable au jour de la mesure ainsi que l’erreur de mesure. Ces sources de variation sont imprévisibles et seront donc appelées l’aléatoire.

Dans l’exemple ci-dessus, bien que les valeurs à T1 et T2 soient identiques pour chacun des sujets, la pente de la régression linéaire faisant le lien entre les deux mesures est à 0.50.

Comment interpréter cette pente de régression à 0.50 ? Cela veut dire que l’espérance de la mesure à T2, conditionnelle à une mesure à T1, est égale à la moyenne générale plus 0.50 fois la mesure à T1. C’est-à-dire, un sujet dont la mesure à T1 est à +1 écart-type de la moyenne générale, devrait se trouver, en moyenne à +0.5 écart-type de la moyenne générale à T2. Un sujet qui se trouve à -1 écart-type de la moyenne générale à T1, devrait se trouver, en moyenne à -0.5 écart-type de la moyenne générale. Toute mesure écartée de la moyenne générale à T1, devrait se trouver, en moyenne, plus proche de la moyenne générale à T1. Ce phénomène est connu sous le nom de « régression vers la moyenne ». L’explication intuitive de ce phénomène, c’est qu’une mesure à +1 écart-type à T1 est explicable, en partie par le fait que le sujet à une valeur à T1 plus grande que les autres (ici, +0.5 écart-type en moyenne), mais aussi parce qu’il a eu une source de variation aléatoire plus grande que les autres (ici +0.5 écart-type en moyenne). Comme l’aléatoire est, par principe, indépendant de tout ce qui est observable, lors de la deuxième mesure à T2, l’aléatoire sera en moyenne nul (légèrement positif ou négatif sur une observation particulière), de telle sorte qu’en moyenne, il ne restera que les +0.5 écarts-types attribuables à une valeur réellement plus élevée à T1, alors qu’en moyenne l’aléatoire sera revenu à 0.

NB : Cette idée qu’une valeur à T1 à +1 écart-type de la moyenne générale est attribuable en partie à une différence réelle de valeur, et en partie attribuable à de l’aléatoire, est vraie, en moyenne seulement. C’est-à-dire, qu’à un niveau individuel, il est possible qu’une mesure à +1 écart-type soit la somme d’une valeur réelle à -0.2 écart-type et d’un aléatoire à +1.2 écart-type. Néanmoins, en moyenne, le phénomène décrit ci-dessus s’appliquera.

L’exemple fourni est assez artificiel, avec une pente à 0.50, explicable par le fait que la variance inter-sujet de la valeur réelle est égal à la variance de l’aléatoire. Si la part d’aléatoire est plus importante, alors la pente va diminuer, conduisant à une régression vers la moyenne plus violente. Au contraire, si la part d’aléatoire est minime, on tendra vers une pente à 1.

ratio = 3 # ratio entre l'écart-type aléatoire et l'écart-type
# de la vraie valeur

vraie_valeur_T1=rnorm(1000000)
vraie_valeur_T2=vraie_valeur_T1

mesure_T1 = vraie_valeur_T1 + ratio*rnorm(1000000)
mesure_T2 = vraie_valeur_T2 + ratio*rnorm(1000000)

lm(mesure_T2 ~ mesure_T1) # pente à 0.10

Dans l’exemple ci-dessus, le rapport des écarts-types est à 3, le rapport des variances est alors à 9 et la pente est à 1/(9+1) = 0.10. Le phénomène de régression vers la moyenne est très fort, parce que l’aléatoire est très fort; ainsi, une mesure très élevée témoigne très certainement d’une grosse part d’aléatoire plutôt que d’une valeur réellement très forte.

Régression vers la moyenne sous traitement

Comme décrit ci-dessus, lorsqu’il y a des mesures répétées en état stable, la deuxième mesure tendra, en moyenne, à se rapprocher de la moyenne générale par rapport à la première. Lorsque les patients bénéficient d’un traitement, alors il y aura une tendance générale à l’amélioration pour deux phénomènes :

  1. Les effets contextuels. Notamment la sélection des sujets au moment où ils vont le moins bien, et donc, dont l’aléatoire est défavorable à l’inclusion. Cela peut aussi correspondre à une maladie qui tend à la guérison spontanée.
  2. L’effet réel du traitement

Cette tendance générale s’exprimera comme la différence de moyennes entre T1 et T2. Le phénomène de régression vers la moyenne persistera après soustraction de cette tendance générale aux mesures de T2. Par ailleurs, de nouveaux phénomènes apparaîtront pour les scores de symptômes. Par exemple, si l’évolution est favorable pour la majorité des sujets, il est probable que la variance des scores soit plus faible après traitement qu’avant. En effet, pour un score symptomatique, d’autant plus élevé que les symptômes sont sévères et nombreux, les sujets auront majoritairement des scores proches de zéro après traitement. Dans ce cas, la pente de régression expliquant la mesure à T2 par la mesure à T1 sera encore plus proche de zéro. Plus rarement, lorsque l’effet du traitement est très variable, avec des sujets répondeurs et des non répondeurs, il est possible que la variance augmente, mais encore une fois, la pente de régression devrait rester inférieure à 1. D’un point de vue théorique, cette pente pourrait dépasser 1, dans des conditions qui ne sont jamais rencontrées dans la pratique : faibles erreurs de mesures et très faibles fluctuations d’échantillonnage et réponse au traitement extrêmement corrélée à la valeur à baseline, avec une variance à T2 largement supérieure à la variance à T1.

Ajustement sur la mesure pré-intervention

Comment comparer deux traitements sur une mesure post-intervention (T2) lorsque les mesures pré-intervention (T1) diffèrent ?

L’idée, c’est de rectifier les différences à T1 telles qu’elles se répercutent à T2. Cela se fait par une estimation empirique de la pente de la régression de la mesure à T2 par la mesure à T1. Cela se fait très bien dans un modèle linéaire général expliquant la mesure à T2 par l’effet groupe (tt innovant vs tt de référence) et par la mesure à T1.

Ci-dessous un exemple illustratif, dans lequel il existerait un biais d’indication majeur où les patients ayant le plus de symptômes (score élevé) bénéficieraient du traitement innovant alors que les patients ayant le moins de symptômes auraient le traitement de référence:

set.seed(2021) # graine pseudo-aléatoire pour avoir des résultats reproductibles

effet_tt = -1 # même effet traitement dans les deux groupes
N1 = 5000000 # taille du groupe 1
N2 = 5000000 # taille du groupe 2
N = N1+N2
ratio = 3 # ratio entre l'écart-type aléatoire et l'écart-type de la vraie valeur

vraie_valeur_T1=rnorm(N)
mesure_T1 = vraie_valeur_T1 + ratio*rnorm(N) # on ajoute l'erreur de mesure/fluctuation aléatoire
# le traitement innovant est donné aux sujets avec plus de s
ymptômes
tt_innovant = (mesure_T1 > 1.00) 

# peu importe le groupe, l'effet du traitement est le même dans les deux groupes
# car le traitement innovant n'est pas meilleur que le traitement de référence
vraie_valeur_T2=vraie_valeur_T1 + effet_tt

mesure_T2 = vraie_valeur_T2 + ratio*rnorm(N) # on ajoute l'erreur de mesure/fluctuation aléatoire

mean(mesure_T1[tt_innovant]) # mesure à T1 en moyenne à 3.19 dans le groupe tt innovant
mean(mesure_T1[!tt_innovant]) # mesure à T1 en moyenne à -1.92 dans le groupe tt de référence
mean(mesure_T2[tt_innovant]) # mesure à T1 en moyenne à -0.68 dans le groupe tt innovant
mean(mesure_T2[!tt_innovant]) # mesure à T1 en moyenne à -1.19 dans le groupe tt de référence

# le modèle correct
# trouve une différence nulle entre traitement innovant et traitement de référence
# et trouve un effet (pente) à 0.10 de la mesure à T1 sur la mesure à T2
lm(mesure_T2 ~ mesure_T1+tt_innovant)

Dans cet exemple, il existe un fort effet de régression vers la moyenne et quand bien même les deux traitements sont équivalents, la différence entre les groupes de traitement que l’on trouve à T1 (moyenne à 3.19 vs -1.92, différence à 5.1) se réduit par un facteur dix à T2 (-0.68 vs -1.19, différence à 0.51). Ce facteur dix est explicable par une pente de régression de la mesure à T1 sur la mesure à T2 qui est égale à un dixième (0.10), révélant une forte régression vers la moyenne. Le modèle linéaire ajusté sur la mesure à T1 est tout à fait capable de comparer les valeurs à T2 (-0.68 vs -1.19) en rectifiant la différence attribuable à la différence de moyennes à T1 (3.19 vs -1.92). Cela permet de conclure à l’équivalence des traitements (ici, l’échantillon étant immense, on peut considérer que tout est exact) car la différence à T2 n’est pas supérieure à ce qui est attendu (0.51 unité), compte tenu de la différence à baseline (5.1 unités) et de la pente de régression de T1 sur T2 (pente à 0.10).

Autres ajustements corrects ou inappropriés

Cependant, si on comparait les moyennes des mesures à T2 avec un test de Student sur séries indépendantes, en ignorant ainsi, les différences à T1, on conclurait que les sujets avec traitement innovant ont un plus grand nombre de symptômes (+0.51) que les sujets avec le traitement de référence. Ainsi, on conclurait, à tort, que le traitement innovant est moins bon que le traitement de référence alors qu’ils sont en réalité équivalents. Cet effet à +0.51 est due au biais d’indication.

Enfin, si on comparait les moyennes des changements (différences appariées T2 moins T1), on conclurait à tort que le traitement innovant est très largement supérieur au traitement de référence. En effet, cette différence de différences est égale à 0.51-5.1 = -4.6, témoignant d’une réduction des symptômes beaucoup plus marquée dans le groupe de traitement innovant bien qu’en réalité les deux traitements soient équivalents.

Comme dit en introduction, la comparaison des moyennes de changements conduit à une sur-correction des différences à baseline, transformant le désavantage du groupe traité par le traitement innovant, en un avantage.

Un autre point de vue, sur ces deux analyses biaisées, c’est de les interpréter comme une analyse ajustée dans lesquelles la pente de régression de la mesure T2 sur la mesure T1 est artificiellement imposée à zéro (pour le Student à T2 sans ajustement sur la baseline) ou artificiellement imposée à 1 (pour la comparaison de moyennes des changements). Cela peut se décrire par le code R suivant, complétant le précédent:

lm(mesure_T2 ~ offset(mesure_T1*0)+tt_innovant) # modèle ignorant les différences à baseline (Student, sans ajustemnet)
lm(mesure_T2 ~ offset(mesure_T1*1)+tt_innovant) # pente imposée à 1, comparant les moyennes des changements

En effet, les deux équations suivantes, de modèles linéaires, sont équivalentes:

T2 = beta0 + beta1×tt + 1×T1 + epsilon

(T2-T1) = beta0 + beta1×tt + epsilon

Où epsilon suit une loi normale de moyenne zéro et écart-type sigma.

On remarquera aussi que l’analyse des changements, ajustée sur les mesures à T1, est équivalente à l’analyse des mesures à T2, ajustée sur les mesures à T1. C’est-à-dire qu’on obtient exactement le même effet traitement avec les deux modèles suivants:

lm(mesure_T2 ~ mesure_T1+tt_innovant)
lm(mesure_T2-mesure_T1 ~ mesure_T1+tt_innovant)

Encore une fois, ça découle assez directement des équations des modèles linéaires:

T2 = beta0 + beta1×tt + beta2×T1 + epsilon

(T2-T1) = beta0′ + beta1’×tt + beta2’×T1 + epsilon

On constate que les deux équations sont équivalentes pour beta0=beta0′, beta1=beta1′ et beta2’=beta2-1. En bref, les coefficients de l’ordonnée à l’origine (intercept) et de l’effet traitement sont inchangés; seul change le coefficient de la pente, qui est réduit d’une unité pour la seconde analyse par rapport à la première. Autrement, les modèles sont équivalents, au sens où ils prédisent exactement les mêmes valeurs pour les mêmes sujets. Les résidus des modèles sont identiques. Les coefficients beta0 et beta1 ont exactement les mêmes estimations ponctuelles sur n’importe quel échantillon.

C’est très intéressant parce que ça permet de présenter exactement les mêmes résultats sous un angle plus plaisant pour les investigateurs qui ne comprennent rien aux phénomènes de régression vers la moyenne et qui veulent absolument comparer les moyennes des changements : on peut leur dire qu’on a comparé les moyennes des changements, comme ils le souhaitaient, mais qu’on a ajusté sur la mesure à T1.

Modèle à effets mixtes

Comme présenté en introduction, le modèle à effets mixtes expliquant à la fois la mesure à T1 et à T2, avec un intercept aléatoire sujet, un effet temps, un effet groupe et une interaction temps×groupe, est équivalent à une comparaison de moyennes des changements (différences appariées) et conduit donc a une sur-correction de la différence à baseline. En l’absence de données manquantes, les estimations ponctuelles sont identiques. Néanmoins, ces modèles à effets mixtes rajoutent un grand nombre d’hypothèses dont on doit réfléchir aux conséquences. D’abord, il y a une hypothèse d’homoscédasticité (égalité des variances résiduelles) à T1 et à T2 alors que le simple Student fait seulement l’hypothèse d’homoscédasticité entre les groupes sur les différences appariées. Heureusement, comme le nombre de mesures à T1 et à T2 sont égales, en l’absence de données manquantes, les écarts à cette hypothèse de validité semblent n’engendrer aucun biais sur les intervalles de confiance. Il y a aussi une hypothèse de normalité des résidus qui semble, comme le Student, être relâchée lorsque l’échantillon est suffisamment grand. Le conditionnement à l’exposition, notamment à l’effet sujet, ne pose pas non plus de problème lorsque le modèle est linéaire avec fonction de lien identité. On peut aussi redouter que la variabilité de l’effet du traitement d’un sujet à l’autre pose problème, mais en réalité, cela est intégré dans le résidu, et se contente d’augmenter la variance résiduelle à T2, avec des problèmes d’hétéroscédasticité qui s’avèrent n’avoir pas de conséquence. En l’absence de données manquantes, c’est donc, équivalent, en estimation ponctuelle, et en incertitude, à un Student sur les différences appariées. En pratique, il y a quelques minimes biais supplémentaires, tels que le décompte des degrés de libertés qui est souvent incorrect dans les modèles à effets mixtes (dépend du logiciel et du package utilisé). En présence de données manquantes, le modèle à effets mixtes donne l’illusion de faire participer les perdus de vue, alors qu’en réalité, ils ne participent quasiment pas à la statistique car ils ne fournissent qu’une information infime sur la différence appariée ; et cette information repose énormément sur l’hypothèse de normalité. Au mieux, ils servent un tout petit peu à estimer la variance, en risquant de la biaiser un tout petit peu ; mais même là, leur variance étant la somme de la variance inter-sujet et intra-sujet, ils ne fournissent quasiment aucune information sur l’un ou l’autre des deux ; et cette information repose aussi massivement sur l’hypothèse de normalité. En bref, c’est une manière d’exclure les données manquantes sans le dire.

Essais cliniques randomisés

Pour les essais cliniques randomisés bien menés, la différence à baseline a une espérance nulle et les trois analyses ont la même espérance : comparaison sans ajustement à T2, comparaison ajustée sur T1 et comparaison des différences T2-T1. Néanmoins, la variance résiduelle sera minimale avec la comparaison ajustée sur T1, la précision statistique sera maximale et la puissance sera maximale, sauf si la pente de régression de T1 sur T2 est extrêmement proche de 0 ou de 1. J’affectionne particulièrement la comparaison à T2 sans ajustement, car cela est l’analyse la plus simple possible. Cela évite d’être accusé de p-Hacking.

D’ailleurs, c’est un des problèmes de ces multiples possibilités d’analyse : il est très facile de p-hacker. Même si les analyses sont équivalentes en espérance, sur un échantillon donné, elles peuvent différer de manière substantielle, fournissant alors une grande liberté de choix du petit p.

Erreur fréquente : facteurs de réponse au traitement

Un petit mot pour dire qu’il ne faut pas confondre pronostic sous traitement et réponse au traitement. La réponse à un traitement, c’est la différence d’évolution entre un sujet qui aurait le traitement et le même sujet qui ne l’aurait pas. Généralement, on peut évaluer la réponse moyenne à un traitement par un essai clinique randomisé en groupes parallèles. La réponse individuelle est bien plus difficile à évaluer, voire impossible, car il n’est pas forcément possible de savoir qu’elle aurait été l’évolution si le patient n’avait pas reçu la prise en charge qu’il eût.

Ce qu’il faut faire

L’identification d’un facteur de réponse au traitement nécessite schématiquement quatre groupes:

  1. un groupe exposé au facteur et prenant le traitement
  2. un groupe exposé au facteur et ne prenant pas le traitement
  3. un groupe non exposé au facteur et prenant le traitement
  4. un groupe non exposé au facteur et ne prenant pas le traitement

(pour un facteur quantitatif, on peut distinguer différents niveaux d’exposition, mais l’idée reste la même, il doit y avoir une variance de ce facteur d’exposition dans un groupe traité comme dans un groupe non traité)

La différence moyenne entre (1) et (2) représente la réponse moyenne des exposés alors que la différence moyenne entre (3) et (4) représente la réponse moyenne des non exposés. La différence des différences (interaction) représente la différence de réponses et si elle est statistiquement significativement différente de zéro (ou d’un seuil de significativité clinique) conduit à la conclusion que l’exposition considérée est un facteur de réponse au traitement.

Ces analyses de facteur de réponse au traitement correspondent aux fameuses analyses en sous-groupes que l’on retrouve dans bon nombre d’essais cliniques randomisés.

Ce qu’il ne faut pas faire

Malheureusement la majorité des soi-disant recherche de facteurs de réponse à un traitement qu’il m’ait été donné de voir ne contenaient que deux groupes: les patients traités et exposés au facteur et les patients traités et non exposés au facteur. Cela permet seulement de trouver des facteurs pronostics sous traitement, c’est-à-dire des facteurs prédictifs d’une évolution favorable ou défavorable, et ce, sur une population de patients tous traités.

Comme exemple frappant, je suggère de considérer l’analyse de soi-disant facteurs de réponse à l’homéopathie sur le cancer de la prostate. Le critère de jugement principal serait la survie globale de patients. On constaterait alors que les patients peu symptomatiques, dont le stade tumoral est précoce (notamment sans métastases) et de bas grade Gleason sont les meilleurs répondeurs à l’homéopathie.

Régression vers la moyenne

Pourquoi confond-on la réponse au traitement et le pronostic sous traitement ? Pourquoi toute évolution clinique n’est-elle pas totalement attribuable au traitement ?

Certaines maladies ont une évolution spontanée tendant vers une évolution inexorablement défavorable; cela comprend notamment la plupart des maladies neurodégénératives telles que les maladies démentielles, la maladie de Parkinson, la sclérose latérale amyotrophique. Même si cette évolution défavorable ne concerne pas forcément 100% des patients, on peut dire que l’état clinique moyen se dégrade.

D’autres maladies ont une évolution moyenne plus stable, comme l’asthme chronique. Néanmoins, il est très rare que la stabilité individuelle soit parfaite. Il existera presque toujours des fluctuations intra-sujet, c’est-à-dire, des jours, semaines ou mois plus symptomatiques que d’autres. Le phénomène de régression vers la moyenne s’applique alors à ces sujets. En moyenne un sujet sélectionné sur un état clinique moins bon que la moyenne, tendra à en voir une amélioration dans les jours, semaines ou mois avenir. Or, l’initiation des traitements est rarement faite au moment où le sujet va le mieux; au contraire, ce sera lors d’un état clinique médiocre ou d’une dégradation de l’état habituel. Parfois cela est même formalisé explicitement dans les critères d’inclusions, sous forme d’un seuil sur un score ! Par simple régression vers la moyenne, l’état clinique du sujet tendra à s’améliorer quel que soit le traitement donné; c’est une des principales raisons justifiant la perception d’efficacité de l’homéopathie. Il s’agit alors de l’évolution spontanée de la maladie.

Si on analyse la différence entre deux mesures répétées d’un score des symptômes, à baseline et un certain temps après (p.e. 3 mois), alors il s’agit de la somme de deux composantes:

  1. L’évolution spontanée
  2. La réponse au traitement

Malheureusement, c’est souvent interprété comme la réponse au traitement.

Il apparaît alors que les patients dont l’état est le pire vont le mieux s’améliorer, ce qui pourra être, à tort, considéré comme une meilleure réponse au traitement.

Si on a en conscience cela, il faut savoir qu’on peut avoir une réponse au traitement quand bien même l’état clinique du patient est stable, comme pour la sclérose latérale amyotrophique pour laquelle l’évolution naturelle est très défavorable et une stabilisation prolongée déjà acceptable pour un traitement. Et bien sûr, il peut y avoir une évolution spontanée favorable sans la moindre réponse au traitement. Un traitement homéopathique d’une angine tendra à une résolution totale des symptômes en généralement quelques jours, ainsi que l’absence totale de traitement. La réponse doit être évaluée par la différence entre les des deux effets.

C’est peut-être pour cette confusion entre réponse au traitement et pronostic sous traitement que l’homéopathie est aussi populaire en France.

Pour aller plus loin

Comme mentionné au début, un facteur de réponse au traitement est un facteur ayant une interaction statistique avec l’effet du traitement, c’est-à-dire, la différence entre un groupe traité et un groupe non traité. La notion d’interaction est assez simple lorsque les états cliniques sans traitement sont identiques chez les exposés et les non exposés au facteur. Par contre, si ces états diffèrent, l’interaction statistique dépend alors du modèle employé, pouvant conduire à des conclusions opposées.

Considérons, par exemple, que l’on s’intéresse à un critère de jugement binaire, tel que « mortalité à 1 an ».

Non traitésTraités
Non exposés30%10%
Exposés60%30%
Taux de mortalité à 1 an selon la présence d’exposition et l’usage d’un traitement spécifique

Pour une différence absolue de mortalité, on peut dire que les non exposés ont une meilleure réponse au traitement (-30% de décès) que les non exposés (-20%). Pour un risque relatif de décès, au contraire, les non exposés ont une réponse moindre (RR = 0.50) aux exposés (RR = 0.33). Pour un odds ratio de risque de décès, c’est à nouveau les non exposés qui ont une meilleure réponse (0.26 vs 0.29). La réponse pourrait encore différer si l’on s’intéressait aux rapport des chances de survie plutôt que des risques de décès. Bien sûr, les hazard ratio conduisent encore à des conclusions différentes.

Est-ce vraiment si important que ça ? De toute façon, les deux groupes, exposés et non exposés, bénéficient chacun du traitement et ces deux groupes ne sont pas comparables de toute façon. Au final, à quoi sert-il de comparer leur réponse ? Il pourrait finalement être plus sage de juste comparer leur pronostic sous traitement ou de ne rien comparer du tout.

Là où il est crucial d’identifier des facteurs de réponse au traitement, c’est lorsque certains sous-groupes ne répondent pas du tout, voire pire, ont une réponse négative au traitement (interaction qualitative). Dans ces situations, la statistique d’analyse de l’effet du traitement n’importe pas, puisque toutes tendront à la même conclusion.

On peut aussi s’interroger sur la pratique consistant à tenter de prouver l’homogénéité de l’effet d’un traitement dans divers sous-groupes en formulant une hypothèse nulle d’absence d’interaction, puis en acceptant cette hypothèse après réalisation d’un test statistique sous-puissant. Comme précédemment décrit, les interactions quantitatives, consistant à des effets plus ou moins forts du traitement selon les sous-groupes, sont bien moins préoccupantes que les interactions qualitatives; mieux vaudrait juste prouver l’existence d’un effet positif du traitement dans chacun des sous-groupes plutôt que de comparer les effets entre eux ; mais cette méthode mettrait en évidence l’incapacité à conclure sur certains sous-groupes en situation de sous-puissance statistique plutôt que de fournir une confortable acceptation de l’hypothèse nulle en situtation de sous-puissance majorée.

Sur ce, je vous laisse méditer…