outliertree 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +1 -1
- data/lib/outliertree/version.rb +1 -1
- data/lib/outliertree.rb +4 -4
- data/vendor/outliertree/src/clusters.cpp +37 -22
- data/vendor/outliertree/src/fit_model.cpp +21 -11
- data/vendor/outliertree/src/outlier_tree.hpp +2 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 107a39daf1b8743880c65c0c9bd20f6b2430687a843aa3394e4f57ba38b58766
|
4
|
+
data.tar.gz: 81e5e13612dd119624a6ec12652b048002c0c2103ee6389709682fb6bcb27e5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a8c6276389a465d548b7b06e7933e64094059960301b4393015bd906dd8deed361887876c152017bc2427fe54b81271e076de24f3e1df801f8f0c330a6c0f76
|
7
|
+
data.tar.gz: 27b9eb4c42adc7abf6c905ec3c787f6947aae6475ecb37283c9b00e560ebb49a8a6bd7ebacfce2c636ba289f014b6dd87821d65311cd3a8640700a4dae44464d
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Price (2.50) looks low given Department is Books and Sale is false
|
|
10
10
|
|
11
11
|
:evergreen_tree: Check out [IsoTree](https://github.com/ankane/isotree-ruby) for an alternative approach that uses Isolation Forest
|
12
12
|
|
13
|
-
[![Build Status](https://github.com/ankane/outliertree-ruby/workflows/build/badge.svg
|
13
|
+
[![Build Status](https://github.com/ankane/outliertree-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/outliertree-ruby/actions)
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
data/lib/outliertree/version.rb
CHANGED
data/lib/outliertree.rb
CHANGED
@@ -5,10 +5,10 @@ require "outliertree/ext"
|
|
5
5
|
require "etc"
|
6
6
|
|
7
7
|
# modules
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
require_relative "outliertree/dataset"
|
9
|
+
require_relative "outliertree/model"
|
10
|
+
require_relative "outliertree/result"
|
11
|
+
require_relative "outliertree/version"
|
12
12
|
|
13
13
|
module OutlierTree
|
14
14
|
def self.new(**options)
|
@@ -11,7 +11,7 @@
|
|
11
11
|
* arXiv preprint arXiv:2001.00636 (2020).
|
12
12
|
*
|
13
13
|
*
|
14
|
-
* Copyright 2020 David Cortes.
|
14
|
+
* Copyright 2020-2024 David Cortes.
|
15
15
|
*
|
16
16
|
* Written for C++11 standard and OpenMP 2.0 or later. Code is meant to be wrapped into scripting languages
|
17
17
|
* such as R or Python.
|
@@ -97,6 +97,10 @@
|
|
97
97
|
* Model parameter. Default is 2.67.
|
98
98
|
* - z_outlier (in)
|
99
99
|
* Model parameter. Default is 8.0. Must be greater than z_norm.
|
100
|
+
* - check_nonneg_outliers (in)
|
101
|
+
* Whether to add an extra check for possible outliers defined as having negative values while all
|
102
|
+
* the rest have positive values, regardless of how many standard deviations away they are.
|
103
|
+
* This is currently only done on the first cluster (no conditions on any variable).
|
100
104
|
*
|
101
105
|
* Returns:
|
102
106
|
* - Whether there were any outliers detected.
|
@@ -107,7 +111,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
107
111
|
size_t cluster_num, size_t tree_num, size_t tree_depth,
|
108
112
|
bool is_log_transf, double log_minval, bool is_exp_transf, double orig_mean, double orig_sd,
|
109
113
|
double left_tail, double right_tail, double *restrict orig_x,
|
110
|
-
double max_perc_outliers, double z_norm, double z_outlier
|
114
|
+
double max_perc_outliers, double z_norm, double z_outlier,
|
115
|
+
bool check_nonneg_outliers)
|
111
116
|
{
|
112
117
|
|
113
118
|
/* TODO: this function could try to determine if the distribution is multimodal, and if so,
|
@@ -120,6 +125,7 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
120
125
|
/* NAs and Inf should have already been removed, and outliers with fewer conditionals already discarded */
|
121
126
|
bool has_low_values = false;
|
122
127
|
bool has_high_values = false;
|
128
|
+
bool has_outlier_neg_values = false;
|
123
129
|
long double running_mean = 0;
|
124
130
|
long double running_ssq = 0;
|
125
131
|
long double mean_prev = 0;
|
@@ -127,14 +133,15 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
127
133
|
double mean;
|
128
134
|
double sd;
|
129
135
|
size_t cnt;
|
130
|
-
size_t
|
136
|
+
size_t tot = end - st + 1;
|
137
|
+
size_t tail_size = (size_t) calculate_max_outliers((long double)tot, max_perc_outliers);
|
131
138
|
size_t st_non_tail = st + tail_size;
|
132
139
|
size_t end_non_tail = end - tail_size;
|
133
140
|
size_t st_normals = 0;
|
134
141
|
size_t end_normals = 0;
|
135
142
|
double min_gap = z_outlier - z_norm;
|
136
143
|
|
137
|
-
double curr_gap, next_gap,
|
144
|
+
double curr_gap, next_gap, lim_by_orig;
|
138
145
|
|
139
146
|
/* Note: there is no good reason and no theory behind these numbers.
|
140
147
|
TODO: find a better way of setting this */
|
@@ -166,9 +173,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
166
173
|
if ((!isinf(left_tail) || !isinf(right_tail)) && !is_log_transf && !is_exp_transf) {
|
167
174
|
sd *= 0.5;
|
168
175
|
}
|
169
|
-
sd = std::fmax(sd, 1e-15);
|
170
176
|
while (std::numeric_limits<double>::epsilon() > sd*std::fmin(min_gap, z_norm))
|
171
|
-
sd
|
177
|
+
sd = std::nextafter(sd, std::numeric_limits<double>::infinity());
|
172
178
|
cluster.cluster_mean = mean;
|
173
179
|
cluster.cluster_sd = sd;
|
174
180
|
cnt = end - st + 1;
|
@@ -212,10 +218,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
212
218
|
cluster.display_lim_low = orig_x[ix_arr[row + 1]];
|
213
219
|
cluster.perc_above = (long double)(end - st_normals + 1) / (long double)(end - st + 1);
|
214
220
|
|
215
|
-
eps = 1e-15;
|
216
221
|
while (cluster.display_lim_low <= cluster.lower_lim) {
|
217
|
-
cluster.lower_lim
|
218
|
-
eps *= 4;
|
222
|
+
cluster.lower_lim = std::nextafter(cluster.lower_lim, -std::numeric_limits<double>::infinity());
|
219
223
|
}
|
220
224
|
break;
|
221
225
|
}
|
@@ -225,6 +229,7 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
225
229
|
if (st_normals == 0) {
|
226
230
|
has_low_values = false;
|
227
231
|
} else {
|
232
|
+
assign_low_outliers:
|
228
233
|
for (size_t row = st; row < st_normals; row++) {
|
229
234
|
|
230
235
|
/* assign outlier if it's a better cluster than previously assigned */
|
@@ -254,7 +259,23 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
254
259
|
}
|
255
260
|
}
|
256
261
|
}
|
257
|
-
|
262
|
+
/* special type of outliers not based on standard deviations */
|
263
|
+
if (check_nonneg_outliers && st_normals == 0 && tot >= 500 && orig_x[ix_arr[st]] < 0. && orig_x[ix_arr[end]] >= 2.) {
|
264
|
+
size_t max_neg_outliers = (tot < 10000)? 1 : ((tot < 100000)? 2 : 3);
|
265
|
+
if (orig_x[ix_arr[st + max_neg_outliers]] > 0.) {
|
266
|
+
size_t num_neg = 0;
|
267
|
+
for (size_t row = st; row < st + max_neg_outliers; row++) {
|
268
|
+
num_neg += orig_x[ix_arr[row]] < 0.;
|
269
|
+
}
|
270
|
+
st_normals = st + num_neg;
|
271
|
+
cluster.lower_lim = 0.;
|
272
|
+
cluster.display_lim_low = orig_x[ix_arr[st + st_normals]];
|
273
|
+
cluster.perc_above = (long double)(end - st_normals + 1) / (long double)(end - st + 1);
|
274
|
+
has_outlier_neg_values = true;
|
275
|
+
goto assign_low_outliers;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
if (!has_low_values && !has_outlier_neg_values) {
|
258
279
|
cluster.perc_above = 1.0;
|
259
280
|
if (!is_log_transf && !is_exp_transf) {
|
260
281
|
|
@@ -271,10 +292,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
271
292
|
}
|
272
293
|
|
273
294
|
if (cluster.lower_lim > -HUGE_VAL) {
|
274
|
-
eps = 1e-15;
|
275
295
|
while (cluster.lower_lim >= orig_x[ix_arr[st]]) {
|
276
|
-
cluster.lower_lim
|
277
|
-
eps *= 4.;
|
296
|
+
cluster.lower_lim = std::nextafter(cluster.lower_lim, -std::numeric_limits<double>::infinity());
|
278
297
|
}
|
279
298
|
}
|
280
299
|
|
@@ -324,10 +343,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
324
343
|
cluster.display_lim_high = orig_x[ix_arr[row - 1]];
|
325
344
|
cluster.perc_below = (long double)(end_normals - st + 1) / (long double)(end - st + 1);
|
326
345
|
|
327
|
-
eps = 1e-15;
|
328
346
|
while (cluster.display_lim_high >= cluster.upper_lim) {
|
329
|
-
cluster.upper_lim
|
330
|
-
eps *= 4;
|
347
|
+
cluster.upper_lim = std::nextafter(cluster.upper_lim, std::numeric_limits<double>::infinity());
|
331
348
|
}
|
332
349
|
break;
|
333
350
|
}
|
@@ -384,10 +401,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
384
401
|
}
|
385
402
|
|
386
403
|
if (cluster.upper_lim < HUGE_VAL) {
|
387
|
-
eps = 1e-15;
|
388
404
|
while (cluster.upper_lim <= orig_x[ix_arr[end]]) {
|
389
|
-
cluster.upper_lim
|
390
|
-
eps *= 4.;
|
405
|
+
cluster.upper_lim = std::nextafter(cluster.upper_lim, std::numeric_limits<double>::infinity());
|
391
406
|
}
|
392
407
|
}
|
393
408
|
|
@@ -406,8 +421,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
406
421
|
}
|
407
422
|
|
408
423
|
/* save displayed statistics for cluster */
|
409
|
-
if (has_high_values || has_low_values || is_log_transf || is_exp_transf) {
|
410
|
-
size_t st_disp = has_low_values? st_normals : st;
|
424
|
+
if (has_high_values || has_low_values || is_log_transf || is_exp_transf || has_outlier_neg_values) {
|
425
|
+
size_t st_disp = (has_low_values || has_outlier_neg_values)? st_normals : st;
|
411
426
|
size_t end_disp = has_high_values? end_normals : end;
|
412
427
|
running_mean = 0;
|
413
428
|
running_ssq = 0;
|
@@ -428,7 +443,7 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
428
443
|
}
|
429
444
|
|
430
445
|
/* report whether outliers were found or not */
|
431
|
-
return has_low_values || has_high_values;
|
446
|
+
return has_low_values || has_high_values || has_outlier_neg_values;
|
432
447
|
}
|
433
448
|
|
434
449
|
|
@@ -11,7 +11,7 @@
|
|
11
11
|
* arXiv preprint arXiv:2001.00636 (2020).
|
12
12
|
*
|
13
13
|
*
|
14
|
-
* Copyright 2020 David Cortes.
|
14
|
+
* Copyright 2020-2024 David Cortes.
|
15
15
|
*
|
16
16
|
* Written for C++11 standard and OpenMP 2.0 or later. Code is meant to be wrapped into scripting languages
|
17
17
|
* such as R or Python.
|
@@ -552,7 +552,8 @@ void process_numeric_col(std::vector<Cluster> &cluster_root,
|
|
552
552
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
553
553
|
workspace.orig_mean, workspace.orig_sd,
|
554
554
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
555
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
555
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
556
|
+
true);
|
556
557
|
workspace.tree->back().clusters.push_back(0);
|
557
558
|
|
558
559
|
/* remove outliers if any were found */
|
@@ -636,7 +637,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
636
637
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
637
638
|
workspace.orig_mean, workspace.orig_sd,
|
638
639
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
639
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
640
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
641
|
+
false);
|
640
642
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
641
643
|
|
642
644
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -663,7 +665,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
663
665
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
664
666
|
workspace.orig_mean, workspace.orig_sd,
|
665
667
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
666
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
668
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
669
|
+
false);
|
667
670
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
668
671
|
|
669
672
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -687,7 +690,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
687
690
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
688
691
|
workspace.orig_mean, workspace.orig_sd,
|
689
692
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
690
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
693
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
694
|
+
false);
|
691
695
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
692
696
|
|
693
697
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -750,7 +754,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
750
754
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
751
755
|
workspace.orig_mean, workspace.orig_sd,
|
752
756
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
753
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
757
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
758
|
+
false);
|
754
759
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
755
760
|
|
756
761
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -777,7 +782,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
777
782
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
778
783
|
workspace.orig_mean, workspace.orig_sd,
|
779
784
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
780
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
785
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
786
|
+
false);
|
781
787
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
782
788
|
|
783
789
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -805,7 +811,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
805
811
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
806
812
|
workspace.orig_mean, workspace.orig_sd,
|
807
813
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
808
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
814
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
815
|
+
false);
|
809
816
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
810
817
|
|
811
818
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -871,7 +878,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
871
878
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
872
879
|
workspace.orig_mean, workspace.orig_sd,
|
873
880
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
874
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
881
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
882
|
+
false);
|
875
883
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
876
884
|
|
877
885
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -898,7 +906,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
898
906
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
899
907
|
workspace.orig_mean, workspace.orig_sd,
|
900
908
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
901
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
909
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
910
|
+
false);
|
902
911
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
903
912
|
|
904
913
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -928,7 +937,8 @@ void recursive_split_numeric(Workspace &workspace,
|
|
928
937
|
workspace.log_transf, workspace.log_minval, workspace.exp_transf,
|
929
938
|
workspace.orig_mean, workspace.orig_sd,
|
930
939
|
workspace.left_tail, workspace.right_tail, workspace.orig_target_col,
|
931
|
-
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier
|
940
|
+
model_params.max_perc_outliers, model_params.z_norm, model_params.z_outlier,
|
941
|
+
false);
|
932
942
|
workspace.lev_has_outliers = workspace.has_outliers? true : workspace.lev_has_outliers;
|
933
943
|
|
934
944
|
if (model_params.follow_all && ((curr_depth + 1) < model_params.max_depth)) {
|
@@ -733,7 +733,8 @@ bool define_numerical_cluster(double *restrict x, size_t *restrict ix_arr, size_
|
|
733
733
|
size_t *restrict outlier_depth, Cluster &cluster, std::vector<Cluster> &clusters, size_t cluster_num, size_t tree_num, size_t tree_depth,
|
734
734
|
bool is_log_transf, double log_minval, bool is_exp_transf, double orig_mean, double orig_sd,
|
735
735
|
double left_tail, double right_tail, double *restrict orig_x,
|
736
|
-
double max_perc_outliers, double z_norm, double z_outlier
|
736
|
+
double max_perc_outliers, double z_norm, double z_outlier,
|
737
|
+
bool check_nonneg_outliers);
|
737
738
|
void define_categ_cluster_no_cond(int *restrict x, size_t *restrict ix_arr, size_t st, size_t end, size_t ncateg,
|
738
739
|
double *restrict outlier_scores, size_t *restrict outlier_clusters, size_t *restrict outlier_trees,
|
739
740
|
size_t *restrict outlier_depth, Cluster &cluster,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: outliertree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rice
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 4.
|
19
|
+
version: '4.3'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 4.
|
26
|
+
version: '4.3'
|
27
27
|
description:
|
28
28
|
email: andrew@ankane.org
|
29
29
|
executables: []
|
@@ -68,14 +68,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
68
|
requirements:
|
69
69
|
- - ">="
|
70
70
|
- !ruby/object:Gem::Version
|
71
|
-
version: '
|
71
|
+
version: '3.1'
|
72
72
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
74
|
- - ">="
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: '0'
|
77
77
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
78
|
+
rubygems_version: 3.5.9
|
79
79
|
signing_key:
|
80
80
|
specification_version: 4
|
81
81
|
summary: Explainable outlier/anomaly detection for Ruby
|