cassionAnalyse de données

Retour à la leçonLeçon 5 sur 8Où les comparaisons cèdent

Vingt-quatre tests, deux constats, aucun effet

Le même diaporama que les téléchargements, rendu sous forme de page. Lancez le diaporama pour le présenter en plein écran — les flèches ou un clic avancent d'une diapositive, Échap quitte.

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 27

    Ce que couvre cette leçon

    • Une comparaison dont on connaît la réponse
    • Deux, c'est ce que le hasard vous devait
    • Bonferroni : divisez le seuil par le nombre de tests
    • Benjamini-Hochberg, quand vingt-quatre devient deux cents
    • La correction ne détruit pas les constats réels
    • Comptez les tests, y compris ceux que vous n'avez pas rapportés
    • Rapportez-le en entier
    • La suite
    Notes du présentateur
    Le registre d'inscription tire le sexe indépendamment de tout le reste, si bien que l'écart de retard d'âge selon le sexe vaut exactement zéro par construction. Testez-le école par école et deux des vingt-quatre ressortent significatives, ce qui est à peu près ce que le hasard vous devait.
  2. Diapositive 2 / 27

    Une comparaison dont on connaît la réponse — En Python (suite)

    import pandas as pd
    import numpy as np
    from statsmodels.stats.proportion import proportions_ztest
    
    enrolment = pd.read_csv("school-enrolment-2024.v1.csv")
    clean = enrolment[(enrolment["school_year"] == 2024)
                      & (enrolment["age_years"] <= 20)
                      & enrolment["grade"].between(1, 6)]
    clean = clean.assign(over_age=clean["age_years"] > clean["grade"] + 5)
    
    results = []
    for school, group in clean.groupby("school_id"):
        counts = group.groupby("sex")["over_age"].agg(["sum", "size"])
        if counts["size"].min() < 5:
            continue
        stat, p = proportions_ztest(counts["sum"], counts["size"])
    Notes du présentateur
    Les deux leçons précédentes testaient une comparaison à la fois. Une vraie analyse s'arrête rarement à une seule : un relecteur demande le même indicateur par district, par année, par école, par statut de déplacement, et l'analyste lance chacun. Cette leçon en lance vingt-quatre sur une comparaison dont la vraie réponse est connue, parce que le jeu de données a été produit avec le sexe tiré indépendamment de l'âge, de l'année, du redoublement et de l'école. La vraie différence de retard d'âge selon le sexe vaut exactement zéro, et tout ce qu'un test y trouve est un artefact d'échantillonnage.
  3. Diapositive 3 / 27

    Une comparaison dont on connaît la réponse — En Python (suite)

        results.append({"school": school, "p": p,
                        "boys": counts.loc["m", "sum"] / counts.loc["m", "size"],
                        "girls": counts.loc["f", "sum"] / counts.loc["f", "size"]})
    
    tests = pd.DataFrame(results).sort_values("p")
    print(f"{len(tests)} schools tested, {(tests['p'] < 0.05).sum()} significant")
  4. Diapositive 4 / 27

    Une comparaison dont on connaît la réponse — En R

    library(dplyr)
    
    clean |>
      summarise(k = sum(over_age), n = n(), .by = c(school_id, sex)) |>
      tidyr::pivot_wider(names_from = sex, values_from = c(k, n)) |>
      filter(n_m >= 5, n_f >= 5) |>
      rowwise() |>
      mutate(p = prop.test(c(k_m, k_f), c(n_m, n_f))$p.value)
  5. Diapositive 5 / 27

    Une comparaison dont on connaît la réponse

    ÉcoleGarçonsFillesp
    SCH2362,5 % (10/16)25,9 % (7/27)0,018
    SCH0639,1 % (9/23)12,5 % (3/24)0,036
    SCH2143,5 % (10/23)25,8 % (8/31)0,173
    SCH0350,0 % (11/22)30,8 % (8/26)0,175
    SCH0427,3 % (3/11)55,6 % (5/9)0,199
  6. Diapositive 6 / 27

    Une comparaison dont on connaît la réponse

    • Vingt-quatre écoles testées, deux significatives à p < 0,05 — Les deux seraient écrites
    • Aucune des deux n'est réelle — Le générateur n'a jamais laissé le sexe toucher quoi que ce soit
    Notes du présentateur
    Vingt-quatre écoles testées, deux significatives à p < 0,05. Les deux seraient écrites. SCH23 en particulier paraît frappante — des garçons en retard d'âge à plus du double du taux des filles, dans une école de 43 enfants. Aucune des deux n'est réelle. Le générateur n'a jamais laissé le sexe toucher quoi que ce soit.
  7. Diapositive 7 / 27

    Deux, c'est ce que le hasard vous devait — En Python

    n_tests = len(tests)
    print(f"tests run: {n_tests}")
    print(f"expected significant if nothing is real: {0.05 * n_tests:.1f}")
    print(f"chance of at least one: {1 - 0.95 ** n_tests:.1%}")
  8. Diapositive 8 / 27

    Deux, c'est ce que le hasard vous devait — En R

    # 1 - 0.95^24. The arithmetic is one line and it is the whole lesson.
  9. Diapositive 9 / 27

    Deux, c'est ce que le hasard vous devait

    • Faux positifs attendus : 1,2. Observés : 2 — La probabilité d'obtenir au moins un résultat significatif à partir de…
    • Un seuil de 0,05 est une promesse portant sur un test — Il dit que si rien ne se passe, vous serez trompé une fois sur…
    • C'est pourquoi « nous avons regardé par école et deux écoles se détachent » n'est pas un constat — C'est une…
    Notes du présentateur
    Faux positifs attendus : 1,2. Observés : 2. La probabilité d'obtenir au moins un résultat significatif à partir de vingt-quatre hypothèses nulles vraies vaut 70,8 % — le résultat surprenant aurait donc été de n'en trouver aucun. Un seuil de 0,05 est une promesse portant sur un test. Il dit que si rien ne se passe, vous serez trompé une fois sur vingt. Lancez vingt tests et être trompé une fois est le cas attendu, non l'accident. C'est pourquoi « nous avons regardé par école et deux écoles se détachent » n'est pas un constat. C'est une description de ce que font vingt-quatre tests.
  10. Diapositive 10 / 27

    Bonferroni : divisez le seuil par le nombre de tests — En Python

    BONFERRONI = 0.05 / n_tests
    print(f"corrected threshold: {BONFERRONI:.5f}")
    print(f"surviving: {(tests['p'] < BONFERRONI).sum()}")
  11. Diapositive 11 / 27

    Bonferroni : divisez le seuil par le nombre de tests — En R

    p.adjust(tests$p, method = "bonferroni") < 0.05
  12. Diapositive 12 / 27

    Bonferroni : divisez le seuil par le nombre de tests

    • Seuil 0,05/24 = 0,00208. Survivants : aucun — Le plus petit p des vingt-quatre vaut 0,018, un ordre de grandeur plus…
    Notes du présentateur
    Seuil 0,05/24 = 0,00208. Survivants : aucun. Le plus petit p des vingt-quatre vaut 0,018, un ordre de grandeur plus loin. La correction est grossière — elle suppose le pire cas, que tous les tests sont indépendants et toutes les hypothèses nulles vraies — et elle est grossière du bon côté. C'est le bon choix par défaut quand vous n'avez pas de raison d'en préférer un autre, et elle tient sur une ligne.
  13. Diapositive 13 / 27

    Benjamini-Hochberg, quand vingt-quatre devient deux cents — En Python

    from statsmodels.stats.multitest import multipletests
    
    reject, adjusted, _, _ = multipletests(tests["p"], alpha=0.05, method="fdr_bh")
    print(f"BH survivors: {reject.sum()}")
    Notes du présentateur
    Bonferroni contrôle la probabilité d'un seul faux positif, ce qui est strict. Quand une analyse de dépistage lance des centaines de tests et s'attend à quelques effets réels parmi eux, contrôler la proportion de fausses découvertes est la garantie la plus utile.
  14. Diapositive 14 / 27

    Benjamini-Hochberg, quand vingt-quatre devient deux cents — En R

    p.adjust(tests$p, method = "BH") < 0.05
  15. Diapositive 15 / 27

    Benjamini-Hochberg, quand vingt-quatre devient deux cents

    • Benjamini-Hochberg ne retient lui non plus aucune des vingt-quatre — ce qui est la bonne réponse ici — il n'y a rien à…
    • Employez Bonferroni pour une petite famille confirmatoire ; BH pour un large dépistage exploratoire — Les deux tiennent…
    Notes du présentateur
    Benjamini-Hochberg ne retient lui non plus aucune des vingt-quatre, ce qui est la bonne réponse ici — il n'y a rien à trouver. Son avantage apparaît quand il y a quelque chose : sur deux cents tests dont vingt effets réels, Bonferroni en manquera la plupart et BH non. Employez Bonferroni pour une petite famille confirmatoire ; BH pour un large dépistage exploratoire. Les deux tiennent sur une ligne, et le choix importe beaucoup moins que le fait d'en faire un.
  16. Diapositive 16 / 27

    La correction ne détruit pas les constats réels — En Python

    previous = enrolment[enrolment["school_year"] == 2023].set_index("student_id")
    clean = clean.assign(last_year=clean["student_id"].map(previous["end_of_year_status"]))
    returning = clean[clean["last_year"].isin(["repeated", "promoted"])]
    
    survivors = []
    for school, group in returning.groupby("school_id"):
        counts = group.groupby("last_year")["over_age"].agg(["sum", "size"])
        if len(counts) < 2 or counts["size"].min() < 5:
            continue
        stat, p = proportions_ztest(counts["sum"], counts["size"])
        survivors.append(p)
    
    survivors = np.array(survivors)
    print(f"{len(survivors)} schools, {(survivors < 0.05).sum()} significant, "
          f"{(survivors < 0.05 / len(survivors)).sum()} survive Bonferroni")
    Notes du présentateur
    La crainte qui empêche de corriger est que cela enterre quelque chose de vrai. Testez un effet réel de la même façon et regardez ce qui se produit.
  17. Diapositive 17 / 27

    La correction ne détruit pas les constats réels — En R

    # Same loop, different comparison. The generator made this one real.
  18. Diapositive 18 / 27

    La correction ne détruit pas les constats réels

    ComparaisonTestsSignificatifs à 0,05Survivent à Bonferroni
    Retard d'âge selon le sexe (aucun effet réel)2420
    Retard d'âge selon le redoublement de 2023 (réel)14149
  19. Diapositive 19 / 27

    La correction ne détruit pas les constats réels

    • L'écart de 94,5 % contre 30,6 % du cours d'éducation survit à la correction dans neuf des quatorze écoles où il peut…
    Notes du présentateur
    L'écart de 94,5 % contre 30,6 % du cours d'éducation survit à la correction dans neuf des quatorze écoles où il peut être testé. Un écart qui n'existe pas ne survit nulle part. La correction n'est pas un impôt sur les constats ; c'est ce qui les sépare des deux écoles qui allaient de toute façon paraître intéressantes.
  20. Diapositive 20 / 27

    Comptez les tests, y compris ceux que vous n'avez pas rapportés

    • Les sous-groupes regardés puis abandonnés — Découper par district, ne rien voir, et découper par année ensuite fait…
    • Les résultats essayés tour à tour — Tester le retard d'âge, puis la présence, puis l'achèvement contre le même…
    • Les seuils déplacés — « Retard d'âge » à année + 2 plutôt qu'à année + 1 est un second test de la même idée
    • L'analyse que quelqu'un d'autre a lancée sur les mêmes données — Rarement connue, et à demander quand un relecteur…
    Notes du présentateur
    Le nombre qui entre dans la correction est le nombre de tests que vous avez lancés, non celui que vous avez écrit. Quatre sortes de tests échappent régulièrement au compte : Les sous-groupes regardés puis abandonnés. Découper par district, ne rien voir, et découper par année ensuite fait deux familles de tests, non une. Les résultats essayés tour à tour. Tester le retard d'âge, puis la présence, puis l'achèvement contre le même regroupement fait trois tests. Les seuils déplacés. « Retard d'âge » à année + 2 plutôt qu'à année + 1 est un second test de la même idée. L'analyse que quelqu'un d'autre a lancée sur les mêmes données. Rarement connue, et à demander quand un relecteur arrive avec un résultat frappant sur un sous-groupe.
  21. Diapositive 21 / 27

    Comptez les tests, y compris ceux que vous n'avez pas rapportés — En Python

    plan = {
        "outcomes": ["over_age"],
        "groupings": ["sex"],
        "subgroups": ["school_id"],
    }
    n_planned = 1 * 1 * 24
    print(f"tests declared in the analysis plan: {n_planned}")
  22. Diapositive 22 / 27

    Comptez les tests, y compris ceux que vous n'avez pas rapportés — En R

    # Write the number down before running anything. It cannot be recovered after.
  23. Diapositive 23 / 27

    Comptez les tests, y compris ceux que vous n'avez pas rapportés

    • Déclarez la famille avant de la lancer — Après coup, le nombre de tests est affaire de mémoire et d'intérêt personnel,…
    Notes du présentateur
    Déclarez la famille avant de la lancer. Après coup, le nombre de tests est affaire de mémoire et d'intérêt personnel, et les deux le réduisent.
  24. Diapositive 24 / 27

    Rapportez-le en entier — Exemple

    Over-age enrolment by sex, school-level analysis
    
      24 schools tested (both sexes with at least 5 students in grades 1-6).
      2 schools significant at p < 0.05: SCH23 (p = 0.018), SCH06 (p = 0.036).
    
      With 24 tests, 1.2 significant results are expected by chance alone and
      the probability of at least one is 71%. Neither school survives the
      Bonferroni threshold of 0.0021, and neither is reported as a finding.
    
      For comparison, over-age by 2023 repetition is significant in all 14
      schools testable and survives Bonferroni in 9 of them.
  25. Diapositive 25 / 27

    Rapportez-le en entier

    • La ligne de comparaison est ce qui rend le bloc convaincant plutôt que défensif — Elle montre la même procédure…
    Notes du présentateur
    La ligne de comparaison est ce qui rend le bloc convaincant plutôt que défensif. Elle montre la même procédure trouvant quelque chose quand il y a quelque chose à trouver, ce qu'un responsable de programme a besoin de voir avant d'accepter que deux écoles frappantes soient du bruit.
  26. Diapositive 26 / 27

    La suite

    • Tous les tests jusqu'ici ont supposé qu'une ligne était une observation indépendante.
    Notes du présentateur
    Tous les tests jusqu'ici ont supposé qu'une ligne était une observation indépendante. La leçon suivante prend une comparaison où cette hypothèse tombe — une cantine scolaire attribuée par école, testée sur des élèves — et trouve une statistique t presque deux fois plus grande que l'honnête.
  27. Diapositive 27 / 27

    La suite

    Lire la leçon complète, avec le code exécutable Retour à la leçon