-
Notifications
You must be signed in to change notification settings - Fork 3
/
TwoProportionZTest.cs
154 lines (130 loc) · 5.75 KB
/
TwoProportionZTest.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
using System;
using System.Linq;
using System.Text;
namespace ABTesting
{
public sealed class TwoProportionZTest : AStatisticalTestBase, IStatisticalTest
{
private static readonly double[,] ZScores = { { 0.10, 1.29 }, { 0.05, 1.65 }, { 0.01, 2.33 }, { 0.001, 3.08 }, { 0.0000, 3.90 } };
#region IStatisticalTest Members
public string GetResultDescription(Experiment test)
{
double p;
try
{
p = GetPValue(test);
}
catch (Exception e)
{
return e.Message;
}
StringBuilder builder = new StringBuilder();
//CF: This is wrong. The assumption for a two-proportion z-test is that each sample have at least 10 succeses AND 10 failures!
//if (Alternatives[0].Participants < 10 || Alternatives[1].Participants < 10)
if (test.Alternatives.Count(x => !x.SampleMeetsTestAssumtions) > 0)
{
builder.Append("Take these results with a grain of salt since your samples do not meet the required assumptions: ");
}
ABAlternative best = test.GetBestAlternative();
ABAlternative worst = test.GetWorstAlternative();
builder.Append(String.Format(@"
The best alternative you have is: [{0}], which had
{1} conversions from {2} participants
({3}). The other alternative was [{4}],
which had {5} conversions from {6} participants
({7}). "
, best.Content
, best.Conversions
, best.Participants
, best.PrettyConversionRate
, worst.Content
, worst.Conversions
, worst.Participants
, worst.PrettyConversionRate
));
if (p == 1)
{
builder.Append("However, this difference is not statistically significant.");
}
else
{
builder.Append(String.Format(@"
This difference is <b>{0} likely to be statistically significant (p <= {2})</b>, which means you can be
{1} that it is the result of your alternatives actually mattering, rather than
being due to random chance. However, this statistical test can't measure how likely the currently
observed magnitude of the difference is to be accurate or not. It only says ""better"", not ""better
by so much"". ",
//Percentages[p],
ToPercentageString(p),
Descriptions[p],
p
));
}
return builder.ToString();
}
public double GetPValue(Experiment test)
{
double z = GetZScore(test);
z = Math.Abs(z);
if (!double.IsPositiveInfinity(z))
{
//CF: BUG: any z above 1.29 returns p = 0.1. Eg: z = 10.4 should be p = 0.0 (highly significant) but instead returns 0.1!
//for (int a=0; a<ZScores.Length/2; a++)
int arrayLen = ZScores.GetLength(0) - 1;
for (int a = arrayLen; a >= 0; a--)
{
if (z >= ZScores[a, 1])
{
return ZScores[a, 0];
}
}
}
return 1;
}
public bool IsStatisticallySignificant(Experiment test)
{
return IsStatisticallySignificant(test, 0.05);
}
public bool IsStatisticallySignificant(Experiment test, double pValue)
{
return GetPValue(test) <= pValue;
}
#endregion
private double GetZScore(Experiment test)
{
if (test.Alternatives.Count != 2)
{
//throw new Exception("Sorry, can't currently automatically calculate statistics for A/B tests with > 2 alternatives.");
return double.PositiveInfinity;
}
//if (test.Alternatives[0].Participants == 0 || test.Alternatives[1].Participants == 0)
if (!test.AllAlternativesHaveParticipants)
{
//throw new Exception("Can't calculate the z score if either of the alternatives lacks participants.");
return double.PositiveInfinity;
}
/* CF: These variable names are not great. What's happening here is we are performing a Two-Proportion Z-test with a pooled difference of the standard errors
* of the two samples. For details, see page 566, "Intro Stats", De Veax, Velleman, and Bock */
double cr1 = test.Alternatives[0].ConversionRate;
double cr2 = test.Alternatives[1].ConversionRate;
double successes1 = test.Alternatives[0].Successes;
double successes2 = test.Alternatives[1].Successes;
int n1 = test.Alternatives[0].Participants;
int n2 = test.Alternatives[1].Participants;
int n = n1 + n2;
double pHatPooled = (successes1 + successes2) / n;
double frac1 = pHatPooled * (1 - pHatPooled) / n1;
double frac2 = pHatPooled * (1 - pHatPooled) / n2;
double SE = Math.Sqrt(frac1 + frac2);
//z-score:
return (cr1 - cr2) / SE;
}
public string[] AssumptionsToCheck
{
get
{
return new string[] { @"Randomization condition", @"10% condition", @"Independent groups assumption", @"Success/Failure condition (checked automatically)" };
}
}
}
}