Une première étude statistique

Introduction

Le monde étant complexe, nous construisons constamment – consciemment ou non – des modèles pour le simplifier, afin d’émettre des questions et valider des hypothèses qui vont ensuite guider nos décisions et nos actes. Par exemple, pour lutter contre les discriminations de genre, d’origine, d’âge, on catalogue la population entre hommes et femmes, habitants des villes, des banlieues ou des campagnes, nationaux et étrangers, jeunes et vieux, pour étudier des questions comme «y-a-t’il discrimination entre X et Y?», afin de prendre des mesures adéquates.

Un des rôles de la statistique en général et de la sciences des données en particulier est de formaliser ce processus afin d’exercer un regard critique sur toute la chaîne. Regard critique essentiel car, comme chacun sait, «il y a les petits mensonges, les gros mensonges, et les statistiques»: entre modèles trop-simplifiés (par exemple la classification non inclusive homme/femme) et biais en tous genres, il n’est que trop facile de faire dire (volontairement ou non) n’importe quoi. Pour interpréter un résultat statistique, il est essentiel de connaître tout le contexte (réalité terrain, données, modèle, …) qui a amené à son calcul.

Dans cette feuille, nous allons illustrer sur un exemple comment on peut étudier une question et tenter d’y répondre en mettant en action la démarche statistique. Nous nous appuyons sur un exemple tiré du cours Data 8 de sélection du jury d’assise pour les procès de crimes. Cet exemple est inspiré par des études statistiques réalisées dans le contexte de la lutte pour les droits civiques aux États-Unis (voir par exemple U.S. Supreme Court, 1965: Swain vs. Alabama) et prend une actualité toute particulière avec le procès en cours suite à la mort de Georges Floyd et plus généralement le mouvement «Black Lives Matter».

Ce sont des sujets importants et très sensibles; la plus grande prudence doit être observée dans l’interprétation des résultats. Voir notamment le préambule de l’exemple dans le cours Data 8.

De l’impartialité des jurys d’assise

Le point de départ est amendement suivant de la constitution américaine qui y encadre la composition des jurys populaires dans l’équivalent de nos cours d’assise où l’on juge des crimes:

«Amendment VI of the United States Constitution states that, “In all criminal prosecutions, the accused shall enjoy the right to a speedy and public trial, by an impartial jury of the State and district wherein the crime shall have been committed.” One characteristic of an impartial jury is that it should be selected from a jury panel that is representative of the population of the relevant region. The jury panel is the group of people from which jurors are selected.»

Nous traduirons librement «jury panel» par «présélection de jurés».

La question de savoir si une présélection de jurés est en effet représentative de la population de la région a une implication légale importante: on pourrait en effet remettre en cause l’impartialité du jury si un groupe de la population était systématiquement sous-représenté dans cette présélection.

La question étudiée

Prenons par exemple une région hypothétique dont la population serait répartie en deux groupes (par exemple habitants des villes ou des campagnes). Disons que 26% des jurés potentiels sont dans A. Supposons de plus que, à l’occasion d’un procès donné, une présélection \(P\) de cent jurés ait été faite dans laquelle seuls huit membres sont dans A, soit (8%). On pourrait certainement questionner cette différence de proportion entre 26% et 8%, tout particulièrement si l’accusé est dans A. Considérons l’hypothèse «cette différence est l’effet du hasard plutôt que d’un biais systématique contre la sélection de juré dans A». On ne peut exclure cette hypothèse, mais est-elle plausible?

Modélisation

Quantifions cela. Pour commencer introduisons un modèle pour le processus de sélection des jurés basé sur un processus aléatoire.

Modèle: les 100 membres de la présélection sont tirés de façon aléatoire uniforme à partir de la population éligible de la région, dont 26% est dans A.

Nous allons maintenant étudier l’hypothèse suivante: ce modèle est représentatif du processus réel qui a conduit à la préselection \(P\) (que nous appellerons réalité terrain); ce n’est que l’effet du hasard qui s’il n’y a au final que 8 jurés de A.

