Guides
Expériences en ligne (tests A/B)
Votre comparaison hors ligne a dit que la nouvelle version est meilleure. Un test A/B en production pose la même question à de vrais utilisateurs. Votre propre routeur répartit au hasard des unités (utilisateurs ou sessions) entre deux bras ; oloproof experiment lit ses journaux et répond avec les quatre mêmes états que toute autre règle. La méthode est admise par un audit indépendant à des conditions énoncées : observed_until est votre parole sur le moment où le journal des résultats a été extrait, les unités ont été randomisées indépendamment, le résultat d'aucune unité ne dépend du bras d'une autre, les résultats n'ont pas dérivé pendant que la répartition changeait, et chaque regard lit le journal entier. En dehors de ces conditions, la réponse n'est pas couverte.
Les journaux et la déclaration
Deux fichiers, CSV ou JSONL : un journal d'affectation avec une ligne par unité (la colonne d'unité que vous déclarez, et arm), et un journal de résultats avec l'unité et sa value.
experiment:
name: checkout-v2
unit: user
allocation: {control: 0.5, treatment: 0.5}
control: control
treatment: treatment
assignments: logs/assignments.csv
outcomes: logs/outcomes.csv
kind: rate
rules:
- {id: lift, kind: superiority}
- {id: no-harm, kind: non_inferiority, margin: 0.01}oloproof experiment experiment.yaml --policy release.yaml --jsonkind: mean prend un range: [low, high] déclaré ; direction: max lit le plus bas comme le meilleur. Une unité affectée sans ligne de résultat est manquante et bornée à son pire de chaque côté, sauf si missing_outcome: zero dit qu'aucune ligne signifie zéro. Enregistrez une ligne pour chaque unité, zéros compris : un journal de conversions qui ne contient que les convertis lit toutes les autres unités comme manquantes, et la réponse reste INSUFFICIENT_EVIDENCE. Le remplissage par zéro suppose que la fenêtre de résultat de chaque unité comptée est close ; une unité affectée il y a une heure n'a pas encore converti, et la compter comme zéro favorise le bras qui convertit le plus vite. Déclarez outcome_window_hours, as_of et observed_until (le moment où le journal des résultats a été extrait), avec une colonne assigned_at dans le journal d'affectation, et seules les unités dont la fenêtre s'est close à as_of sont comptées ; missing_outcome: zero est refusé sans eux, quel que soit le suivi. Écrivez chaque heure avec un décalage ou Z (2026-10-11T00:00:00Z) ; une heure sans l'un ni l'autre est refusée. as_of ne peut pas être postérieur à observed_until, et aucun des deux ne peut être dans le futur : un résultat postérieur à l'extraction n'est pas dans le journal. La fenêtre choisit la question : une fenêtre de 48 heures répond à « a converti en 48 heures », elle doit donc couvrir le délai du résultat visé. Les identifiants d'unité sont comparés comme du texte après suppression des espaces, 42.0 étant lu comme 42. Le code de sortie est celui de la pire règle : 0 PASS, 1 FAIL, 3 INSUFFICIENT_EVIDENCE, 4 MANUAL_REVIEW.
Ce qu'il vérifie d'abord
- Le ratio d'échantillon. Les effectifs d'unités des bras sont testés contre l'allocation déclarée (test binomial exact pour deux bras, khi-deux pour davantage, seuil 0,001). Un écart rend chaque règle MANUAL_REVIEW : une répartition cassée signifie que les bras ne sont plus comparables, et davantage de données du même genre ne la répare pas. Trouvez la cause (une redirection qui perd des utilisateurs, un filtre anti-robots appliqué à un seul bras) avant de lire toute différence.
- Une unité dans deux bras. L'affectation a fui ; chaque règle se lit MANUAL_REVIEW.
- Plusieurs lignes de résultat pour une unité. Refusé, car les événements d'un même utilisateur ne sont pas indépendants. Déclarez aggregate: unit_mean pour que la moyenne propre à chaque unité soit son résultat : la réponse porte alors sur l'utilisateur moyen, non sur l'événement moyen.
- Un bras vide. INSUFFICIENT_EVIDENCE jusqu'à ce qu'il ait des résultats.
- Des lignes de résultat qui ne correspondent à aucune unité affectée, sous missing_outcome: zero. Les zéros reposent sur la jointure, donc une jointure cassée lirait de vraies unités comme des zéros ; chaque règle lit MANUAL_REVIEW jusqu'à ce que les identifiants concordent. Par défaut, elles sont comptées et laissées de côté.
Comment il décide
La moyenne de chaque bras reçoit l'intervalle qu'Oloproof utilise déjà pour une exécution (Clopper-Pearson exact pour un taux, la moyenne bornée par pari sinon), chacun à la moitié du budget d'erreur, et la différence est bornée par les extrémités opposées des deux. Elle tient à toute taille d'échantillon et est plus large qu'un intervalle normal de manuel. Une règle de supériorité passe quand tout l'intervalle est au-dessus de zéro ; une règle de non-infériorité passe quand sa borne inférieure est au moins égale à moins la marge.
monitoring: anytime utilise l'intervalle par pari pour les deux bras, qui tient quelle que soit la fréquence de vos lectures et quel que soit le moment où vous arrêtez. Cela tient à condition que chaque lecture passe le journal entier jusqu'à ce moment : ne supprimez ni ne filtrez jamais de lignes par période après les avoir lues. Le défaut, fixed, se lit une fois, à la taille d'échantillon que vous avez planifiée.
Plusieurs traitements
Déclarez treatments: [b, c] au lieu de treatment: et donnez à chaque règle le traitement qu'elle compare (treatment: b). L'intervalle de chaque bras est pris à 1 - alpha/m pour les m bras, si bien que chaque traitement moins le témoin est couvert d'un coup par la borne de l'union : une règle par traitement, une affirmation chacune, et la famille tient ensemble. Chaque intervalle est plus large qu'avec deux bras, car chaque bras paie pour les autres. Avec plusieurs traitements, le rapport nomme le meilleur parmi ceux qui passent chaque règle qui les nomme, et s'il est séparé de chaque autre traitement qui passe : chaque différence, bornée par les mêmes intervalles simultanés, exclut zéro. Les égalités gardent l'ordre déclaré et ne sont jamais séparées. La vérification du ratio d'échantillon lit déjà chaque bras. La garantie conjointe couvre les bras d'un seul fichier d'expérience : répartir les traitements sur plusieurs fichiers y renonce.
Métriques de ratio
Un taux de clics, ce sont des clics sur des sessions, et un utilisateur avec beaucoup de sessions n'est pas beaucoup d'utilisateurs indépendants. Déclarez le ratio sur une ligne par unité, avec des plages dans lesquelles se trouvent les valeurs de chaque unité :
ratio: {numerator: clicks, denominator: sessions, numerator_range: [0, 50], denominator_range: [1, 50]}
aggregate: unit_sum # only if a unit has several rows: they are addedL'estimande est le numérateur moyen de chaque bras sur son dénominateur moyen. Les deux moyennes sont bornées par l'intervalle admis, chacune à sa part du budget d'erreur, et le ratio en divisant leurs extrémités, si bien qu'il tient à toute taille d'échantillon sans approximation normale. Un dénominateur dont l'intervalle atteint zéro ne donne aucun intervalle. Une valeur hors de sa plage est une erreur de configuration qui nomme l'unité, jamais écrêtée. L'intervalle de la méthode delta, généralement plus étroit, est montré à côté et ne décide jamais.
Une covariable d'avant le test
Un résultat qui suit une grandeur mesurée avant le test (les dépenses du mois dernier, les sessions de la semaine dernière) varie moins une fois celle-ci retirée. Apportez la valeur de chaque unité d'avant le test et un coefficient fixé avant que les résultats de ce test ne soient lus :
covariate: {values: logs/pre_period.csv, range: [0, 500], theta_from: logs/history.csv}Le résultat décidé est Y - theta (X - c). theta est déclaré (theta:, avec un center: facultatif, le milieu de la plage par défaut), ou ajusté sur un fichier d'historique d'une période antérieure au test (colonnes covariate et outcome), jamais sur les propres résultats de ce test. Comme X a été mesuré avant l'affectation, et que les unités qui ont un X ont aussi été fixées avant l'affectation, la différence ajustée a le même estimande. Une unité sans valeur dans le fichier n'est pas ajustée. Un ajustement valide peut tout de même être plus large que l'absence d'ajustement, car la plage du résultat ajusté est plus large ; le rapport montre donc les deux intervalles et indique lequel est le plus étroit ; il ne revendique aucune économie.
Nouveauté et interférence, montrées
novelty: {exposure_column: exposed_at, time_column: at, days: 7} lit la première exposition de chaque unité dans le journal d'affectation et l'heure de chaque ligne de résultat, et montre la différence pendant les days premiers jours d'exposition et ensuite. Une différence précoce décidée avec une différence ultérieure non décidée ou de sens opposé est nommée « possibly a novelty effect » (peut-être un effet de nouveauté). Ce découpage ne décide jamais une règle : les unités sans lignes dans une tranche en sont laissées de côté, non bornées au pire. Il prend un résultat par ligne, pas un ratio ni une covariable.
cluster_column: team compte les équipes qui contiennent des unités de plusieurs bras et demande si le résultat d'une unité peut dépendre du bras d'une autre. Si c'est le cas, randomisez par équipe et déclarez-la comme unité.
Plusieurs traitements, ratios et covariables ont été admis par un audit indépendant et sa confirmation le 3 novembre 2026, aux mêmes conditions que deux bras.
Hors ligne et en ligne
Nommez la comparaison hors ligne et sa métrique (offline_comparison: <id>, offline_metric: pass_rate) et la réponse indique si les deux sens décidés concordent. C'est rapporté et ne décide jamais rien : un désaccord est une raison d'examiner la suite, pas un verdict sur la version.
Pièges
- Regarder en cours de route. Lire chaque jour un test à horizon fixe et s'arrêter sur un bon jour gonfle les fausses victoires. Planifiez la taille d'échantillon, ou déclarez monitoring: anytime.
- Nouveauté et apprentissage. Les utilisateurs réagissent à toute nouveauté, puis se stabilisent. Un gain de la première semaine peut s'estomper ; laissez tourner assez longtemps pour couvrir les cycles de vos utilisateurs, et déclarez novelty: pour voir le découpage.
- Interférence. L'intervalle suppose que le résultat d'une unité ne dépend pas du bras d'une autre. Un stock partagé, des places de marché, des fonctions sociales ou un cache réchauffé par un bras rompent cette hypothèse ; randomisez alors par une unité plus large (une région, une équipe), et déclarez cluster_column: pour voir à quel point les grappes sont mélangées.
- Une dérive sous une répartition changeante. L'intervalle suppose que l'allocation est restée constante pendant le test, ou que les résultats n'ont pas dérivé pendant qu'elle changeait. Une montée de 10 % à 50 %, ou 90/10 puis 10/90 sur un jour de semaine et un week-end, pondère les périodes différemment dans chaque bras et mêle un effet du temps à la différence ; le contrôle du ratio d'échantillon lit des effectifs agrégés et ne le voit pas. Gardez la répartition fixe, ou ne décidez que sur la période qui suit la montée.
- Changer d'unité. Randomisez et analysez par la même unité. Randomiser des utilisateurs et compter les sessions comme indépendantes rétrécit l'intervalle à tort.
Ce que cela ne peut pas vous dire
- Si le résultat tient pour des utilisateurs hors de la fenêtre, de la région ou du trafic du test.
- Quoi que ce soit sur une métrique que vous n'avez pas déclarée avant le test ; une métrique choisie après coup est une description.
- Si votre routeur a vraiment randomisé : la vérification du ratio d'échantillon attrape les grosses fautes, pas les subtiles.
- Lequel des traitements qui n'ont pas passé est le meilleur : seuls les traitements qui passent sont classés. L'allocation stratifiée et les plans en alternance (switchback) ne sont pas construits.