field_test 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70d3fdf1a39390631baa12589f108d99852f02009daaaf9ca0d50756630bb3c9
4
- data.tar.gz: ba0700b9b9eaed0610d7343e7dff096e6e0e3572919832e8deae189fe5833df8
3
+ metadata.gz: 613be253211f115808fccbc17aba8eae3a6bf2a713eaee82d3c0a8d27ad02330
4
+ data.tar.gz: 11f3a88aa737f8c69dde0760863a6ec2a25995172a478f32afeb262276dcdbe8
5
5
  SHA512:
6
- metadata.gz: 4f6c1c6c82b05b4373ef899c1c53ee4d788eb5d9eabc90d827db7e04ee66ec8bc398047f2d080be14af031fd461632078cfa88dcbe9a776ef64d0647f7854cbe
7
- data.tar.gz: 768f3018bbb87d22f4d39b41b09ec02914f2a7c4de679236675a2280fbee7bf90c64f09e4645f8966dbc79204ed267a2dcf03df6cfecc432b3103e15eeb5a4cd
6
+ metadata.gz: a9daeb6b4dcff1b8deb97c94e3cab45c046137bf3c6665f1ba2833bcf43fd4f772b5f0c2f91e471824a394f3261baa9c712160f6fb521e3878716161f054fbb7
7
+ data.tar.gz: c3c77d85c8eb6eef81a7b8d545e51c086635b6ed509b354e7f8473d1b6064fadca790411668c783dd6044d45b8ab55d17ceeddaaf5bee4dabf9b0e3f810eb00e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 1.0.0 (2026-04-01)
2
+
3
+ - Improved installation time
4
+ - Dropped support for Ruby < 3.3 and Rails < 7.2
5
+
1
6
  ## 0.8.0 (2025-05-05)
2
7
 
3
8
  - Dropped support for Ruby < 3.2 and Rails < 7.1
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2016-2024 Andrew Kane
1
+ Copyright (c) 2016-2026 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -189,8 +189,8 @@ Keep track of when experiments started and ended. Use any format `Time.parse` ac
189
189
  ```yml
190
190
  experiments:
191
191
  button_color:
192
- started_at: Dec 1, 2024 8 am PST
193
- ended_at: Dec 8, 2024 2 pm PST
192
+ started_at: Dec 1, 2025 8 am PST
193
+ ended_at: Dec 8, 2025 2 pm PST
194
194
  ```
195
195
 
196
196
  Add a friendlier name and description with:
@@ -1,9 +1,9 @@
1
1
  module FieldTest
2
2
  class BaseController < ActionController::Base
3
- layout "field_test/application"
3
+ http_basic_authenticate_with name: ENV["FIELD_TEST_USERNAME"], password: ENV["FIELD_TEST_PASSWORD"] if ENV["FIELD_TEST_PASSWORD"]
4
4
 
5
5
  protect_from_forgery with: :exception
6
6
 
7
- http_basic_authenticate_with name: ENV["FIELD_TEST_USERNAME"], password: ENV["FIELD_TEST_PASSWORD"] if ENV["FIELD_TEST_PASSWORD"]
7
+ layout "field_test/application"
8
8
  end
9
9
  end