Comment tester la validité de cette hypothèse?

Le modèle étant très simple, il serait possible de mener une étude probabiliste. Nous allons ici nous contenter de procéder par simulation, en produisant un jeu de donnée sur la base de ce modèle, c’est-à-dire en tirant un grand nombre de présélections aléatoirement. Ces données donneront des prédictions sur ce à quoi ressemble une présélection aléatoire. Si ces prédictions ne sont pas cohérente avec la présélection, cela donnera un faisceau d’indice contre l’hypothèse et donc contre l’impartialité du procès.

Voyons cela en détail.

Étude par simulation

De même que dans le modèle, il est souhaitable d’avoir la simulation la plus simple qui ne perde pas l’essence du problème.

Pour le problème étudié, la seule information pertinente a propos d’un juré est son appartenance au groupe A ou B. Nous allons donc représenter une présélection par une liste de cent lettres, où la i-ème lettre est ‘A’ ou ‘B’ selon si le i-ème membre du jury est dans A ou B.

Si l’on poursuit le raisonnement, l’ordre d’apparition des A et B dans notre liste n’a pas d’importance, puisqu’elle représente un ensemble de jurés. Tout ce qui compte est le nombre de A (et donc de B). Ce nombre est la statistique sur les présélections que nous allons simuler, pour la comparer ensuite avec celle dans la présélection \(P\) du jury réel.

TODO: décider comment adapter cet exercice sachant que .count(A) fait l’affaire

Exercice: Implanter une fonction qui, étant donné une liste de ‘A’ et de ‘B’, renvoie le nombre de ‘A’.

def compteA(preselection: list) -> int:
    """
    Renvoie le nombre de 'A' dans preselection
    """
    ### BEGIN SOLUTION
    return len([l for l in preselection if l == 'A'])
    ### END SOLUTION

Vérifiez votre fonction sur l’exemple suivant:

compteA(['A', 'B', 'B', 'A'])

Faites quelques essais supplémentaires:

Lancez ces tests sur les cas de base:

assert compteA([]) == 0
assert compteA(['A']) == 1
assert compteA(['B']) == 0

Lancez les tests systématiques suivants:

import random
for n in range(10):
    for a in range(n):
        # Fabrication d'une liste aléatoire de taille n avec a 'A'
        l = ['A'] * a + ['B'] * (n-a)
        random.shuffle(l)
        assert compteA(l) == a

Simulation d’un tirage aléatoire d’une présélection

Nous souhaitons maintenant simuler le tirage au hasard de 100 personnes distinctes dans la population, puis compter le nombre d’entre eux dans A. Un algorithme possible serait de réellement tirer 100 personnes distinctes dans la population (on pourrait utiliser random.sample pour cela, ou tirer à la main les personnes les unes après les autres); pour ce faire, il faudrait connaître la taille totale de la population. Pour simplifier, nous supposerons qu’elle est très grande par rapport à la présélection. De ce fait, même lorsque l’on a tiré un certain nombre de personnes, la proportion de membres de A dans la population restante n’est pas modifiée significativement.

Du coup, à chaque tirage, la probabilité que la nouvelle personne choisie soit dans A est de 0,26.

Exercice: implantez une fonction lettreAleatoire(p) qui renvoie aléatoirement ‘A’ ou ‘B’ avec probabilité \(p\) pour ‘A’.
Indications:

  • Consultez la documentation de random.random:

random.random?
  • Essayez plusieurs fois random.random:

random.random()

À vous de jouer maintenant pour lettreAleatoire:

import random
def lettreAleatoire(p: float) -> str:
    ### BEGIN SOLUTION
    if random.random() < p:
        return 'A'
    else:
        return 'B'
    ### END SOLUTION

Essayez quelques fois votre fonction:

lettreAleatoire(0.26)

Lancez les tests suivants qui vérifient 20 fois que lettreAleatoire() renvoie ‘A’ ou ‘B’

for i in range(20):
    assert lettreAleatoire(0.26) in ['A', 'B']

