fathom 0.3.7 → 0.5.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.
- data/.autotest +7 -5
- data/.document +2 -2
- data/Gemfile +9 -10
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.md +29 -90
- data/Rakefile +34 -32
- data/VERSION +1 -1
- data/fathom.gemspec +105 -0
- data/features/fathom.feature +26 -0
- data/features/step_definitions/fathom_steps.rb +23 -0
- data/features/support/env.rb +13 -0
- data/lib/ext/array.rb +6 -2
- data/lib/ext/string.rb +86 -7
- data/lib/fathom.rb +51 -88
- data/lib/fathom/behaviors/attribute_system.rb +91 -0
- data/lib/fathom/behaviors/context_behavior.rb +28 -0
- data/lib/fathom/behaviors/plugins.rb +16 -0
- data/lib/fathom/contexts/network_population.rb +47 -0
- data/lib/fathom/contexts/network_traversal.rb +4 -0
- data/lib/fathom/data/adjacency_matrix.rb +27 -0
- data/lib/fathom/data/definition.rb +22 -0
- data/lib/fathom/data/edge.rb +58 -0
- data/lib/fathom/data/network.rb +35 -0
- data/lib/fathom/data/outcome.rb +30 -0
- data/lib/fathom/data/property.rb +31 -0
- data/lib/fathom/data/variable.rb +59 -0
- data/lib/fathom/roles/general_graph_tools.rb +87 -0
- data/lib/fathom/roles/network_builder.rb +61 -0
- data/spec/fathom/behaviors/attribute_system_spec.rb +141 -0
- data/spec/fathom/behaviors/context_behavior_spec.rb +15 -0
- data/spec/fathom/behaviors/plugins_spec.rb +80 -0
- data/spec/fathom/contexts/network_population_spec.rb +55 -0
- data/spec/fathom/contexts/network_traversal_spec.rb +11 -0
- data/spec/fathom/data/adjacency_matrix_spec.rb +42 -0
- data/spec/fathom/data/definition_spec.rb +19 -0
- data/spec/fathom/data/edge_spec.rb +77 -0
- data/spec/fathom/data/network_spec.rb +72 -0
- data/spec/fathom/data/outcome_spec.rb +17 -0
- data/spec/fathom/data/property_spec.rb +17 -0
- data/spec/fathom/data/variable_spec.rb +101 -0
- data/spec/fathom/ext/array_spec.rb +17 -0
- data/spec/fathom/ext/string_spec.rb +90 -0
- data/spec/fathom/roles/general_graph_tools_spec.rb +95 -0
- data/spec/fathom/roles/network_builder_spec.rb +90 -0
- data/spec/fathom_spec.rb +28 -49
- data/spec/spec_helper.rb +7 -11
- data/spec/support/context_behavior.rb +14 -0
- data/spec/support/custom_matchers.rb +12 -0
- data/spec/support/files.rb +8 -0
- data/spec/support/network.yml +42 -0
- metadata +133 -174
- data/.bundle/config +0 -2
- data/.gitignore +0 -6
- data/Gemfile.lock +0 -42
- data/TODO.md +0 -127
- data/autotest/discover.rb +0 -1
- data/lib/ext/faster_csv.rb +0 -1
- data/lib/ext/open_struct.rb +0 -17
- data/lib/fathom/agent.rb +0 -48
- data/lib/fathom/agent/agent_cluster.rb +0 -23
- data/lib/fathom/agent/properties.rb +0 -48
- data/lib/fathom/archive/causal_graph.rb +0 -12
- data/lib/fathom/archive/concept.rb +0 -83
- data/lib/fathom/archive/conditional_probability_matrix.rb +0 -119
- data/lib/fathom/archive/inverter.rb +0 -20
- data/lib/fathom/archive/n2.rb +0 -198
- data/lib/fathom/archive/n3.rb +0 -119
- data/lib/fathom/archive/node.rb +0 -97
- data/lib/fathom/archive/noodle.rb +0 -136
- data/lib/fathom/archive/scratch.rb +0 -45
- data/lib/fathom/distributions.rb +0 -8
- data/lib/fathom/distributions/discrete_gaussian.rb +0 -44
- data/lib/fathom/distributions/discrete_uniform.rb +0 -25
- data/lib/fathom/distributions/gaussian.rb +0 -46
- data/lib/fathom/distributions/uniform.rb +0 -35
- data/lib/fathom/import.rb +0 -85
- data/lib/fathom/import/csv_import.rb +0 -59
- data/lib/fathom/import/import_node.rb +0 -17
- data/lib/fathom/import/yaml_import.rb +0 -74
- data/lib/fathom/knowledge_base.rb +0 -46
- data/lib/fathom/knowledge_base/search.rb +0 -19
- data/lib/fathom/monte_carlo_set.rb +0 -152
- data/lib/fathom/node.rb +0 -139
- data/lib/fathom/node/belief_node.rb +0 -121
- data/lib/fathom/node/cpm_node.rb +0 -100
- data/lib/fathom/node/data_collection.rb +0 -97
- data/lib/fathom/node/data_node.rb +0 -22
- data/lib/fathom/node/decision.rb +0 -11
- data/lib/fathom/node/discrete_node.rb +0 -41
- data/lib/fathom/node/fact.rb +0 -24
- data/lib/fathom/node/mc_node.rb +0 -70
- data/lib/fathom/node/node_extensions/enforced_name.rb +0 -12
- data/lib/fathom/node/node_extensions/numeric_methods.rb +0 -68
- data/lib/fathom/node/plausible_range.rb +0 -98
- data/lib/fathom/simulation.rb +0 -59
- data/lib/fathom/simulation/tick_methods.rb +0 -25
- data/lib/fathom/simulation/tick_simulation.rb +0 -12
- data/lib/fathom/value_description.rb +0 -79
- data/lib/options_hash.rb +0 -186
- data/spec/ext/array_spec.rb +0 -10
- data/spec/ext/faster_csv_spec.rb +0 -10
- data/spec/ext/open_struct_spec.rb +0 -20
- data/spec/ext/string_spec.rb +0 -7
- data/spec/fathom/agent/agent_cluster_spec.rb +0 -17
- data/spec/fathom/agent_spec.rb +0 -51
- data/spec/fathom/distributions/discrete_gaussian_spec.rb +0 -64
- data/spec/fathom/distributions/discrete_uniform_spec.rb +0 -0
- data/spec/fathom/distributions/gaussian_spec.rb +0 -64
- data/spec/fathom/distributions/uniform_spec.rb +0 -0
- data/spec/fathom/import/csv_import_spec.rb +0 -52
- data/spec/fathom/import/import_node_spec.rb +0 -10
- data/spec/fathom/import/yaml_import_spec.rb +0 -73
- data/spec/fathom/import_spec.rb +0 -36
- data/spec/fathom/knowledge_base_spec.rb +0 -20
- data/spec/fathom/monte_carlo_set_spec.rb +0 -149
- data/spec/fathom/node/belief_node_spec.rb +0 -180
- data/spec/fathom/node/cpm_node_spec.rb +0 -144
- data/spec/fathom/node/data_collection_spec.rb +0 -26
- data/spec/fathom/node/data_node_spec.rb +0 -102
- data/spec/fathom/node/decision_spec.rb +0 -15
- data/spec/fathom/node/discrete_node_spec.rb +0 -56
- data/spec/fathom/node/fact_spec.rb +0 -33
- data/spec/fathom/node/mc_node_spec.rb +0 -66
- data/spec/fathom/node/node_extensions/enforced_name_spec.rb +0 -15
- data/spec/fathom/node/node_extensions/numeric_methods_spec.rb +0 -124
- data/spec/fathom/node/plausible_range_spec.rb +0 -151
- data/spec/fathom/node_spec.rb +0 -172
- data/spec/fathom/simulation/tick_simulation_spec.rb +0 -32
- data/spec/fathom/simulation_spec.rb +0 -24
- data/spec/fathom/value_description_spec.rb +0 -70
- data/spec/support/demo.yml +0 -17
- data/spec/support/demo_agent.rb +0 -8
- data/spec/support/dummy_numeric_node.rb +0 -8
- data/spec/support/fact.yml +0 -11
|
File without changes
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom::Distributions
|
|
4
|
-
|
|
5
|
-
describe Gaussian do
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
@opts = {:mean => 0, :sd => 0.1, :confidence_interval => 0.05}
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
it "should provide a GSL::Rng through rng" do
|
|
12
|
-
Gaussian.rng.should be_a(GSL::Rng)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "should be able to generate a random variable through GSL::Rng#gaussian" do
|
|
16
|
-
Gaussian.rng.should_receive(:gaussian).and_return(0.5)
|
|
17
|
-
Gaussian.rand(1)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "should be able to generate an inverse CDF" do
|
|
21
|
-
Gaussian.inverse_cdf(@opts).should be_within(0.00001).of(-0.16448)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it "should require mean as an option for an inverse CDF" do
|
|
25
|
-
@opts.delete(:mean)
|
|
26
|
-
lambda{Gaussian.inverse_cdf(@opts)}.should raise_error
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
it "should require a Gaussian deviation for an inverse CDF" do
|
|
30
|
-
@opts.delete(:sd)
|
|
31
|
-
lambda{Gaussian.inverse_cdf(@opts)}.should raise_error
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it "should take std as an alias for sd when creating an inverse CDF" do
|
|
35
|
-
@opts.delete(:sd)
|
|
36
|
-
Gaussian.inverse_cdf(@opts.merge(:std => 0.1)).should be_within(0.00001).of(-0.16448)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
it "should take standard_deviation as an alias for sd when creating an inverse CDF" do
|
|
40
|
-
@opts.delete(:sd)
|
|
41
|
-
Gaussian.inverse_cdf(@opts.merge(:standard_deviation => 0.1)).should be_within(0.00001).of(-0.16448)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
it "should be able to set upper to true and get Q instead of P" do
|
|
45
|
-
Gaussian.inverse_cdf(@opts.merge(:upper =>true)).should be_within(0.00001).of(0.16448)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "should be able to take a different confidence interval" do
|
|
49
|
-
Gaussian.inverse_cdf(@opts.merge(:confidence_interval => 0.1)).should be_within(0.00001).of(-0.12815)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
it "should have a lower_bound alias for inverse_cdf" do
|
|
53
|
-
Gaussian.lower_bound(@opts).should eql(Gaussian.inverse_cdf(@opts))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "should have an upper_bound shortcut for inverse_cdf(:upper => true, ...)" do
|
|
57
|
-
Gaussian.upper_bound(@opts).should eql(Gaussian.inverse_cdf(@opts.merge(:upper => true)))
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it "should provide interval values, an array of the lower and upper bounds" do
|
|
61
|
-
Gaussian.interval_values(@opts.merge(:confidence_interval => 0.9)).should eql([Gaussian.lower_bound(@opts), Gaussian.upper_bound(@opts)])
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
File without changes
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom
|
|
4
|
-
|
|
5
|
-
describe CSVImport do
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
@content =<<-END
|
|
9
|
-
this,and,that
|
|
10
|
-
1,2,3
|
|
11
|
-
4,5,6
|
|
12
|
-
7,8,9
|
|
13
|
-
END
|
|
14
|
-
|
|
15
|
-
@opts = {:content => @content}
|
|
16
|
-
@ci = CSVImport.new(@opts)
|
|
17
|
-
@result = @ci.import
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "should not work unless content is set" do
|
|
21
|
-
lambda{CSVImport.new.import}.should raise_error(NoMethodError)
|
|
22
|
-
lambda{CSVImport.new(@opts)}.should_not raise_error
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
it "should return the ImportNode as the result" do
|
|
26
|
-
@result.should be_a(ImportNode)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
it "should create as many data nodes as there are columns" do
|
|
30
|
-
@result.children.size.should eql(3)
|
|
31
|
-
@result.children.each {|dn| dn.should be_a(DataNode)}
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it "should import the values from each column into each data node" do
|
|
35
|
-
@result.this.values.should eql([1,4,7])
|
|
36
|
-
@result.and.values.should eql([2,5,8])
|
|
37
|
-
@result.that.values.should eql([3,6,9])
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# KB Override
|
|
41
|
-
# it "should store the imported values in the knowledge base" do
|
|
42
|
-
# Fathom.knowledge_base[:this].should be_a(DataNode)
|
|
43
|
-
# Fathom.kb[:this].values.should eql([1,4,7])
|
|
44
|
-
# end
|
|
45
|
-
|
|
46
|
-
# KB Override
|
|
47
|
-
# it "should import from the class level" do
|
|
48
|
-
# CSVImport.import(@opts)
|
|
49
|
-
# Fathom.knowledge_base[:this].should be_a(DataNode)
|
|
50
|
-
# Fathom.kb[:this].values.should eql([1,4,7])
|
|
51
|
-
# end
|
|
52
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom
|
|
4
|
-
|
|
5
|
-
describe YAMLImport do
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
@demo_yaml_location = File.expand_path(File.dirname(__FILE__) + "/../../support/demo.yml")
|
|
9
|
-
@demo_yaml = open(@demo_yaml_location).read
|
|
10
|
-
@opts = {:content => @demo_yaml}
|
|
11
|
-
@yi = YAMLImport.new(@opts)
|
|
12
|
-
@result = @yi.import
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "should not work unless content is set" do
|
|
16
|
-
lambda{YAMLImport.new.import}.should raise_error
|
|
17
|
-
lambda{YAMLImport.new(@opts)}.should_not raise_error
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "should create an ImportNode as a return value" do
|
|
21
|
-
@result.should be_an(ImportNode)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it "should create PlausibleRange nodes for any hashes with at least a min and max key in it" do
|
|
25
|
-
@result.co2_emissions.should_not be_nil
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it "should not create a PlausibleRange for entries missing min and max" do
|
|
29
|
-
@result.should_not respond_to(:invalid_hash)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it "should be able to create a PlausibleRange with more complete information" do
|
|
33
|
-
@result.more_complete_range.ci.should eql(0.6)
|
|
34
|
-
@result.more_complete_range.description.should eql('Some good description')
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
it "should create DataNodes for entries that have an array of information" do
|
|
38
|
-
@result.co2_readings.should be_a(DataNode)
|
|
39
|
-
@result.co2_readings.values.should eql([10,20,30])
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# KB Override
|
|
43
|
-
# it "should store the imported values in the knowledge base" do
|
|
44
|
-
# Fathom.knowledge_base['CO2 Emissions'].should be_a(PlausibleRange)
|
|
45
|
-
# Fathom.kb['CO2 Emissions'].min.should eql(1_000_000)
|
|
46
|
-
# end
|
|
47
|
-
|
|
48
|
-
# KB Override
|
|
49
|
-
# it "should import from the class level" do
|
|
50
|
-
# YAMLImport.import(@opts)
|
|
51
|
-
# Fathom.knowledge_base['CO2 Emissions'].should be_a(PlausibleRange)
|
|
52
|
-
# Fathom.kb['CO2 Emissions'].min.should eql(1_000_000)
|
|
53
|
-
# end
|
|
54
|
-
|
|
55
|
-
it "should not complain if there's an empty YAML file" do
|
|
56
|
-
filename = '/tmp/empty_yaml_test.yml'
|
|
57
|
-
File.open(filename, 'w') {|f| f.puts ''}
|
|
58
|
-
lambda{YAMLImport.import(:content => filename)}.should_not raise_error
|
|
59
|
-
`rm -rf #{filename}`
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it "should create a Fact if it is an object named fact and has a name and value function" do
|
|
63
|
-
filename = File.expand_path(File.dirname(__FILE__) + "/../../support/fact.yml")
|
|
64
|
-
yaml = open(filename).read
|
|
65
|
-
opts = {:content => yaml}
|
|
66
|
-
yi = YAMLImport.new(opts)
|
|
67
|
-
result = yi.import
|
|
68
|
-
result.one.value.should eql(1)
|
|
69
|
-
result.two.value.should eql(2)
|
|
70
|
-
result.three.value.should eql(3)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
end
|
data/spec/fathom/import_spec.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom
|
|
4
|
-
|
|
5
|
-
describe Import do
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
@content = 'some content'
|
|
9
|
-
@options = OptionsHash.new({:content => @content})
|
|
10
|
-
@i = Import.new(@options)
|
|
11
|
-
@values = [1,2,3,4,5]
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
it "should initialize with optional content" do
|
|
15
|
-
@i.content.should eql(@content)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
it "should record the initialization options" do
|
|
19
|
-
@i.options.should eql(@options)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
it "should have a class-level import method" do
|
|
23
|
-
Import.should be_respond_to(:import)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it "should create an import node to attach its imports to in the knowledge base" do
|
|
27
|
-
@i.import_node.should be_a(ImportNode)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
it "should pass its options to the import node for that node to record the parts it is interested in." do
|
|
31
|
-
i = Import.new(:name => "New Import", :content => @content, :description => "This gets passed along too.")
|
|
32
|
-
i.import_node.name.should eql("New Import")
|
|
33
|
-
i.import_node.description.should eql("This gets passed along too.")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom
|
|
4
|
-
|
|
5
|
-
describe KnowledgeBase do
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
@kb = KnowledgeBase.new
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
it "should have a find accessor" do
|
|
12
|
-
KnowledgeBase.should respond_to(:find)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# it "should be able to add a node" do
|
|
16
|
-
# @dn = DataNode.new(:name => :new_node, :values => [1,2,3])
|
|
17
|
-
# @kb[:new_node] = @dn
|
|
18
|
-
# @kb[:new_node].should eql(@dn)
|
|
19
|
-
# end
|
|
20
|
-
end
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom
|
|
4
|
-
|
|
5
|
-
describe MonteCarloSet do
|
|
6
|
-
|
|
7
|
-
before(:all) do
|
|
8
|
-
@q1_sales = PlausibleRange.new(:min => 10, :max => 20, :hard_lower_bound => 0, :name => "First Quarter Sales")
|
|
9
|
-
@q1_prices = PlausibleRange.new(:min => 10_000, :max => 12_000, :name => "First Quarter Prices")
|
|
10
|
-
@q1_sales_commissions = PlausibleRange.new(:min => 0.2, :max => 0.2, :name => "Sales Commission Rate")
|
|
11
|
-
|
|
12
|
-
@q1_gross_margins = ValueDescription.new(@q1_sales, @q1_prices, @q1_sales_commissions) do |random_sample|
|
|
13
|
-
revenue = (random_sample.first_quarter_sales * random_sample.first_quarter_prices)
|
|
14
|
-
commissions_paid = random_sample.sales_commission_rate * revenue
|
|
15
|
-
gross_margins = revenue - commissions_paid
|
|
16
|
-
{:revenue => revenue, :commissions_paid => commissions_paid, :gross_margins => gross_margins}
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
@fields = [:commissions_paid, :gross_margins, :revenue]
|
|
20
|
-
@summary_fields = [:coefficient_of_variation, :lower_bound, :max, :mean, :min, :sd, :upper_bound]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
before do
|
|
24
|
-
@mcs = MonteCarloSet.new(@q1_gross_margins)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
it "should initialize with a ValueDescription" do
|
|
28
|
-
lambda{MonteCarloSet.new}.should raise_error
|
|
29
|
-
lambda{MonteCarloSet.new(@q1_gross_margins)}.should_not raise_error
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it "should expose the value_description" do
|
|
33
|
-
@mcs.value_description.should eql(@q1_gross_margins)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
it "should process with the default number of runs at 10,000", :slow => true do
|
|
37
|
-
lambda{@mcs.process}.should_not raise_error
|
|
38
|
-
@mcs.samples_taken.should eql(10_000)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
it "should be able to process with a specified number of runs" do
|
|
42
|
-
@mcs.process(3)
|
|
43
|
-
@mcs.samples_taken.should eql(3)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
it "should define lookup methods for all keys in the result set" do
|
|
47
|
-
@mcs.process(1)
|
|
48
|
-
@mcs.revenue.should be_a(GSL::Vector)
|
|
49
|
-
@mcs.revenue.length.should eql(1)
|
|
50
|
-
@mcs.commissions_paid.should be_a(GSL::Vector)
|
|
51
|
-
@mcs.commissions_paid.length.should eql(1)
|
|
52
|
-
@mcs.gross_margins.should be_a(GSL::Vector)
|
|
53
|
-
@mcs.gross_margins.length.should eql(1)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "should be resetable" do
|
|
57
|
-
@mcs.process(1)
|
|
58
|
-
@mcs.reset!
|
|
59
|
-
lambda{@mcs.process(1)}.should_not raise_error
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it "should expose the fields from the samples" do
|
|
63
|
-
@mcs.process(1)
|
|
64
|
-
sort_array_of_symbols(@mcs.fields).should eql(@fields)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
it "should offer a summary of the fields" do
|
|
68
|
-
@mcs.process(1)
|
|
69
|
-
summary = @mcs.summary
|
|
70
|
-
summary.should be_a(Hash)
|
|
71
|
-
sort_array_of_symbols(summary.keys).should eql(@fields)
|
|
72
|
-
summary.each do |key, value|
|
|
73
|
-
value.should be_a(Hash)
|
|
74
|
-
sort_array_of_symbols(value.keys).should eql(@summary_fields)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "should be able to summarize a single field" do
|
|
79
|
-
@mcs.process(2)
|
|
80
|
-
summary = @mcs.summary(:revenue)
|
|
81
|
-
summary.should be_a(Hash)
|
|
82
|
-
sort_array_of_symbols(summary.keys).should eql(@summary_fields)
|
|
83
|
-
summary[:coefficient_of_variation].should eql(@mcs.revenue.sd / @mcs.revenue.mean)
|
|
84
|
-
summary[:max].should eql(@mcs.revenue.max)
|
|
85
|
-
summary[:min].should eql(@mcs.revenue.min)
|
|
86
|
-
summary[:sd].should eql(@mcs.revenue.sd)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "should define summary methods on the object" do
|
|
90
|
-
@mcs.process(2)
|
|
91
|
-
@mcs.revenue_summary.should eql(@mcs.summary(:revenue))
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it "should allow the model to be able to produce non-array values" do
|
|
95
|
-
margin_description = ValueDescription.new(@q1_sales) do |s|
|
|
96
|
-
{
|
|
97
|
-
:description => {:first_quarter_sales => s.first_quarter_sales, :first_quarter_prices => s.first_quarter_prices}
|
|
98
|
-
}
|
|
99
|
-
end
|
|
100
|
-
mcs = MonteCarloSet.new(margin_description)
|
|
101
|
-
lambda{mcs.process(2)}.should_not raise_error
|
|
102
|
-
lambda{mcs.summary}.should_not raise_error
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it "should set the lower bound in the summary to be no more than the minimum (when hard_lower_bound truncates the curve)" do
|
|
106
|
-
@q1_sales = PlausibleRange.new(:min => 0, :max => 2, :hard_lower_bound => 0, :name => "First Quarter Sales")
|
|
107
|
-
@q1_prices = PlausibleRange.new(:min => 1, :max => 1, :name => "First Quarter Prices")
|
|
108
|
-
@q1_sales_commissions = PlausibleRange.new(:min => 0.2, :max => 0.2, :name => "Sales Commission Rate")
|
|
109
|
-
|
|
110
|
-
@q1_gross_margins = ValueDescription.new(@q1_sales, @q1_prices, @q1_sales_commissions) do |random_sample|
|
|
111
|
-
revenue = (random_sample.first_quarter_sales * random_sample.first_quarter_prices)
|
|
112
|
-
commissions_paid = random_sample.sales_commission_rate * revenue
|
|
113
|
-
gross_margins = revenue - commissions_paid
|
|
114
|
-
{:revenue => revenue, :commissions_paid => commissions_paid, :gross_margins => gross_margins}
|
|
115
|
-
end
|
|
116
|
-
@mcs = MonteCarloSet.new(@q1_gross_margins)
|
|
117
|
-
@mcs.process(5)
|
|
118
|
-
# This is an environment where the lower bound would usually be below the minimum.
|
|
119
|
-
# So, the minimum adheres to the hard_lower_bound constraints in the plausible range (tested elsewhere)
|
|
120
|
-
# and now we're expecting the lower bound here to reflect an actual minimum, or a 5% confidence interval,
|
|
121
|
-
# whichever is higher.
|
|
122
|
-
(@mcs.summary[:revenue][:lower_bound] >= @mcs.revenue.min).should be_true
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
it "should set the upper bound in the summary to be no more than the minimum (when hard_upper_bound truncates the curve)" do
|
|
126
|
-
@q1_sales = PlausibleRange.new(:min => 0, :max => 2, :hard_upper_bound => 2, :name => "First Quarter Sales")
|
|
127
|
-
@q1_prices = PlausibleRange.new(:min => 1, :max => 1, :name => "First Quarter Prices")
|
|
128
|
-
@q1_sales_commissions = PlausibleRange.new(:min => 0.2, :max => 0.2, :name => "Sales Commission Rate")
|
|
129
|
-
|
|
130
|
-
@q1_gross_margins = ValueDescription.new(@q1_sales, @q1_prices, @q1_sales_commissions) do |random_sample|
|
|
131
|
-
revenue = (random_sample.first_quarter_sales * random_sample.first_quarter_prices)
|
|
132
|
-
commissions_paid = random_sample.sales_commission_rate * revenue
|
|
133
|
-
gross_margins = revenue - commissions_paid
|
|
134
|
-
{:revenue => revenue, :commissions_paid => commissions_paid, :gross_margins => gross_margins}
|
|
135
|
-
end
|
|
136
|
-
@mcs = MonteCarloSet.new(@q1_gross_margins)
|
|
137
|
-
@mcs.process(5)
|
|
138
|
-
# This is an environment where the upper bound would usually be above the maximum
|
|
139
|
-
# So, the maximum adheres to the hard_upper_bound constraints in the plausible range (tested elsewhere)
|
|
140
|
-
# and now we're expecting the upper bound here to reflect an actual maximum, or a 95% confidence interval,
|
|
141
|
-
# whichever is higher.
|
|
142
|
-
(@mcs.summary[:revenue][:upper_bound] <= @mcs.revenue.max).should be_true
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def sort_array_of_symbols(array)
|
|
148
|
-
array.map {|e| e.to_s}.sort.map {|e| e.to_sym}
|
|
149
|
-
end
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
|
2
|
-
|
|
3
|
-
include Fathom
|
|
4
|
-
|
|
5
|
-
describe BeliefNode do
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
@bn = BeliefNode.new(:labels => [:red, :green, :blue])
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
it "should be a Node" do
|
|
12
|
-
BeliefNode.ancestors.should be_include(Node)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "should be a DiscreteNode as well" do
|
|
16
|
-
BeliefNode.ancestors.should be_include(DiscreteNode)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it "should initialize with a uniform probability distribution" do
|
|
20
|
-
@bn.probabilities.should ==(GSL::Vector[1.0/3, 1.0/3, 1.0/3])
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it "should allow a pre-defined probability distribution" do
|
|
24
|
-
@bn = BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [0.1, 0.2, 0.7])
|
|
25
|
-
@bn.probabilities.should ==(GSL::Vector[0.1, 0.2, 0.7])
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it "should raise an error if the supplied probability distribution is of the wrong size" do
|
|
29
|
-
lambda{BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [0.1, 0.2, 0.7, 0.1])}.should raise_error(/Probabilities must be 3/)
|
|
30
|
-
lambda{BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [0.1, 0.2])}.should raise_error(/Probabilities must be 3/)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
it "should normalize even the supplied probabilities" do
|
|
34
|
-
@bn = BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [1, 2, 7])
|
|
35
|
-
@bn.probabilities.should ==(GSL::Vector[0.1, 0.2, 0.7])
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
it "should initialize with a uniform likelihood vector" do
|
|
39
|
-
@bn.likelihoods.should ==(GSL::Vector[1,1,1])
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "should allow pre-defined likelihoods to be supplied" do
|
|
43
|
-
@bn = BeliefNode.new(:labels => [:red, :green, :blue], :likelihoods => [1,1,2])
|
|
44
|
-
@bn.likelihoods.should ==(GSL::Vector[1,1,2])
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it "should raise an error if the supplied likelihoods are the wrong size" do
|
|
48
|
-
lambda{BeliefNode.new(:labels => [:red, :green, :blue], :likelihoods => [1,1,2,1])}.should raise_error(/Likelihoods must be 3/)
|
|
49
|
-
lambda{BeliefNode.new(:labels => [:red, :green, :blue], :likelihoods => [1,1])}.should raise_error(/Likelihoods must be 3/)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
it "should establish a precision threshold at 1.0e-05" do
|
|
53
|
-
@bn.precision_threshold.should eql(0.00001)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "should allow the precision_threshold as a parameter" do
|
|
57
|
-
@bn = BeliefNode.new(:labels => [:red, :green, :blue], :precision_threshold => 0.01)
|
|
58
|
-
@bn.precision_threshold.should eql(0.01)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
it "should allow a initialization from a values hash" do
|
|
62
|
-
@bn = BeliefNode.new(:values => {:red => 0.1, :green => 0.2, :yellow => 0.3, :blue => 0.4})
|
|
63
|
-
@bn.probabilities.to_a.sort.should eql([0.1, 0.2, 0.3, 0.4])
|
|
64
|
-
@bn.labels.map(&:to_s).sort.should eql(%w(blue green red yellow))
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
context "CPMNodes" do
|
|
68
|
-
before do
|
|
69
|
-
@b1 = BeliefNode.new(:name => :person, :values => {:david => 0.4, :adina => 0.6})
|
|
70
|
-
@b2 = BeliefNode.new(:name => :movie_preference, :values => {:drama => 0.5, :comedy => 0.4, :romance => 0.1})
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it "should automatically create an intermediary cpm when adding a child" do
|
|
74
|
-
@b1.add_child(@b2)
|
|
75
|
-
@b1.children.first.should be_a(CPMNode)
|
|
76
|
-
@b1.children.first.child.should eql(@b2)
|
|
77
|
-
@b2.parents.first.should be_a(CPMNode)
|
|
78
|
-
@b2.parents.first.parent.should eql(@b1)
|
|
79
|
-
@cpm = @b1.children.first
|
|
80
|
-
@cpm.should eql(@b2.parents.first)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
it "should create an accessor for the child, rather than the cpm" do
|
|
84
|
-
@b1.add_child(@b2)
|
|
85
|
-
@b1.should_not be_respond_to(:cpm)
|
|
86
|
-
@b1.should respond_to(:movie_preference)
|
|
87
|
-
@b1.movie_preference.should eql(@b2)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
it "should create a cpm_for_* accessor for the cpm" do
|
|
91
|
-
@b1.add_child(@b2)
|
|
92
|
-
@b1.should respond_to(:cpm_for_movie_preference)
|
|
93
|
-
@b1.cpm_for_movie_preference.should eql(@b1.children.first)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
it "should create a cpm_for_* accessor for the child" do
|
|
97
|
-
@b1.add_child(@b2)
|
|
98
|
-
@b2.should respond_to(:cpm_for_person)
|
|
99
|
-
@b2.cpm_for_person.should eql(@b2.parents.first)
|
|
100
|
-
@b2.cpm_for_person.should eql(@b1.children.first)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it "should automatically create an intermediary cpm when adding a parent" do
|
|
104
|
-
@b2.add_parent(@b1)
|
|
105
|
-
@b1.children.first.should be_a(CPMNode)
|
|
106
|
-
@b1.children.first.child.should eql(@b2)
|
|
107
|
-
@b2.parents.first.should be_a(CPMNode)
|
|
108
|
-
@b2.parents.first.parent.should eql(@b1)
|
|
109
|
-
@cpm = @b1.children.first
|
|
110
|
-
@cpm.should eql(@b2.parents.first)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
it "should create an accessor for the parent, rather than the cpm" do
|
|
114
|
-
@b2.add_parent(@b1)
|
|
115
|
-
@b2.should_not be_respond_to(:cpm)
|
|
116
|
-
@b2.should respond_to(:person)
|
|
117
|
-
@b2.person.should eql(@b1)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
it "should create a cpm_for_* accessor for the cpm" do
|
|
121
|
-
@b2.add_parent(@b1)
|
|
122
|
-
@b2.should respond_to(:cpm_for_person)
|
|
123
|
-
@b2.cpm_for_person.should eql(@b2.parents.first)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
it "should create an inspect string that reflects the grandchild, rather than the cpm" do
|
|
127
|
-
@b1.add_child(@b2)
|
|
128
|
-
@b1.inspect.should eql("Fathom::BeliefNode: person, children:, [\"movie_preference (Fathom::BeliefNode)\", \"movie_preference (Fathom::BeliefNode)\"], parents: , []")
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
it "should create an inspect string that reflects the grandparent, rather than the cpm" do
|
|
132
|
-
@b2.add_parent(@b1)
|
|
133
|
-
@b2.inspect.should eql("Fathom::BeliefNode: movie_preference, children:, [], parents: , [\"person (Fathom::BeliefNode)\", \"person (Fathom::BeliefNode)\"]")
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
it "should create a cpm_for_* accessor for the parent" do
|
|
137
|
-
@b2.add_parent(@b1)
|
|
138
|
-
@b1.should respond_to(:cpm_for_movie_preference)
|
|
139
|
-
@b1.cpm_for_movie_preference.should eql(@b1.children.first)
|
|
140
|
-
@b1.cpm_for_movie_preference.should eql(@b2.parents.first)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
context "Belief and Propagation" do
|
|
146
|
-
|
|
147
|
-
before do
|
|
148
|
-
|
|
149
|
-
@season = BeliefNode.new(:name => :season, :labels => [:winter, :spring, :summer, :fall])
|
|
150
|
-
@sprinkler = BeliefNode.new(:name => :sprinkler)
|
|
151
|
-
@rain = BeliefNode.new(:name => :rain)
|
|
152
|
-
@wet = BeliefNode.new(:name => :wet)
|
|
153
|
-
@slippery = BeliefNode.new(:name => :slippery)
|
|
154
|
-
|
|
155
|
-
@season.add_child(@sprinkler)
|
|
156
|
-
@sprinkler.add_child(@wet)
|
|
157
|
-
@season.add_child(@rain)
|
|
158
|
-
@rain.add_child(@wet)
|
|
159
|
-
@wet.add_child(@slippery)
|
|
160
|
-
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
it "should provide the likelihood for a particular value" do
|
|
164
|
-
@sprinkler.should be_respond_to(:likelihood)
|
|
165
|
-
output = @sprinkler.likelihood(:true)
|
|
166
|
-
output.should be_a(OpenStruct)
|
|
167
|
-
# @sprinkler.parents.map(&:name_sym).each {|key| output.table.keys.should be_include(key)}
|
|
168
|
-
# output.
|
|
169
|
-
# output.table.keys.should eql([:matrix, :parents])
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# What's next:
|
|
174
|
-
# Deal with multiple parents and the matrix that comes in
|
|
175
|
-
# Propagate the likelihoods and probabilities up and down
|
|
176
|
-
# Calculate the belief
|
|
177
|
-
# Update the information directly on the node and propagate
|
|
178
|
-
# Receive an update from a child or parent and propagate
|
|
179
|
-
# Adhere to the precision_threshold when propagating
|
|
180
|
-
end
|