@@ -0,0 +1,381 @@
1
+ /*
2
+ * BayesTest C v0.1.1
3
+ * https://github.com/ankane/bayestest-c
4
+ * MIT License
5
+ */
6
+
7
+ #pragma once
8
+
9
+ #include <limits.h>
10
+ #include <math.h>
11
+ #include <stdlib.h>
12
+
13
+ /// @private
14
+ static inline double bayestest_logbeta(double a, double b) {
15
+ // TODO use lgamma_r when available
16
+ return lgamma(a) + lgamma(b) - lgamma(a + b);
17
+ }
18
+
19
+ /// @private
20
+ static inline double bayestest_prob_b_beats_a(int alpha_a, int beta_a, int alpha_b, int beta_b) {
21
+ double total = 0.0;
22
+ double logbeta_aa_ba = bayestest_logbeta(alpha_a, beta_a);
23
+ double beta_ba = beta_b + beta_a;
24
+
25
+ for (int i = 0; i < alpha_b; i++) {
26
+ total += exp(
27
+ bayestest_logbeta(alpha_a + i, beta_ba) - log(beta_b + i)
28
+ - bayestest_logbeta(1 + i, beta_b) - logbeta_aa_ba
29
+ );
30
+ }
31
+
32
+ return total;
33
+ }
34
+
35
+ /// @private
36
+ static inline double bayestest_prob_c_beats_ab(
37
+ int alpha_a,
38
+ int beta_a,
39
+ int alpha_b,
40
+ int beta_b,
41
+ int alpha_c,
42
+ int beta_c
43
+ ) {
44
+ double* log_bb_j_logbeta_j_bb = malloc(sizeof(double) * (unsigned int) alpha_b);
45
+ double* logbeta_ac_i_j = malloc(sizeof(double) * (unsigned int) (alpha_a + alpha_b));
46
+
47
+ if (log_bb_j_logbeta_j_bb == NULL || logbeta_ac_i_j == NULL) {
48
+ free(log_bb_j_logbeta_j_bb);
49
+ free(logbeta_ac_i_j);
50
+ return NAN;
51
+ }
52
+
53
+ double total = 0.0;
54
+ double logbeta_ac_bc = bayestest_logbeta(alpha_c, beta_c);
55
+
56
+ for (int j = 0; j < alpha_b; j++) {
57
+ log_bb_j_logbeta_j_bb[j] = log(beta_b + j) + bayestest_logbeta(1 + j, beta_b);
58
+ }
59
+
60
+ double abc = beta_a + beta_b + beta_c;
61
+ for (int i = 0; i < alpha_a + alpha_b; i++) {
62
+ logbeta_ac_i_j[i] = bayestest_logbeta(alpha_c + i, abc);
63
+ }
64
+
65
+ for (int i = 0; i < alpha_a; i++) {
66
+ double sum_i = -log(beta_a + i) - bayestest_logbeta(1 + i, beta_a) - logbeta_ac_bc;
67
+
68
+ for (int j = 0; j < alpha_b; j++) {
69
+ total += exp(sum_i + logbeta_ac_i_j[i + j] - log_bb_j_logbeta_j_bb[j]);
70
+ }
71
+ }
72
+
73
+ free(log_bb_j_logbeta_j_bb);
74
+ free(logbeta_ac_i_j);
75
+
76
+ return 1 - bayestest_prob_b_beats_a(alpha_c, beta_c, alpha_a, beta_a)
77
+ - bayestest_prob_b_beats_a(alpha_c, beta_c, alpha_b, beta_b) + total;
78
+ }
79
+
80
+ /// @private
81
+ static inline double bayestest_prob_d_beats_abc(
82
+ int alpha_a,
83
+ int beta_a,
84
+ int alpha_b,
85
+ int beta_b,
86
+ int alpha_c,
87
+ int beta_c,
88
+ int alpha_d,
89
+ int beta_d
90
+ ) {
91
+ double* log_bb_j_logbeta_j_bb = malloc(sizeof(double) * (unsigned int) alpha_b);
92
+ double* log_bc_k_logbeta_k_bc = malloc(sizeof(double) * (unsigned int) alpha_c);
93
+ double* logbeta_bd_i_j_k = malloc(
94
+ sizeof(double) * (unsigned int) (alpha_a + alpha_b + alpha_c)
95
+ );
96
+
97
+ if (log_bb_j_logbeta_j_bb == NULL || log_bc_k_logbeta_k_bc == NULL
98
+ || logbeta_bd_i_j_k == NULL) {
99
+ free(log_bb_j_logbeta_j_bb);
100
+ free(log_bc_k_logbeta_k_bc);
101
+ free(logbeta_bd_i_j_k);
102
+ return NAN;
103
+ }
104
+
105
+ double total = 0.0;
106
+ double logbeta_ad_bd = bayestest_logbeta(alpha_d, beta_d);
107
+
108
+ for (int j = 0; j < alpha_b; j++) {
109
+ log_bb_j_logbeta_j_bb[j] = log(beta_b + j) + bayestest_logbeta(1 + j, beta_b);
110
+ }
111
+
112
+ for (int k = 0; k < alpha_c; k++) {
113
+ log_bc_k_logbeta_k_bc[k] = log(beta_c + k) + bayestest_logbeta(1 + k, beta_c);
114
+ }
115
+
116
+ double abcd = beta_a + beta_b + beta_c + beta_d;
117
+ for (int i = 0; i < alpha_a + alpha_b + alpha_c; i++) {
118
+ logbeta_bd_i_j_k[i] = bayestest_logbeta(alpha_d + i, abcd);
119
+ }
120
+
121
+ for (int i = 0; i < alpha_a; i++) {
122
+ double sum_i = -log(beta_a + i) - bayestest_logbeta(1 + i, beta_a) - logbeta_ad_bd;
123
+
124
+ for (int j = 0; j < alpha_b; j++) {
125
+ double sum_j = sum_i - log_bb_j_logbeta_j_bb[j];
126
+
127
+ for (int k = 0; k < alpha_c; k++) {
128
+ total += exp(sum_j + logbeta_bd_i_j_k[i + j + k] - log_bc_k_logbeta_k_bc[k]);
129
+ }
130
+ }
131
+ }
132
+
133
+ free(log_bb_j_logbeta_j_bb);
134
+ free(log_bc_k_logbeta_k_bc);
135
+ free(logbeta_bd_i_j_k);
136
+
137
+ return 1 - bayestest_prob_b_beats_a(alpha_a, beta_a, alpha_d, beta_d)
138
+ - bayestest_prob_b_beats_a(alpha_b, beta_b, alpha_d, beta_d)
139
+ - bayestest_prob_b_beats_a(alpha_c, beta_c, alpha_d, beta_d)
140
+ + bayestest_prob_c_beats_ab(alpha_a, beta_a, alpha_b, beta_b, alpha_d, beta_d)
141
+ + bayestest_prob_c_beats_ab(alpha_a, beta_a, alpha_c, beta_c, alpha_d, beta_d)
142
+ + bayestest_prob_c_beats_ab(alpha_b, beta_b, alpha_c, beta_c, alpha_d, beta_d) - total;
143
+ }
144
+
145
+ /// Returns the winning probability of each variant for binary outcomes.
146
+ static inline int bayestest_binary(
147
+ int variants,
148
+ const int* participants,
149
+ const int* conversions,
150
+ double* probabilities
151
+ ) {
152
+ if (variants < 0 || variants > 4) {
153
+ return -1;
154
+ }
155
+
156
+ for (int i = 0; i < variants; i++) {
157
+ if (participants[i] < 0) {
158
+ return -1;
159
+ }
160
+
161
+ if (participants[i] > INT_MAX / (int) sizeof(double) / 4) {
162
+ return -1;
163
+ }
164
+
165
+ if (conversions[i] < 0) {
166
+ return -1;
167
+ }
168
+
169
+ if (conversions[i] > INT_MAX / (int) sizeof(double) / 4) {
170
+ return -1;
171
+ }
172
+
173
+ if (conversions[i] > participants[i]) {
174
+ return -1;
175
+ }
176
+ }
177
+
178
+ switch (variants) {
179
+ case 0: {
180
+ break;
181
+ }
182
+ case 1: {
183
+ probabilities[0] = 1;
184
+ break;
185
+ }
186
+ case 2: {
187
+ int a = 1;
188
+ int b = 0;
189
+ double prob = bayestest_prob_b_beats_a(
190
+ 1 + conversions[a],
191
+ 1 + participants[a] - conversions[a],
192
+ 1 + conversions[b],
193
+ 1 + participants[b] - conversions[b]
194
+ );
195
+
196
+ if (isnan(prob)) {
197
+ return -1;
198
+ }
199
+ probabilities[0] = prob;
200
+ probabilities[1] = 1 - prob;
201
+ break;
202
+ }
203
+ case 3: {
204
+ double total = 0.0;
205
+ for (int i = 0; i < 2; i++) {
206
+ int c = i;
207
+ int b = (i + 1) % 3;
208
+ int a = (i + 2) % 3;
209
+
210
+ double prob = bayestest_prob_c_beats_ab(
211
+ 1 + conversions[a],
212
+ 1 + participants[a] - conversions[a],
213
+ 1 + conversions[b],
214
+ 1 + participants[b] - conversions[b],
215
+ 1 + conversions[c],
216
+ 1 + participants[c] - conversions[c]
217
+ );
218
+
219
+ if (isnan(prob)) {
220
+ return -1;
221
+ }
222
+ probabilities[i] = prob;
223
+ total += prob;
224
+ }
225
+ probabilities[2] = 1 - total;
226
+ break;
227
+ }
228
+ case 4: {
229
+ double total = 0.0;
230
+ for (int i = 0; i < 3; i++) {
231
+ int d = i;
232
+ int c = (i + 1) % 4;
233
+ int b = (i + 2) % 4;
234
+ int a = (i + 3) % 4;
235
+
236
+ double prob = bayestest_prob_d_beats_abc(
237
+ 1 + conversions[a],
238
+ 1 + participants[a] - conversions[a],
239
+ 1 + conversions[b],
240
+ 1 + participants[b] - conversions[b],
241
+ 1 + conversions[c],
242
+ 1 + participants[c] - conversions[c],
243
+ 1 + conversions[d],
244
+ 1 + participants[d] - conversions[d]
245
+ );
246
+
247
+ if (isnan(prob)) {
248
+ return -1;
249
+ }
250
+ probabilities[i] = prob;
251
+ total += prob;
252
+ }
253
+ probabilities[3] = 1 - total;
254
+ break;
255
+ }
256
+ default: {
257
+ return -1;
258
+ }
259
+ }
260
+
261
+ return 0;
262
+ }
263
+
264
+ /// @private
265
+ static inline double bayestest_prob_1_beats_2(int alpha_1, int beta_1, int alpha_2, int beta_2) {
266
+ double total = 0.0;
267
+ double log_b1 = log(beta_1);
268
+ double a2_log_b2 = alpha_2 * log(beta_2);
269
+ double log_b1_b2 = log(beta_1 + beta_2);
270
+
271
+ for (int k = 0; k < alpha_1; k++) {
272
+ total += exp(
273
+ k * log_b1 + a2_log_b2 - (k + alpha_2) * log_b1_b2 - log(k + alpha_2)
274
+ - bayestest_logbeta(k + 1, alpha_2)
275
+ );
276
+ }
277
+
278
+ return total;
279
+ }
280
+
281
+ /// @private
282
+ static inline double bayestest_prob_1_beats_23(
283
+ int alpha_1,
284
+ int beta_1,
285
+ int alpha_2,
286
+ int beta_2,
287
+ int alpha_3,
288
+ int beta_3
289
+ ) {
290
+ double total = 0.0;
291
+ double log_b1_b2_b3 = log(beta_1 + beta_2 + beta_3);
292
+ double a1_log_b1 = alpha_1 * log(beta_1);
293
+ double log_b2 = log(beta_2);
294
+ double log_b3 = log(beta_3);
295
+ double loggamma_a1 = lgamma(alpha_1);
296
+
297
+ for (int k = 0; k < alpha_2; k++) {
298
+ double sum_k = a1_log_b1 + k * log_b2 - lgamma(k + 1);
299
+
300
+ for (int l = 0; l < alpha_3; l++) {
301
+ total += exp(
302
+ sum_k + l * log_b3 - (k + l + alpha_1) * log_b1_b2_b3 + lgamma(k + l + alpha_1)
303
+ - lgamma(l + 1) - loggamma_a1
304
+ );
305
+ }
306
+ }
307
+
308
+ return 1 - bayestest_prob_1_beats_2(alpha_2, beta_2, alpha_1, beta_1)
309
+ - bayestest_prob_1_beats_2(alpha_3, beta_3, alpha_1, beta_1) + total;
310
+ }
311
+
312
+ /// Returns the winning probability of each variant for count data.
313
+ static inline int bayestest_count(
314
+ int variants,
315
+ const int* events,
316
+ const int* exposure,
317
+ double* probabilities
318
+ ) {
319
+ if (variants < 0 || variants > 3) {
320
+ return -1;
321
+ }
322
+
323
+ for (int i = 0; i < variants; i++) {
324
+ if (events[i] < 0) {
325
+ return -1;
326
+ }
327
+
328
+ if (events[i] > INT_MAX / 4) {
329
+ return -1;
330
+ }
331
+
332
+ if (exposure[i] < 0) {
333
+ return -1;
334
+ }
335
+
336
+ if (exposure[i] > INT_MAX / 4) {
337
+ return -1;
338
+ }
339
+ }
340
+
341
+ switch (variants) {
342
+ case 0: {
343
+ break;
344
+ }
345
+ case 1: {
346
+ probabilities[0] = 1;
347
+ break;
348
+ }
349
+ case 2: {
350
+ int a = 0;
351
+ int b = 1;
352
+ double prob = bayestest_prob_1_beats_2(events[a], exposure[a], events[b], exposure[b]);
353
+
354
+ probabilities[0] = prob;
355
+ probabilities[1] = 1 - prob;
356
+ break;
357
+ }
358
+ case 3: {
359
+ double total = 0.0;
360
+ for (int i = 0; i < 2; i++) {
361
+ int a = i;
362
+ int b = (i + 1) % 3;
363
+ int c = (i + 2) % 3;
364
+
365
+ double prob = bayestest_prob_1_beats_23(
366
+ events[a], exposure[a], events[b], exposure[b], events[c], exposure[c]
367
+ );
368
+
369
+ probabilities[i] = prob;
370
+ total += prob;
371
+ }
372
+ probabilities[2] = 1 - total;
373
+ break;
374
+ }
375
+ default: {
376
+ return -1;
377
+ }
378
+ }
379
+
380
+ return 0;
381
+ }
@@ -0,0 +1,42 @@
1
+ #include <ruby.h>
2
+
3
+ #include "bayestest.h"
4
+
5
+ static VALUE probabilities(VALUE self, VALUE results)
6
+ {
7
+ Check_Type(results, T_ARRAY);
8
+
9
+ long count = RARRAY_LEN(results);
10
+ if (count > 4) {
11
+ rb_raise(rb_eArgError, "too many variants");
12
+ }
13
+
14
+ int participants[4];
15
+ int conversions[4];
16
+ double probabilities[4];
17
+
18
+ VALUE *results_ptr = RARRAY_PTR(results);
19
+ for (long i = 0; i < count; i++) {
20
+ VALUE v = results_ptr[i];
21
+ participants[i] = NUM2INT(rb_hash_aref(v, ID2SYM(rb_intern("participated"))));
22
+ conversions[i] = NUM2INT(rb_hash_aref(v, ID2SYM(rb_intern("converted"))));
23
+ }
24
+
25
+ int status = bayestest_binary((int) count, participants, conversions, probabilities);
26
+ if (status != 0) {
27
+ rb_raise(rb_eRuntimeError, "bad status");
28
+ }
29
+
30
+ VALUE rb_probabilities = rb_ary_new_capa(count);
31
+ for (long i = 0; i < count; i++) {
32
+ rb_ary_push(rb_probabilities, DBL2NUM(probabilities[i]));
33
+ }
34
+ return rb_probabilities;
35
+ }
36
+
37
+ void Init_ext(void)
38
+ {
39
+ VALUE rb_mFieldTest = rb_define_module("FieldTest");
40
+ VALUE rb_mBinaryTest = rb_define_module_under(rb_mFieldTest, "BinaryTest");
41
+ rb_define_singleton_method(rb_mBinaryTest, "probabilities", probabilities, 1);
42
+ }
@@ -1,5 +1,7 @@
1
- require "mkmf-rice"
1
+ require "mkmf"
2
2
 