Exercice: implantez une fonction preselection qui construit une liste de n lettres avec n=100 par défaut, où chaque lettre est tirée aléatoirement de façons indépendant parmi ‘A’ et ‘B’ avec probabilité 0,26 pour ‘A’.
Indication: utilisez une compréhension

def preselection(n: int = 100) -> list:
    ### BEGIN SOLUTION
    return [lettreAleatoire(0.26)
            for i in range(n)
           ]
    ### END SOLUTION

Essayez quelques fois votre fonction:

preselection(10)

Lancez les tests suivants qui vérifient que la fonction renvoie bien des listes de 100 lettres A ou B:

for i in range(20):
    l = preselection()
    assert isinstance(l, list)
    assert len(l) == 100
    assert all(lettre in ['A', 'B'] for lettre in l)

Il serait souhaitable aussi de vérifier que la liste est bien aléatoire. Cela demanderait des tests statistiques; ce sera pour plus tard :-)

Calculez la statistique du nombre de A pour un échantillon aléatoire; réitérer plusieurs fois le calcul pour vous faire une première intuition de la variabilité de cette statistique.

### BEGIN SOLUTION
compteA(preselection())
### END SOLUTION

Simulation d’un grand nombre de tirages aléatoires de présélections

Pour affiner cette intuition, nous allons faire un grand nombre de tirages aléatoires et des statistiques sur le résultat.

Tirez aléatoirement dix mille présélections, calculez la statistique de chacune d’entre elle et stockez le résultat dans une série pandas dataset de nom «nombre de jurés dans A».

Indications:

  • utiliser une compréhension;

  • revoir les exemples d’utilisation de pandas.Series et notamment l’argument name=....

import pandas
dataset = pandas.Series((compteA(preselection()) for i in range(1000)),
                        name="nombre de jurés dans A")

Ce sera notre jeu de données; un tel jeu de donnée est souvent dit synthétique car obtenu par une simulation.

Reste à faire interpréter les résultats de la simulation. Cela commence toujours par de l’extraction de statistiques simples et de la visualisation!

Affichez les statistiques simples du jeu de donnée (describe):

### BEGIN SOLUTION
dataset.describe()
### END SOLUTION

Affichez un histogramme du jeu de données (hist):

### BEGIN SOLUTION
dataset.hist()
### END SOLUTION

Cet histogramme nous prédit empiriquement le nombre de jurés dans A dans une présélection aléatoire. Comme on s’y attendait ce nombre est autour de 26, avec des variations.

Relancez la simulation pour évaluer intuitivement si la forme générale de l’histogramme varie d’une simulation à l’autre.

Comparaison entre prédiction et réalité terrain

La marque rouge sur l’axe horizontal ci-dessous indique la position de la valeur 8 dans l’histogramme:

from matplotlib import pyplot
dataset.hist()
pyplot.scatter(x=[8], y=[0], color="red", marker=2)

Comment interprétez vous le résultat des simulations? Le modèle colle-t’il avec la réalité terrain? Autrement dit paraît-il plausible que la présélection ait été tirée aléatoirement? Est-ce impossible?

BEGIN SOLUTION

La valeur 8 est très à gauche dans la queue de l’histogramme. C’est-à-dire que, même si le nombre de jurés dans A varie d’un tirage à l’autre, très peu d’entre eux donnent un nombre inférieur ou égal à 8.

La simulation montre que si nous tirons une présélection aléatoire de jurés en suivant le modèle, il est très peu probable – quoi que non impossible – d’obtenir un décompte de jurés dans A inférieur ou égal à 8 comme dans la réalité terrain. C’est une indication forte que ce modèle ne colle pas à cette dernière.

Il est donc raisonnable de remettre en question le processus de sélection, et de suspecter fortement que celui-ci était soumis à un biais en faveur de la population B.

END SOLUTION

Conclusion

Nous avons esquissé sur cet exemple une méthode très générale pour évaluer la validité d’un modèle, méthode que nous formaliserons et développerons dans les cours suivants.