anomaly 0.1.0 → 0.3.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 +7 -0
- data/CHANGELOG.md +18 -0
- data/{LICENSE → LICENSE.txt} +2 -2
- data/README.md +52 -43
- data/lib/anomaly/detector.rb +56 -28
- data/lib/anomaly/version.rb +1 -1
- metadata +20 -70
- data/.gitignore +0 -17
- data/.rspec +0 -1
- data/Gemfile +0 -4
- data/Rakefile +0 -25
- data/anomaly.gemspec +0 -21
- data/spec/anomaly/detector_spec.rb +0 -71
- data/spec/spec_helper.rb +0 -8
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7cf7957fa3d0c9ddf4fb5dbc59c6b153ae0122d6777dffc4b088156a34b44c04
|
4
|
+
data.tar.gz: a4130a661e44bf2363ef024c8e5296803cab479d9bc69cff7661a5d702feccb7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3c13a829fdc03e54b408b7bfc8ab0ea5ccb6b7092a12146ee092071ccf81e73da278b7910b0dd44d846b2a3619580567da5c2116968d0d73b4bd13d5df414c1c
|
7
|
+
data.tar.gz: e7b7aff28acbaf2c9c3a95d62ccf8c10829689896a98e32a5f85ce22114754d42eeb0e5ccf70bbf49a4fda850fadcb367747caabc41b944db5e82bfd7686f351
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
## 0.3.0 (2022-09-05)
|
2
|
+
|
3
|
+
- Dropped support for `narray` (use `numo-narray` instead)
|
4
|
+
- Dropped support for Ruby < 2.7
|
5
|
+
|
6
|
+
## 0.2.1 (2020-04-16)
|
7
|
+
|
8
|
+
- Added support for multiple predictions
|
9
|
+
|
10
|
+
## 0.2.0 (2019-10-27)
|
11
|
+
|
12
|
+
- Switched to Ruby `sum` for performance
|
13
|
+
- Added support for Numo::NArray
|
14
|
+
- Use keyword arguments
|
15
|
+
|
16
|
+
## 0.1.0 (2011-12-18)
|
17
|
+
|
18
|
+
- Started changelog
|
data/{LICENSE → LICENSE.txt}
RENAMED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2011 Andrew Kane
|
1
|
+
Copyright (c) 2011-2022 Andrew Kane
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,32 +1,20 @@
|
|
1
1
|
# Anomaly
|
2
2
|
|
3
|
-
Easy-to-use anomaly detection
|
3
|
+
Easy-to-use anomaly detection for Ruby
|
4
|
+
|
5
|
+
[](https://github.com/ankane/anomaly/actions)
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
7
|
-
Add this line to your application
|
9
|
+
Add this line to your application’s Gemfile:
|
8
10
|
|
9
11
|
```ruby
|
10
12
|
gem "anomaly"
|
11
13
|
```
|
12
14
|
|
13
|
-
|
15
|
+
## Getting Started
|
14
16
|
|
15
|
-
|
16
|
-
bundle install
|
17
|
-
```
|
18
|
-
|
19
|
-
For max performance (trains ~3x faster for large datasets), also install the NArray gem:
|
20
|
-
|
21
|
-
```ruby
|
22
|
-
gem "narray"
|
23
|
-
```
|
24
|
-
|
25
|
-
Anomaly will automatically detect it and use it.
|
26
|
-
|
27
|
-
## How to Use
|
28
|
-
|
29
|
-
Say we have weather data and we want to predict if it's sunny. In this example, sunny days are non-anomalies, and days with other types of weather (rain, snow, etc.) are anomalies. The data looks like:
|
17
|
+
Say we have weather data and we want to predict if it’s sunny. In this example, sunny days are non-anomalies, and days with other types of weather (rain, snow, etc.) are anomalies. The data looks like:
|
30
18
|
|
31
19
|
```ruby
|
32
20
|
# [temperature(°F), humidity(%), pressure(in), sunny?(y=0, n=1)]
|
@@ -41,57 +29,78 @@ weather_data = [
|
|
41
29
|
|
42
30
|
The last column **must** be 0 for non-anomalies, 1 for anomalies. Non-anomalies are used to train the detector, and both anomalies and non-anomalies are used to find the best value of ε.
|
43
31
|
|
44
|
-
|
32
|
+
Train the detector
|
45
33
|
|
46
34
|
```ruby
|
47
|
-
|
35
|
+
detector = Anomaly::Detector.new(weather_data)
|
36
|
+
```
|
37
|
+
|
38
|
+
Test for anomalies
|
48
39
|
|
40
|
+
```ruby
|
49
41
|
# 85°F, 42% humidity, 12.3 in. pressure
|
50
|
-
|
51
|
-
# => true
|
42
|
+
detector.anomaly?([85, 42, 12.3])
|
52
43
|
```
|
53
44
|
|
54
45
|
Anomaly automatically finds the best value for ε, which you can access with:
|
55
46
|
|
56
47
|
```ruby
|
57
|
-
|
48
|
+
detector.eps
|
58
49
|
```
|
59
50
|
|
60
51
|
If you already know you want ε = 0.01, initialize the detector with:
|
61
52
|
|
62
53
|
```ruby
|
63
|
-
|
54
|
+
detector = Anomaly::Detector.new(weather_data, eps: 0.01)
|
64
55
|
```
|
65
56
|
|
66
|
-
|
57
|
+
## Persistence
|
67
58
|
|
68
|
-
You can easily persist the detector to a file or database - it
|
59
|
+
You can easily persist the detector to a file or database - it’s very tiny.
|
69
60
|
|
70
61
|
```ruby
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
File.open("anomaly_detector.dump", "w") {|f| f.write(serialized_ad) }
|
62
|
+
bin = Marshal.dump(detector)
|
63
|
+
File.binwrite("detector.bin", bin)
|
64
|
+
```
|
75
65
|
|
76
|
-
|
66
|
+
Then read it later:
|
77
67
|
|
78
|
-
|
79
|
-
|
68
|
+
```ruby
|
69
|
+
bin = File.binread("detector.bin")
|
70
|
+
detector = Marshal.load(bin)
|
80
71
|
```
|
81
72
|
|
82
|
-
##
|
73
|
+
## Related Projects
|
74
|
+
|
75
|
+
- [AnomalyDetection.rb](https://github.com/ankane/AnomalyDetection.rb) - Time series anomaly detection for Ruby
|
76
|
+
- [Prophet.rb](https://github.com/ankane/prophet-ruby) - Time series forecasting (and anomaly detection) for Ruby
|
77
|
+
- [IsoTree](https://github.com/ankane/isotree-ruby) - Outlier/anomaly detection for Ruby using Isolation Forest
|
78
|
+
- [OutlierTree](https://github.com/ankane/outliertree-ruby) - Explainable outlier/anomaly detection for Ruby
|
79
|
+
- [MIDAS](https://github.com/ankane/midas-ruby) - Edge stream anomaly detection for Ruby
|
80
|
+
- [Trend](https://github.com/ankane/trend-ruby) - Anomaly detection and forecasting for Ruby
|
81
|
+
|
82
|
+
## Credits
|
83
83
|
|
84
|
-
|
85
|
-
|
84
|
+
A special thanks to [Andrew Ng](https://www.coursera.org/learn/machine-learning).
|
85
|
+
|
86
|
+
## History
|
87
|
+
|
88
|
+
View the [changelog](https://github.com/ankane/anomaly/blob/master/CHANGELOG.md)
|
86
89
|
|
87
90
|
## Contributing
|
88
91
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
92
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
93
|
+
|
94
|
+
- [Report bugs](https://github.com/ankane/anomaly/issues)
|
95
|
+
- Fix bugs and [submit pull requests](https://github.com/ankane/anomaly/pulls)
|
96
|
+
- Write, clarify, or fix documentation
|
97
|
+
- Suggest or add new features
|
94
98
|
|
95
|
-
|
99
|
+
To get started with development:
|
96
100
|
|
97
|
-
|
101
|
+
```sh
|
102
|
+
git clone https://github.com/ankane/anomaly.git
|
103
|
+
cd anomaly
|
104
|
+
bundle install
|
105
|
+
bundle exec rake spec
|
106
|
+
```
|
data/lib/anomaly/detector.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
module Anomaly
|
2
2
|
class Detector
|
3
|
+
attr_reader :mean, :std
|
3
4
|
attr_accessor :eps
|
4
5
|
|
5
|
-
def initialize(examples = nil, opts
|
6
|
+
def initialize(examples = nil, **opts)
|
6
7
|
@m = 0
|
7
|
-
train(examples, opts) if examples
|
8
|
+
train(examples, **opts) if examples
|
8
9
|
end
|
9
10
|
|
10
|
-
def train(examples,
|
11
|
+
def train(examples, eps: 0)
|
12
|
+
# for Numo::NArray
|
13
|
+
# TODO make more efficient when possible
|
14
|
+
examples = examples.to_a
|
15
|
+
|
11
16
|
raise "No examples" if examples.empty?
|
12
17
|
raise "Must have at least two columns" if examples.first.size < 2
|
13
18
|
|
@@ -24,7 +29,7 @@ module Anomaly
|
|
24
29
|
|
25
30
|
raise "Must have at least one non-anomaly" if non_anomalies.empty?
|
26
31
|
|
27
|
-
@eps =
|
32
|
+
@eps = eps
|
28
33
|
if @eps > 0
|
29
34
|
# Use all non-anomalies to train.
|
30
35
|
training_examples = non_anomalies
|
@@ -33,28 +38,33 @@ module Anomaly
|
|
33
38
|
test_examples.concat(anomalies)
|
34
39
|
end
|
35
40
|
# Remove last column.
|
36
|
-
training_examples = training_examples.map{|e| e[0..-2]}
|
41
|
+
training_examples = training_examples.map { |e| e[0..-2] }
|
37
42
|
@m = training_examples.size
|
38
43
|
@n = training_examples.first.size
|
39
44
|
|
40
|
-
if defined?(
|
45
|
+
if defined?(Numo::SFloat)
|
46
|
+
training_examples = Numo::SFloat.cast(training_examples)
|
47
|
+
# Convert these to an Array for Marshal.dump
|
48
|
+
@mean = training_examples.mean(0).to_a
|
49
|
+
@std = training_examples.stddev(0).to_a
|
50
|
+
elsif defined?(NMatrix)
|
41
51
|
training_examples = NMatrix.to_na(training_examples)
|
42
52
|
# Convert these to an Array for Marshal.dump
|
43
53
|
@mean = training_examples.mean(1).to_a
|
44
54
|
@std = training_examples.stddev(1).to_a
|
45
55
|
else
|
46
56
|
# Default to Array, since built-in Matrix does not give us a big performance advantage.
|
47
|
-
cols = @n.times.map{|i| training_examples.map{|r| r[i]}}
|
48
|
-
@mean = cols.map{|c|
|
49
|
-
@std = cols.each_with_index.map{|c,i|
|
57
|
+
cols = @n.times.map { |i| training_examples.map { |r| r[i] } }
|
58
|
+
@mean = cols.map { |c| alt_mean(c) }
|
59
|
+
@std = cols.each_with_index.map { |c, i| alt_std(c, @mean[i]) }
|
50
60
|
end
|
51
|
-
@std.map!{|std| (std == 0
|
61
|
+
@std.map! { |std| (std == 0 || std.nan?) ? 1e-10 : std }
|
52
62
|
|
53
63
|
if @eps == 0
|
54
64
|
# Find the best eps.
|
55
|
-
epss = (1..9).map{|i| [1,3,5,7,9].map{|j| (j*10**(-i)).to_f }}.flatten
|
56
|
-
f1_scores = epss.map{|eps| [eps, compute_f1_score(test_examples, eps)] }
|
57
|
-
@eps,
|
65
|
+
epss = (1..9).map { |i| [1, 3, 5, 7, 9].map { |j| (j * 10**(-i)).to_f } }.flatten
|
66
|
+
f1_scores = epss.map { |eps| [eps, compute_f1_score(test_examples, eps)] }
|
67
|
+
@eps, _ = f1_scores.max_by { |v| v[1] }
|
58
68
|
end
|
59
69
|
end
|
60
70
|
|
@@ -64,25 +74,44 @@ module Anomaly
|
|
64
74
|
|
65
75
|
# Limit the probability of features to [0,1]
|
66
76
|
# to keep probabilities at same scale.
|
77
|
+
# Use log to prevent underflow
|
67
78
|
def probability(x)
|
68
79
|
raise "Train me first" unless trained?
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
80
|
+
|
81
|
+
singular = !x.first.is_a?(Array)
|
82
|
+
x = [x] if singular
|
83
|
+
|
84
|
+
y =
|
85
|
+
x.map do |xi|
|
86
|
+
prob = 0
|
87
|
+
@n.times.map do |i|
|
88
|
+
pi = normal_pdf(xi[i], @mean[i], @std[i])
|
89
|
+
prob += Math.log(pi > 1 ? 1 : pi)
|
90
|
+
end
|
91
|
+
Math.exp(prob)
|
92
|
+
end
|
93
|
+
|
94
|
+
singular ? y.first : y
|
74
95
|
end
|
75
96
|
|
76
97
|
def anomaly?(x, eps = @eps)
|
77
|
-
probability(x)
|
98
|
+
y = probability(x)
|
99
|
+
|
100
|
+
if y.is_a?(Array)
|
101
|
+
y.map do |yi|
|
102
|
+
yi < eps
|
103
|
+
end
|
104
|
+
else
|
105
|
+
y < eps
|
106
|
+
end
|
78
107
|
end
|
79
108
|
|
80
109
|
protected
|
81
110
|
|
82
|
-
SQRT2PI = Math.sqrt(2*Math::PI)
|
111
|
+
SQRT2PI = Math.sqrt(2 * Math::PI)
|
83
112
|
|
84
113
|
def normal_pdf(x, mean = 0, std = 1)
|
85
|
-
1/(SQRT2PI*std)*Math.exp(-((x - mean)**2/(2.0*(std**2))))
|
114
|
+
1 / (SQRT2PI * std) * Math.exp(-((x - mean)**2 / (2.0 * (std**2))))
|
86
115
|
end
|
87
116
|
|
88
117
|
# Find best eps.
|
@@ -99,8 +128,8 @@ module Anomaly
|
|
99
128
|
fn = 0
|
100
129
|
examples.each do |example|
|
101
130
|
act = example.last != 0
|
102
|
-
pred =
|
103
|
-
if act
|
131
|
+
pred = anomaly?(example[0..-2], eps)
|
132
|
+
if act && pred
|
104
133
|
tp += 1
|
105
134
|
elsif pred # and !act
|
106
135
|
fp += 1
|
@@ -120,13 +149,12 @@ module Anomaly
|
|
120
149
|
|
121
150
|
# Not used for NArray
|
122
151
|
|
123
|
-
def
|
124
|
-
x.
|
152
|
+
def alt_mean(x)
|
153
|
+
x.sum / x.size
|
125
154
|
end
|
126
155
|
|
127
|
-
def
|
128
|
-
Math.sqrt(x.
|
156
|
+
def alt_std(x, mean)
|
157
|
+
Math.sqrt(x.sum { |i| (i - mean)**2 }.to_f / (x.size - 1))
|
129
158
|
end
|
130
|
-
|
131
159
|
end
|
132
160
|
end
|
data/lib/anomaly/version.rb
CHANGED
metadata
CHANGED
@@ -1,98 +1,48 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anomaly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.3.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Andrew Kane
|
9
|
-
autorequire:
|
8
|
+
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
13
|
-
dependencies:
|
14
|
-
|
15
|
-
|
16
|
-
requirement: &2155813680 !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - ! '>='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '0'
|
22
|
-
type: :development
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: *2155813680
|
25
|
-
- !ruby/object:Gem::Dependency
|
26
|
-
name: rspec
|
27
|
-
requirement: &2155813180 !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
|
-
requirements:
|
30
|
-
- - ! '>='
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 2.0.0
|
33
|
-
type: :development
|
34
|
-
prerelease: false
|
35
|
-
version_requirements: *2155813180
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: narray
|
38
|
-
requirement: &2155812760 !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
|
-
requirements:
|
41
|
-
- - ! '>='
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '0'
|
44
|
-
type: :development
|
45
|
-
prerelease: false
|
46
|
-
version_requirements: *2155812760
|
47
|
-
description: Easy-to-use anomaly detection
|
48
|
-
email:
|
49
|
-
- andrew@getformidable.com
|
11
|
+
date: 2022-09-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: andrew@ankane.org
|
50
15
|
executables: []
|
51
16
|
extensions: []
|
52
17
|
extra_rdoc_files: []
|
53
18
|
files:
|
54
|
-
- .
|
55
|
-
- .
|
56
|
-
- Gemfile
|
57
|
-
- LICENSE
|
19
|
+
- CHANGELOG.md
|
20
|
+
- LICENSE.txt
|
58
21
|
- README.md
|
59
|
-
- Rakefile
|
60
|
-
- anomaly.gemspec
|
61
22
|
- lib/anomaly.rb
|
62
23
|
- lib/anomaly/detector.rb
|
63
24
|
- lib/anomaly/version.rb
|
64
|
-
- spec/anomaly/detector_spec.rb
|
65
|
-
- spec/spec_helper.rb
|
66
25
|
homepage: https://github.com/ankane/anomaly
|
67
|
-
licenses:
|
68
|
-
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
69
30
|
rdoc_options: []
|
70
31
|
require_paths:
|
71
32
|
- lib
|
72
33
|
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
34
|
requirements:
|
75
|
-
- -
|
35
|
+
- - ">="
|
76
36
|
- !ruby/object:Gem::Version
|
77
|
-
version: '
|
78
|
-
segments:
|
79
|
-
- 0
|
80
|
-
hash: 1886385059125072633
|
37
|
+
version: '2.7'
|
81
38
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
-
none: false
|
83
39
|
requirements:
|
84
|
-
- -
|
40
|
+
- - ">="
|
85
41
|
- !ruby/object:Gem::Version
|
86
42
|
version: '0'
|
87
|
-
segments:
|
88
|
-
- 0
|
89
|
-
hash: 1886385059125072633
|
90
43
|
requirements: []
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
test_files:
|
97
|
-
- spec/anomaly/detector_spec.rb
|
98
|
-
- spec/spec_helper.rb
|
44
|
+
rubygems_version: 3.3.7
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Easy-to-use anomaly detection for Ruby
|
48
|
+
test_files: []
|
data/.gitignore
DELETED
data/.rspec
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
--color
|
data/Gemfile
DELETED
data/Rakefile
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env rake
|
2
|
-
require "bundler/gem_tasks"
|
3
|
-
require "rspec/core/rake_task"
|
4
|
-
RSpec::Core::RakeTask.new("spec")
|
5
|
-
|
6
|
-
require "benchmark"
|
7
|
-
require "anomaly"
|
8
|
-
|
9
|
-
task :benchmark do
|
10
|
-
examples = 1_000_000.times.map{ [rand, rand, rand, 0] }
|
11
|
-
|
12
|
-
Benchmark.bm do |x|
|
13
|
-
x.report { Anomaly::Detector.new(examples, {:eps => 0.5}) }
|
14
|
-
require "narray"
|
15
|
-
x.report { Anomaly::Detector.new(examples, {:eps => 0.5}) }
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
task :random_examples do
|
20
|
-
examples = 10_000.times.map{ [rand, rand(10), rand(100), 0] } +
|
21
|
-
100.times.map{ [rand + 1, rand(10) + 2, rand(100) + 20, 1] }
|
22
|
-
|
23
|
-
ad = Anomaly::Detector.new(examples)
|
24
|
-
puts ad.eps
|
25
|
-
end
|
data/anomaly.gemspec
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
require File.expand_path('../lib/anomaly/version', __FILE__)
|
3
|
-
|
4
|
-
Gem::Specification.new do |gem|
|
5
|
-
gem.authors = ["Andrew Kane"]
|
6
|
-
gem.email = ["andrew@getformidable.com"]
|
7
|
-
gem.description = %q{Easy-to-use anomaly detection}
|
8
|
-
gem.summary = %q{Easy-to-use anomaly detection}
|
9
|
-
gem.homepage = "https://github.com/ankane/anomaly"
|
10
|
-
|
11
|
-
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
-
gem.files = `git ls-files`.split("\n")
|
13
|
-
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
-
gem.name = "anomaly"
|
15
|
-
gem.require_paths = ["lib"]
|
16
|
-
gem.version = Anomaly::VERSION
|
17
|
-
|
18
|
-
gem.add_development_dependency "rake"
|
19
|
-
gem.add_development_dependency "rspec", ">= 2.0.0"
|
20
|
-
gem.add_development_dependency "narray"
|
21
|
-
end
|
@@ -1,71 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe Anomaly::Detector do
|
4
|
-
let(:examples) { [[-1,-2,0],[0,0,0],[1,2,0]] }
|
5
|
-
let(:ad) { Anomaly::Detector.new(examples) }
|
6
|
-
|
7
|
-
# mean = [0, 0], std = [1, 2]
|
8
|
-
it "computes the right probability" do
|
9
|
-
ad.probability([0,0]).should == 0.079577471545947667
|
10
|
-
end
|
11
|
-
|
12
|
-
it "marshalizes" do
|
13
|
-
expect{ Marshal.dump(ad) }.to_not raise_error
|
14
|
-
end
|
15
|
-
|
16
|
-
context "when standard deviation is 0" do
|
17
|
-
let(:examples) { [[0,0],[0,0]] }
|
18
|
-
|
19
|
-
it "returns infinity for mean" do
|
20
|
-
ad.probability([0]).should == 1
|
21
|
-
end
|
22
|
-
|
23
|
-
it "returns 0 for not mean" do
|
24
|
-
ad.probability([1]).should == 0
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
context "when examples is an array" do
|
29
|
-
let(:examples) { [[-1,-2,0],[0,0,0],[1,2,0]] }
|
30
|
-
let(:sample) { [rand, rand] }
|
31
|
-
|
32
|
-
it "returns the same probability as an NMatrix" do
|
33
|
-
prob = ad.probability(sample)
|
34
|
-
Object.send(:remove_const, :NMatrix)
|
35
|
-
prob.should == Anomaly::Detector.new(examples).probability(sample)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
context "when lots of samples" do
|
40
|
-
let(:examples) { m.times.map{[0,0]} }
|
41
|
-
let(:m) { rand(100) + 1 }
|
42
|
-
|
43
|
-
it { ad.trained?.should be_true }
|
44
|
-
end
|
45
|
-
|
46
|
-
context "when no samples" do
|
47
|
-
let(:examples) { nil }
|
48
|
-
|
49
|
-
it { ad.trained?.should be_false }
|
50
|
-
end
|
51
|
-
|
52
|
-
context "when pdf is greater than 1" do
|
53
|
-
let(:examples) { 100.times.map{[0,0]}.push([1,0]) }
|
54
|
-
|
55
|
-
it { ad.probability([0]).should == 1 }
|
56
|
-
end
|
57
|
-
|
58
|
-
context "when only anomalies" do
|
59
|
-
let(:examples) { [[0,1]] }
|
60
|
-
|
61
|
-
it "raises error" do
|
62
|
-
expect{ ad }.to raise_error RuntimeError, "Must have at least one non-anomaly"
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
context "when only one non-anomaly" do
|
67
|
-
let(:examples) { [[0,0]] }
|
68
|
-
|
69
|
-
it { ad.eps.should == 1e-1 }
|
70
|
-
end
|
71
|
-
end
|