3
- $CXXFLAGS << " -std=c++17 $(optflags)"
3
+ $CFLAGS << " $(optflags)"
4
+
5
+ $CFLAGS += " -Wall -Wextra -Wconversion"
4
6
 
5
7
  create_makefile("field_test/ext")
@@ -11,7 +11,7 @@ module FieldTest
11
11
  end
12
12
  end
13
13
 
14
- def field_test_upgrade_memberships(options = {})
14
+ def field_test_upgrade_memberships(**options)
15
15
  participants = FieldTest::Participant.standardize(options[:participant] || field_test_participant)
16
16
  preferred = participants.first
17
17
  Array(participants[1..-1]).each do |participant|
@@ -9,9 +9,7 @@ module FieldTest
9
9
  @description = attributes[:description]
10
10
  @variants = attributes[:variants]
11
11
  if @variants.any? { |v| !v.is_a?(String) }
12
- # TODO add support for more types (including query parameters)
13
- # or raise error in 0.6
14
- warn "[field_test] Only string variants are supported (#{id})"
12
+ raise Error, "Only string variants are supported (#{id})"
15
13
  end
16
14
  @weights = @variants.size.times.map { |i| attributes[:weights].to_a[i] || 1 }
17
15
  @winner = attributes[:winner]
@@ -24,7 +22,7 @@ module FieldTest
24
22
  @use_events = attributes[:use_events]
