require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') include Fathom describe MonteCarloSet do before(:all) do @q1_sales = PlausibleRange.new(:min => 10, :max => 20, :hard_lower_bound => 0, :name => "First Quarter Sales") @q1_prices = PlausibleRange.new(:min => 10_000, :max => 12_000, :name => "First Quarter Prices") @q1_sales_commissions = PlausibleRange.new(:min => 0.2, :max => 0.2, :name => "Sales Commission Rate") @q1_gross_margins = ValueDescription.new(@q1_sales, @q1_prices, @q1_sales_commissions) do |random_sample| revenue = (random_sample.first_quarter_sales * random_sample.first_quarter_prices) commissions_paid = random_sample.sales_commission_rate * revenue gross_margins = revenue - commissions_paid {:revenue => revenue, :commissions_paid => commissions_paid, :gross_margins => gross_margins} end @fields = [:commissions_paid, :gross_margins, :revenue] @summary_fields = [:coefficient_of_variation, :lower_bound, :max, :mean, :min, :sd, :upper_bound] end before do @mcs = MonteCarloSet.new(@q1_gross_margins) end it "should initialize with a ValueDescription" do lambda{MonteCarloSet.new}.should raise_error lambda{MonteCarloSet.new(@q1_gross_margins)}.should_not raise_error end it "should expose the value_description" do @mcs.value_description.should eql(@q1_gross_margins) end it "should process with the default number of runs at 10,000", :slow => true do lambda{@mcs.process}.should_not raise_error @mcs.samples_taken.should eql(10_000) end it "should be able to process with a specified number of runs" do @mcs.process(3) @mcs.samples_taken.should eql(3) end it "should define lookup methods for all keys in the result set" do @mcs.process(1) @mcs.revenue.should be_a(GSL::Vector) @mcs.revenue.length.should eql(1) @mcs.commissions_paid.should be_a(GSL::Vector) @mcs.commissions_paid.length.should eql(1) @mcs.gross_margins.should be_a(GSL::Vector) @mcs.gross_margins.length.should eql(1) end it "should be resetable" do @mcs.process(1) @mcs.reset! lambda{@mcs.process(1)}.should_not raise_error end it "should expose the fields from the samples" do @mcs.process(1) sort_array_of_symbols(@mcs.fields).should eql(@fields) end it "should offer a summary of the fields" do @mcs.process(1) summary = @mcs.summary summary.should be_a(Hash) sort_array_of_symbols(summary.keys).should eql(@fields) summary.each do |key, value| value.should be_a(Hash) sort_array_of_symbols(value.keys).should eql(@summary_fields) end end it "should be able to summarize a single field" do @mcs.process(2) summary = @mcs.summary(:revenue) summary.should be_a(Hash) sort_array_of_symbols(summary.keys).should eql(@summary_fields) summary[:coefficient_of_variation].should eql(@mcs.revenue.sd / @mcs.revenue.mean) summary[:max].should eql(@mcs.revenue.max) summary[:min].should eql(@mcs.revenue.min) summary[:sd].should eql(@mcs.revenue.sd) end it "should define summary methods on the object" do @mcs.process(2) @mcs.revenue_summary.should eql(@mcs.summary(:revenue)) end it "should allow the model to be able to produce non-array values" do margin_description = ValueDescription.new(@q1_sales) do |s| { :description => {:first_quarter_sales => s.first_quarter_sales, :first_quarter_prices => s.first_quarter_prices} } end mcs = MonteCarloSet.new(margin_description) lambda{mcs.process(2)}.should_not raise_error lambda{mcs.summary}.should_not raise_error end it "should set the lower bound in the summary to be no more than the minimum (when hard_lower_bound truncates the curve)" do @q1_sales = PlausibleRange.new(:min => 0, :max => 2, :hard_lower_bound => 0, :name => "First Quarter Sales") @q1_prices = PlausibleRange.new(:min => 1, :max => 1, :name => "First Quarter Prices") @q1_sales_commissions = PlausibleRange.new(:min => 0.2, :max => 0.2, :name => "Sales Commission Rate") @q1_gross_margins = ValueDescription.new(@q1_sales, @q1_prices, @q1_sales_commissions) do |random_sample| revenue = (random_sample.first_quarter_sales * random_sample.first_quarter_prices) commissions_paid = random_sample.sales_commission_rate * revenue gross_margins = revenue - commissions_paid {:revenue => revenue, :commissions_paid => commissions_paid, :gross_margins => gross_margins} end @mcs = MonteCarloSet.new(@q1_gross_margins) @mcs.process(5) # This is an environment where the lower bound would usually be below the minimum. # So, the minimum adheres to the hard_lower_bound constraints in the plausible range (tested elsewhere) # and now we're expecting the lower bound here to reflect an actual minimum, or a 5% confidence interval, # whichever is higher. (@mcs.summary[:revenue][:lower_bound] >= @mcs.revenue.min).should be_true end it "should set the upper bound in the summary to be no more than the minimum (when hard_upper_bound truncates the curve)" do @q1_sales = PlausibleRange.new(:min => 0, :max => 2, :hard_upper_bound => 2, :name => "First Quarter Sales") @q1_prices = PlausibleRange.new(:min => 1, :max => 1, :name => "First Quarter Prices") @q1_sales_commissions = PlausibleRange.new(:min => 0.2, :max => 0.2, :name => "Sales Commission Rate") @q1_gross_margins = ValueDescription.new(@q1_sales, @q1_prices, @q1_sales_commissions) do |random_sample| revenue = (random_sample.first_quarter_sales * random_sample.first_quarter_prices) commissions_paid = random_sample.sales_commission_rate * revenue gross_margins = revenue - commissions_paid {:revenue => revenue, :commissions_paid => commissions_paid, :gross_margins => gross_margins} end @mcs = MonteCarloSet.new(@q1_gross_margins) @mcs.process(5) # This is an environment where the upper bound would usually be above the maximum # So, the maximum adheres to the hard_upper_bound constraints in the plausible range (tested elsewhere) # and now we're expecting the upper bound here to reflect an actual maximum, or a 95% confidence interval, # whichever is higher. (@mcs.summary[:revenue][:upper_bound] <= @mcs.revenue.max).should be_true end end def sort_array_of_symbols(array) array.map {|e| e.to_s}.sort.map {|e| e.to_sym} end