field_test 0.7.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: cb5a6ce72e7c8ac19e10ad432f9feb6f31a8cbcc31c8f57585cca0661b0ce66a
4
- data.tar.gz: 193c9cebdb015e685682c347ea83cccf91b9bf8ea429d74410c435dfdcaf8304
3
+ metadata.gz: 613be253211f115808fccbc17aba8eae3a6bf2a713eaee82d3c0a8d27ad02330
4
+ data.tar.gz: 11f3a88aa737f8c69dde0760863a6ec2a25995172a478f32afeb262276dcdbe8
5
5
  SHA512:
6
- metadata.gz: f7bc7a8c2ff54db35b0ebd5f243a150489e5cab41d0213fc0f8ac5b79aeb89bcfbddf49a9f8d64cd924d816521e01e871991d06fe5204e3ea16d178f44a93d14
7
- data.tar.gz: b37b79f2488a5d3dbc1aeaa5a588adde06d6f5c2dcd07f4390f8db24cdae7d53d879efb4e19451512e29b47cd0a96d8562992323c99fd68135143d9df10a4c11
6
+ metadata.gz: a9daeb6b4dcff1b8deb97c94e3cab45c046137bf3c6665f1ba2833bcf43fd4f772b5f0c2f91e471824a394f3261baa9c712160f6fb521e3878716161f054fbb7
7
+ data.tar.gz: c3c77d85c8eb6eef81a7b8d545e51c086635b6ed509b354e7f8473d1b6064fadca790411668c783dd6044d45b8ab55d17ceeddaaf5bee4dabf9b0e3f810eb00e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 1.0.0 (2026-04-01)
2
+
3
+ - Improved installation time
4
+ - Dropped support for Ruby < 3.3 and Rails < 7.2
5
+
6
+ ## 0.8.0 (2025-05-05)
7
+
8
+ - Dropped support for Ruby < 3.2 and Rails < 7.1
9
+
1
10
  ## 0.7.0 (2024-10-22)
2
11
 
3
12
  - Added support for Trilogy
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, 2016 8 am PST
193
- ended_at: Dec 8, 2016 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.7.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: field_test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: railties
@@ -16,28 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '7'
18
+ version: '7.2'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '7'
25
+ version: '7.2'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: activerecord
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '7'
32
+ version: '7.2'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '7'
39
+ version: '7.2'
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: browser
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -52,21 +51,6 @@ dependencies:
52
51
  - - ">="
53
52
  - !ruby/object:Gem::Version
54
53
  version: '2'
55
- - !ruby/object:Gem::Dependency
56
- name: rice
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: 4.3.3
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: 4.3.3
69
- description:
70
54
  email: andrew@ankane.org
71
55
  executables: []
72
56
  extensions:
@@ -89,8 +73,8 @@ files:
89
73
  - app/views/field_test/participants/show.html.erb
90
74
  - app/views/layouts/field_test/application.html.erb
91
75
  - config/routes.rb
92
- - ext/field_test/bayestest.hpp
93
- - ext/field_test/ext.cpp
76
+ - ext/field_test/bayestest.h
77
+ - ext/field_test/ext.c
94
78
  - ext/field_test/extconf.rb
95
79
  - lib/field_test.rb
96
80
  - lib/field_test/controller.rb
@@ -109,7 +93,6 @@ homepage: https://github.com/ankane/field_test
109
93
  licenses:
110
94
  - MIT
111
95
  metadata: {}
112
- post_install_message:
113
96
  rdoc_options: []
114
97
  require_paths:
115
98
  - lib
@@ -117,15 +100,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
100
  requirements:
118
101
  - - ">="
119
102
  - !ruby/object:Gem::Version
120
- version: '3.1'
103
+ version: '3.3'
121
104
  required_rubygems_version: !ruby/object:Gem::Requirement
122
105
  requirements:
123
106
  - - ">="
124
107
  - !ruby/object:Gem::Version
125
108
  version: '0'
