Az Oksági cél és megfigyelési torzítás
alfejezetben azonosítottuk a zavaró változókat és azt, hogy
megfigyeléses adatból csak erős feltételezésekkel lehet oksági hatást
becsülni. Most a Northstar kezdőlabor lab/assignments.csv
és lab/outcomes.csv fájljaira építünk egy előre
rögzített kísérleti protokollt. A bemenet ügyfélszintű: a 300
ügyfél 2025-07-01-én kapott control vagy
treatment hozzárendelést, majd mindenkinél ugyanazt a 7
napos kimenetet figyeltük meg. A primer endpoint a
conversion_7d (0/1), az elemzési egység az ügyfél, a cél az
átlagos kezelési hatás (average treatment effect, ATE) becslése.
Előzetes becslés: ha a kontroll-konverzió 0.24, a kezelési hatás 0.10, és fejenként 150-150 ügyfél van, akkor a normal approximation standard error nagyjából
A 95%-os konfidenciaintervallum szélessége tehát kb. 0.21; a 0.10-es hatás nem lesz élesen kimutatható. Ezért a protokollt becslési és döntési keretként írjuk elő, nem pedig „biztos nyertesként”.
A randomizálás egysége (unit of randomization) az a
megfigyelési egység, amelyet véletlenszerűen rendelünk valamelyik karba.
Itt ez a customer_id. Azért nem a rendelés vagy a tételsor,
mert ugyanazon ügyfél rendelései és tételei korrelálnak: ha egy ügyfelet
kezelünk, az összes rendelésére hatással lehetünk. A
lab/order_items.csv tehát nem szaporíthatja fel a
mintanagyságot; a kísérleti nevező továbbra is 300 ügyfél.
Kis kézi kontroll: ha 10 ügyfélből 5-öt sorsolunk treatmentbe, a randomizálási egységek száma 10. Ha minden ügyfélnek átlagosan 4 rendelési tétele van, attól még a független kísérleti egység nem 40, mert a 4 tétel egy közös sorsolás következménye.
Az exposure a tényleges beavatkozásnak való
kitettség, például az ajánlat megjelenése, az email megnyitása vagy a
push értesítés kézbesítése. A randomizálás a szándékolt
hozzárendelést, a variant oszlopot adja; a tényleges
kitettség ettől eltérhet.
Az intention-to-treat (ITT) elv szerint minden ügyfelet abban a karban elemzünk, ahová randomizálták, függetlenül attól, hogy a kezelést ténylegesen megkapta-e. Ez megőrzi a randomizálás előnyét: a két kar összehasonlíthatóságát. Ha csak a ténylegesen kitett ügyfeleket vizsgálnánk, a kitettség maga is lehet a kezelés következménye, és újra megjelenhet a zavaró hatás.
A laborban az ITT nevező az összes randomizált ügyfél: mind a 300
sor. A primer becslés a treatment és a control kar
conversion_7d átlagának különbsége.
A guardrail őrszem-mutató: olyan előre rögzített
másodlagos kimenet, amelynek romlása esetén a bevezetést leállítjuk, még
akkor is, ha a primer mutató javul. A mi protokollunkban a
revenue_7d_minor ilyen: ha a treatment kar átlagos 7 napos
bevétele több mint 5%-kal a kontroll alá esik, a döntés
block.
Fontos, hogy a guardrail szabályát az adatok megtekintése előtt rögzítsük. Utólag választott guardrail esetén bármilyen véletlen ingadozást „kockázatnak” nevezhetnénk, és a döntés nem lenne reprodukálható.
Az attrition az ügyfelek kiesése a megfigyelési
ablakból: hiányzó kimenet, kilépés, adatvédelmi tiltakozás. A Northstar
laborban nincs ilyen: minden randomizált ügyfélhez tartozik teljes, fix
ablakú outcomes.csv sor. A protokollban mégis előírjuk a
kezelést: ha egy conversion_7d hiányozna, az ügyfél az ITT
nevezőben marad, a hiányzó konverziót 0-ként imputáljuk, és a döntés
mellett hiányzási riportot készítünk. Ez konzervatív szabály; más
imputáció érzékenységvizsgálatként szerepelhet.
Az interference azt jelenti, hogy egy ügyfél kimenete függ más ügyfelek hozzárendelésétől. A randomizált kísérlet standard becslője feltételezi a SUTVA feltevést (stable unit treatment value assumption): minden egység potenciális kimenete csak a saját hozzárendelésétől függ. Ha például a kezelés egy nyilvános weboldali kedvezmény, a kontroll ügyfelek is láthatják, így a kontroll kimenete már nem a tiszta .
A mi mesterséges laborunkban a kezelés ügyfélszintű és elszigetelt; a protokollban ezt feltevésként rögzítjük, és a kísérleti jelentésben megnevezzük mint érvényességi korlátot.
A randomizálás legfontosabb következménye a függetlenség:
ahol treatment, control, és a potenciális kimenetek, pedig minden, a randomizálás előtt mért változó. Emiatt nincs szükség zavaró változóra való kiigazításra; az egyszerű átlagkülönbség torzítatlanul becsli az ATE-t:
A mintabeli becslő:
ahol
a treatment kar conversion_7d átlaga,
a kontroll karé. Mindkettő arány, dimenzió nélküli, 0 és 1 közötti
érték. A standard error független karok esetén:
ahol és a két kar konverziós aránya, és a karméretek.
A kísérleti erő (power) annak a valószínűsége, hogy a teszt elutasítja a nullhipotézist, ha a valódi hatás . Kétoldali 5%-os szinten, 80%-os erőhöz tartozó minimálisan detektálható hatás közelítőleg:
ahol , , a tervezett kontrollarány, a teljes minta. A mi 300 ügyfelünkre mellett a gyökös tag , így . A 0.10-es hatás kimutatásához tehát a kísérlet alul-powered; a protokoll ezt explicit korlátként tartalmazza.
A protokoll döntési lapja:
| Elem | Rögzített érték |
|---|---|
| Randomizálási egység | customer_id |
| Hozzárendelés | 1:1 control/treatment, 2025-07-01 |
| Primer endpoint | conversion_7d |
| ITT nevező | mind a 300 randomizált ügyfél |
| Megfigyelési ablak | fix 7 nap,
observed_until = 2025-07-08T00:00:00+00:00 |
| Döntési küszöb | bootstrap 95% CI alsó határ > 0.02 → rollout |
| Guardrail | revenue_7d_minor treatment átlag >= 0.95 * kontroll
átlag |
A döntési szabály tehát: ha a 95%-os percentilis bootstrap CI alsó
határa nagyobb, mint 0.02, javasoljuk a bevezetést; ha a CI felső határa
kisebb, mint 0.02, elutasítjuk; különben collect_more. A
guardrail sérelme esetén a döntés mindig block.
Az alábbi kód szemléltető: a kimeneti számok elméletileg vártak, nem
mért futási eredmények. Futtatási környezet: projektgyökér, ahol a
lab/ mappa található.
# s070_protocol.py — szemléltető, nem futtatott kód
import numpy as np
import pandas as pd
ASSIGN_PATH = "lab/assignments.csv"
OUTCOME_PATH = "lab/outcomes.csv"
EXPECTED_OBSERVED_UNTIL = "2025-07-08T00:00:00+00:00"
BOOT_SEED = 20260701
def load_and_validate():
assign = pd.read_csv(ASSIGN_PATH)
outcomes = pd.read_csv(OUTCOME_PATH)
df = assign.merge(outcomes, on="customer_id", validate="one_to_one")
errors = []
if len(df) != 300:
errors.append(f"Várt 300 sor, kapott {len(df)}")
if not df["customer_id"].is_unique:
errors.append("customer_id nem egyedi")
if set(df["variant"]) != {"control", "treatment"}:
errors.append("variant értékkészlet hibás")
if df["conversion_7d"].isna().any():
errors.append("conversion_7d hiányzik")
if (df["observed_until"] != EXPECTED_OBSERVED_UNTIL).any():
errors.append("observed_until eltér a fix ablaktól")
if (df["revenue_7d_minor"] < 0).any():
errors.append("negatív revenue_7d_minor")
return df, errors
def bootstrap_diff_ci(df, n_boot=2000, seed=BOOT_SEED):
rng = np.random.default_rng(seed)
control = df.loc[df["variant"] == "control", "conversion_7d"].to_numpy()
treatment = df.loc[df["variant"] == "treatment", "conversion_7d"].to_numpy()
diffs = np.empty(n_boot)
for i in range(n_boot):
c = rng.choice(control, size=len(control), replace=True)
t = rng.choice(treatment, size=len(treatment), replace=True)
diffs[i] = t.mean() - c.mean()
return np.quantile(diffs, [0.025, 0.975])
def evaluate_protocol(df, n_boot=2000, seed=BOOT_SEED):
control = df.loc[df["variant"] == "control", "conversion_7d"]
treatment = df.loc[df["variant"] == "treatment", "conversion_7d"]
delta = treatment.mean() - control.mean()
se = np.sqrt(
treatment.mean() * (1 - treatment.mean()) / len(treatment)
+ control.mean() * (1 - control.mean()) / len(control)
)
ci_lo, ci_hi = bootstrap_diff_ci(df, n_boot, seed)
control_rev = df.loc[df["variant"] == "control", "revenue_7d_minor"].mean()
treatment_rev = df.loc[df["variant"] == "treatment", "revenue_7d_minor"].mean()
guardrail_ok = treatment_rev >= 0.95 * control_rev
if not guardrail_ok:
decision = "block"
elif ci_lo > 0.02:
decision = "rollout"
elif ci_hi < 0.02:
decision = "reject"
else:
decision = "collect_more"
return {
"delta": delta,
"se": se,
"ci": (ci_lo, ci_hi),
"guardrail_ok": guardrail_ok,
"decision": decision,
}
df, errors = load_and_validate()
print(errors) # elvártan üres lista
if not errors:
result = evaluate_protocol(df)
print(result)A kód ellenőrzi a protokoll feltételeit, majd kiszámítja az ITT becslést. A bootstrap csoporton belüli: minden ismétlésben külön-külön mintavételezünk a kontroll és a treatment karból, mert a karok méretét a protokoll rögzíti, és a randomizálási egység az ügyfél.
A merge után a DataFrame 300 soros; a
validate="one_to_one" garantálja, hogy minden ügyfélhez
pontosan egy kimenet tartozik. A conversion_7d dtype
várhatóan integer (0/1), a variant object típusú
kategória.
Kézi kontroll: a normal approximationból számolt se és a
bootstrap CI közelítőleg összhangban van; 300 mintánál a percentilis
bootstrap CI nem feltétlenül szimmetrikus, de a két módszer ugyanazt a
nagyságrendet adja. A becsült delta a generáló folyamat
miatt várhatóan 0.10 körül mozog, de a véletlen miatt ettől több
százalékponttal is eltérhet. A döntési szabály ezért nem a pontbecslést,
hanem a CI és a 0.02-es küszöb viszonyát nézi.
Ha a guardrail megsérül, a döntés block akkor is, ha a
primer hatás pozitív. Ez a protokoll előre rögzített része; az adat
megtekintése után nem módosítható.
Kísérleti minőségbiztosítás: külön kell választani a randomizált kiosztást, a tényleges expozíciót és a kimenet elérhetőségét. ITT-elemzésnél a randomizált nevezőt kell alapul venni; a hiányzó kimenetek nem kezelhetők automatikusan nullásként.
Értelmezés. Miért nem lehet a kísérlet elemzési
egysége a rendelési tétel vagy a rendelés, ha a
lab/order_items.csv-t összekapcsoljuk az ügyfelekkel?
Magyarázd meg az ITT nevező és a függetlenség szempontjából.
Implementálás. Írj
validate_protocol(df) függvényt, amely a fenti
ellenőrzéseken túl azt is vizsgálja, hogy a variant
karonként 150-150 ügyfelet tartalmaz, és hogy minden
assigned_at értéke 2025-07-01T00:00:00+00:00.
A függvény hibák listájával térjen vissza; adj meg egy invariant,
amellyel a kimenetet ellenőrzöd.
Hibakeresés. Egy elemző csak azokat az
ügyfeleket tartja meg, akiknél revenue_7d_minor > 0,
mert szerinte csak ők „aktívak”, és a bevételi hatást csak rajtuk akarja
mérni. Mi a hiba? Adj egy 10+10 ügyféles számpéldát, ahol az ITT
bevételi hatás +48 cent, de a szűrt becslés +35 cent.
Megváltozott feltétel. Tegyük fel, hogy a kezelés egy nyilvános weboldali kedvezmény, amelyet a kontroll ügyfelek is láthatnak, ha ugyanabban a régióban élnek. Melyik protokollfeltétel sérül? Javasolj alternatív protokollt, és nevezd meg annak korlátját.
Transzfer. A primer endpoint
revenue_7d_minor-ra változik. Hogyan módosul a standard
error képlete és a döntési szabály? Milyen hatásméretet
használnál?
1. megoldás. A randomizálás ügyfélszintű; a rendelések és tételek egy ügyfélhez tartoznak, tehát nem független megfigyelések. Ha a tételeket külön sorként kezelnénk, a minta látszólag 4796 sorra nőne, de az azonos ügyfélhez tartozó sorok közös sorsolást örökölnek, így a standard error alulbecsült lenne, és a CI túl szűk. Az ITT nevező ezért mindig a randomizálási egységek száma: 300 ügyfél.
2. megoldás. Egy lehetséges függvény:
def validate_protocol(df):
errors = []
if df["variant"].value_counts().to_dict() != {"control": 150, "treatment": 150}:
errors.append("A karok mérete nem 150-150")
if (df["assigned_at"] != "2025-07-01T00:00:00+00:00").any():
errors.append("assigned_at eltér a protokolltól")
return errorsInvariant: ha a bemenet a tiszta laborfájl, a hibák listája üres.
Ellenőrzés: a value_counts().to_dict() pontosan a két várt
kulcsot adja; a dátum-összehasonlítás stringként is működik, mert az ISO
formátum fix.
3. megoldás. A szűrés exposure utáni változón
alapul: a revenue_7d_minor > 0 maga is a kezelés
következménye lehet. Ha a kezelés növeli a vásárlási valószínűséget, a
treatment karból több ügyfél kerül a szűrt halmazba, és a két csoport
összetétele már nem véletlenszerű. Számpélda: 10 treatment ügyfél közül
6 vásárol, revenue-k 100, 110, 120, 130, 140, 150 cent; 4 nem vásárol
(0). Treatment ITT átlag:
.
Kontroll: 10-ből 3 vásárol, revenue-k 80, 90, 100 cent; ITT átlag:
.
A valódi ITT hatás
cent. Ha csak a pozitív revenue-jű sorokat nézzük: treatment átlag 125,
kontroll átlag 90, különbség 35 cent. A szűrés eltünteti a
konverziónövelő hatás egy részét.
4. megoldás. Az interference sérül: a kontroll ügyfelek kimenete nem a tiszta , mert ők is látják a kedvezményt, ami a kontroll-konverziót is megemelheti, így az ATE alulbecsült. Alternatíva: régiószintű klaszter-randomizálás, ahol az egész régió kap vagy nem kap kezelést. Ennek korlátja: a Northstar adatban csak 3 régió van, ami túl kevés független klaszter a megbízható következtetéshez; nagyobb mintában viszont a klaszteren belüli korrelációt figyelembe vevő standard error kell. Egy másik alternatíva: a kezelést egyénileg elszigetelni, például emailben küldött egyedi kuponnal.
5. megoldás. Folytonos kimenetre a standard error:
ahol és a karok mintabeli varianciája, centben kifejezve. A döntési szabály ugyanaz maradhat, de a 0.02-es küszöb helyett pénzben rögzített minimális gyakorlati hatás kell, például 20 cent. Hatásméretként Cohen-féle is használható: , ahol a két kar egyesített szórása.
A leggyakoribb hiba az exposure utáni szelekció: a randomizálás után csak azokat az ügyfeleket elemezni, akiknél a kezelés „működött”, vagy akiknél van megfigyelt aktivitás. A randomizálás az eredeti karok összehasonlíthatóságát adja; ha utána szűrünk, a szűrés függhet a kezeléstől, és az Oksági cél és megfigyelési torzítás alfejezetben megismert collider torzítás lép fel. A fenti 3. feladat számpéldája mutatja, hogy a valódi +48 centes ITT hatásból a szűrt becslés +35 cent lesz. Az ITT elv pontosan ezt védi ki: minden randomizált ügyfél a saját karjában marad, akkor is, ha nem kapta meg a kezelést vagy nem konvertált.
Önellenőrzés: tudd megnevezni a randomizálási egységet a laborban; tudd, miért nem a rendelési tétel; tudd, mi a különbség a hozzárendelés és a tényleges exposure között; tudd, hogy a guardrail szabályát mikor kell rögzíteni; tudd, melyik feltevés sérül, ha a kontroll ügyfelek is látják a kezelést.
A randomizált kísérlet protokollja a randomizálási egységet, a hozzárendelést, a primer endpointot, az időablakot és a döntési szabályt előre rögzíti. Az ITT elv szerint minden randomizált ügyfél a saját karjában marad; a tényleges exposure szerinti szűrés újra zavaró hatást vihet be. A guardrail a bevezetést védi, az attrition és az interference pedig érvényességi feltétel. A Northstar 300 ügyfele 1:1 arányban alkalmas a hatás becslésére, de 80%-os erővel csak kb. 14 százalékpontos hatást tudna kimutatni, ezért a döntés CI-alapú és a 0.02-es küszöbhöz kötött. Következő lépésként Megfigyeléses becslés és érzékenység alfejezetben megfigyeléses becslést és érzékenységvizsgálatot nézünk, amikor randomizálás nem áll rendelkezésre.