25
23
  end
26
24
 
27
- def variant(participants, options = {})
25
+ def variant(participants, **options)
28
26
  return winner if winner && !keep_variant?
29
27
  return control if options[:exclude]
30
28
 
@@ -149,11 +147,7 @@ module FieldTest
149
147
  if variants.size <= 3
150
148
  probabilities =
151
149
  cache_fetch(["field_test", "probabilities"] + results.flat_map { |_, v| [v[:participated], v[:converted]] }) do
152
- binary_test = BinaryTest.new
153
- results.each do |_, v|
154
- binary_test.add(v[:participated], v[:converted])
155
- end
156
- binary_test.probabilities.to_a
150
+ BinaryTest.probabilities(results.values)
157
151
  end
158
152
 
159
153
  results.each_key.zip(probabilities) do |variant, prob_winning|
@@ -27,10 +27,9 @@ module FieldTest
27
27
  params_variant
28
28
  else
29
29
  # cache results for request
30
- # TODO possibly remove in 0.4.0
31
30
  cache_key = [exp.id, participants.map(&:where_values), options.slice(:variant, :exclude)]
32
31
  @field_test_cache ||= {}
33
- @field_test_cache[cache_key] ||= exp.variant(participants, options)
32
+ @field_test_cache[cache_key] ||= exp.variant(participants, **options)
34
33
  end
