Jelentésminőség: minden numerikus ábrához add meg a nevezőt, a megfigyelési időszakot, a hiányzók számát és az alkalmazott bizonytalanságmérőt. A nem szignifikáns eredmény nem bizonyítja a hatás hiányát; a jelentésben hatásbecslés és intervallum szükséges.
A II. záróprojekt kísérleti ágának utolsó lépése a tudományos
jelentés és egy újrafuttatható bemutató terve. A bemenet a Northstar
kísérleti projekt már elemzett adata:
lab/data/assignments.csv és
lab/data/outcomes.csv, valamint a szegmenshez szükséges
lab/data/customers.csv. A megfigyelési egység a randomizált
ügyfél, a kimenet a conversion_7d bináris érték, a becsült
mennyiség a treatment és control kar 7 napos konverziós arányának
különbsége. Az assigned_at és observed_until a
protokoll szerinti 7 napos ablakot rögzíti, az ITT-nevező mind a 300
randomizált ügyfél.
A jelentés akkor jó, ha egy olvasó a módszer, az eredmény és a korlát kapcsolatát látja anélkül, hogy a kódot vagy a notebookot megnyitná. A bemutató akkor reprodukálható, ha a közönség tíz perc után a rögzített paranccsal ugyanazokat az összefoglaló számokat és ábrákat tudja előállítani.
Induló feladat. Mielőtt továbbolvas, írjon egy mondatot a jelentés eredmény rovatához, ha az elemzés egy lehetséges kimenete ez lenne: , 95%-os percentilis bootstrap CI: . Fogalmazza meg úgy, hogy a bizonytalanság és a döntési korlát is benne legyen.
A jelentés nem a kérdéssel kezdődik, hanem azzal, hogy a kérdés mit jelent a mérhető világban. A kísérleti kérdés operacionalizált alakja:
A véletlenszerűen kezelt Northstar-ügyfelek körében a treatment 7 napos konverziós arányra gyakorolt átlagos oksági hatása mekkora a controlhoz képest, az összes randomizált ügyfelet a hozzárendelt karjában megtartva?
A becsült paraméter tehát az ITT-hatás (intention-to-treat effect):
ahol és a potenciális kimenetek, , . A paraméter mértékegysége százalékpont, nem „százalékos relatív növekedés”. A jelentésben a generáló szimuláció paramétere nem szerepelhet érvként; a tanuló feladata a mintából való becslés, nem az igazság visszaolvasása.
A módszer rovat négy dolgot rögzít:
Itt , a randomizált kar, a konverziós indikátor. A képletben minden vektor hossza a megfigyelt ügyfélszám, a karok mérete a manifestből olvasandó.
A záróprojektben a Becslés, intervallum és érzékenység alfejezetben tanult csoporton belüli, ügyfélszintű percentilis bootstrap az elsődleges intervallum. A resampling egység az ügyfél, nem a sor és nem a rendelési tétel. Mindkét karban külön-külön húzunk visszatevésesen , illetve ügyfelet, kiszámítjuk -ot, és a ismétlésből a 2,5. és 97,5. percentilist vesszük. A seed csak a húzások ismételhetőségét szolgálja, és nem garantál platformfüggetlen bitazonosságot.
Az eredmény rovat három számot és azok értelmezését tartalmazza: , a bootstrap CI, és ha a protokoll tartalmazza, a permutációs p-érték. A p-érték nem a nullhipotézis igaz voltának valószínűsége, hanem a nullmodell szerinti szélsőségesség mértéke. A CI ismételt mintavételi lefedettség: hosszú távon az eljárás az esetek 95%-ában lefedi a paramétert; egyetlen intervallumról nem mondjuk, hogy „95% eséllyel tartalmazza az igazi hatást”.
A gyakorlati állítás három elemet köt össze: a becslés nagysága, a CI által megengedett hatások és az üzleti döntési küszöb. Például ha a vállalat 2,5 százalékpontos hatás alatt nem vezetné be a kezelést, akkor a intervallum azt jelenti, hogy a mintából még nem zárható ki sem a hatástalanság, sem a 2,5 pont alatti, sem a afeletti hatás.
A 4–6 oldalas jelentéshez három célzott ábra elég. Mindegyiknek egy kérdése van:
A reprodukálhatóság rovat a bemeneti fájlneveket, a seedet, a
csomagverziókat, a parancsot, a kimeneti hash-eket és a környezet nevét
adja meg. A nyers customer_id nem kerülhet a jelentésbe
vagy az ábrákra; a szegmensábra aggregált, a notebook-kimenet pedig
titokmentes.
Először egy 40 ügyfeles, kézzel ellenőrizhető kontrollpélda:
import numpy as np
import pandas as pd
# Kézi kontrollpélda: 20-20 ügyfél, pontosan 4 és 8 konverzió
y = np.array(
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # control
+ [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # treatment
)
df_smoke = pd.DataFrame({
"customer_id": [f"c{i:02d}" for i in range(40)],
"variant": ["control"] * 20 + ["treatment"] * 20,
"conversion_7d": y,
})
def risk_diff(data, treatment="treatment", control="control"):
t = data.loc[data["variant"] == treatment, "conversion_7d"].to_numpy()
c = data.loc[data["variant"] == control, "conversion_7d"].to_numpy()
return t.mean() - c.mean()
def bootstrap_risk_diff(data, seed=20261020, B=10000):
rng = np.random.default_rng(seed)
controls = data.loc[data["variant"] == "control", "conversion_7d"].to_numpy()
treats = data.loc[data["variant"] == "treatment", "conversion_7d"].to_numpy()
out = np.empty(B)
for b in range(B):
c_idx = rng.integers(0, len(controls), size=len(controls))
t_idx = rng.integers(0, len(treats), size=len(treats))
out[b] = treats[t_idx].mean() - controls[c_idx].mean()
return out
boot = bootstrap_risk_diff(df_smoke)
delta_hat = risk_diff(df_smoke)
ci_lo, ci_hi = np.percentile(boot, [2.5, 97.5])
print(f"n_control={20}, n_treatment={20}")
print(f"RD={delta_hat:.4f}; 95%-os percentilis bootstrap CI=[{ci_lo:.4f}, {ci_hi:.4f}]")A kézi kontroll: , , . A normál közelítéses standard error
így a 95%-os normál CI . Ez szemlélteti, hogy 40 ügyféllel még egy 20 százalékpontos mintabeli különbség is összefér a nullával.
A Northstar-fájlokra mutató, jelentést és ábrákat előállító parancsfájl váza:
"""scripts/experiment_report.py -- kísérleti jelentés számai és ábrái."""
from pathlib import Path
import argparse, json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def load(data_dir: Path) -> pd.DataFrame:
a = pd.read_csv(data_dir / "assignments.csv")
o = pd.read_csv(data_dir / "outcomes.csv")
df = a.merge(o, on="customer_id", how="inner", validate="one_to_one")
assert df["customer_id"].is_unique
assert df["variant"].isin(["control", "treatment"]).all()
assert df["conversion_7d"].isin([0, 1]).all()
customers = pd.read_csv(data_dir / "customers.csv", usecols=["customer_id", "segment"])
df = df.merge(customers, on="customer_id", how="left", validate="one_to_one")
return df
def make_figures(df, boot, ci, out_dir: Path) -> dict:
out = {}
# 1. ábra: karonkénti arány pontbecsléssel és bootstrap CI-vel
fig, ax = plt.subplots(figsize=(5, 3.5))
for k, group in df.groupby("variant"):
vals = group["conversion_7d"].to_numpy()
b = np.array([vals[np.random.default_rng(20261020 + j).integers(0, len(vals), len(vals))].mean()
for j in range(2000)])
lo, hi = np.percentile(b, [2.5, 97.5])
ax.errorbar(k, vals.mean(), yerr=[[vals.mean()-lo], [hi-vals.mean()]],
fmt="o", capsize=6, label=k)
ax.set_ylim(0, 1); ax.set_ylabel("7 napos konverziós arány")
fig.tight_layout(); fig.savefig(out_dir / "fig1_carok.png"); plt.close(fig)
out["fig1"] = "fig1_carok.png"
# 2. ábra: bootstrap hatáseloszlás
fig, ax = plt.subplots(figsize=(5, 3.5))
ax.hist(boot, bins=40, color="0.8", edgecolor="0.4")
ax.axvline(ci[0], color="k", lw=1, ls="--")
ax.axvline(ci[1], color="k", lw=1, ls="--")
ax.axvline(0, color="r", lw=1, alpha=0.7)
ax.set_xlabel("kockázatkülönbség (százalékpont)")
ax.set_ylabel("bootstrap ismétlések")
fig.tight_layout(); fig.savefig(out_dir / "fig2_bootstrap.png"); plt.close(fig)
out["fig2"] = "fig2_bootstrap.png"
# 3. ábra: szegmensenkénti feltáró érzékenység
fig, ax = plt.subplots(figsize=(5, 3.5))
labels = []
for seg in ["consumer", "business"]:
sub = df[df["segment"] == seg]
if len(sub) < 20:
continue
labels.append(seg)
est = risk_diff(sub)
ax.scatter(est, len(labels)-0.5)
ax.axvline(0, color="k", lw=1, ls=":")
ax.set_yticks(range(len(labels))); ax.set_yticklabels(labels)
ax.set_xlabel("feltáró RD (százalékpont)")
ax.set_title("Szegmensenkénti érzékenység; nem megerősítő hipotézisteszt")
fig.tight_layout(); fig.savefig(out_dir / "fig3_szegmensek.png"); plt.close(fig)
out["fig3"] = "fig3_szegmensek.png"
return out
def main():
p = argparse.ArgumentParser()
p.add_argument("--data", type=Path, required=True)
p.add_argument("--out", type=Path, required=True)
p.add_argument("--seed", type=int, default=20261020)
p.add_argument("--B", type=int, default=10000)
args = p.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
df = load(args.data)
delta_hat = risk_diff(df)
boot = bootstrap_risk_diff(df, seed=args.seed, B=args.B)
ci = np.percentile(boot, [2.5, 97.5]).tolist()
figs = make_figures(df, boot, ci, args.out)
result = {
"n": int(len(df)),
"n_control": int((df["variant"] == "control").sum()),
"n_treatment": int((df["variant"] == "treatment").sum()),
"delta_hat": float(delta_hat),
"ci_95_percentile_bootstrap": ci,
"seed": args.seed,
"B": args.B,
"figures": figs,
}
(args.out / "result.json").write_text(json.dumps(result, indent=2))
if __name__ == "__main__":
main()A futtatási parancs projektgyökérből, a lab/ könyvtárban
lévő projekt esetén:
# Linux/macOS, munkakönyvtár: a lab projekt gyökere
uv run --locked python scripts/experiment_report.py \
--data data --out artifacts/report --B 10000# Windows PowerShell, munkakönyvtár: a lab projekt gyökere
uv run --locked python scripts/experiment_report.py `
--data data --out artifacts/report --B 10000A fenti echo nélküli ábrarészlet a 3. ábrán egyszerű pontokat rajzol CI nélkül; a teljes záróprojektben a Becslés, intervallum és érzékenység alfejezetben tanult módon minden szegmenshez külön csoporton belüli bootstrap CI-t kell számolni. A kódban a 2. ábra hisztogramja a tényleges bootstrap-vektorból készül, tehát az ábra nem önmagában dekoráció, hanem a CI forrását mutatja.
A bemutató terv tíz percre:
| Idő | Lépés | Mit mutat | Ellenőrzés |
|---|---|---|---|
| 0–1 perc | Kérdés és döntési helyzet | Egy mondatos operacionalizált kérdés | Nincs értelmezhetetlen cél |
| 2–4 perc | Módszer és eredmény | ITT, RD, bootstrap CI | A CI tengelye és mértékegysége helyes |
| 5–7 perc | Három ábra | 1. karok, 2. bootstrap, 3. szegmens | Minden ábra aggregált, nincs customer_id |
| 8–9 perc | Korlát és következő kísérlet | Bizonytalanság és előre regisztrált folytatás | Nem állít okságot a szegmensábrából |
| 10. perc | Újrafuttatás | A rögzített parancs és hash-összegzés | A közönség el tudja ismételni |
A kontrollpélda invariánsai: a karok mérete 20-20, az átlagok 0,20 és
0,40, a különbség 0,20. A normál SE 0,1414, ezért a normál CI széles és
tartalmazza a nullát. A percentilis bootstrap CI a kis mintán ettől
eltérhet, mert a bootstrap a becslő mintavételi eloszlását a minta
újramintavételezésével közelíti; nem a normál képletet használja. A
Northstar-fájlokon kapott result.json a helyi gépen mérendő
kimenet; a jelentésben csak a konkrét futásból származó értékek
szerepelhetnek.
Ha egy lehetséges elemzési kimenet és a CI , akkor a helyes megfogalmazás: „A megfigyelt 8,2 százalékpontos különbség a bootstrap CI alapján −1,1 és 17,5 százalékpont közötti hatásokkal is összeegyeztethető; a minta tehát nem dönti el, hogy a hatás a bevezetési küszöb felett van-e, és a nullát sem zárja ki.” A CI értelmezése ismételt mintavételi lefedettség, nem egyetlen intervallum valószínűségi állítása.
Értelmezés. A jelentő ezt írta: „A kezelés 8,2 százalékpontos konverziónövekedést okozott (95% CI: −1,1 és 17,5; p = 0,082), ezért a kezelést be kell vezetni.” Nevezzen meg legalább három pontatlanságot, és írja át egyetlen helyes mondattá.
Megvalósítás. Egészítse ki a fenti
make_figures kódot úgy, hogy a 3. ábrán minden szegmenshez
kiszámolja a csoporton belüli ügyfélszintű bootstrap CI-t is, és a
pontokat vízszintes hibasávval rajzolja. A függvény ne használjon
globális df-et; minden adatot paraméterként
kapjon.
Hibadiagnózis. Egy kolléga ezt a bootstrapet írta:
def wrong_bootstrap(data, rng, B=10000):
out = []
for _ in range(B):
sample = data.sample(frac=1.0, replace=True, random_state=42)
out.append(risk_diff(sample))
return np.array(out)Magyarázza el, miért nem ez a megfelelő resampling a kísérleti becslés bizonytalanságához, és mi romlik el a CI értelmezésében. Adjon javító kódvázat.
Megváltozott feltétel. A primer kimenetet
revenue_7d_minor-ra cserélik. A kontrollpéldában a
bevételek EUR-centben: control
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1200,1200,800,800],
treatment
[0,0,0,0,0,0,0,0,0,0,0,0,1200,1200,1200,1200,800,800,800,800].
Számolja ki az átlagos bevételkülönbséget, és mondja meg, hogyan
változik a jelentés képlet- és CI-része.
Önellenőrzés. Készítsen tízpontos ellenőrzőlistát a saját jelentéséhez, amely tartalmazza a kérdés, módszer, eredmény, ábra, reprodukálhatóság és következő kísérlet rovatait. Minden ponthoz írjon egy eldöntendő kérdést.
1. megoldás. A mondat hibái: (a) „okozott” helyett a kísérleti elemzés a becsült oksági hatást közli, a CI a becslés bizonytalanságát; a szóhasználat nem törölheti a bizonytalanságot. (b) A CI tartalmazza a nullát, ezért a minta nem szolgáltat erős bizonyítékot a nullától különböző hatásra. (c) A p-érték 0,082 nem a „hatás valószínűsége”, és önmagában nem döntés. (d) A bevezetéshez döntési küszöb és költség-haszon elemzés kell. Helyes mondat: „A pontbecslés 8,2 százalékpont, de a 95%-os bootstrap CI −1,1 és 17,5 között mozog; a minta ezért még nem dönti el, hogy a hatás eléri-e a 2,5 százalékpontos bevezetési küszöböt.”
2. megoldás. A 3. ábra javított váza:
def segment_ci(sub, rng, B=2000):
controls = sub.loc[sub["variant"] == "control", "conversion_7d"].to_numpy()
treats = sub.loc[sub["variant"] == "treatment", "conversion_7d"].to_numpy()
out = np.empty(B)
for b in range(B):
c_idx = rng.integers(0, len(controls), len(controls))
t_idx = rng.integers(0, len(treats), len(treats))
out[b] = treats[t_idx].mean() - controls[c_idx].mean()
return out
def make_figures(df, boot, ci, out_dir, seed=20261020):
# ...
rng = np.random.default_rng(seed)
for i, seg in enumerate(["consumer", "business"]):
sub = df[df["segment"] == seg]
if len(sub) < 20:
continue
est = risk_diff(sub)
b = segment_ci(sub, rng)
lo, hi = np.percentile(b, [2.5, 97.5])
ax.scatter(est, i, color="k")
ax.hlines(i, lo, hi, color="k")A javítás lényege, hogy a bootstrap minden szegmensben is csoporton belül, ügyfélszinten húz, és a CI-t a szegmens saját karjainak méretére építi.
3. megoldás. A kolléga kódja a teljes
data keretet mintavételezi soronként. Ez három hibát okoz:
(a) a resampling egység sor, nem ügyfél; ha egy ügyfél több sorban
szerepelne, az összefüggést megtörné. (b) A karonkénti méretek
véletlenszerűvé válnak, pedig a randomizálás közel kiegyensúlyozott
karjait rögzíteni kell. (c) A random_state=42 minden
húzásnál ugyanazt a mintát adja, így a out vektor konstans,
a CI hamisan nulla szélességű lesz. Javítás: a fenti
bootstrap_risk_diff, ahol a két kar ügyfélértékeit külön
tömbökből húzzuk visszatevésesen, és a generátor minden iterációban lép
tovább.
4. megoldás. Az átlagok: control cent, treatment cent, átlagos bevételkülönbség cent, azaz 2,00 EUR. A jelentésben a paraméter mértékegysége EUR-cent; a normál SE képlet nem alkalmazható közvetlenül, mert a bevétel eloszlása erősen jobbra ferde és sok nullát tartalmaz. A bootstrap percentilis CI továbbra is használható, de a pontbecslés a teljes randomizált sokaságra vonatkozik, és a konverziós, valamint a konvertálók költési hatását keveri. A jelentés a bevételt másodlagos kimenetként közli, és nem állít primer oksági következtetést kizárólag ebből.
5. megoldás. Példa lista: (1) A kérdés megnevezi a populációt, beavatkozást, kontrollt, kimenetet és időablakot? (2) A módszer rögzíti az ITT-nevezőt és az adatkizárási szabályt? (3) A becslő és a mértékegység képlettel szerepel? (4) A resampling egység ügyfél, nem sor? (5) A CI értelmezése ismételt mintavételi lefedettség? (6) Minden ábra aggregált, azonosítók nélkül? (7) A seed, verzió és parancs a reprodukálhatósági rovatban van? (8) A korlátok megnevezik a feltáró alcsoportok státuszát? (9) A következő kísérlet előre regisztrált? (10) A jelentés nem tartalmaz titkot, nyers ügyfél-azonosítót vagy naplózott kulcsot?
A leggyakoribb hiba az, hogy a vizuális vagy retorikai magabiztosságot bizonyítéknak tekintik. Egy szép hisztogram, egy határozott cím és egy „egyértelmű növekedés” mondat nem erősíti a statisztikai következtetést. Ellenpélda: ugyanaz a 40 ügyfeles minta a 20 százalékpontos mintabeli különbséggel a hisztogramon látványos, a CI mégis , tehát a nullától való eltérés nem erős bizonyíték. A jelentés akkor korrekt, ha a pontbecslést, a bizonytalanságot és a döntési küszöböt együtt közli. Ez visszautal a Becslés, intervallum és érzékenység bootstrap feltételeire, a Hozzáférés, titkok és adatminimalizálás titokhatárokra és a Generalizáció és gyorsító választás seed-korlátra: a reprodukálható kód növeli az ellenőrizhetőséget, de nem helyettesíti a mintavételi bizonytalanság értelmezését.
Önellenőrzés. Tegyen fel három kérdést a jelentés olvasása közben: (1) Meg tudom-e ismételni az elemzést a megadott parancsból? (2) A CI tágassága tükrözi-e a minta méretét, nem a kívánságot? (3) A szegmensábrából levont állítás meg van-e jelölve feltáróként?
A kísérleti jelentés a módszer, az eredmény és a korlát egyetlen láncát írja le. A kérdés operacionalizált, a módszer rögzíti az ITT-nevezőt és az ügyfélszintű bootstrapet, az eredmény a pontbecslést és a bizonytalanságot együtt közli. A három ábra a karok arányát, a hatáseloszlást és a szegmensenkénti érzékenységet mutatja, minden azonosító nélkül. A reprodukálhatósági rovat a parancsot, seedet és kimeneti ellenőrző összeget adja, a bemutató pedig tíz percben elismételhető útvonalat kínál. A következő lépés a X. ellenőrző pont: a teljes záróprojekt önálló data scientist munkamenetként való értékelése.