-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
113 lines (98 loc) · 3.36 KB
/
index.js
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
const BigNumber = require('bignumber.js');
const simpleStatistics = require('simple-statistics');
module.exports = {
calcResult
};
function calcResult(test, confidence) {
validateInput(test, confidence);
confidence = confidence || 0.95;
const A_visits = new BigNumber(test.controlVisits);
const A_conversions = new BigNumber(test.controlConversions);
const B_visits = new BigNumber(test.challengerVisits);
const B_conversions = new BigNumber(test.challengerConversions);
const A_rate = calcConversionRate(A_visits, A_conversions);
const B_rate = calcConversionRate(B_visits, B_conversions);
var B_improvement;
if (A_rate.isZero() || B_rate.isZero()) {
B_improvement = new BigNumber(0);
} else {
B_improvement = B_rate.sub(A_rate).div(A_rate);
}
const A_stdErr = calcStandardError(A_visits, A_conversions);
const B_stdErr = calcStandardError(B_visits, B_conversions);
const zScore = calcZScore(A_rate, B_rate, A_stdErr, B_stdErr);
const pValue = calcPValue(A_rate, B_rate, zScore);
const isSignificant = determineSignificance(zScore, pValue, confidence);
return {
controlConversionRate: A_rate.toNumber(),
challengerConversionRate: B_rate.toNumber(),
challengerImprovement: B_improvement.toNumber(),
isSignificant,
statistics: {
controlStandardError: A_stdErr.toNumber(),
challengerStandardError: B_stdErr.toNumber(),
zScore: zScore.toNumber(),
pValue
}
};
}
function calcConversionRate(visits, conversions) {
if (conversions.isZero()) {
return new BigNumber(0);
}
return conversions.div(visits);
}
function calcStandardError(visits, conversions) {
if (visits.isZero()) {
return new BigNumber(0);
}
const rate = calcConversionRate(visits, conversions);
const dividend = rate.mul( new BigNumber(1).sub(rate) );
return dividend.div(visits).sqrt();
}
function calcZScore(A_rate, B_rate, A_stdErr, B_stdErr) {
const rateDiff = B_rate.sub(A_rate);
const stdErrOfDiff = A_stdErr.pow(2).add(B_stdErr.pow(2)).sqrt();
if (stdErrOfDiff.isZero()) {
return new BigNumber(0);
}
return (rateDiff).div(stdErrOfDiff);
}
function calcPValue(A_rate, B_rate, zScore) {
const zScoreAbs = zScore.abs().toNumber();
const csnp = simpleStatistics.cumulativeStdNormalProbability(zScoreAbs);
if (A_rate.gt(B_rate)) {
return csnp;
}
return 1 - csnp;
}
function determineSignificance(zScore, pValue, confidence) {
if (zScore.gte(new BigNumber(0))) {
return pValue < 1 - confidence;
}
//else use two-sided hypothesis
return pValue > confidence + ( (1 - confidence) / 2 );
}
function validateInput(test, confidence) {
validateInputNumber(test, 'controlVisits');
validateInputNumber(test, 'controlConversions');
validateInputNumber(test, 'challengerVisits');
validateInputNumber(test, 'challengerConversions');
if (test.controlVisits < test.controlConversions) {
throw new Error('controlVisits must be greater than or equal to controlConversions');
}
if (test.challengerVisits < test.challengerConversions) {
throw new Error('challengerVisits must be greater than or equal to challengerConversions');
}
if (confidence && (confidence < 0 || confidence > 1 )) {
throw new Error('confidence must be a number between 0 and 1');
}
}
function validateInputNumber(test, propName) {
if (!Number.isInteger(test[propName])) {
throw new TypeError(propName + ' must be a number');
}
if (test[propName] < 0) {
throw new Error(propName + ' cannot be negative');
}
}