35
34
  end
36
35
 
@@ -1,3 +1,3 @@
1
1
  module FieldTest
2
- VERSION = "0.8.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: field_test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.1'
18
+ version: '7.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '7.1'
25
+ version: '7.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activerecord
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '7.1'
32
+ version: '7.2'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '7.1'
39
+ version: '7.2'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: browser
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '2'
54
- - !ruby/object:Gem::Dependency
55
- name: rice
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: 4.3.3
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: 4.3.3
68
54
  email: andrew@ankane.org
69
55
  executables: []
70
56
  extensions:
@@ -87,8 +73,8 @@ files:
87
73
  - app/views/field_test/participants/show.html.erb
88
74
  - app/views/layouts/field_test/application.html.erb
89
75
  - config/routes.rb
90
- - ext/field_test/bayestest.hpp
91
- - ext/field_test/ext.cpp
76
+ - ext/field_test/bayestest.h
77
+ - ext/field_test/ext.c
92
78
  - ext/field_test/extconf.rb
93
79
  - lib/field_test.rb
94
80
  - lib/field_test/controller.rb
@@ -114,14 +100,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
100
  requirements:
115
101
  - - ">="
116
102
  - !ruby/object:Gem::Version
117
- version: '3.2'
103
+ version: '3.3'
118
104
  required_rubygems_version: !ruby/object:Gem::Requirement
