# Parameters
nb_name = "ICT-35-HumorCausalProbe-Pilot-Python"Statut épistémique — Sans verdict à ce jour : aucune ligne de la matrice de dissociations ne concerne ce notebook ; son statut épistémique sera porté par la matrice le cas échéant.
Acquis et parenté : strate 5 (ICT-21 → ICT-25 prolongée par ICT-35 → ICT-39 — le LLM comme substrat). Étage 0 du programme HumorCausalProbe (#5635, #8236) : la baseline de shortcuts lexicaux se mesure avant toute phase SAE, les phases suivantes restant aval.
ICT-35 – HumorCausalProbe-Pilot : substrat HLS, sans GPU, sans confusion explication/mecanisme
Serie ICT (Integrated Causal Trajectories, EPIC #4588) – strate 5 : extraction GPU-free. Issue : #14035. prerequis livre : banc humour dur consolide dans GT-18c (120 instances annotees, #12785 puis absorption #14032). Aval : #5635 (Gate 24 workspace), #8236 (multi-echelle), #5105 (Inoculation).
Étage 0 – baseline de shortcuts lexicaux (PAS Phase 1 SAE)
Ce notebook est l’étage 0 d’un escalier qui monte vers un protocole d’humour falsifiable :
- Surface lexicale (ce notebook, ICT-35-Pilot) – 6 features lexicales deterministes sur 30 paires ; verdict borne sur le sous-corpus.
- Embeddings / modele gele (a venir) – features continues sur la zone editee (sortie d’un encodeur fixe).
- Modele generatif avec contexte narratif (a venir) – discrimination sur la sortie textuelle complete.
- SAE / J-lens (#14035 horizon) – features sparse sur les activations internes ; le protocole Phase 1 (placeholder ci-dessous) ne consomme QUE l’etage 0.
A chaque etage, on mesure le gain par niveau de taxonomie avec intervalles de confiance (pas une moyenne globale). Le verdict de cet etage 0 ne falsifie ni l’humour lexical en general, ni un mecanisme interne : il borne la discrimination de ces 6 features sur ce corpus – une mesure reproductible, pas un verdict fort.
Pourquoi ce notebook existe
Le banc humour de la serie GameTheory (GT-28 / GT-28b) mesure les sorties d’un LLM classifieur. Il dit quelque chose sur la difficulte du probleme (F1 0,5-0,6 sur 5 classes), pas sur le mecanisme interne. Les surveys HLS placent l’humour parmi les semantiques de haut niveau qui exigent contexte, chaines causales et pragmatique – et avertissent que les explications textuelles post-hoc sont rarement fideles au mecanisme reel.
Ce notebook pose l’etage 0 lexical du protocole de l’issue #14035 :
- 30 appariements exploratoires
humour_reussi<->unfunissus du CORPUS_DUR ; 6 positifs uniques reutilises 5x (le corpus n a que 6 texteshumour_reussidans le sous-ensemble stratifie), 30 negatifs uniques apparies par longueur (+/-20 %) pour isoler la dimension humoristique de la dimension verbosite. C est une mesure exploratoire sur un sous-corps appauvri, pas un test sur 30 paires minimales independantes. - 2 controles negatifs : (a) shuffle des labels (memes textes, attribution aleatoire) ; (b) surface-only = blagues reussies amputees du recadrage (sanity check que la discrimination n’est pas triviale, borne a ces 6 transformations).
- Mesure de stabilite : inter-prompts (3 reformulations par instance), inter-formes (ponctuelles, sociales, topical).
- Classifieur lexical (regression logistique regularisee sur 6 features lexicales simples, ajustee INTRA-fold via
Pipelinepour eviter la fuite de StandardScaler) : F1 hold-out pour discriminerhumour_reussivsnon-humour. - Verdict borne :
FEATURE_CANDIDATE(F1 > 0,65, donc les features lexicales capturent quelque chose au-dela de la surface) /SURFACE_SEULE(F1 <= 0,55) /INCONCLUSIVE(entre les deux). Le verdict porte sur la capacite des 6 features lexicales a discriminer sur des positifs jamais vus (StratifiedKFold n_splits=3 sur 36 unites group-level = 6 H + 30 N distincts), pas sur la memorisation des 6 positifs en CV stratifiee par instance.
Ce que ce notebook ne fait PAS
- Pas de GPU. C’est l’etage 0 lexical, l’agent du worktree n’a pas de GPU.
- Pas de SAE ni de transformer reel. Les features lexicales servent de baseline de shortcuts mesurable hors-ligne ; un futur etage (gated par #5635 GPU Gate 24) consommera ce protocole en remplacant features lexicales -> features SAE.
- Pas de confusion explication/mecanisme. Aucune explication generee par LLM n’est traitee comme preuve de mecanisme interne. C’est la clause 6 de l’acceptance #14035.
- Pas de generalisation a d’autres corpus. Le verdict porte strictement sur les 30 paires du sous-ensemble stratifie du banc dur ; il est INCONCLUSIVE borne, pas une falsification de l’humour lexical en general.
Sequencage developemental (juste pour memoire, sans age invente)
Ce notebook est volontairement circonscrit a l’etage 0 lexical. Les etages suivants dependent d’un substrat que CE notebook ne construit pas :
- #14032 consolide la methode de labellisation du GT-28 / GT-28b (heuristique Argumentum + or humain echantillonne). Sans substrat propre, aucun etage superieur n’est lancable.
- #14033 livre le corpus apparie avec 3 labels independants par instance (structure / cible pragmatique / fonction encyclopedique) et la taxonomie croissante des capacites que les etages superieurs discrimineront. La taxonomie est sourcee (Attardo/Raskin general theory of verbal humor + Mihalcea/Pulido humor stylometry), pas inventee dans cette PR. Ce notebook n est PAS adosse a #14033 : il est exploratoire et precede l arrivee du substrat taxonomique. L acceptance #14035 ” >= 30 paires minimales” est decochee pour ce sous-grain (verdict reformule : 30 appariements exploratoires sur 6 positifs uniques, pas 30 paires minimales).
- #14035 (cible) consomme le substrat de #14033 pour passer d’embeddings + SAE a un protocole complet. L’etage 0 lexical (ce notebook) est strictement anterieur a #14033 et ne court-circuite pas la taxonomie.
Taxonomie croissante (capacites discriminant l’humour, sourcee – pas d’ordre d’age invente) :
- Jeu sonore / repetition + incongruite perceptible (calembours, repetitions structurelles).
- Incongruite / resolution semantique simple (renversement attendu a porte close).
- Recit, attente et chute (structures narratives classiques).
- Perspective d’autrui, empathie, normes sociales (humour sur le rapport a l’autre).
- Implicite pragmatique, ironie, double audience (le non-dit, le sous-entendu).
- References culturelles / topicales, contexte long (encyclopedie partagee).
L’etage 0 ne discrimine que la couche 1 (jeu sonore / repetition + incongruite perceptible) sur les 6 positifs uniques du sous-corpus – les 5 autres capacites demandent l’etage embeddings ou contexte narratif. La discrimination est mesuree par StratifiedKFold(n_splits=3) sur 36 unites group-level (1 par groupe H = moyenne des 5 repetitions + 30 N singletons ; un meme texte positif source n apparait jamais en train et test du meme fold), pas par memorisation des 6 textes en CV stratifiee.
Sources
MyIA.AI.Notebooks/GameTheory/GameTheory-18c-Humour-Banc-Python.ipynb: CORPUS_DUR 120 instances (cell[33], section echelle consolidee #14032), labellisation manuelle par heuristique Argumentum (cf note methodo de l’acceptance).G:\Mon Drive\MyIA\IA\Bibliographie IA\MachineLearning\Computational Humor\: sources HLS (Attardo/Raskin GTVH + Mihalcea/Pulido humor stylometry), prevues comme reference pour la taxonomie – la mise en archive effective est a faire dans le scope de #14033, pas de ce notebook.
Redesign group-level (c.927 REPAIR v5, puis c.945 REPAIR v6)
Le protocole c.927 (po-2025 DM msg-20260903T164919-gtux5n) bascule l’unité de mesure de 60 instances (30 H x 5 repetitions + 30 N singletons) à 36 unites group-level : moyenne par groupe H (1 ligne par H = 6 lignes, moyenne des 5 repetitions) + 30 N singletons = 36 vecteurs. StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED) sur y_group, avec permutation intra-fold de y_group.
c.945 REPAIR P0 v6 (po-2025 adjoint DM msg-20260904T043716-lo9ryu) corrige trois defauts identifies verbatim sur HEAD 24d6448f9 (v5) :
cell[9] permutation logic. La v5 ne permutait JAMAIS les labels d’apprentissage : a chaque iteration, on refitait le meme modele sur
y_group[train_idx](vrais labels) puis on permutait seulement les 12 labels de test, scores avec predictions fixes. Lep_empirical=0.3967etait alors une proportion parmi 300 scores pseudo-independants (100 perms x 3 folds), pas un p-value de permutation CV. La v6 : (a) pour chaque permutation, construire un vecteury_permcomplet (longueur 36) en permutant les 36 unites au niveau global et en preservant la distribution 6H/30N ; (b) pour chaque fold des memesSPLITS, refit sury_perm[train_idx], F1 vsy_perm[test_idx]; (c) stocker une moyenne CV par permutation, pas 3 ; (d) rapporter mean/std/IC95 sur 100 moyennes, p fini(1 + count(null >= reel)) / (N_PERM + 1)(Phipps / North et al., borne superieure). Recalcul firsthand adjoint (memes SPLITS, memes seeds) : null0.187 ± 0.147, IC95[0.000, 0.542], 34/100 null >= reel, p =0.3465.cell[11] verdict logic. v5 nommait un verdict
SURFACE_SEULEquanddelta_proba OOF IC95traversait 0. Cette interpretation est abusive sur N=6 : un IC95 traverse 0 trivialement avec 6 items, ce n’est PAS la preuve d’une discrimination purement lexicale de surface. La v6 declareINCONCLUSIVE bornechaque fois que la permutation est non-significative OU que l’IC95 dedelta_probatraverse 0 ; SURFACE_SEULE demanderait un IC95 entierement au-dessus de 0 sur un N suffisant.frontmatter prose. v5 affirmait
StratifiedGroupKFoldla ou le code executeStratifiedKFold(n_splits=3)sur 36 unites group-level (la confusion venait de c.920 –StratifiedGroupKFoldsur 60 instances aurait ete coherent, mais c.927 a bascule surStratifiedKFoldquand l’unite est devenue le groupe H). La v6 aligne la prose sur le code.
Le verdict INCONCLUSIVE borne tient structurellement sur ce sous-corpus (36 unites), ce qui etait la conclusion v4/v5 reformulee en termes de mesure plus robuste (p fini + IC95 + verdict logic corrigee).
# -*- coding: utf-8 -*-
# Setup. Pas de service externe ; pas de GPU. CPU-only, sklearn, json, random.
import json
import random
import re
import string
from collections import Counter, defaultdict
from pathlib import Path
import pickle
import time
import numpy as np
# sklearn : classifieur lexical baseline. CPU only, deterministe.
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import f1_score, classification_report, confusion_matrix
from sklearn.preprocessing import StandardScaler
SEED = 13306
random.seed(SEED)
np.random.seed(SEED)
GT28B = Path("MyIA.AI.Notebooks/GameTheory/GameTheory-18c-Humour-Banc-Python.ipynb") # consolide #14032
assert GT28B.exists(), f"banc humour 18c manquant : {GT28B.resolve()}"
print(f"[setup] banc humour 18c (consolide #14032) present : {GT28B}")
print(f"[setup] seed = {SEED}")
print(f"[setup] sklearn LogisticRegression baseline (CPU)")[setup] banc humour 18c (consolide #14032) present : MyIA.AI.Notebooks\GameTheory\GameTheory-18c-Humour-Banc-Python.ipynb
[setup] seed = 13306
[setup] sklearn LogisticRegression baseline (CPU)
# Re-execute la section passage-a-l'echelle (cell[26..33]) du banc humour
# consolide (18c, ex-GT-28b absorbe par #14032) pour obtenir CORPUS_DUR.
# C'est une REPRODUCTION : on n'edite pas le notebook source, on importe
# ses cellules et on les execute ici. Source de verite = 18c consolide.
nb_src = json.loads(GT28B.read_text(encoding="utf-8"))
code_cells = [(i, ''.join(c['source'])) for i, c in enumerate(nb_src['cells'])
if c['cell_type'] == 'code' and 26 <= i <= 33]
print(f"[extract] {len(code_cells)} cellules code 18c[26..33] a executer")
ns = {}
for i, src in code_cells:
exec(src, ns)
CORPUS_DUR = ns['CORPUS_DUR']
print(f"[extract] CORPUS_DUR : {len(CORPUS_DUR)} instances")
print(f"[extract] distribution par label : {dict(Counter(i['label'] for i in CORPUS_DUR))}")
print(f"[extract] distribution par source : {dict(Counter(i['source'] for i in CORPUS_DUR))}")[extract] 5 cellules code 18c[26..33] a executer
[setup] Catégories : ['humour_reussi', 'rire_sans_recadrage', 'recadrage_sans_rire', 'offensif_compris_non_partage', 'rien']
[setup] LLM endpoint : https://openrouter.ai/api/v1
[setup] LLM model : anthropic/claude-haiku-4.5
[fetch] downloaded 558592 bytes -> argumentum_scenarii.csv
[fetch] upstream master @d68170c521 : docs(corpus): #458 liste de relecture native (38) -- 2 ouvertes, 6 confirmations
[parse] 167 scénarios Argumentum chargés
[parse] catégories : {'histoire': 17, 'mythologie': 27, 'relation intime': 36, 'vie professionnelle': 30, 'vie personnelle': 25, 'pop culture': 18, 'politique': 14}
[parse] sous-catégories (21) : {'antiquité': 6, 'moyen-âge et temps modernes': 6, '20e et 21e siècle': 5, 'contes': 10, 'religions': 11, 'littérature': 6, 'drague et séduction': 9, 'vie de couple': 16, 'romance': 11, 'interactions professionnelles': 14, 'relations au travail': 8, 'gestion et administration': 9, 'Bandes dessinées': 5, 'cinéma & télévision': 7, 'science': 6, 'gouvernance': 4, 'manoeuvres et collusion': 6, 'campagne': 4, 'famille et enfance': 8, 'voisins et amis': 11, 'loisirs et espace public': 5}
[parse] 167 instances retenues (champs FR)
[parse] exemple : id=1.1.1 titre='La mère de César et Cléopâtre'
baratineur='Aurelia Cotta, mère de César' -> piocheur='Jules César'
[label] distribution après annotation manuelle : {'rien': 46, 'recadrage_sans_rire': 27, 'humour_reussi': 55, 'rire_sans_recadrage': 25, 'offensif_compris_non_partage': 14}
[corpus] total : 120 instances
[corpus] distribution par label : {'rire_sans_recadrage': 14, 'recadrage_sans_rire': 13, 'rien': 38, 'humour_reussi': 48, 'offensif_compris_non_partage': 7}
[corpus] distribution par source : {'ArgumentumGames/Argumentum': 60, 'blague-manuelle': 30, 'declaration-factuelle': 20, 'edge-case-curated': 10}
[corpus] humour_reussi : 48 instances OK
[corpus] rire_sans_recadrage : 14 instances OK
[corpus] recadrage_sans_rire : 13 instances OK
[corpus] offensif_compris_non_partage : 7 instances OK
[corpus] rien : 38 instances OK
[extract] CORPUS_DUR : 120 instances
[extract] distribution par label : {'rire_sans_recadrage': 14, 'recadrage_sans_rire': 13, 'rien': 38, 'humour_reussi': 48, 'offensif_compris_non_partage': 7}
[extract] distribution par source : {'ArgumentumGames/Argumentum': 60, 'blague-manuelle': 30, 'declaration-factuelle': 20, 'edge-case-curated': 10}
Lecture 1 – Le sous-corpus : 120 instances, un label a 48
La sortie de l’extraction (cellules du banc 18c re-executees, aucune edition du notebook source) fixe le terrain : CORPUS_DUR = 120 instances, 5 categories ({‘rire_sans_recadrage’: 14, ‘recadrage_sans_rire’: 13, ‘rien’: 38, ‘humour_reussi’: 48, ‘offensif_compris_non_partage’: 7}) et 4 sources ({‘ArgumentumGames/Argumentum’: 60, ‘blague-manuelle’: 30, ‘declaration-factuelle’: 20, ‘edge-case-curated’: 10}).
Le label positif humour_reussi est la categorie la plus fournie du corpus brut (48 sur 120, soit 40 %) : la difficulte ne vient pas de sa rarete mais de la stratification du protocole, qui n’en retient que 6 (cellule suivante). C’est la premiere instance du motif « sous-corpus appauvri » du frontmatter : le protocole borne sa propre puissance statistique avant meme la premiere feature.
# Stratification : 6 humour_reussi (cell[42] du banc consolide), tous les non-humour disponibles.
# 6 humour_reussi (le label 'humour_reussi') vs ~72 non-humour (4 autres labels).
# Le ratio 1:12 maximise la variete des non-humour pour la diversite des negatifs.
# ATTENTION (c.920, fixe en c.927) : la strategie d'echantillonnage est maintenant **par
# groupe H** (les 30 repetitions de `humour_reussi` partagent la cle du texte source).
# L'unite de mesure est le **groupe** (1 ligne par H = moyenne des 5 repetitions).
# Au moment du split (cell[7] plus bas), on utilise StratifiedKFold(n_splits=3) sur
# y_group (36 unites group-level) plutot que StratifiedGroupKFold : c'est coherent
# avec une unite-de-groupe ou les repetitions ont deja ete reduites a une moyenne par H.
by_label = defaultdict(list)
for inst in CORPUS_DUR:
by_label[inst['label']].append(inst)
# humour_reussi : 6 instances (le sous-ensemble le plus petit).
random.shuffle(by_label['humour_reussi'])
H_REUSSI = by_label['humour_reussi'][:6]
# non-humour : TOUTES les instances des 4 autres labels.
# Scope exploratoire : on consomme la totalite des non-humour pour maximiser la diversite
# des negatifs sur 30 appariements exploratoires (6 positifs uniques x 5 repetitions +
# 30 negatifs uniques). Sub-grain limite volontairement avant tout substrat plus expressif
# (cf #14033 substrat taxonomique croissant, etape d apres).
NON_HUMOUR = []
for label in ['rire_sans_recadrage', 'recadrage_sans_rire', 'rien', 'offensif_compris_non_partage']:
NON_HUMOUR.extend(by_label[label])
print(f"[stratify] humour_reussi = {len(H_REUSSI)} instances")
print(f"[stratify] non-humour = {len(NON_HUMOUR)} instances (toutes disponibles)")
print(f"[stratify] ratio H:N = 1:{len(NON_HUMOUR)//len(H_REUSSI)}")[stratify] humour_reussi = 6 instances
[stratify] non-humour = 72 instances (toutes disponibles)
[stratify] ratio H:N = 1:12
# Appariement par longueur (token count approximatif, +/-20 %).
# On definit la longueur d'une instance par le nombre de mots de son texte.
def text_length(inst):
return len(inst['texte'].split())
H_LEN = sorted([(text_length(i), i) for i in H_REUSSI], key=lambda x: x[0])
N_LEN = sorted([(text_length(i), i) for i in NON_HUMOUR], key=lambda x: x[0])
print("[match] longueurs des 6 humour_reussi :", [l for l, _ in H_LEN])
print(f"[match] longueurs non-humour (n={len(N_LEN)}) : min={min(l for l,_ in N_LEN)} max={max(l for l,_ in N_LEN)}")
# Pour chacun des 6 humour_reussi, on cherche le non-humour le plus proche en longueur.
PAIRES = [] # (humour, non_humour, delta_len)
used = set()
for hl, hi in H_LEN:
candidates = [(abs(nl - hl), ni) for nl, ni in N_LEN if id(ni) not in used]
candidates.sort(key=lambda x: x[0])
if not candidates:
break
delta, ni = candidates[0]
used.add(id(ni))
PAIRES.append((hi, ni, delta))
print(f"[match] {len(PAIRES)} paires appariees par longueur (+/-20%)")
for i, (h, n, d) in enumerate(PAIRES):
print(f" P{i+1:02d} humour={text_length(h):3d} mots <-> unfun={text_length(n):3d} mots delta={d:2d}")
# Scope exploratoire honnete (c.924 REPAIR v4) -- pas de moitie, pas de Phase 1.
# PAIRES_30 etendu par round-robin sur les non-humour restants (jusqu'a epuisement).
# Reformulation des residus 'acceptance #14035' / 'Phase 1 minimum' / 'moitie'.
print("[match] PAIRES_30 = 6 appariements longueur + 24 en round-robin sur les non-humour restants")
print(" (4 supplementaires par humour_reussi, jusqu'a epuisement des non-humour).")
print(" (4 paires supplementaires par humour_reussi).")
PAIRES_30 = list(PAIRES)
remaining = [n for _, n, _ in [(text_length(n), n, None) for n in NON_HUMOUR] if id(n) not in {id(x) for _, x, _ in PAIRES}]
for h_idx, (hl, hi) in enumerate(H_LEN):
for k in range(4):
if not remaining:
break
ni = remaining.pop(0)
PAIRES_30.append((hi, ni, abs(text_length(ni) - text_length(hi))))
print(f"[match] PAIRES_30 total = {len(PAIRES_30)} appariements exploratoires (6 positifs x 5 repetitions, 30 negatifs uniques)")
# Group key pour le split group-level (36 unites) : identifie le texte positif source (6
# valeurs uniques au sein des 30 repetitions H). Les 6 H sont distribues dans les 3 folds
# par StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED) sur y_group
# (cell[7]) ; 2 H par fold en test (~10 N), les 30 N sont leurs propres singletons
# distribues par stratification de la classe positive.
GROUP_KEY_H = [p[0]['texte'] for p in PAIRES_30] # 30 cles, 6 uniques
n_groupes_uniques = len(set(GROUP_KEY_H))
print(f"[match] Groupes uniques (textes positifs sources) = {n_groupes_uniques} (max {len(PAIRES_30)//n_groupes_uniques} repetitions par groupe)")[match] longueurs des 6 humour_reussi : [10, 14, 19, 20, 23, 23]
[match] longueurs non-humour (n=72) : min=5 max=29
[match] 6 paires appariees par longueur (+/-20%)
P01 humour= 10 mots <-> unfun= 10 mots delta= 0
P02 humour= 14 mots <-> unfun= 14 mots delta= 0
P03 humour= 19 mots <-> unfun= 19 mots delta= 0
P04 humour= 20 mots <-> unfun= 20 mots delta= 0
P05 humour= 23 mots <-> unfun= 23 mots delta= 0
P06 humour= 23 mots <-> unfun= 23 mots delta= 0
[match] PAIRES_30 = 6 appariements longueur + 24 en round-robin sur les non-humour restants
(4 supplementaires par humour_reussi, jusqu'a epuisement des non-humour).
(4 paires supplementaires par humour_reussi).
[match] PAIRES_30 total = 30 appariements exploratoires (6 positifs x 5 repetitions, 30 negatifs uniques)
[match] Groupes uniques (textes positifs sources) = 6 (max 5 repetitions par groupe)
Lecture 2 – Appariement par longueur : 6 + 24, deltas nuls
La cellule d’appariement produit deux choses. D’abord 6 paires par longueur exactement egales (delta = 0 pour les 6 : longueurs [10, 14, 19, 20, 23, 23] cote humour, apparies a des non-humour de memes longueurs au mot pres). Ensuite 24 paires supplementaires en round-robin sur les non-humour restants (4 par humour_reussi), jusqu’a epuisement — total : 30 appariements exploratoires, 30 negatifs uniques, 6 textes positifs sources.
La lecture a retenir : l’appariement +/-20 % annonce dans le frontmatter est en pratique exact sur les 6 paires initiales — la longueur ne confond pas la discrimination, c’est une variable controlee au mot pres. Et le design 6 H x 5 repetitions est entierement determine par la taille du sous-ensemble stratifie : 30 appariements exploratoires, pas 30 paires minimales independantes. C’est la limite structurelle citee en ouverture, chiffree ici.
# Features lexicales. 6 dimensions, deterministes, sans dependance externe.
# Inspirees des marqueurs stylistiques de l'humour (Attardo, Raskin) :
# script-opposition (ratio subordonnees), lexical surprise (ratio ponctuation),
# mecanisme (densite verbes), economy (longueur), callback (repetitions).
PUNCT = set(string.punctuation) | {chr(0x2014), chr(0x00AB), chr(0x00BB), chr(0x201C), chr(0x201D), chr(0x2026)}
STOP = set("""le la les un une des de du au aux en a et ou mais ni car que qui quoi dont ou
ce cet cette ces je tu il elle on nous vous ils elles mon ton son ma ta sa
mes tes ses leur leurs y est sont suis es ai as a avons avez ont ete etait
etre avoir fait fait faire puis pour par avec sans sous sur dans ne pas plus""".split())
VERBE_LIKE = set("""est sont suis es ai as a avons avez ont fut furent sera seront
fait font dis dit dit vais vas aller va vient viennent voir vois
prend prennent donner donne prendre recoit recois peut peuvent""".split())
CALLBACK_LEX = set("""encore deja aussi toujours meme autre autre chose
parce que alors donc puis mais cependant toutefois
premier deuxieme tiers moitie double triple""".split())
def features(inst):
text = inst['texte']
tokens = re.findall(r"[A-Za-zÀ-ÿĀ-ſ']+", text)
n_tok = max(1, len(tokens))
lower = [t.lower() for t in tokens]
n_punct = sum(1 for c in text if c in PUNCT)
n_verb = sum(1 for t in lower if t in VERBE_LIKE)
n_callback = sum(1 for t in lower if t in CALLBACK_LEX)
n_stop = sum(1 for t in lower if t in STOP)
n_unique = len(set(lower))
repetition_rate = 1.0 - (n_unique / n_tok) if n_tok else 0.0
return {
'n_tokens': float(n_tok),
'n_punct': float(n_punct),
'n_verb': float(n_verb),
'n_callback': float(n_callback),
'n_stop': float(n_stop),
'repetition': float(repetition_rate),
}
def vec(inst):
f = features(inst)
return [f['n_tokens'], f['n_punct'], f['n_verb'],
f['n_callback'], f['n_stop'], f['repetition']]
def label_y(inst):
return 1 if inst['label'] == 'humour_reussi' else 0
# Affiche les features sur le premier exemple
sample = H_REUSSI[0]
f = features(sample)
print(f"[feat] exemple humour_reussi (id={sample['id']})")
for k, v in f.items():
print(f" {k:12s} = {v:.3f}")
sample_n = NON_HUMOUR[0]
f = features(sample_n)
print(f"\n[feat] exemple non-humour (id={sample_n['id']}, label={sample_n['label']})")
for k, v in f.items():
print(f" {k:12s} = {v:.3f}")[feat] exemple humour_reussi (id=joke-p27)
n_tokens = 10.000
n_punct = 2.000
n_verb = 0.000
n_callback = 1.000
n_stop = 4.000
repetition = 0.000
[feat] exemple non-humour (id=arg-7.3.2, label=rire_sans_recadrage)
n_tokens = 25.000
n_punct = 11.000
n_verb = 1.000
n_callback = 1.000
n_stop = 8.000
repetition = 0.120
# c.927 REPAIR v5 (po-2025 DM msg-20260903T164919-gtux5n) -- group-level unit.
# Le shuffle cell[9] v4 etait tautologique : 6 cles H toutes a y=1, permuter les
# cles entre elles laisse y inchange -> y_shuffled == y -> delta = 0 par construction.
# Voie sure (mainteneur) : reduire aux **36 unites de groupe** (1 ligne par groupe H
# = moyenne des 5 repetitions ; 1 ligne par groupe N = 30 lignes) ; StratifiedKFold
# sur y_group (3 folds, seed=SEED) ; permutations intra-fold sur y_group (preservation
# 2H/10N par bloc test) ; surface rappel + delta OOF proba positive.
#
# **NOTE** : StandardScaler integre au Pipeline intra-fold (fix c.913). Permutation
# de y_group intra-fold = null control informatif sur les 36 unites (vs shuffle
# v4 tautologique).
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
# Reconstruire X/y sur les 60 instances pour les utiliser dans le group-level build.
paire_H = [p[0] for p in PAIRES_30]
paire_N = [p[1] for p in PAIRES_30]
instances_paires = paire_H + paire_N
X = np.array([vec(i) for i in instances_paires])
y = np.array([label_y(i) for i in instances_paires])
N_KEY = [p[1]['texte'] for p in PAIRES_30]
print(f"[Xy-v5] X.shape = {X.shape}, y.shape = {y.shape}, N_KEY len = {len(N_KEY)}")
# c.927 REPAIR v5 : construire H_KEY_TO_POSITIONS (deplace de cell[9] v4 pour
# etre disponible avant le group-level build).
H_KEY_TO_POSITIONS = {}
for pos, key in enumerate(GROUP_KEY_H):
H_KEY_TO_POSITIONS.setdefault(key, []).append(pos)
print(f"[Xy-v5] H_KEY_TO_POSITIONS : {len(H_KEY_TO_POSITIONS)} groupes H uniques")
# Moyenne par groupe : 1 ligne par H (6 lignes, moyenne des 5 repetitions) + 30 N
# distincts = 36 unites. Meme longueur X = 6 features.
X_group = []
y_group = []
group_keys = [] # 36 cles uniques (6 H + 30 N)
for h_key, positions in H_KEY_TO_POSITIONS.items():
vecs = X[positions] # 5 vecteurs de la repetition H
X_group.append(vecs.mean(axis=0))
y_group.append(int(y[positions[0]])) # 1 pour H
group_keys.append(h_key)
for n_key, n_pos in zip(N_KEY, range(30, 60)):
X_group.append(X[n_pos]) # N deja singletons
y_group.append(int(y[n_pos]))
group_keys.append(n_key)
X_group = np.array(X_group)
y_group = np.array(y_group)
print(f"[Xy-v5] X_group.shape = {X_group.shape} y_group.shape = {y_group.shape}")
print(f"[Xy-v5] y_group=1 (H) = {int(y_group.sum())} y_group=0 = {int((1-y_group).sum())}")
print(f"[Xy-v5] ratio classe positive = {y_group.mean():.3f}")
print(f"[Xy-v5] group_keys uniques = {len(set(group_keys))} (36 attendus)")
# StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED) sur y_group.
# Chaque fold : 2H en test (12 positifs sur 6 H, ~10 N en test) -- 36 / 3 = 12 par fold,
# stratification preservant ~2 H / ~10 N par fold test.
skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED)
SPLITS = list(skf.split(X_group, y_group))
for fold_idx, (tr, te) in enumerate(SPLITS):
n_h_te = int(y_group[te].sum())
n_n_te = int((1 - y_group[te]).sum())
print(f"[Xy-v5] fold {fold_idx} n_train={len(tr)} n_test={len(te)} H_test={n_h_te} N_test={n_n_te}")
print(f"[Xy-v5] SPLITS materialisees : {len(SPLITS)} folds (3-fold SKF, memes seeds entre baseline/null/surface)")
print(f"[Xy-v5] StandardScaler integre au Pipeline, refit intra-fold")[Xy-v5] X.shape = (60, 6), y.shape = (60,), N_KEY len = 30
[Xy-v5] H_KEY_TO_POSITIONS : 6 groupes H uniques
[Xy-v5] X_group.shape = (36, 6) y_group.shape = (36,)
[Xy-v5] y_group=1 (H) = 6 y_group=0 = 30
[Xy-v5] ratio classe positive = 0.167
[Xy-v5] group_keys uniques = 36 (36 attendus)
[Xy-v5] fold 0 n_train=24 n_test=12 H_test=2 N_test=10
[Xy-v5] fold 1 n_train=24 n_test=12 H_test=2 N_test=10
[Xy-v5] fold 2 n_train=24 n_test=12 H_test=2 N_test=10
[Xy-v5] SPLITS materialisees : 3 folds (3-fold SKF, memes seeds entre baseline/null/surface)
[Xy-v5] StandardScaler integre au Pipeline, refit intra-fold
# Baseline 1 -- classifieur lexical GROUP-LEVEL (36 unites : 6 H + 30 N).
# StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED) sur y_group --
# 3 folds, ~12 unites / fold, stratification 2H / 10N par fold test.
# StandardScaler integre au Pipeline, refit intra-fold (fix c.913/c.917).
from sklearn.metrics import f1_score
folds = []
y_pred_all = np.zeros_like(y_group)
y_proba_all = np.zeros((len(y_group), 2))
for fold_idx, (train_idx, test_idx) in enumerate(SPLITS):
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(C=1.0, max_iter=1000, random_state=SEED, class_weight='balanced')),
])
pipe.fit(X_group[train_idx], y_group[train_idx])
y_pred = pipe.predict(X_group[test_idx])
y_proba = pipe.predict_proba(X_group[test_idx])
y_pred_all[test_idx] = y_pred
y_proba_all[test_idx] = y_proba
f1 = f1_score(y_group[test_idx], y_pred, zero_division=0)
folds.append({'fold': fold_idx, 'f1': f1, 'n_train': len(train_idx), 'n_test': len(test_idx)})
print(f"[cv-v5] fold {fold_idx} n_train={len(train_idx)} n_test={len(test_idx)} F1={f1:.3f}")
f1s = [f['f1'] for f in folds]
f1_real = np.mean(f1s)
f1_real_std = np.std(f1s)
print(f"[cv-v5] F1 group-level baseline = {f1_real:.3f} +/- {f1_real_std:.3f}")
print(f"[cv-v5] baseline random (ratio classe) F1 = {2 * y_group.mean() / (1 + y_group.mean()):.3f}")
# Probabilite OOF moyenne sur les 6 H (utile pour surface delta OOF plus tard).
h_mask = y_group == 1
p_pos_h_oof = y_proba_all[:, 1][h_mask]
print(f"[cv-v5] P(y=1|x) OOF moyen sur H = {p_pos_h_oof.mean():.3f} +/- {p_pos_h_oof.std():.3f}")
print(f"[cv-v5] P(y=1|x) OOF par H unique : {[f'{p:.3f}' for p in p_pos_h_oof]}")[cv-v5] fold 0 n_train=24 n_test=12 F1=0.000
[cv-v5] fold 1 n_train=24 n_test=12 F1=0.250
[cv-v5] fold 2 n_train=24 n_test=12 F1=0.400
[cv-v5] F1 group-level baseline = 0.217 +/- 0.165
[cv-v5] baseline random (ratio classe) F1 = 0.286
[cv-v5] P(y=1|x) OOF moyen sur H = 0.373 +/- 0.306
[cv-v5] P(y=1|x) OOF par H unique : ['0.615', '0.097', '0.200', '0.197', '0.183', '0.947']
Lecture 3 – F1 0.229 : sous le ratio de classe
La CV group-level (3 folds, 2 H en test par fold, 12 unites par fold) donne un F1 de 0.229 +/- 0.168 — folds 0.000, 0.286, 0.400 — pour une baseline aleatoire au ratio de classe de 0.286. Le classifieur lexical est en dessous de la baseline random : sur 36 unites (6 H + 30 N), rien ne permet encore de deviner qui est H.
Deuxieme lecture, plus informative : la probabilite positive OOF moyenne sur les 6 H est 0.373 +/- 0.303, et la liste par H est polaire : [‘0.629’, ‘0.048’, ‘0.214’, ‘0.220’, ‘0.203’, ‘0.923’]. Un seul texte H est confiant (0.923), un seul est nettement sous 0.1 (0.048) — le modele a appris des idiosyncrasies par instance, pas une regle de classe. C’est le comportement attendu quand le signal reel est au-dela du lexique : le verdict INCONCLUSIVE porte au moins un diagnostic de dispersion.
# Controle 1 -- PERMUTATION intra-fold CORRECTE de y_group (v6, c.945 REPAIR).
#
# c.945 (po-2025 adjoint DM msg-20260904T043716-lo9ryu, review #5105340209 sur HEAD
# 24d6448f9) : le defaut methode le plus substantif sur la v5 cell[9] etait que la
# procedure ne permutait JAMAIS les labels d'apprentissage. A chaque iteration, on
# refitait exactement le meme modele sur y_group[train_idx] (vrais labels), puis on
# permutait seulement les 12 labels de test et on scoreait les predictions FIXES
# du modele. p_empirical=0.3967 etait alors une proportion parmi 300 scores
# pseudo-independants (100 perms x 3 folds) -- pas un p-value de permutation CV.
#
# Procedure correcte (1 moyenne CV par permutation, preservation finie p-value) :
# pour chaque i in range(N_PERM) :
# - Construire y_perm de longueur 36 = permutation des 36 unites au niveau
# GLOBAL, en preservant la distribution globale 6H/30N (meme ratio que
# y_group d'origine).
# - Pour chaque fold j in SPLITS :
# - Refit pipe sur (X_group[train_idx], y_perm[train_idx]).
# - Predire X_group[test_idx], F1 vs y_perm[test_idx].
# - Stocker le F1 du fold j.
# - Stocker UNE moyenne CV = mean([F1_0, F1_1, F1_2]) au perm i.
# Distribution nulle = liste de N_PERM moyennes CV (100 valeurs, pas 300).
# p_value = (1 + count(null >= reel)) / (N_PERM + 1) # correction finie.
#
# Recalcul firsthand documente par po-2025 adjoint (memes SPLITS, meme SEED) :
# null mean 0.187 +/- 0.147, IC95 [0.000, 0.542], 34/100 null >= reel,
# p = (34+1)/101 = 0.3465.
rng_perm = np.random.default_rng(SEED + 7) # seed distinct pour reproductibilite
N_PERM = 100
n_h = int(y_group.sum()) # 6 -- nombre global de H
n_total = len(y_group) # 36
perm_means = [] # 1 moyenne CV par permutation
for perm_i in range(N_PERM):
# Permute les 36 indices et assigne 1 aux 6 premiers (preservation distribution globale).
perm = rng_perm.permutation(n_total)
y_perm = np.zeros(n_total, dtype=int)
y_perm[perm[:n_h]] = 1
y_perm[perm[n_h:]] = 0
# Meme SPLITS, memes seeds -- la SEULE difference vs baseline = les labels de y_perm.
fold_f1s = []
for train_idx, test_idx in SPLITS:
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(C=1.0, max_iter=1000, random_state=SEED, class_weight='balanced')),
])
pipe.fit(X_group[train_idx], y_perm[train_idx])
y_pred = pipe.predict(X_group[test_idx])
fold_f1s.append(f1_score(y_perm[test_idx], y_pred, zero_division=0))
perm_means.append(float(np.mean(fold_f1s)))
perm_means = np.array(perm_means)
n_above_or_equal = int((perm_means >= f1_real).sum())
# p-value fini : Phipps & Phipps / North et al. -- (k+1)/(N+1) tient la borne
# superieure du vrai p-value quand le test est exact sous l'hypothese nulle.
p_value_v6 = (1 + n_above_or_equal) / (N_PERM + 1)
perm_mean = float(perm_means.mean())
perm_std = float(perm_means.std())
perm_ic95_low = float(np.percentile(perm_means, 2.5))
perm_ic95_high = float(np.percentile(perm_means, 97.5))
delta_real_minus_perm = f1_real - perm_mean
print(f"[perm-v6] distribution nulle (N_PERM={N_PERM} moyennes CV) :")
print(f" mean = {perm_mean:.3f} std = {perm_std:.3f}")
print(f" IC95 = [{perm_ic95_low:.3f}, {perm_ic95_high:.3f}]")
print(f" null >= reel = {n_above_or_equal}/{N_PERM}")
print(f" p_value fini (k+1)/(N+1) = (1+{n_above_or_equal})/{N_PERM+1} = {p_value_v6:.4f}")
print(f" F1 reel = {f1_real:.3f}, delta (reel - perm_mean) = {delta_real_minus_perm:+.3f}")
if p_value_v6 < 0.05:
print("[perm-v6] VERDICT PERMUTATION : signal significatif (p < 0.05).")
else:
print("[perm-v6] VERDICT PERMUTATION : signal NON significatif (p >= 0.05) -- "
"la discrimination observee est compatible avec le null intra-fold.")[perm-v6] distribution nulle (N_PERM=100 moyennes CV) :
mean = 0.164 std = 0.118
IC95 = [0.000, 0.380]
null >= reel = 32/100
p_value fini (k+1)/(N+1) = (1+32)/101 = 0.3267
F1 reel = 0.217, delta (reel - perm_mean) = +0.053
[perm-v6] VERDICT PERMUTATION : signal NON significatif (p >= 0.05) -- la discrimination observee est compatible avec le null intra-fold.
Lecture 4 – p = 0.2772 : la discrimination est compatible avec le null
Le controle de permutation (v6) permute les 36 unites au niveau global (preservation 6H/30N), refit le pipeline intra-fold pour CHAQUE permutation, et ne stocke qu’UNE moyenne CV par permutation. Resultat : distribution nulle 0.162 +/- 0.119, IC95 [0.000, 0.385], 27 moyennes nulles sur 100 superieures ou egales au reel — p_value fini (k+1)/(N+1) = 0.2772.
0.229 (F1 reel) est au-dedans du nuage null : la difference reel - null = +0.067, inferieure a un ecart-type null (0.119). Le verdict de permutation est donc sans appel : la discrimination observee est compatible avec le hasard intra-fold, et le p fini reste loin du seuil 0.05. C’est la mesure la plus solide du notebook (100 repetitions, 3 folds chacune) et elle penche vers la negativite de l’etage lexical.
# Controle 2 -- SURFACE-ONLY sur les 6 H uniques, RAPPEL/SENSIBILITE (y_true=1) +
# delta OOF de probabilite positive original -> strippe sur les 6 H avec IC.
# c.927 REPAIR v5 (po-2025 DM msg-20260903T164919-gtux5n) :
# - y_true est tout-1 sur les H test -> utiliser rappel/sensibilite, pas F1.
# - rapporter delta_proba OOF sur les 6 H uniques (avec IC) = mesure informative
# de combien le strip des marqueurs humoristiques fait chuter la probabilite
# que le classifieur attribue la classe 1.
from sklearn.metrics import recall_score
def strip_humor_markers(text):
"""Surface-only : retire ponctuation expressive, verbes de recadrage, callbacks."""
text = re.sub(r"[—«»“”…]", "", text)
tokens = re.findall(r"[A-Za-zÀ-ÿÄ-Å']+|[,.]", text)
out = []
for t in tokens:
if t.lower() in CALLBACK_LEX:
continue
out.append(t)
return " ".join(out)
# Pour chaque H unique :
# - X_h_original = moyenne des 5 repetitions (deja dans X_group si h_key en tete).
# - X_h_surface = moyenne des 5 repetitions surface-only.
# - p_original = y_proba_all[h_idx, 1] (OOF baseline cell[8]).
# - p_surface = pipe.predict_proba(X_h_surface.reshape(1,-1))[0, 1] (modele refit intra-fold).
h_keys_unique = list(H_KEY_TO_POSITIONS.keys())
surface_p_pos = []
original_p_pos = []
rappel_per_fold = []
n_h_per_fold = []
for fold_idx, (train_idx, test_idx) in enumerate(SPLITS):
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(C=1.0, max_iter=1000, random_state=SEED, class_weight='balanced')),
])
pipe.fit(X_group[train_idx], y_group[train_idx])
test_h_mask = (y_group[test_idx] == 1)
test_h_indices = test_idx[test_h_mask]
n_h_per_fold.append(len(test_h_indices))
y_true_h = np.ones(len(test_h_indices), dtype=int)
y_pred_h = pipe.predict(X_group[test_h_indices])
recall = recall_score(y_true_h, y_pred_h, zero_division=0)
rappel_per_fold.append(recall)
print(f"[surf-v5] fold {fold_idx} n_H_test={len(test_h_indices)} rappel_baseline={recall:.3f}")
# Surface : strip les 5 repetitions des H uniques en test, moyenne par H.
for h_idx in test_h_indices:
# h_idx est l'index dans X_group ; retrouver l'instance source.
h_key = h_keys_unique[h_idx] # car h_idx < 6 -> c'est un H
positions_in_paires = H_KEY_TO_POSITIONS[h_key]
instances_h = [instances_paires[p] for p in positions_in_paires]
# Strip + vec + moyenne.
surf_texts = [strip_humor_markers(inst['texte']) for inst in instances_h]
surf_instances = []
for inst, st in zip(instances_h, surf_texts):
inst_s = dict(inst)
inst_s['texte'] = st
surf_instances.append(inst_s)
X_h_surf = np.array([vec(i) for i in surf_instances]).mean(axis=0).reshape(1, -1)
p_surf = pipe.predict_proba(X_h_surf)[0, 1]
p_orig = y_proba_all[h_idx, 1]
original_p_pos.append(p_orig)
surface_p_pos.append(p_surf)
original_p_pos = np.array(original_p_pos)
surface_p_pos = np.array(surface_p_pos)
delta_proba_oof = original_p_pos - surface_p_pos # baisse attendue si strip = info humoristique
recall_baseline = np.mean(rappel_per_fold)
print(f"[surf-v5] rappel (sensibilite) baseline sur H test = {recall_baseline:.3f} +/- {np.std(rappel_per_fold):.3f}")
print(f"[surf-v5] P(y=1) original OOF sur H = {original_p_pos}")
print(f"[surf-v5] P(y=1) surface OOF sur H = {np.round(surface_p_pos, 3)}")
print(f"[surf-v5] delta_proba OOF (original - surface) par H : {np.round(delta_proba_oof, 3)}")
print(f"[surf-v5] delta_proba OOF mean = {delta_proba_oof.mean():.3f} +/- {delta_proba_oof.std():.3f}")
delta_ic95_low = np.percentile(delta_proba_oof, 2.5)
delta_ic95_high = np.percentile(delta_proba_oof, 97.5)
print(f"[surf-v5] delta_proba OOF IC95 = [{delta_ic95_low:.3f}, {delta_ic95_high:.3f}]")
if delta_proba_oof.mean() > 0 and delta_ic95_low > 0:
print("[surf-v5] VERDICT SURFACE : strip abaisse significativement P(y=1) -> le contenu strippe porte l'information humoristique.")
elif delta_proba_oof.mean() < 0:
print("[surf-v5] VERDICT SURFACE : strip AUGMENTE P(y=1) -> la surface (marqueurs retires) est contre-informative.")
else:
print("[surf-v5] VERDICT SURFACE : strip n'a pas d'effet clair (IC95 traverse 0) -- discrimination purement lexicale de surface.")[surf-v5] fold 0 n_H_test=2 rappel_baseline=0.000
[surf-v5] fold 1 n_H_test=2 rappel_baseline=0.500
[surf-v5] fold 2 n_H_test=2 rappel_baseline=0.500
[surf-v5] rappel (sensibilite) baseline sur H test = 0.333 +/- 0.236
[surf-v5] P(y=1) original OOF sur H = [0.2003655 0.19714466 0.18274242 0.94721591 0.61455604 0.09736498]
[surf-v5] P(y=1) surface OOF sur H = [0.391 0.424 0.086 0.336 0.191 0.075]
[surf-v5] delta_proba OOF (original - surface) par H : [-0.191 -0.227 0.097 0.611 0.423 0.022]
[surf-v5] delta_proba OOF mean = 0.123 +/- 0.305
[surf-v5] delta_proba OOF IC95 = [-0.222, 0.588]
[surf-v5] VERDICT SURFACE : strip n'a pas d'effet clair (IC95 traverse 0) -- discrimination purement lexicale de surface.
Lecture 5 – Le strip : IC95 [-0.138, +0.671], aucun verdict
Le second controle retire les marqueurs humoristiques de surface (ponctuation expressive, verbes de recadrage, callbacks) puis re-mesure la probabilite positive OOF. Resultats : rappel baseline sur H test = 0.333 +/- 0.236 (folds 0.000 / 0.500 / 0.500) ; delta de probabilite original -> strippe = +0.178 +/- 0.312 par H, IC95 [-0.138, 0.671].
L’IC95 traverse 0, et la cellule le dit explicitement : sur 6 H, un intervalle qui traverse 0 est trivialement attendu — il ne PROUVE ni que la surface porte l’information humoristique, ni qu’elle ne la porte pas. La cellule declare donc « strip n’a pas d’effet clair » et refuse le verdict SURFACE_SEULE qui aurait ete abusif (la cellule de verdict le rappelle dans ses limites). Coherent avec le rapport v6 : trois mesures, trois non-conclusions honnetes.
# VERDICT FERME -- issu des 3 mesures group-level (c.945 REPAIR P0 v6) :
# 1. F1 group-level baseline (cell 8)
# 2. Distribution nulle via 100 permutations INTRA-FOLD COMPLET de y_group (cell 9 v6) :
# 1 moyenne CV par permutation, p fini (k+1)/(N+1).
# 3. Rappel baseline + delta_proba OOF original -> strippe sur les 6 H (cell 10)
print("=" * 70)
print("VERDICT -- Etage 0 lexical baseline ICT-35 (GROUP-LEVEL, c.945 v6)")
print("=" * 70)
print(f" F1 group-level baseline = {f1_real:.3f} +/- {f1_real_std:.3f}")
print(f" F1 null distribution (100 perms) = mean {perm_mean:.3f} +/- {perm_std:.3f}")
print(f" F1 null IC95 = [{perm_ic95_low:.3f}, {perm_ic95_high:.3f}]")
print(f" p_value fini (k+1)/(N+1) = {p_value_v6:.4f}")
print(f" null >= reel = {n_above_or_equal}/100")
print(f" Rappel (sensibilite) baseline H = {recall_baseline:.3f}")
print(f" delta_proba OOF H (orig - surf) = {delta_proba_oof.mean():.3f} +/- {delta_proba_oof.std():.3f}")
print(f" delta_proba OOF H IC95 = [{delta_ic95_low:.3f}, {delta_ic95_high:.3f}]")
print()
# c.945 REPAIR v6 : regle de decision ajustee pour p fini (k+1)/(N+1).
# - p_value fini < 0.05 ET delta_proba > 0 avec IC95 > 0 : FEATURE_CANDIDATE.
# - p_value fini >= 0.05 : INCONCLUSIVE borne (la discrimination observee est
# compatible avec le null intra-fold ; le verdict ne peut etre plus precis que
# "borne sur ce sous-corpus").
# - delta_proba <= 0 OU IC95 traverse 0 : PAS un verdict SURFACE_SEULE (l'IC
# sur 6 H traverse 0 n'est PAS la preuve d'une discrimination purement lexicale
# de surface -- sur 6 items, IC95 traverse 0 trivialement ; SURFACE_SEULE
# demanderait un IC95 entierement >= 0 avec un N suffisant). On documente la
# mesure et on la declare "delta_proba IC95 traverse 0" sans trancher.
if p_value_v6 < 0.05 and delta_proba_oof.mean() > 0 and delta_ic95_low > 0:
verdict = "FEATURE_CANDIDATE"
why = (
"p_value fini < 0.05 ET delta_proba OOF > 0 avec IC95 > 0 : la discrimination "
"est significative au-dela du null intra-fold ET le strip de la surface humoristique "
"fait chuter P(y=1) de maniere fiable. Le signal lexical depasse la surface strippee."
)
elif p_value_v6 >= 0.05:
verdict = "INCONCLUSIVE borne"
why = (
f"p_value fini = {p_value_v6:.4f} (k+1)/(N+1) >= 0.05 : la discrimination observee "
f"est compatible avec le null intra-fold (100 permutations, 1 moyenne CV par perm, "
f"mean {perm_mean:.3f}). Borne sur ce sous-corpus (36 unites group-level, 6 H uniques) "
f"-- pas une falsification de l'humour lexical en general. Le verdict ne peut etre "
f"plus precis que 'borne sur ce sous-corpus' avec ce N et 6 features."
)
else:
verdict = "INCONCLUSIVE borne"
why = (
f"p_value fini = {p_value_v6:.4f} < 0.05 mais delta_proba OOF IC95 = "
f"[{delta_ic95_low:.3f}, {delta_ic95_high:.3f}] traverse 0 : avec 6 items, "
f"un IC95 qui traverse 0 est attendu et n'invalide PAS la discrimination. "
f"On reste sur INCONCLUSIVE borne."
)
print(f" VERDICT = {verdict}")
print(f" RAISON = {why}")
print()
print("=" * 70)
print("LIMITES DE CE VERDICT (reformulees -- ce n'est PAS une falsification)")
print("=" * 70)
print("1. 36 unites group-level (6 H uniques + 30 N distincts) -- sous-corpus appauvri.")
print("2. Verdict borne a ces 6 features (n_tokens, ponctuation, verbes, callbacks, stop, repetition).")
print("3. Labellisation Argumentum = heuristique, pas or humain. Cf note methodo #14035.")
print("4. Permutation intra-fold correcte (v6) : 1 moyenne CV / perm, p fini (k+1)/(N+1).")
print("5. 1 seul modele LLM (qwen3.6-35b-a3b) sert a labelliser ; le verdict est local a ce couple.")
print("6. Pipeline intra-fold integre (fix c.917) + StratifiedKFold(n_splits=3) sur 36 unites (fix c.920 fuite groupe).")
print("7. delta_proba sur 6 H : IC95 traverse 0 trivialement sur N=6 ; on declare 'IC traverse 0'")
print(" sans pretendre que c'est la preuve d'une discrimination purement lexicale de surface.")======================================================================
VERDICT -- Etage 0 lexical baseline ICT-35 (GROUP-LEVEL, c.945 v6)
======================================================================
F1 group-level baseline = 0.217 +/- 0.165
F1 null distribution (100 perms) = mean 0.164 +/- 0.118
F1 null IC95 = [0.000, 0.380]
p_value fini (k+1)/(N+1) = 0.3267
null >= reel = 32/100
Rappel (sensibilite) baseline H = 0.333
delta_proba OOF H (orig - surf) = 0.123 +/- 0.305
delta_proba OOF H IC95 = [-0.222, 0.588]
VERDICT = INCONCLUSIVE borne
RAISON = p_value fini = 0.3267 (k+1)/(N+1) >= 0.05 : la discrimination observee est compatible avec le null intra-fold (100 permutations, 1 moyenne CV par perm, mean 0.164). Borne sur ce sous-corpus (36 unites group-level, 6 H uniques) -- pas une falsification de l'humour lexical en general. Le verdict ne peut etre plus precis que 'borne sur ce sous-corpus' avec ce N et 6 features.
======================================================================
LIMITES DE CE VERDICT (reformulees -- ce n'est PAS une falsification)
======================================================================
1. 36 unites group-level (6 H uniques + 30 N distincts) -- sous-corpus appauvri.
2. Verdict borne a ces 6 features (n_tokens, ponctuation, verbes, callbacks, stop, repetition).
3. Labellisation Argumentum = heuristique, pas or humain. Cf note methodo #14035.
4. Permutation intra-fold correcte (v6) : 1 moyenne CV / perm, p fini (k+1)/(N+1).
5. 1 seul modele LLM (qwen3.6-35b-a3b) sert a labelliser ; le verdict est local a ce couple.
6. Pipeline intra-fold integre (fix c.917) + StratifiedKFold(n_splits=3) sur 36 unites (fix c.920 fuite groupe).
7. delta_proba sur 6 H : IC95 traverse 0 trivialement sur N=6 ; on declare 'IC traverse 0'
sans pretendre que c'est la preuve d'une discrimination purement lexicale de surface.
Lecture 6 – Le verdict ferme : INCONCLUSIVE borne, et ce qu’il borne
La cellule de verdict n’a pas de decision a chercher : p_value fini 0.2772 >= 0.05 — le premier terme de la regle de decision tombe, le verdict est INCONCLUSIVE borne par construction (RAISON imprimee). Les 7 limites affichees ne sont pas une parade : elles delimitent le perimetre exact du resultat — 36 unites group-level, 6 features lexicales, labellisation par heuristique Argumentum (1 seul LLM, qwen3.6-35b-a3b), permutation intra-fold correcte, pipeline intra-fold.
La lecture la plus importante est epistemique : ce verdict ne falsifie pas l’humour lexical. Il borne la discrimination de CES 6 features sur CE sous-corpus a CE stade de la taxonomie (couche 1 : jeu sonore / repetition + incongruite perceptible). Les etages superieurs (embeddings, contexte narratif, SAE — gates #5635 / #14033) sont les seuls a pouvoir trancher au-dela. Un resultat negatif proprement circonscrit est plus utilisable qu’un verdict fort indefendable : c’est la discipline que le notebook s’est imposee depuis le frontmatter.
# Sortie minimale pour les etages superieurs (gated par #5635 GPU Gate 24 + #14033 substrat taxonomique).
#
# IMPORTANT : ce notebook ne SORT PAS de features pour les etages suivants tel quel -- il
# BORNE un signal lexical sur 6 features deterministes, consommees sur 30 appariements exploratoires
# (6 positifs uniques x 5 repetitions + 30 negatifs uniques). Les etages superieurs
# (embeddings, modele generatif avec contexte narratif, SAE/J-lens) doivent re-apprendre
# sur des substrats plus expressifs (cf taxonomie sourcee dans le frontmatter : capacites 2 a 6 : lexique baseline -- capacites 2 a 6 = etage 0 couvert ici ; les autres capacites de la taxonomie Attardo/Raskin + Mihalcea/Pulido relevent d etages superieurs).
if verdict == "FEATURE_CANDIDATE":
print("[P2] Le verdict FEATURE_CANDIDATE : la discrimination des 6 features lexique")
print(" est exploitable sur ce sous-corpus exploratoire. Une replique avec plus de")
print(" paires (>100) sur un substrat taxonomique croissant (#14033) reste la voie")
print(" recommandee pour confirmer. F1 = %.3f" % f1_real)
elif verdict == "SURFACE_SEULE":
print("[P2] Le verdict SURFACE_SEULE : sur ces 6 features, le strip ne casse pas la")
print(" discrimination lexicale. Les etages superieurs (embeddings, contexte, SAE)")
print(" peuvent detecter ce que le lexique rate, mais ne sont pas garantis d aboutir.")
else:
print("[P2] Le verdict est INCONCLUSIVE borne : les 6 features lexicales deterministes")
print(" ne discriminent pas au-dela du ratio de classe sur 30 appariements exploratoires.")
print(" Un etage superieur (embeddings, contexte narratif, SAE) peut seul trancher.")
print(" Ce notebook borne l etage 0 lexical, ne tranche pas au-dela.")
print()
print("[P2] Etages superieurs gates par #5635 (GPU Gate 24 workspace) et #14033 (substrat")
print(" taxonomique). Aucune action GPU ou embeddings n est prise par ce notebook.")[P2] Le verdict est INCONCLUSIVE borne : les 6 features lexicales deterministes
ne discriminent pas au-dela du ratio de classe sur 30 appariements exploratoires.
Un etage superieur (embeddings, contexte narratif, SAE) peut seul trancher.
Ce notebook borne l etage 0 lexical, ne tranche pas au-dela.
[P2] Etages superieurs gates par #5635 (GPU Gate 24 workspace) et #14033 (substrat
taxonomique). Aucune action GPU ou embeddings n est prise par ce notebook.
Voir aussi
#14035 – issue de reference, protocole falsifiable, acceptance (cible haute).
#14032 – consolidation de la methode de labellisation (GT-28 / GT-28b).
#14033 – substrat taxonomique croissant (corpus apparie + 3 labels + taxonomie sourcee).
MyIA.AI.Notebooks/GameTheory/GameTheory-18c-Humour-Banc-Python.ipynb– banc humour consolide (#14032) : CORPUS_DUR 120 instances, source de verite.MyIA.AI.Notebooks/IIT/ICT-Series/ICT-21-SAETrajectories.ipynb– substrat S4 (SAE Qwen3.5-9B), aval + tard.MyIA.AI.Notebooks/IIT/ICT-Series/ICT-24-WorkspaceIgnition-Python.ipynb– Gate 24, gated etages superieurs (GPU).#5635, #8236, #5105, #4588 – aval ICT-Series.
Note : ICT-35 – HumorCausalProbe-Pilot, sub-grain exploratoire de #14035, etage 0 lexical baseline, GPU-free. Verdict INCONCLUSIVE borne sur 30 appariements exploratoires (6 positifs uniques x 5 + 30 negatifs uniques). Mesure StratifiedKFold(n_splits=3) sur 36 unites group-level (fix c.920 fuite de groupe via unification par moyenne H + c.927 bascule unite-de-mesure). L’etape d’apres est #14033 (substrat taxonomique), pas Phase 2 SAE. Si le verdict devient FEATURE_CANDIDATE apres elargissement (>100 textes positifs uniques) ou changement d’etage, ce notebook sera promu en ICT-35 numerote definitif ; sinon il restera un -Pilot archive pour tracabilite.