126
109
  requirements: []
127
- rubygems_version: 3.5.16
128
- signing_key:
110
+ rubygems_version: 4.0.6
129
111
  specification_version: 4
130
112
  summary: A/B testing for Rails
131
113
  test_files: []
@@ -1,313 +0,0 @@
1
- /*!
2
- * BayesTest C++ v0.1.0
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
- double logbeta(double a, double b) {
15
- return std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b);
16
- }
17
-
18
- double prob_b_beats_a(int alpha_a, int beta_a, int alpha_b, int beta_b) {
19
- double total = 0.0;
20
- double logbeta_aa_ba = logbeta(alpha_a, beta_a);
21
- double beta_ba = beta_b + beta_a;
22
-
23
- for (auto i = 0; i < alpha_b; i++) {
24
- total += exp(logbeta(alpha_a + i, beta_ba) - log(beta_b + i) - logbeta(1 + i, beta_b) - logbeta_aa_ba);
25
- }
26
-
27
- return total;
28
- }
29
-
30
- double prob_c_beats_ab(int alpha_a, int beta_a, int alpha_b, int beta_b, int alpha_c, int beta_c) {
31
- double total = 0.0;
32
-
33
- double logbeta_ac_bc = logbeta(alpha_c, beta_c);
34
-
35
- std::vector<double> log_bb_j_logbeta_j_bb;
36
- log_bb_j_logbeta_j_bb.reserve(alpha_b);
37
-
38
- for (auto j = 0; j < alpha_b; j++) {
39
- log_bb_j_logbeta_j_bb.push_back(log(beta_b + j) + logbeta(1 + j, beta_b));
40
- }
41
-
42
- double abc = beta_a + beta_b + beta_c;
43
- std::vector<double> logbeta_ac_i_j;
44
- logbeta_ac_i_j.reserve(alpha_a + alpha_b);
45
-
46
- for (auto i = 0; i < alpha_a + alpha_b; i++) {
47
- logbeta_ac_i_j.push_back(logbeta(alpha_c + i, abc));
48
- }
49
-
50
- for (auto i = 0; i < alpha_a; i++) {
51
- double sum_i = -log(beta_a + i) - logbeta(1 + i, beta_a) - logbeta_ac_bc;
52
-
53
- for (auto j = 0; j < alpha_b; j++) {
54
- total += exp(sum_i + logbeta_ac_i_j[i + j] - log_bb_j_logbeta_j_bb[j]);
55
- }
56
- }
57
-
58
- return 1 - prob_b_beats_a(alpha_c, beta_c, alpha_a, beta_a) -
59
- prob_b_beats_a(alpha_c, beta_c, alpha_b, beta_b) + total;
60
- }
61
-
62
- 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) {
63
- double total = 0.0;
64
-
65
- double logbeta_ad_bd = logbeta(alpha_d, beta_d);
66
-
67
- std::vector<double> log_bb_j_logbeta_j_bb;
68
- log_bb_j_logbeta_j_bb.reserve(alpha_b);
69
-
70
- for (auto j = 0; j < alpha_b; j++) {
71
- log_bb_j_logbeta_j_bb.push_back(log(beta_b + j) + logbeta(1 + j, beta_b));
72
- }
73
-
74
- std::vector<double> log_bc_k_logbeta_k_bc;
75
- log_bc_k_logbeta_k_bc.reserve(alpha_c);
76
-
77
- for (auto k = 0; k < alpha_c; k++) {
78
- log_bc_k_logbeta_k_bc.push_back(log(beta_c + k) + logbeta(1 + k, beta_c));
79
- }
80
-
81
- double abcd = beta_a + beta_b + beta_c + beta_d;
82
- std::vector<double> logbeta_bd_i_j_k;
83
- logbeta_bd_i_j_k.reserve(alpha_a + alpha_b + alpha_c);
84
-
85
- for (auto i = 0; i < alpha_a + alpha_b + alpha_c; i++) {
86
- logbeta_bd_i_j_k.push_back(logbeta(alpha_d + i, abcd));
87
- }
88
-
89
- for (auto i = 0; i < alpha_a; i++) {
90
- double sum_i = -log(beta_a + i) - logbeta(1 + i, beta_a) - logbeta_ad_bd;
91
-
92
- for (auto j = 0; j < alpha_b; j++) {
93
- double sum_j = sum_i - log_bb_j_logbeta_j_bb[j];
94
-
95
- for (auto k = 0; k < alpha_c; k++) {
96
- total += exp(sum_j + logbeta_bd_i_j_k[i + j + k] - log_bc_k_logbeta_k_bc[k]);
97
- }
98
- }
99
- }
100
-
101
- return 1 - prob_b_beats_a(alpha_a, beta_a, alpha_d, beta_d) -
102
- prob_b_beats_a(alpha_b, beta_b, alpha_d, beta_d) -
103
- prob_b_beats_a(alpha_c, beta_c, alpha_d, beta_d) +
104
- prob_c_beats_ab(alpha_a, beta_a, alpha_b, beta_b, alpha_d, beta_d) +
105
- prob_c_beats_ab(alpha_a, beta_a, alpha_c, beta_c, alpha_d, beta_d) +
106
- prob_c_beats_ab(alpha_b, beta_b, alpha_c, beta_c, alpha_d, beta_d) - total;
107
- }
108
-
109
- double prob_1_beats_2(int alpha_1, int beta_1, int alpha_2, int beta_2) {
110
- double total = 0.0;
111
- double log_b1 = log(beta_1);
112
- double a2_log_b2 = alpha_2 * log(beta_2);
113
- double log_b1_b2 = log(beta_1 + beta_2);
114
-
115
- for (auto k = 0; k < alpha_1; k++) {
116
- total += exp(k * log_b1 +
117
- a2_log_b2 -
118
- (k + alpha_2) * log_b1_b2 -
119
- log(k + alpha_2) -
120
- logbeta(k + 1, alpha_2));
121
- }
122
-
123
- return total;
124
- }
125
-
126
- double prob_1_beats_23(int alpha_1, int beta_1, int alpha_2, int beta_2, int alpha_3, int beta_3) {
127
- double total = 0.0;
128
-
129
- double log_b1_b2_b3 = log(beta_1 + beta_2 + beta_3);
130
- double a1_log_b1 = alpha_1 * log(beta_1);
131
- double log_b2 = log(beta_2);
132
- double log_b3 = log(beta_3);
133
- double loggamma_a1 = std::lgamma(alpha_1);
134
-
135
- for (auto k = 0; k < alpha_2; k++) {
136
- double sum_k = a1_log_b1 + k * log_b2 - std::lgamma(k + 1);
137
-
138
- for (auto l = 0; l < alpha_3; l++) {
139
- total += exp(sum_k + l * log_b3
140
- - (k + l + alpha_1) * log_b1_b2_b3
141
- + std::lgamma(k + l + alpha_1) - std::lgamma(l + 1) - loggamma_a1);
142
- }
143
- }
144
-
145
- return 1.0 - prob_1_beats_2(alpha_2, beta_2, alpha_1, beta_1)
146
- - prob_1_beats_2(alpha_3, beta_3, alpha_1, beta_1) + total;
147
- }
148
-
149
- class BinaryTest {
150
- public:
151
- void add(int participants, int conversions) {
152
- variants.emplace_back(participants, conversions);
153
- }
154
-
155
- std::vector<double> probabilities() {
156
- std::vector<double> probs;
157
- probs.reserve(variants.size());
158
-
159
- switch (variants.size()) {
160
- case 0: {
161
- break;
162
- }
163
- case 1: {
164
- probs.push_back(1);
165
-
166
- break;
167
- }
168
- case 2: {
169
- auto b = variants[0];
170
- auto a = variants[1];
171
-
172
- auto prob = prob_b_beats_a(
173
- 1 + a.conversions,
174
- 1 + a.participants - a.conversions,
175
- 1 + b.conversions,
176
- 1 + b.participants - b.conversions
177
- );
178
- probs.push_back(prob);
179
- probs.push_back(1 - prob);
180
-
181
- break;
182
- }
183
- case 3: {
184
- auto total = 0.0;
185
- for (auto i = 0; i < 2; i++) {
186
- auto c = variants[i];
187
- auto b = variants[(i + 1) % 3];
188
- auto a = variants[(i + 2) % 3];
189
-
190
- auto prob = prob_c_beats_ab(
191
- 1 + a.conversions,
192
- 1 + a.participants - a.conversions,
193
- 1 + b.conversions,
194
- 1 + b.participants - b.conversions,
195
- 1 + c.conversions,
196
- 1 + c.participants - c.conversions
197
- );
198
-
199
- probs.push_back(prob);
200
- total += prob;
201
- }
202
- probs.push_back(1 - total);
203
-
204
- break;
205
- }
206
- default: {
207
- auto total = 0.0;
208
- for (auto i = 0; i < 3; i++) {
209
- auto d = variants[i];
210
- auto c = variants[(i + 1) % 4];
211
- auto b = variants[(i + 2) % 4];
212
- auto a = variants[(i + 3) % 4];
213
-
214
- auto prob = prob_d_beats_abc(
215
- 1 + a.conversions,
216
- 1 + a.participants - a.conversions,
217
- 1 + b.conversions,
218
- 1 + b.participants - b.conversions,
219
- 1 + c.conversions,
220
- 1 + c.participants - c.conversions,
221
- 1 + d.conversions,
222
- 1 + d.participants - d.conversions
223
- );
224
-
225
- probs.push_back(prob);
226
- total += prob;
227
- }
228
- probs.push_back(1 - total);
229
- }
230
- }
231
- return probs;
232
- }
233
-
234
- private:
235
- struct Variant {
236
- Variant(int participants, int conversions) : participants(participants), conversions(conversions) {};
237
- int participants;
238
- int conversions;
239
- };
240
-
241
- std::vector<Variant> variants;
242
- };
243
-
244
- class CountTest {
245
- public:
246
- void add(int events, int exposure) {
247
- variants.emplace_back(events, exposure);
248
- }
249
-
250
- std::vector<double> probabilities() {
251
- std::vector<double> probs;
252
- probs.reserve(variants.size());
253
-
254
- switch (variants.size()) {
255
- case 0: {
256
- break;
257
- }
258
- case 1: {
259
- probs.push_back(1);
260
-
261
- break;
262
- }
263
- case 2: {
264
- auto a = variants[0];
265
- auto b = variants[1];
266
-
267
- auto prob = prob_1_beats_2(
268
- a.events,
269
- a.exposure,
270
- b.events,
271
- b.exposure
272
- );
273
- probs.push_back(prob);
274
- probs.push_back(1 - prob);
275
-
276
- break;
277
- }
278
- default: {
279
- auto total = 0.0;
280
- for (auto i = 0; i < 2; i++) {
281
- auto a = variants[i];
282
- auto b = variants[(i + 1) % 3];
283
- auto c = variants[(i + 2) % 3];
284
-
285
- auto prob = prob_1_beats_23(
286
- a.events,
287
- a.exposure,
288
- b.events,
289
- b.exposure,
290
- c.events,
291
- c.exposure
292
- );
293
-
294
- probs.push_back(prob);
295
- total += prob;
296
- }
297
- probs.push_back(1 - total);
298
- }
299
- }
300
- return probs;
301
- }
302
-
303
- private:
304
- struct Variant {
305
- Variant(int events, int exposure) : events(events), exposure(exposure) {};
306
- int events;
307
- int exposure;
308
- };
309
-
310
- std::vector<Variant> variants;
311
- };
312
-
313
- }
@@ -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
- }