119
105
  requirements:
120
106
  - - ">="
121
107
  - !ruby/object:Gem::Version
122
108
  version: '0'
123
109
  requirements: []
124
- rubygems_version: 3.6.7
110
+ rubygems_version: 4.0.6
125
111
  specification_version: 4
126
112
  summary: A/B testing for Rails
127
113
  test_files: []
@@ -1,329 +0,0 @@
1
- /*!
2
- * BayesTest C++ v0.1.2
3
- * https://github.com/ankane/bayestest-cpp
4
- * MIT License
5
- */
6
-
7
- #pragma once
8
-
9
- #include <cmath>
10
- #include <vector>
11
-
12
- namespace bayestest {
13
-
14
- namespace {
15
-
16
- double logbeta(double a, double b) {
17
- return std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b);
18
- }
19
-
20
- double prob_b_beats_a(int alpha_a, int beta_a, int alpha_b, int beta_b) {
21
- double total = 0.0;
22
- double logbeta_aa_ba = logbeta(alpha_a, beta_a);
23
- double beta_ba = beta_b + beta_a;
24
-
25
- for (auto i = 0; i < alpha_b; i++) {
26
- total += std::exp(logbeta(alpha_a + i, beta_ba) - std::log(beta_b + i) - logbeta(1 + i, beta_b) - logbeta_aa_ba);
27
- }
28
-
29
- return total;
30
- }
31
-
32
- double prob_c_beats_ab(int alpha_a, int beta_a, int alpha_b, int beta_b, int alpha_c, int beta_c) {
33
- double total = 0.0;
34
-
35
- double logbeta_ac_bc = logbeta(alpha_c, beta_c);
36
-
37
- std::vector<double> log_bb_j_logbeta_j_bb;
38
- log_bb_j_logbeta_j_bb.reserve(alpha_b);
39
-
40
- for (auto j = 0; j < alpha_b; j++) {
41
- log_bb_j_logbeta_j_bb.push_back(std::log(beta_b + j) + logbeta(1 + j, beta_b));
42
- }
43
-
44
- double abc = beta_a + beta_b + beta_c;
45
- std::vector<double> logbeta_ac_i_j;
46
- logbeta_ac_i_j.reserve(alpha_a + alpha_b);
47
-
48
- for (auto i = 0; i < alpha_a + alpha_b; i++) {
49
- logbeta_ac_i_j.push_back(logbeta(alpha_c + i, abc));
50
- }
51
-
52
- for (auto i = 0; i < alpha_a; i++) {
53
- double sum_i = -std::log(beta_a + i) - logbeta(1 + i, beta_a) - logbeta_ac_bc;
54
-
55
- for (auto j = 0; j < alpha_b; j++) {
56
- total += std::exp(sum_i + logbeta_ac_i_j[i + j] - log_bb_j_logbeta_j_bb[j]);
57
- }
58
- }
59
-
60
- return 1
61
- - prob_b_beats_a(alpha_c, beta_c, alpha_a, beta_a)
62
- - prob_b_beats_a(alpha_c, beta_c, alpha_b, beta_b)
63
- + total;
64
- }
65
-
66
- double prob_d_beats_abc(int alpha_a, int beta_a, int alpha_b, int beta_b, int alpha_c, int beta_c, int alpha_d, int beta_d) {
67
- double total = 0.0;
68
-
69
- double logbeta_ad_bd = logbeta(alpha_d, beta_d);
70
-
71
- std::vector<double> log_bb_j_logbeta_j_bb;
72
- log_bb_j_logbeta_j_bb.reserve(alpha_b);
73
-
74
- for (auto j = 0; j < alpha_b; j++) {
75
- log_bb_j_logbeta_j_bb.push_back(std::log(beta_b + j) + logbeta(1 + j, beta_b));
76
- }
77
-
78
- std::vector<double> log_bc_k_logbeta_k_bc;
79
- log_bc_k_logbeta_k_bc.reserve(alpha_c);
80
-
81
- for (auto k = 0; k < alpha_c; k++) {
82
- log_bc_k_logbeta_k_bc.push_back(std::log(beta_c + k) + logbeta(1 + k, beta_c));
83
- }
84
-
85
- double abcd = beta_a + beta_b + beta_c + beta_d;
86
- std::vector<double> logbeta_bd_i_j_k;
87
- logbeta_bd_i_j_k.reserve(alpha_a + alpha_b + alpha_c);
88
-
89
- for (auto i = 0; i < alpha_a + alpha_b + alpha_c; i++) {
90
- logbeta_bd_i_j_k.push_back(logbeta(alpha_d + i, abcd));
91
- }
92
-
93
- for (auto i = 0; i < alpha_a; i++) {
94
- double sum_i = -std::log(beta_a + i) - logbeta(1 + i, beta_a) - logbeta_ad_bd;
95
-
96
- for (auto j = 0; j < alpha_b; j++) {
97
- double sum_j = sum_i - log_bb_j_logbeta_j_bb[j];
98
-
99
- for (auto k = 0; k < alpha_c; k++) {
100
- total += std::exp(sum_j + logbeta_bd_i_j_k[i + j + k] - log_bc_k_logbeta_k_bc[k]);
101
- }
102
- }
103
- }
104
-
105
- return 1
106
- - prob_b_beats_a(alpha_a, beta_a, alpha_d, beta_d)
107
- - prob_b_beats_a(alpha_b, beta_b, alpha_d, beta_d)
108
- - prob_b_beats_a(alpha_c, beta_c, alpha_d, beta_d)
109
- + prob_c_beats_ab(alpha_a, beta_a, alpha_b, beta_b, alpha_d, beta_d)
110
- + prob_c_beats_ab(alpha_a, beta_a, alpha_c, beta_c, alpha_d, beta_d)
111
- + prob_c_beats_ab(alpha_b, beta_b, alpha_c, beta_c, alpha_d, beta_d)
112
- - total;
113
- }
114
-
115
- double prob_1_beats_2(int alpha_1, int beta_1, int alpha_2, int beta_2) {
116
- double total = 0.0;
117
- double log_b1 = std::log(beta_1);
118
- double a2_log_b2 = alpha_2 * std::log(beta_2);
119
- double log_b1_b2 = std::log(beta_1 + beta_2);
120
-
121
- for (auto k = 0; k < alpha_1; k++) {
122
- total += std::exp(k * log_b1 +
123
- a2_log_b2 -
124
- (k + alpha_2) * log_b1_b2 -
125
- std::log(k + alpha_2) -
126
- logbeta(k + 1, alpha_2));
127
- }
128
-
129
- return total;
130
- }
131
-
132
- double prob_1_beats_23(int alpha_1, int beta_1, int alpha_2, int beta_2, int alpha_3, int beta_3) {
133
- double total = 0.0;
134
-
135
- double log_b1_b2_b3 = std::log(beta_1 + beta_2 + beta_3);
136
- double a1_log_b1 = alpha_1 * std::log(beta_1);
137
- double log_b2 = std::log(beta_2);
138
- double log_b3 = std::log(beta_3);
139
- double loggamma_a1 = std::lgamma(alpha_1);
140
-
141
- for (auto k = 0; k < alpha_2; k++) {
142
- double sum_k = a1_log_b1 + k * log_b2 - std::lgamma(k + 1);
143
-
144
- for (auto l = 0; l < alpha_3; l++) {
145
- total += std::exp(sum_k + l * log_b3
146
- - (k + l + alpha_1) * log_b1_b2_b3
147
- + std::lgamma(k + l + alpha_1) - std::lgamma(l + 1) - loggamma_a1);
148
- }
149
- }
150
-
151
- return 1
152
- - prob_1_beats_2(alpha_2, beta_2, alpha_1, beta_1)
153
- - prob_1_beats_2(alpha_3, beta_3, alpha_1, beta_1)
154
- + total;
155
- }
156
-
157
- }
158
-
159
- /// A test for binary outcomes.
160
- class BinaryTest {
161
- public:
162
- /// Adds a new variant.
163
- void add(int participants, int conversions) {
164
- variants.emplace_back(participants, conversions);
165
- }
166
-
167
- /// Returns the winning probability of each variant.
168
- std::vector<double> probabilities() const {
169
- std::vector<double> probs;
170
- probs.reserve(variants.size());
171
-
172
- switch (variants.size()) {
173
- case 0: {
174
- break;
175
- }
176
- case 1: {
177
- probs.push_back(1);
178
-
179
- break;
180
- }
181
- case 2: {
182
- auto b = variants[0];
183
- auto a = variants[1];
184
-
185
- auto prob = prob_b_beats_a(
186
- 1 + a.conversions,
187
- 1 + a.participants - a.conversions,
188
- 1 + b.conversions,
189
- 1 + b.participants - b.conversions
190
- );
191
- probs.push_back(prob);
192
- probs.push_back(1 - prob);
193
-
194
- break;
195
- }
196
- case 3: {
197
- auto total = 0.0;
198
- for (auto i = 0; i < 2; i++) {
199
- auto c = variants[i];
200
- auto b = variants[(i + 1) % 3];
201
- auto a = variants[(i + 2) % 3];
202
-
203
- auto prob = prob_c_beats_ab(
204
- 1 + a.conversions,
205
- 1 + a.participants - a.conversions,
206
- 1 + b.conversions,
207
- 1 + b.participants - b.conversions,
208
- 1 + c.conversions,
209
- 1 + c.participants - c.conversions
210
- );
211
-
212
- probs.push_back(prob);
213
- total += prob;
214
- }
215
- probs.push_back(1 - total);
216
-
217
- break;
218
- }
219
- default: {
220
- auto total = 0.0;
221
- for (auto i = 0; i < 3; i++) {
222
- auto d = variants[i];
223
- auto c = variants[(i + 1) % 4];
224
- auto b = variants[(i + 2) % 4];
225
- auto a = variants[(i + 3) % 4];
226
-
227
- auto prob = prob_d_beats_abc(
228
- 1 + a.conversions,
229
- 1 + a.participants - a.conversions,
230
- 1 + b.conversions,
231
- 1 + b.participants - b.conversions,
232
- 1 + c.conversions,
233
- 1 + c.participants - c.conversions,
234
- 1 + d.conversions,
235
- 1 + d.participants - d.conversions
236
- );
237
-
238
- probs.push_back(prob);
239
- total += prob;
240
- }
241
- probs.push_back(1 - total);
242
- }
243
- }
244
- return probs;
245
- }
246
-
247
- private:
248
- struct Variant {
249
- Variant(int participants, int conversions) : participants(participants), conversions(conversions) {}
250
- int participants;
251
- int conversions;
252
- };
253
-
254
- std::vector<Variant> variants;
255
- };
256
-
257
- /// A test for count data.
258
- class CountTest {
259
- public:
260
- /// Adds a new variant.
261
- void add(int events, int exposure) {
262
- variants.emplace_back(events, exposure);
263
- }
264
-
265
- /// Returns the winning probability of each variant.
266
- std::vector<double> probabilities() const {
267
- std::vector<double> probs;
268
- probs.reserve(variants.size());
269
-
270
- switch (variants.size()) {
271
- case 0: {
272
- break;
273
- }
274
- case 1: {
275
- probs.push_back(1);
276
-
277
- break;
278
- }
279
- case 2: {
280
- auto a = variants[0];
281
- auto b = variants[1];
282
-
283
- auto prob = prob_1_beats_2(
284
- a.events,
285
- a.exposure,
286
- b.events,
287
- b.exposure
288
- );
289
- probs.push_back(prob);
290
- probs.push_back(1 - prob);
291
-
292
- break;
293
- }
294
- default: {
295
- auto total = 0.0;
296
- for (auto i = 0; i < 2; i++) {
297
- auto a = variants[i];
298
- auto b = variants[(i + 1) % 3];
299
- auto c = variants[(i + 2) % 3];
300
-
301
- auto prob = prob_1_beats_23(
302
- a.events,
303
- a.exposure,
304
- b.events,
305
- b.exposure,
306
- c.events,
307
- c.exposure
308
- );
309
-
310
- probs.push_back(prob);
311
- total += prob;
312
- }
313
- probs.push_back(1 - total);
314
- }
315
- }
316
- return probs;
317
- }
318
-
319
- private:
320
- struct Variant {
321
- Variant(int events, int exposure) : events(events), exposure(exposure) {}
322
- int events;
323
- int exposure;
324
- };
325
-
326
- std::vector<Variant> variants;
327
- };
328
-
329
- }
@@ -1,16 +0,0 @@
1
- #include <rice/rice.hpp>
2
- #include <rice/stl.hpp>
3
-
4
- #include "bayestest.hpp"
5
-
6
- using bayestest::BinaryTest;
7
-
8
- extern "C"
9
- void Init_ext() {
10
- auto rb_mFieldTest = Rice::define_module("FieldTest");
11
-
12
- Rice::define_class_under<BinaryTest>(rb_mFieldTest, "BinaryTest")
13
- .define_constructor(Rice::Constructor<BinaryTest>())
14
- .define_method("add", &BinaryTest::add)
15
- .define_method("probabilities", &BinaryTest::probabilities);
16
- }