Experimentell design, del 2
Detta är del 2 i min serie om Experimentell design. Vi kommer fortsätta att bygga vidare på de exempel och kodexempel som vi påbörjade under del 1, men vi börjar med att backa 100 år tillbaka i tiden. En bonde vill utvärdera två olika typer av gödsel och för detta har det avsatts två markplättar under en säsong. Månaderna går och till slut har det blivit dags att se över skörden och resultatet...
Den stora frågan som nu uppstår är: hur stor är sannolikheten för att vi skulle se detta resultat, även om det inte vore någon skillnad på gödsel A och B? För 100 år sedan hade personen som hjälpt oss att svara denna fråga varit en utbildad statistiker, idag ser situationen lite annorlunda ut: nästan vem som helst kan titta på en instrumentpanel och ge oss detta svar. Vad som mer skiljer oss ifrån vår bonde är att vi dessutom är bortskämda med on-demand; att vår tid är pengar; att vi vill kunna monitorera vår instrumentpanel i realtid och få ett svar så snart som möjligt—för att sedan avsluta testet och gå vidare med nästa.
Design är av naturen kontextuell, det finns inga fasta värden, någon generisk praxis eller kanoniskt rätt eller fel svar på hur något ska se ut eller fungera. Olika saker fungerar olika bra på olika webbplatser och det går inte alltid att veta vad i förväg. Med hjälp av ett A/B-test lär vi oss vad som fungerar på en given webbplats; det hjälper oss att underbygga våra beslut kring hur något ska se ut eller fungera. För detta behöver du inte vara en tränad statistiker, men om du däremot inte har läst mitt tidigare inlägg om Permutationstestet rekommenderar jag att du börjar där. Vi kan använda koden från Permutationstestet som ett första steg i vårt A/B-test, vilket är att iterera igenom vårt data för att organisera det i två objekt, A och B, samt beräkna vår teststatistik:
Dataset
{
a: {
count: 10,
conversions: 3,
conversionRate: 0.3
},
b: {
count: 10,
conversions: 5,
conversionRate: 0.5,
},
testStatistic: 66.66666666666667,
}
Vad vi ser ovan är såklart ett mock-data (påhittat), men en av de de stora fördelarna med ett A/B-test är att det faktiskt genomförs på riktiga användare. Till skillnad från ett Permutationstest, som är en simulering, används bara insamlat datat för att beräkna den statistiska signifikansen. Detta förutsätter dock att det finns tillräckligt med data att tillgå. Precis som vid Permutationstestet är vad vi i slutänden vill veta om vi ska använda variation A eller B av något på vår webbplats framgent, men för detta behöver vi först ta reda på svaret på två andra nyckelfrågor:
Är vårt data signifikant?
När är testet klart?
I detta inlägg kommer vi fokusera på att besvara den första av dessa frågor. Vi kommer att bygga vidare på koden från vårt Permutationstest och göra detta i JavaScript. Till en början tror jag att de flesta, eller kanske alla, som läser detta vet vad ett medelvärde är för något. Vi behöver dock denna grund i vår kod för andra beräkningar längre ned så vi kommer ändå börja med det:
Medelvärde
const data = [a.conversionRate, b.conversionRate]
const mean = arr => arr.reduce((a, b) => a + b, 0) / arr.length
console.log(mean(data)) // 0.4
Eftersom vi i detta fall bara har två urval kan vi göra det lite enklare:
const mean = (a.conversionRate + b.conversionRate) / 2
Standardavvikelse och Standardfel är två olika mått på variabilitet (osäkerhet) i ett data. Standardavvikelsen beskriver variabiliteten inom ett urval, medan standardfelet beskriver variabiliteten över flera urval av en population. Om en skolklass skriver ett nationellt prov kan vi beräkna klassens genomsnittspoäng, som vi kan säga är 220. Skolklassen är då urvalet, medan alla landets elever som skriver detta prov är populationen. Har vi en standardavvikelse på 50 poäng, innebär det att denna siffra reflekterar medelvärdet för hur mycket de enskilda elevernas resultat skiljer sig/avviker från klassens genomsnitt på 220. Standardfelet å andra sidan, berättar hur mycket skolklassens medelpoäng (220) skiljer sig från medelvärdet av alla skolklassers genomsnittspoäng runtom i landet.
I diagrammet nedan ser vi en normaldistribution, där varje kolumn representerar en standardavvikelse från mitten (skolklassens medelvärde). Ligger en elev i klassens provresultat inom de innersta kolumnerna närmast mitten (68.2%) betyder det att de befinner sig inom en standardavvikelse, över eller under medel.
När vi har räknat ut vårt medelvärde för ett urval (se ovan) kan vi med hjälp av detta också räkna ut dess standardavvikelse:
Standardavvikelse
const standardDeviation = (arr, mean) => {
const sum = arr.reduce((acc, cur) => acc + (cur - mean) * (cur - mean), 0)
return Math.sqrt(sum / (arr.length - 1))
}
const sd = standardDeviation([a.conversionRate, b.conversionRate], mean)
Vi kan också räkna ut våra urvalsgruppers respektive standardfel såhär:
Standardfelet
const aStdError = Math.sqrt((a.conversionRate * (1 - a.conversionRate)) / a.count)
const bStdError = Math.sqrt((b.conversionRate * (1 - b.conversionRate)) / b.count)
Genomför vi ett A/B-test tittar vi på två urval (stickprov) av en population. Det är inte alltid vi heller har tillgång till populationsdatat, dess medelvärde och standardavvikelse, vilket då betyder att vi bara har medel från våra urval att vägleda oss—och dessa kommer troligtvis inte att matcha perfekt med medelvärdet eller standardavvikelsen för vår population. Liksom medelvärdet för våra urvalsgrupper, A och B, har ett varsitt standardfel, har även skillnaden mellan dessa två urval ett standardfel. För att kunna uppskatta hur väl våra stickprov representerar populationen (eller kan hänföras till slumpen) beräknar vi först differensens standardfel:
Standardfel för differensen
const seDiff = Math.sqrt(Math.pow(aStdError, 2) + Math.pow(bStdError, 2))
Genom att öka urvalsstorleken kan vi minska standardfelet. Att använda ett stort slumpmässigt urval är det bästa sättet för att minimera provtagningsbias. Det står dock i relation till hur länge ett test måste genomföras. Vi kommer inte att gå in på exakt hur länge just nu, utan istället fortsätta med att beräkna vår så kallade z-score, eller Standardpoäng på svenska. Standardpoängen är antalet standardavvikelser som värdet av ett observerat värde är över eller under medelvärdet av vad vi nu mäter. Det beräknas enligt följande:
Standardpoäng
const zScore = (b.conversionRate - a.conversionRate) / seDiff
För att till sist fastställa vårt p-värde (och därmed vår statistiska signifikans) tar vi hjälp av en så kallad kumulativ fördelningsfunktion (CDF), där vi anger medelvärdet 0 och standardavvikelsen 1 tillsammans med X, som är vår z-score.
P-värde och Statistisk signifikans
const ncdf = (x, mean, sd) => {
x = (x - mean) / sd
const t = 1 / (1 + 0.2315419 * Math.abs(x))
const d = 0.3989423 * Math.exp((-x * x) / 2)
let prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))))
if (x > 0) prob = 1 - prob
return prob
}
const pValue = 1 - ncdf(zScore, 0, 1)
const statisticalSignificance = 1 - pValue;
Vår kod ger oss en statistisk signifikans på 82,45%. Vilket alltså betyder att vi behöver mer data innan vi kan börja förlita oss på det. På min GitHub-sida har jag sammanställt denna kod (då i TypeScript) och även lagt till en liten bonus.