├── spec
├── fixtures
│ ├── empty_file.pmml
│ ├── logistic_regression.pmml
│ ├── gbm_tree.pmml
│ ├── decision_tree.pmml
│ ├── binary_split_decision_tree.pmml
│ ├── naive_bayes.pmml
│ ├── cars_rf.pmml
│ ├── titanic_gbm.pmml
│ └── titanic_gbm_4_2.pmml
├── spec_helper.rb
└── scoruby
│ ├── features_spec.rb
│ ├── models
│ ├── logistic_regression
│ │ ├── model_spec.rb
│ │ └── data_spec.rb
│ ├── random_forest
│ │ ├── data_spec.rb
│ │ └── model_spec.rb
│ ├── gradient_boosted_model
│ │ ├── data_spec.rb
│ │ └── model_spec.rb
│ ├── naive_bayes
│ │ └── naive_bayes_spec.rb
│ └── decision_tree_spec.rb
│ ├── predicates
│ ├── simple_set_predicate_spec.rb
│ ├── simple_predicate_spec.rb
│ └── compound_predicate_spec.rb
│ └── model_factory_spec.rb
├── .gitignore
├── .rspec
├── .codeclimate.yml
├── .travis.yml
├── lib
├── scoruby
│ ├── version.rb
│ ├── predicates
│ │ ├── true_predicate.rb
│ │ ├── false_predicate.rb
│ │ ├── simple_set_predicate.rb
│ │ ├── compound_predicate.rb
│ │ └── simple_predicate.rb
│ ├── features.rb
│ ├── predicate_factory.rb
│ ├── models
│ │ ├── logistic_regression
│ │ │ ├── model.rb
│ │ │ └── data.rb
│ │ ├── gradient_boosted_model
│ │ │ ├── model.rb
│ │ │ └── data.rb
│ │ ├── decision_tree.rb
│ │ ├── random_forest
│ │ │ ├── data.rb
│ │ │ └── model.rb
│ │ └── naive_bayes
│ │ │ ├── model_data.rb
│ │ │ └── model.rb
│ ├── node.rb
│ ├── decision.rb
│ └── model_factory.rb
└── scoruby.rb
├── Gemfile
├── bin
├── setup
└── console
├── .rubocop.yml
├── Rakefile
├── LICENSE.txt
├── scoruby.gemspec
├── CODE_OF_CONDUCT.md
├── Gemfile.lock
└── README.md
/spec/fixtures/empty_file.pmml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | coverage
3 |
4 | *.log
5 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | rubocop:
3 | enabled: true
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.3.8
4 | - 2.4.6
5 | - 2.5.5
6 | - 2.6.3
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/scoruby/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | VERSION = '0.3.4'
5 | end
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in scoruby.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | bundle install
6 |
7 | # Do any other automated setup that you need to do here
8 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | Metrics/BlockLength:
2 | ExcludedMethods: ['describe', 'context']
3 | AllCops:
4 | Excludes:
5 | - '**'
6 | TargetRubyVersion: 2.4
7 | Documentation:
8 | Enabled: false
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'coveralls'
4 | Coveralls.wear!
5 |
6 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
7 | require 'scoruby'
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task :default => :spec
7 |
8 | task :console do
9 | exec "pry -r scoruby -I ./lib"
10 | end
11 |
--------------------------------------------------------------------------------
/spec/scoruby/features_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Features do
6 | let(:features) { { f2: true, f3: false } }
7 |
8 | it 'formats booleans' do
9 | expect(described_class.new(features).formatted).to eq(f2: 't', f3: 'f')
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/scoruby/predicates/true_predicate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Predicates
5 | class TruePredicate
6 | def field
7 | nil
8 | end
9 |
10 | def true?(_)
11 | true
12 | end
13 |
14 | def missing?(_)
15 | false
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/scoruby/predicates/false_predicate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Predicates
5 | class FalsePredicate
6 | def field
7 | nil
8 | end
9 |
10 | def true?(_)
11 | false
12 | end
13 |
14 | def missing?(_)
15 | false
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'scoruby'
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require 'pry'
15 | Pry.start
16 |
--------------------------------------------------------------------------------
/lib/scoruby/features.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | class Features
5 | attr_reader :formatted
6 |
7 | def initialize(features)
8 | @formatted = format_booleans(features)
9 | end
10 |
11 | def format_booleans(features)
12 | features.map do |k, v|
13 | features[k] = 'f' if v == false
14 | features[k] = 't' if v == true
15 | end
16 | features
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/scoruby/models/logistic_regression/model_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::LogisticRegression::Model do
6 | let(:logistic_regression_file) { 'spec/fixtures/logistic_regression.pmml' }
7 | let(:xml) { Scoruby.xml_from_file_path(logistic_regression_file) }
8 | let(:logistic_regression) { described_class.new(xml) }
9 | let(:features) do
10 | {
11 | 'prob' => 0.13,
12 | 'noise_var' => 0.5
13 | }
14 | end
15 |
16 | context 'default' do
17 | it 'scores features' do
18 | expect(logistic_regression.score(features).round(8)).to eq 0.08243046
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/scoruby/predicate_factory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/predicates/compound_predicate'
4 | require 'scoruby/predicates/simple_predicate'
5 | require 'scoruby/predicates/simple_set_predicate'
6 | require 'scoruby/predicates/true_predicate'
7 | require 'scoruby/predicates/false_predicate'
8 |
9 | module Scoruby
10 | class PredicateFactory
11 | def self.for(pred_xml)
12 | return Predicates::TruePredicate.new if pred_xml.name == 'True'
13 | return Predicates::FalsePredicate.new if pred_xml.name == 'False'
14 | predicate = Object.const_get("Scoruby::Predicates::#{pred_xml.name}")
15 | predicate.new pred_xml
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/scoruby/models/logistic_regression/model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/models/logistic_regression/data'
4 | require 'forwardable'
5 |
6 | module Scoruby
7 | module Models
8 | module LogisticRegression
9 | class Model
10 | extend Forwardable
11 | def_delegators :@data, :coefficients, :coefficient_values
12 |
13 | def initialize(xml)
14 | @data = Data.new(xml)
15 | end
16 |
17 | def intercept
18 | coefficient_values.first
19 | end
20 |
21 | def score(features)
22 | logodds = intercept
23 | features.each do |key, value|
24 | logodds += coefficients[key.to_s] * value
25 | end
26 |
27 | 1.0 / (1.0 + Math.exp(-logodds))
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/scoruby/models/logistic_regression/data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Models
5 | module LogisticRegression
6 | class Data
7 | COEFFICIENTS_VALUES_PATH = '//PMML/GeneralRegressionModel/ParamMatrix/PCell/@beta'
8 | COEFFICIENTS_LABELS_PATH = '//PMML/GeneralRegressionModel/ParameterList/Parameter/@label'
9 |
10 | def initialize(xml)
11 | @xml = xml
12 | end
13 |
14 | def coefficient_values
15 | @xml.xpath(COEFFICIENTS_VALUES_PATH).map{|attribute| attribute.value.to_f}
16 | end
17 |
18 | def coefficient_labels
19 | @xml.xpath(COEFFICIENTS_LABELS_PATH).map(&:value)
20 | end
21 |
22 | def coefficients
23 | @coefficients ||= Hash[coefficient_labels.zip(coefficient_values)]
24 | end
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/scoruby/models/logistic_regression/data_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::LogisticRegression::Data do
6 | let(:xml) { Scoruby.xml_from_file_path(logistic_regression_file) }
7 | let(:logistic_regression_file) { 'spec/fixtures/logistic_regression.pmml' }
8 | let(:data) { described_class.new(xml) }
9 |
10 | # it 'loads correct number of trees' do
11 | # expect(data.decision_trees.count).to eq 15
12 | # end
13 | #
14 | it 'loads correct number of coefficient values' do
15 | expect(data.coefficient_values.count).to eq 3
16 | end
17 |
18 | it 'loads correct number of coefficient labels' do
19 | expect(data.coefficient_labels.count).to eq 3
20 | end
21 |
22 | it 'loads correct number of coefficients' do
23 | puts data.coefficients
24 | expect(data.coefficients.count).to eq 3
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/scoruby/node.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/predicate_factory'
4 | require 'scoruby/decision'
5 |
6 | module Scoruby
7 | class Node
8 | attr_reader :decision, :pred, :children
9 |
10 | def initialize(xml)
11 | children = xml.children
12 | @decision = Decision.new(xml)
13 | children = remove_nodes(children)
14 | @pred = PredicateFactory.for(children[0])
15 | @children = children_nodes(children)
16 | end
17 |
18 | def true?(features)
19 | @pred.nil? || @pred.true?(features)
20 | end
21 |
22 | private
23 |
24 | def children_nodes(children)
25 | children.select { |c| c.name == 'Node' }
26 | .map { |child| Node.new(child) }
27 | end
28 |
29 | def remove_nodes(children)
30 | children.reject { |c| %w[Extension ScoreDistribution].include? c.name }
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/scoruby/models/random_forest/data_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::RandomForest::Data do
6 | let(:rf_file) { 'spec/fixtures/titanic_rf.pmml' }
7 | let(:xml) { Scoruby.xml_from_file_path(rf_file) }
8 | let(:data) { described_class.new(xml) }
9 | let(:trees_count) { data.decision_trees.count }
10 | let(:continuous_features) { %i[Age Fare Parch Pclass SibSp] }
11 | let(:categorical_features) do
12 | { Sex: %w[female male], Embarked: ['C', '', 'Q', 'S'] }
13 | end
14 |
15 | it 'loads correct number of trees' do
16 | expect(trees_count).to eq 15
17 | end
18 |
19 | it 'loads continuous features' do
20 | expect(data.continuous_features).to match_array continuous_features
21 | end
22 |
23 | it 'loads categorical features' do
24 | expect(data.categorical_features).to match categorical_features
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/scoruby/predicates/simple_set_predicate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Predicates
5 | class SimpleSetPredicate
6 | IS_IN = 'isIn'
7 |
8 | attr_reader :field, :array
9 |
10 | def initialize(pred_xml)
11 | attributes = pred_xml.attributes
12 | @field = attributes['field'].value.to_sym
13 | @array = single_or_quoted_words(pred_xml.children[0].content)
14 | @operator = attributes['booleanOperator'].value
15 | end
16 |
17 | def true?(features)
18 | @array.include? features[@field] if @operator == IS_IN
19 | end
20 |
21 | def missing?(features)
22 | !features.keys.include?(@field)
23 | end
24 |
25 | private
26 |
27 | def single_or_quoted_words(string)
28 | string.split(/\s(?=(?:[^"]|"[^"]*")*$)/)
29 | .reject(&:empty?)
30 | .map { |w| w.tr('"', '') }
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/scoruby.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/version'
4 | require 'scoruby/model_factory'
5 | require 'nokogiri'
6 | require 'logger'
7 |
8 | module Scoruby
9 | class << self
10 | attr_writer :logger
11 |
12 | def logger
13 | @logger ||= Logger.new($stdout).tap do |log|
14 | log.progname = name
15 | end
16 | end
17 | end
18 |
19 | def self.load_model(pmml_file_name)
20 | xml = xml_from_file_path(pmml_file_name)
21 | ModelFactory.factory_for(xml)
22 | end
23 |
24 | def self.load_model_from_string(pmml_string)
25 | xml = xml_from_string(pmml_string)
26 | ModelFactory.factory_for(xml)
27 | end
28 |
29 | def self.xml_from_file_path(pmml_file_name)
30 | pmml_string = File.open(pmml_file_name, 'rb').read
31 | xml_from_string(pmml_string)
32 | end
33 |
34 | def self.xml_from_string(pmml_string)
35 | xml = Nokogiri::XML(pmml_string, &:noblanks)
36 | xml.remove_namespaces!
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/scoruby/decision.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | class Decision
5 | attr_reader :score, :score_distribution
6 |
7 | def initialize(xml)
8 | children = xml.children
9 | distributions = children.select { |c| c.name == 'ScoreDistribution' }
10 |
11 | @score = xml.attribute('score').to_s
12 | return if distributions.empty?
13 |
14 | @score_distribution = {}
15 | distributions.each do |score_distribution|
16 | value = score_distribution.attributes['value'].to_s
17 | @score_distribution[value] = probability(score_distribution, xml)
18 | end
19 | end
20 |
21 | def probability(score_distribution, xml)
22 | probability = score_distribution.attributes['probability'].to_s
23 | return probability.to_f if probability != ''
24 | record_count(score_distribution) / record_count(xml)
25 | end
26 |
27 | def record_count(xml)
28 | xml.attributes['recordCount'].to_s.to_f
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/scoruby/models/gradient_boosted_model/model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/features'
4 | require 'forwardable'
5 | require 'scoruby/models/gradient_boosted_model/data'
6 |
7 | module Scoruby
8 | module Models
9 | module GradientBoostedModel
10 | class Model
11 | extend Forwardable
12 | def_delegators :@data, :decision_trees, :const, :continuous_features,
13 | :categorical_features
14 |
15 | def initialize(xml)
16 | @data = Data.new(xml)
17 | end
18 |
19 | def score(features)
20 | formatted_features = Features.new(features).formatted
21 | scores = traverse_trees(formatted_features)
22 | sum = scores.reduce(:+) + const
23 | Math.exp(sum) / (1 + Math.exp(sum))
24 | end
25 |
26 | def traverse_trees(formatted_features)
27 | decision_trees.map do |dt|
28 | dt.decide(formatted_features).score.to_s.to_f
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/scoruby/models/decision_tree.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/node'
4 |
5 | module Scoruby
6 | module Models
7 | class DecisionTree
8 | attr_reader :root
9 |
10 | def initialize(tree_xml)
11 | @id = tree_xml.attribute('id')
12 | @root = Node.new(tree_xml.at_xpath('TreeModel/Node'))
13 | end
14 |
15 | def decide(features)
16 | curr = @root
17 | while curr.children[0]
18 | prev = curr
19 | curr = step(curr, features)
20 | return if didnt_step?(curr, prev)
21 | end
22 |
23 | curr.decision
24 | end
25 |
26 | private
27 |
28 | def step(curr, features)
29 | return curr unless curr.children
30 | next_step = curr.children.find { |c| c && c.true?(features) }
31 | next_step || curr
32 | end
33 |
34 | def didnt_step?(curr, prev)
35 | return false if prev.pred != curr.pred
36 | feature = curr.children[0].pred.field
37 | Scoruby.logger.error "Null tree: #{@id}, bad feature: #{feature}"
38 | true
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 asaf schers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/spec/scoruby/models/gradient_boosted_model/data_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::GradientBoostedModel::Data do
6 | let(:xml) { Scoruby.xml_from_file_path(gbm_file) }
7 | let(:gbm_file) { 'spec/fixtures/titanic_gbm.pmml' }
8 | let(:data) { described_class.new(xml) }
9 | let(:categorical_features) { { Sex: %w[female male] } }
10 |
11 | it 'loads correct number of trees' do
12 | expect(data.decision_trees.count).to eq 15
13 | end
14 |
15 | it 'loads continuous features' do
16 | expect(data.continuous_features).to match_array %i[Survived Pclass Age Fare]
17 | end
18 |
19 | it 'loads categorical features' do
20 | expect(data.categorical_features).to match categorical_features
21 | end
22 |
23 | context 'const value' do
24 | context 'pmml 4.3' do
25 | let(:const) { 1.3838383838383839 }
26 | it 'loads const' do
27 | expect(data.const).to eq const
28 | end
29 | end
30 |
31 | context 'pmml 4.2' do
32 | let(:const) { -0.4732877044469254 }
33 | let(:gbm_file) { 'spec/fixtures/titanic_gbm_4_2.pmml' }
34 | it 'loads const' do
35 | expect(data.const).to eq const
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/scoruby.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'scoruby/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "scoruby"
8 | spec.version = Scoruby::VERSION
9 | spec.authors = ["Asaf Schers"]
10 | spec.email = ["schers@riskified.com"]
11 |
12 | spec.summary = %q{Ruby Scoring API for PMML.}
13 | spec.homepage = 'https://github.com/asafschers/scoruby'
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17 | spec.bindir = "exe"
18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19 | spec.require_paths = ["lib", "lib/scoruby", "lib/scoruby/models/random_forest", "lib/scoruby/models/gbm"]
20 |
21 | spec.add_development_dependency "bundler", "~> 1.10"
22 | spec.add_development_dependency "rake", "~> 12.0"
23 | spec.add_development_dependency "rspec", "~> 3.5"
24 | spec.add_development_dependency "pry", "~> 0.10"
25 | spec.add_development_dependency "coveralls"
26 | spec.add_development_dependency "ruby-prof"
27 | spec.add_dependency "nokogiri", ">= 1.8.5", "< 1.11.0"
28 | end
29 |
--------------------------------------------------------------------------------
/spec/scoruby/predicates/simple_set_predicate_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Predicates::SimpleSetPredicate do
6 | let(:pred_xml) { Scoruby.xml_from_string(pred_string) }
7 | let(:relevant_pred_xml) { pred_xml.children[0] }
8 | let(:categorical_predicate) { described_class.new(relevant_pred_xml) }
9 |
10 | context 'quotes' do
11 | let(:pred_string) do
12 | <<-XML
13 |
14 | "Missing" "No Match"
15 |
16 | XML
17 | end
18 |
19 | it 'returns true' do
20 | expect(categorical_predicate.true?(f36: 'No Match')).to eq true
21 | end
22 |
23 | it 'returns false' do
24 | expect(categorical_predicate.true?(f36: 'Match')).to eq false
25 | end
26 | end
27 |
28 | context 'no quotes' do
29 | let(:pred_string) do
30 | <<-XML
31 |
32 | f2v1 f2v2 f2v3
33 |
34 | XML
35 | end
36 |
37 | it 'returns true' do
38 | expect(categorical_predicate.true?(f36: 'f2v2')).to eq true
39 | end
40 |
41 | it 'returns false' do
42 | expect(categorical_predicate.true?(f36: 'f2v4')).to eq false
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
14 |
--------------------------------------------------------------------------------
/lib/scoruby/predicates/compound_predicate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Predicates
5 | class CompoundPredicate
6 | attr_reader :field
7 |
8 | def initialize(pred_xml)
9 | attributes = pred_xml.attributes
10 | children = pred_xml.children
11 |
12 | @boolean_operator = attributes['booleanOperator'].value
13 | @predicates = []
14 | @predicates << PredicateFactory.for(children[0])
15 | @predicates << PredicateFactory.for(children[1])
16 | @field = @predicates.map(&:field).flatten.compact
17 | end
18 |
19 | def true?(features)
20 | return surrogate?(features) if @boolean_operator == 'surrogate'
21 | return or?(features) if @boolean_operator == 'or'
22 | and?(features) if @boolean_operator == 'and'
23 | end
24 |
25 | def missing?(features)
26 | @field.any? { |f| !features.keys.include?(f) }
27 | end
28 |
29 | private
30 |
31 | def surrogate?(features)
32 | return @predicates[1].true?(features) if first_missing?(features)
33 | @predicates[0].true?(features)
34 | end
35 |
36 | def first_missing?(features)
37 | @predicates[0].missing?(features)
38 | end
39 |
40 | def or?(features)
41 | @predicates.any? { |p| p.true?(features) }
42 | end
43 |
44 | def and?(features)
45 | @predicates.all? { |p| p.true?(features) }
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/scoruby/models/gradient_boosted_model/model_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::GradientBoostedModel::Model do
6 | let(:xml) { Scoruby.xml_from_file_path(gbm_file) }
7 | let(:gbm) { described_class.new(xml) }
8 |
9 | context 'pmml 4.3' do
10 | let(:gbm_file) { 'spec/fixtures/titanic_gbm.pmml' }
11 | let(:features1) do
12 | { Sex: 'male', Parch: 0, Age: 30, Fare: 9.6875,
13 | Pclass: 2, SibSp: 0, Embarked: 'Q' }
14 | end
15 | let(:features2) do
16 | { Sex: 'female', Parch: 0, Age: 38, Fare: 71.2833,
17 | Pclass: 2, SibSp: 1, Embarked: 'C' }
18 | end
19 |
20 | it 'predicts approve' do
21 | expect(gbm.score(features1)).to eq 0.7990679530500985
22 | end
23 |
24 | it 'predicts decline' do
25 | expect(gbm.score(features2)).to eq 0.8009413525080109
26 | end
27 | end
28 |
29 | context 'pmml 4.2' do
30 | let(:gbm_file) { 'spec/fixtures/titanic_gbm_4_2.pmml' }
31 | let(:features1) do
32 | { Sex: 'male', Parch: 0, Age: 30, Fare: 9.6875,
33 | Pclass: 2, SibSp: 0, Embarked: 'Q' }
34 | end
35 | let(:features2) do
36 | { Sex: 'female', Parch: 0, Age: 38, Fare: 71.2833,
37 | Pclass: 2, SibSp: 1, Embarked: 'C' }
38 | end
39 |
40 | it 'predicts approve' do
41 | expect(gbm.score(features1)).to eq 0.3652639329522468
42 | end
43 |
44 | it 'predicts decline' do
45 | expect(gbm.score(features2)).to eq 0.4178155014037758
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | scoruby (0.3.4)
5 | nokogiri (>= 1.8.5, < 1.11.0)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | coderay (1.1.0)
11 | coveralls (0.8.20)
12 | json (>= 1.8, < 3)
13 | simplecov (~> 0.14.1)
14 | term-ansicolor (~> 1.3)
15 | thor (~> 0.19.4)
16 | tins (~> 1.6)
17 | diff-lcs (1.2.5)
18 | docile (1.1.5)
19 | json (2.1.0)
20 | method_source (0.8.2)
21 | mini_portile2 (2.4.0)
22 | nokogiri (1.10.4)
23 | mini_portile2 (~> 2.4.0)
24 | pry (0.10.3)
25 | coderay (~> 1.1.0)
26 | method_source (~> 0.8.1)
27 | slop (~> 3.4)
28 | rake (12.0.0)
29 | rspec (3.5.0)
30 | rspec-core (~> 3.5.0)
31 | rspec-expectations (~> 3.5.0)
32 | rspec-mocks (~> 3.5.0)
33 | rspec-core (3.5.4)
34 | rspec-support (~> 3.5.0)
35 | rspec-expectations (3.5.0)
36 | diff-lcs (>= 1.2.0, < 2.0)
37 | rspec-support (~> 3.5.0)
38 | rspec-mocks (3.5.0)
39 | diff-lcs (>= 1.2.0, < 2.0)
40 | rspec-support (~> 3.5.0)
41 | rspec-support (3.5.0)
42 | ruby-prof (0.16.2)
43 | simplecov (0.14.1)
44 | docile (~> 1.1.0)
45 | json (>= 1.8, < 3)
46 | simplecov-html (~> 0.10.0)
47 | simplecov-html (0.10.0)
48 | slop (3.6.0)
49 | term-ansicolor (1.6.0)
50 | tins (~> 1.0)
51 | thor (0.19.4)
52 | tins (1.13.2)
53 |
54 | PLATFORMS
55 | ruby
56 |
57 | DEPENDENCIES
58 | bundler (~> 1.10)
59 | coveralls
60 | pry (~> 0.10)
61 | rake (~> 12.0)
62 | rspec (~> 3.5)
63 | ruby-prof
64 | scoruby!
65 |
66 | BUNDLED WITH
67 | 1.16.1
68 |
--------------------------------------------------------------------------------
/lib/scoruby/model_factory.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/models/decision_tree'
4 | require 'scoruby/models/gradient_boosted_model/model'
5 | require 'scoruby/models/random_forest/model'
6 | require 'scoruby/models/naive_bayes/model'
7 | require 'scoruby/models/logistic_regression/model'
8 |
9 | module Scoruby
10 | class ModelFactory
11 | RANDOM_FOREST_MODEL = 'randomForest_Model'
12 | GBM_INDICATION = '//Segmentation[@multipleModelMethod="sum"]'
13 | RF_INDICATION = '//Segmentation[@multipleModelMethod="average"]'
14 | MODEL_NOT_SUPPORTED_ERROR = 'model not supported'
15 |
16 | def self.factory_for(xml)
17 | return Models::RandomForest::Model.new(xml) if random_forest?(xml)
18 | return Models::GradientBoostedModel::Model.new(xml) if gbm?(xml)
19 | return Models::DecisionTree.new(xml.child) if decision_tree?(xml)
20 | return Models::NaiveBayes::Model.new(xml) if naive_bayes?(xml)
21 | return Models::LogisticRegression::Model.new(xml) if logistic_regression?(xml)
22 |
23 | raise MODEL_NOT_SUPPORTED_ERROR
24 | end
25 |
26 | def self.logistic_regression?(xml)
27 | !xml.xpath('PMML/GeneralRegressionModel').empty?
28 | end
29 |
30 | def self.naive_bayes?(xml)
31 | !xml.xpath('PMML/NaiveBayesModel').empty?
32 | end
33 |
34 | def self.decision_tree?(xml)
35 | !xml.xpath('PMML/TreeModel').empty?
36 | end
37 |
38 | def self.random_forest?(xml)
39 | xml.xpath('PMML/MiningModel/@modelName').to_s == RANDOM_FOREST_MODEL ||
40 | xml.at(RF_INDICATION)
41 | end
42 |
43 | def self.gbm?(xml)
44 | xml.at(GBM_INDICATION)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/scoruby/models/random_forest/data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Models
5 | module RandomForest
6 | class Data
7 | RF_FOREST_XPATH = 'PMML/MiningModel/Segmentation/Segment'
8 | FEATURES_XPATH = 'PMML/DataDictionary/DataField'
9 |
10 | def initialize(xml)
11 | @xml = xml
12 | end
13 |
14 | def decision_trees
15 | @decision_trees ||= @xml.xpath(RF_FOREST_XPATH).map do |xml_tree|
16 | DecisionTree.new(xml_tree)
17 | end
18 | end
19 |
20 | def categorical_features
21 | @categorical_features ||= fetch_categorical_features
22 | end
23 |
24 | def continuous_features
25 | @continuous_features ||= fetch_continuous_features
26 | end
27 |
28 | def regression?
29 | @xml.xpath("//MiningModel[@functionName='regression']").any?
30 | end
31 |
32 | private
33 |
34 | def fetch_continuous_features
35 | continuous_predicates.map do |xml|
36 | Predicates::SimplePredicate.new(xml).field
37 | end.uniq
38 | end
39 |
40 | def fetch_categorical_features
41 | categorical_predicates.each_with_object(Hash.new([])) do |xml, res|
42 | predicate = Predicates::SimpleSetPredicate.new(xml)
43 | res[predicate.field] = res[predicate.field] | predicate.array
44 | end
45 | end
46 |
47 | def categorical_predicates
48 | @xml.xpath('//SimpleSetPredicate')
49 | end
50 |
51 | def continuous_predicates
52 | @xml.xpath('//SimplePredicate')
53 | end
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/spec/scoruby/models/random_forest/model_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::RandomForest::Model do
6 | let(:rf_file) { 'spec/fixtures/titanic_rf.pmml' }
7 | let(:xml) { Scoruby.xml_from_file_path(rf_file) }
8 | let(:random_forest) { described_class.new(xml) }
9 | let(:prediction) { random_forest.score(features) }
10 | let(:decisions_count) { random_forest.decisions_count(features) }
11 |
12 | context '0 features' do
13 | let(:features) do
14 | {
15 | Sex: 'male',
16 | Parch: 0,
17 | Age: 30,
18 | Fare: 9.6875,
19 | Pclass: 2,
20 | SibSp: 0,
21 | Embarked: 'Q'
22 | }
23 | end
24 | it 'predicts 0' do
25 | expect(prediction[:label]).to eq '0'
26 | expect(prediction[:score]).to eq 13 / 15.to_f
27 | expect(decisions_count['0']).to eq 13
28 | expect(decisions_count['1']).to eq 2
29 | end
30 | end
31 |
32 | context '1 features' do
33 | let(:features) do
34 | {
35 | Sex: 'female',
36 | Parch: 0,
37 | Age: 38,
38 | Fare: 71.2833,
39 | Pclass: 2,
40 | SibSp: 1,
41 | Embarked: 'C'
42 | }
43 | end
44 |
45 | it 'predicts 0' do
46 | expect(prediction[:label]).to eq '1'
47 | expect(prediction[:score]).to eq 14 / 15.to_f
48 | expect(decisions_count['0']).to eq 1
49 | expect(decisions_count['1']).to eq 14
50 | end
51 | end
52 |
53 | context 'regression' do
54 | let(:rf_file) { 'spec/fixtures/cars_rf.pmml' }
55 | let(:features) do
56 | {
57 | speed: 10
58 | }
59 | end
60 |
61 | it 'predicts correctly' do
62 | expect(prediction[:response]).to be_within(0.001).of(27.37778)
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/scoruby/models/random_forest/model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/models/random_forest/data'
4 | require 'forwardable'
5 |
6 | module Scoruby
7 | module Models
8 | module RandomForest
9 | class Model
10 | extend Forwardable
11 | def_delegators :@data, :decision_trees, :categorical_features,
12 | :continuous_features, :regression?
13 |
14 | def initialize(xml)
15 | @data = Data.new(xml)
16 | end
17 |
18 | def score(features)
19 | decisions_count = decisions_count(features)
20 |
21 | if regression?
22 | {
23 | response: sum(decisions_count.map { |k, v| k.to_f * v }) / sum(decisions_count.values)
24 | }
25 | else
26 | decision = decisions_count.max_by { |_, v| v }
27 | {
28 | label: decision[0],
29 | score: decision[1] / sum(decisions_count.values).to_f
30 | }
31 | end
32 | end
33 |
34 | def decisions_count(features)
35 | formatted_features = Features.new(features).formatted
36 | decisions = traverse_trees(formatted_features)
37 | aggregate_decisions(decisions)
38 | end
39 |
40 | private
41 |
42 | def traverse_trees(formatted_features)
43 | decision_trees.map do |decision_tree|
44 | decision_tree.decide(formatted_features).score
45 | end
46 | end
47 |
48 | def aggregate_decisions(decisions)
49 | decisions.each_with_object(Hash.new(0)) do |score, counts|
50 | counts[score] += 1
51 | end
52 | end
53 |
54 | def sum(values)
55 | values.reduce(0, :+)
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/scoruby/predicates/simple_predicate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Predicates
5 | class SimplePredicate
6 | GREATER_THAN = 'greaterThan'
7 | LESS_THAN = 'lessThan'
8 | LESS_OR_EQUAL = 'lessOrEqual'
9 | GREATER_OR_EQUAL = 'greaterOrEqual'
10 | MATH_OPS = [GREATER_THAN,
11 | LESS_THAN,
12 | LESS_OR_EQUAL,
13 | GREATER_OR_EQUAL].freeze
14 | EQUAL = 'equal'
15 | IS_MISSING = 'isMissing'
16 |
17 | attr_reader :field
18 |
19 | def initialize(pred_xml)
20 | attributes = pred_xml.attributes
21 |
22 | @field = attributes['field'].value.to_sym
23 | @operator = attributes['operator'].value
24 | return if @operator == IS_MISSING
25 | @value = attributes['value'].value
26 | end
27 |
28 | def true?(features)
29 | return num_true?(features) if MATH_OPS.include?(@operator)
30 | return features[@field] == @value if @operator == EQUAL
31 | features[field].nil? || !features.key?(field) if @operator == IS_MISSING
32 | end
33 |
34 | def missing?(features)
35 | !features.keys.include?(@field)
36 | end
37 |
38 | private
39 |
40 | def num_true?(features)
41 | return false unless features[@field]
42 | compare(Float(features[@field]), Float(@value))
43 | end
44 |
45 | def compare(curr_value, value)
46 | return curr_value > value if @operator == GREATER_THAN
47 | return curr_value < value if @operator == LESS_THAN
48 | return curr_value <= value if @operator == LESS_OR_EQUAL
49 | curr_value >= value if @operator == GREATER_OR_EQUAL
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/scoruby/predicates/simple_predicate_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Predicates::SimplePredicate do
6 | context 'less or equal predicate' do
7 | let(:pred_string) do
8 | <<-XML
9 |
10 | XML
11 | end
12 | let(:pred_xml) { Nokogiri::XML(pred_string); }
13 | let(:predicate) { described_class.new(pred_xml.children.first) }
14 |
15 | it 'returns true' do
16 | expect(predicate.true?(f33: 18)).to eq true
17 | end
18 |
19 | it 'returns false' do
20 | expect(predicate.true?(f33: 19)).to eq false
21 | end
22 | end
23 |
24 | context 'is missing predicate' do
25 | let(:pred_string) do
26 | <<-XML
27 |
28 | XML
29 | end
30 |
31 | let(:pred_xml) { Nokogiri::XML(pred_string); }
32 | let(:predicate) { described_class.new(pred_xml.children.first) }
33 |
34 | it 'returns true' do
35 | expect(predicate.true?(f33: nil)).to eq true
36 | end
37 |
38 | it 'returns false' do
39 | expect(predicate.true?({})).to eq true
40 | end
41 |
42 | it 'returns false' do
43 | expect(predicate.true?(f33: '6')).to eq false
44 | end
45 | end
46 |
47 | context 'equals predicate' do
48 | let(:pred_string) do
49 | <<-XML
50 |
51 | XML
52 | end
53 |
54 | let(:pred_xml) { Nokogiri::XML(pred_string); }
55 | let(:predicate) { described_class.new(pred_xml.children.first) }
56 |
57 | it 'returns true' do
58 | expect(predicate.true?(f33: 'f2v3')).to eq true
59 | end
60 |
61 | it 'returns true' do
62 | expect(predicate.true?(f33: nil)).to eq false
63 | end
64 |
65 | it 'returns false' do
66 | expect(predicate.true?({})).to eq false
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/scoruby/models/naive_bayes/naive_bayes_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'scoruby/models/naive_bayes/model'
5 |
6 | describe Scoruby::Models::NaiveBayes::Model do
7 | let(:naive_bayes_file) { 'spec/fixtures/naive_bayes.pmml' }
8 | let(:xml) { Scoruby.xml_from_file_path(naive_bayes_file) }
9 | let(:naive_bayes) { described_class.new(xml) }
10 | let(:features) do
11 | {
12 | 'age of individual': '24',
13 | 'gender': 'male',
14 | 'no of claims': '2',
15 | 'domicile': nil,
16 | 'age of car': '1'
17 | }
18 | end
19 |
20 | let(:l0) { 8723 * 0.001 * 4273 / 8598 * 225 / 8561 * 830 / 8008 }
21 | let(:l1) do
22 | 2557 * (Math.exp(-(24 - 24.936)**2 / (2 * 0.516)) /
23 | Math.sqrt(Math::PI * 2 * 0.516)) * 1321 / 2533 * 10 / 2436 * 182 / 2266
24 | end
25 | let(:l2) do
26 | 1530 * (Math.exp(-(24 - 24.588)**2 / (2 * 0.635)) /
27 | Math.sqrt(Math::PI * 2 * 0.635)) * 780 / 1522 * 9 / 1496 * 51 / 1191
28 | end
29 | let(:l3) do
30 | 709 * (Math.exp(-(24 - 24.428)**2 / (2 * 0.379)) /
31 | Math.sqrt(Math::PI * 2 * 0.379)) * 405 / 697 * 0.001 * 26 / 699
32 | end
33 | let(:l4) do
34 | 100 * (Math.exp(-(24 - 24.770)**2 / (2 * 0.314)) /
35 | Math.sqrt(Math::PI * 2 * 0.314)) * 42 / 90 * 10 / 98 * 6 / 87
36 | end
37 | let(:lvalues) { naive_bayes.lvalues(features) }
38 | let(:l2_probability) { naive_bayes.score(features, '1000') }
39 |
40 | it 'calculates lvalues by http://dmg.org/pmml/v4-2-1/NaiveBayes.html' do
41 | expect(lvalues['100']).to be_within(0.0001).of l0
42 | expect(lvalues['500']).to be_within(0.0001).of l1
43 | expect(lvalues['1000']).to be_within(0.0001).of l2
44 | expect(lvalues['5000']).to be_within(0.0001).of l3
45 | expect(lvalues['10000']).to be_within(0.0001).of l4
46 | end
47 |
48 | let(:score) { (l2 / (l0 + l1 + l2 + l3 + l4)) }
49 |
50 | it 'scores by http://dmg.org/pmml/v4-2-1/NaiveBayes.html' do
51 | expect(l2_probability).to be_within(0.0001).of score
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/scoruby/predicates/compound_predicate_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Predicates::CompoundPredicate do
6 | context 'evaluates and' do
7 | let(:pred_string) do
8 | <<-XML
9 |
10 | XML
11 | end
12 |
13 | let(:pred_xml) { Nokogiri::XML(pred_string); }
14 | let(:predicate) { described_class.new(pred_xml.children.first) }
15 |
16 | it 'returns true' do
17 | expect(predicate.true?(f: 16)).to eq true
18 | end
19 |
20 | it 'returns false' do
21 | expect(predicate.true?(f: 17)).to eq false
22 | end
23 | end
24 |
25 | context 'evaluates or' do
26 | let(:pred_string) do
27 | <<-XML
28 |
29 | XML
30 | end
31 |
32 | let(:pred_xml) { Nokogiri::XML(pred_string); }
33 | let(:predicate) { described_class.new(pred_xml.children.first) }
34 |
35 | it 'returns true' do
36 | expect(predicate.true?(f: 16)).to eq true
37 | end
38 |
39 | it 'returns false' do
40 | expect(predicate.true?(f: 17)).to eq false
41 | end
42 | end
43 |
44 | context 'evaluates surrogate' do
45 | context 'simple predicate' do
46 | let(:pred_string) do
47 | <<-XML
48 |
49 | XML
50 | end
51 | let(:pred_xml) { Nokogiri::XML(pred_string); }
52 | let(:predicate) { described_class.new(pred_xml.children.first) }
53 |
54 | it 'missing' do
55 | expect(predicate.true?(g: 17)).to eq false
56 | end
57 |
58 | it 'not missing' do
59 | expect(predicate.true?(f: 16)).to eq true
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/scoruby/model_factory_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::ModelFactory do
6 | context 'unsupported pmml' do
7 | let(:empty_file) { 'spec/fixtures/empty_file.pmml' }
8 | let(:unsupported_model) { Scoruby.load_model(empty_file) }
9 | let(:unsupported_error) { described_class::MODEL_NOT_SUPPORTED_ERROR }
10 |
11 | it 'raises on unsupported model loading' do
12 | expect { unsupported_model }.to raise_error(unsupported_error)
13 | end
14 | end
15 |
16 | context 'random forest pmml' do
17 | let(:rf_file) { 'spec/fixtures/titanic_rf.pmml' }
18 | let(:rf_model) { Scoruby.load_model(rf_file) }
19 |
20 | it 'loads random forest' do
21 | expect(rf_model).to be_a(Scoruby::Models::RandomForest::Model)
22 | end
23 | end
24 |
25 | context 'gbm pmml' do
26 | context 'pmml 4.2' do
27 | let(:gbm_file) { 'spec/fixtures/titanic_gbm_4_2.pmml' }
28 | let(:gbm_model) { Scoruby.load_model(gbm_file) }
29 |
30 | it 'loads gbm' do
31 | expect(gbm_model).to be_a(Scoruby::Models::GradientBoostedModel::Model)
32 | end
33 | end
34 |
35 | context 'pmml 4.3' do
36 | let(:gbm_file) { 'spec/fixtures/titanic_gbm.pmml' }
37 | let(:gbm_model) { Scoruby.load_model(gbm_file) }
38 |
39 | it 'loads gbm' do
40 | expect(gbm_model).to be_a(Scoruby::Models::GradientBoostedModel::Model)
41 | end
42 | end
43 | end
44 |
45 | context 'naive_bayes pmml' do
46 | let(:naive_bayes_file) { 'spec/fixtures/naive_bayes.pmml' }
47 | let(:naive_bayes_model) { Scoruby.load_model(naive_bayes_file) }
48 |
49 | it 'loads NaiveBayes' do
50 | expect(naive_bayes_model).to be_a Scoruby::Models::NaiveBayes::Model
51 | end
52 | end
53 |
54 | context 'logistic_regression pmml' do
55 | let(:logistic_regression_file) { 'spec/fixtures/logistic_regression.pmml' }
56 | let(:logistic_regression_model) { Scoruby.load_model(logistic_regression_file) }
57 |
58 | it 'loads LogisticRegression' do
59 | expect(logistic_regression_model).to be_a Scoruby::Models::LogisticRegression::Model
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/spec/fixtures/logistic_regression.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 2019-08-07 16:16:51
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/lib/scoruby/models/gradient_boosted_model/data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Models
5 | module GradientBoostedModel
6 | class Data
7 | GBM_FOREST_XPATH = '//Segmentation[@multipleModelMethod="sum"]/Segment'
8 | CONST_XPATH = '//Target/@rescaleConstant'
9 | CONST_XPATH_4_2 = '//Constant'
10 |
11 | def initialize(xml)
12 | @xml = xml
13 | end
14 |
15 | def decision_trees
16 | @decision_trees ||= @xml.xpath(GBM_FOREST_XPATH).map do |xml_tree|
17 | DecisionTree.new(xml_tree)
18 | end
19 | end
20 |
21 | def const
22 | @const ||= const_by_version
23 | end
24 |
25 | def continuous_features
26 | @continuous_features ||= fetch_continuous_features
27 | end
28 |
29 | def categorical_features
30 | @categorical_features ||= fetch_categorical_features
31 | end
32 |
33 | private
34 |
35 | def fetch_continuous_features
36 | @xml.xpath('//DataField')
37 | .select { |xml| xml.attr('optype') == 'continuous' }
38 | .map { |xml| xml.attr('name').to_sym }
39 | end
40 |
41 | def fetch_categorical_features
42 | categorical_features_xml.each_with_object(Hash.new([])) do |xml, res|
43 | res[xml.attr('name').to_sym] = xml.xpath('Value')
44 | .map { |v| v.attr('value') }
45 | end
46 | end
47 |
48 | def categorical_features_xml
49 | @xml.xpath('//DataField')
50 | .select { |xml| xml.attr('optype') == 'categorical' }
51 | .reject { |xml| xml.attr('name') == target }
52 | end
53 |
54 | def target
55 | @target ||= @xml.xpath('//MiningField')
56 | .find { |xml| xml.attr('usageType') == 'target' }
57 | .attr('name').to_s
58 | end
59 |
60 | def const_by_version
61 | const_pmml_4_2_xml ? const_pmml_4_2 : const_pmml_4_3
62 | end
63 |
64 | def const_pmml_4_3
65 | @xml.xpath(CONST_XPATH).to_s.to_f
66 | end
67 |
68 | def const_pmml_4_2
69 | const_pmml_4_2_xml.content.to_f
70 | end
71 |
72 | def const_pmml_4_2_xml
73 | @xml.xpath(CONST_XPATH_4_2).first
74 | end
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/scoruby/models/naive_bayes/model_data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Scoruby
4 | module Models
5 | module NaiveBayes
6 | class ModelData
7 | attr_reader :threshold, :labels, :numerical_features, :category_features
8 |
9 | def initialize(xml)
10 | @xml = xml
11 | fetch_threshold
12 | fetch_features_data
13 | fetch_label_counts
14 | end
15 |
16 | private
17 |
18 | def fetch_threshold
19 | @threshold = @xml.xpath('//NaiveBayesModel').attr('threshold')
20 | .value.to_f
21 | end
22 |
23 | def fetch_features_data
24 | @category_features = {}
25 | @numerical_features = {}
26 | @xml.xpath('//BayesInput').each do |feature|
27 | field_name = feature.attr('fieldName').to_sym
28 | @category_features[field_name] = fetch_category_feature(feature)
29 | @numerical_features[field_name] = fetch_numerical_feature(feature)
30 | end
31 | end
32 |
33 | def fetch_label_counts
34 | @labels = {}
35 | @xml.xpath('//BayesOutput//TargetValueCount').each do |l|
36 | l.attr('value')
37 | @labels[l.attr('value')] = { 'count': l.attr('count').to_f }
38 | end
39 | end
40 |
41 | def fetch_numerical_feature(feature)
42 | return unless feature.child.name == 'TargetValueStats'
43 | features_data = {}
44 | feature.child.children.each do |child|
45 | features_data[child.attr('value').strip] = {
46 | mean: child.child.attr('mean'),
47 | variance: child.child.attr('variance')
48 | }
49 | end
50 | features_data
51 | end
52 |
53 | def fetch_category_feature(feature)
54 | return unless feature.children.any? { |f| f.name == 'PairCounts' }
55 | feature_data = {}
56 | feature.children.each do |category|
57 | feature_data[category.attr('value')] = fetch_category(category)
58 | end
59 | feature_data
60 | end
61 |
62 | def fetch_category(category)
63 | category_data = {}
64 | category.child.children.each do |label|
65 | category_data[label.attr('value')] = label.attr('count')
66 | end
67 | category_data
68 | end
69 | end
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/scoruby/models/naive_bayes/model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'scoruby/models/naive_bayes/model_data'
4 | require 'forwardable'
5 |
6 | module Scoruby
7 | module Models
8 | module NaiveBayes
9 | class Model
10 | extend Forwardable
11 | def_delegators :@model_data, :threshold, :labels, :numerical_features,
12 | :category_features
13 |
14 | def initialize(xml)
15 | @model_data = ModelData.new(xml)
16 | end
17 |
18 | def lvalues(features)
19 | calc_label_feature_values(features)
20 | calc_label_values
21 | end
22 |
23 | def score(features, label)
24 | lvalues(features)[label] / lvalues(features).values.reduce(:+)
25 | end
26 |
27 | private
28 |
29 | def calc_label_values
30 | label_values = {}
31 | labels.each do |label, label_data|
32 | label_data.each do |key, value|
33 | label_data[key] = threshold if value.round(5).zero?
34 | end
35 | label_values[label] = label_data.values.reduce(:*)
36 | end
37 | label_values
38 | end
39 |
40 | def calc_label_feature_values(features)
41 | labels.each_key do |label|
42 | features.each do |feature_name, feature_value|
43 | label_value = category(feature_name, feature_value, label)
44 | label_value ||= numerical(feature_name, feature_value, label)
45 | labels[label][feature_name] = label_value if label_value
46 | end
47 | end
48 | end
49 |
50 | def category(feature_name, feature_value, label)
51 | model_feature = category_features[feature_name]
52 | return unless model_feature && model_feature[feature_value]
53 | value_count = model_feature[feature_value][label].to_f
54 | overall_count = model_feature.map { |_, value| value[label].to_f }
55 | .reduce(0, :+)
56 | value_count / overall_count
57 | end
58 |
59 | def numerical(feature_name, feature_value, label)
60 | model_feature = numerical_features[feature_name]
61 | return unless model_feature && model_feature[label]
62 | calc_numerical(feature_value.to_f,
63 | model_feature[label][:mean].to_f,
64 | model_feature[label][:variance].to_f)
65 | end
66 |
67 | def calc_numerical(feature_value, mean, variance)
68 | Math.exp(-(feature_value - mean)**2 / (2 * variance)) /
69 | Math.sqrt(2 * Math::PI * variance)
70 | end
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/spec/fixtures/gbm_tree.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | f2v1 f2v2 f2v3
18 |
19 |
20 |
21 |
22 |
23 |
24 | f1v1 f1v2 f1v3
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | f3v1 f3v2 f3v3
40 |
41 |
42 |
43 |
44 | f3v4 f3v5 f3v6
45 |
46 |
47 |
48 |
49 |
50 |
51 | f1v4 f1v5 f1v6
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://coveralls.io/github/asafschers/scoruby?branch=master)
3 | [](https://badge.fury.io/rb/scoruby)
4 | [](https://travis-ci.org/asafschers/scoruby)
5 |
6 | # Scoruby
7 |
8 | Ruby scoring API for Predictive Model Markup Language (PMML).
9 |
10 | Currently supports -
11 |
12 | * Decision Tree
13 | * Naive Bayes
14 | * Logistic Regression
15 | * Random Forest
16 | * Gradient Boosted Trees
17 |
18 | Will be happy to implement new models by demand, or assist with any other issue.
19 |
20 | Contact me here or at aschers@gmail.com.
21 |
22 | [Tutorial - Deploy Machine Learning Models from R Research to Ruby Production with PMML](https://medium.com/@aschers/deploy-machine-learning-models-from-r-research-to-ruby-go-production-with-pmml-b41e79445d3d)
23 |
24 |
25 | ## Installation
26 |
27 | Add this line to your application's Gemfile:
28 |
29 | ```ruby
30 | gem 'scoruby'
31 | ```
32 |
33 | And then execute:
34 |
35 | $ bundle
36 |
37 | Or install it yourself as:
38 |
39 | $ gem install scoruby
40 |
41 | ## Usage
42 |
43 | ### Naive Bayes
44 |
45 | ```ruby
46 | naive_bayes = Scoruby.load_model 'naive_bayes.pmml'
47 | features = { f1: v1 , ... }
48 | naive_bayes.lvalues(features)
49 | naive_bayes.score(features, 'l1')
50 | ```
51 |
52 | ### Logistic Regression
53 |
54 | ```ruby
55 | logistic_regression = Scoruby.load_model 'logistic_regression.pmml'
56 | features = { f1: v1 , ... }
57 | logistic_regression.score(features)
58 | ```
59 |
60 | ### Decision Tree
61 |
62 | ```ruby
63 | decision_tree = Scoruby.load_model 'decision_tree.pmml'
64 | features = { f1 : v1, ... }
65 | decision_tree.decide(features)
66 |
67 | => #"0.999615579933873", "1"=>"0.000384420066126561"}>
68 | ```
69 |
70 | ### Random Forest
71 |
72 | [Generate PMML - R](https://github.com/asafschers/scoruby/wiki/Random-Forest)
73 |
74 | ```ruby
75 |
76 | random_forest = Scoruby.load_model 'titanic_rf.pmml'
77 | features = {
78 | Sex: 'male',
79 | Parch: 0,
80 | Age: 30,
81 | Fare: 9.6875,
82 | Pclass: 2,
83 | SibSp: 0,
84 | Embarked: 'Q'
85 | }
86 |
87 | random_forest.score(features)
88 |
89 | => {:label=>"0", :score=>0.882}
90 |
91 | random_forest.decisions_count(features)
92 |
93 | => {"0"=>441, "1"=>59}
94 |
95 | ```
96 |
97 | ### Gradient Boosted model
98 |
99 | [Generate PMML - R](https://github.com/asafschers/scoruby/wiki/Gradient-Boosted-Model)
100 |
101 | ```ruby
102 |
103 | gbm = Scoruby.load_model 'gbm.pmml'
104 |
105 | features = {
106 | Sex: 'male',
107 | Parch: 0,
108 | Age: 30,
109 | Fare: 9.6875,
110 | Pclass: 2,
111 | SibSp: 0,
112 | Embarked: 'Q'
113 | }
114 |
115 | gbm.score(features)
116 |
117 | => 0.3652639329522468
118 |
119 | ```
120 |
121 | ## Development
122 |
123 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124 |
125 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
126 |
127 | ## Contributing
128 |
129 | Bug reports and pull requests are welcome on GitHub at https://github.com/asafschers/scoruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
130 |
131 |
132 | ## License
133 |
134 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
135 |
136 |
--------------------------------------------------------------------------------
/spec/scoruby/models/decision_tree_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Scoruby::Models::DecisionTree do
6 | let(:tree_file) { 'spec/fixtures/gbm_tree.pmml' }
7 | let(:tree_xml) { Scoruby.xml_from_file_path(tree_file) }
8 | let(:decision_tree) { described_class.new(tree_xml.child) }
9 |
10 | context 'sets tree' do
11 | let(:l_node) { decision_tree.root.children[1] }
12 | let(:lr_node) { l_node.children[2] }
13 |
14 | it 'sets node' do
15 | expect(l_node.pred.field).to eq :f2
16 | expect(l_node.decision.score).to eq ''
17 | expect(lr_node.children).to all(be_a Scoruby::Node)
18 | end
19 |
20 | it 'sets leave' do
21 | expect(lr_node.pred.field).to eq :f1
22 | expect(lr_node.decision.score).to eq '0.0011015286521365208'
23 | expect(lr_node.children).to all(be_nil)
24 | end
25 | end
26 |
27 | context 'Score' do
28 | let(:decision) { decision_tree.decide(features) }
29 | let(:score) { decision.score }
30 |
31 | context 'without features' do
32 | let(:features) { {} }
33 | it 'scores' do
34 | expect(score).to eq '4.3463944950723456E-4'
35 | end
36 | end
37 |
38 | context 'f2 first true' do
39 | let(:features) { { f2: 'f2v1' } }
40 | it 'scores' do
41 | expect(score).to eq '-1.8361380219689046E-4'
42 | end
43 | end
44 |
45 | context 'f1, f2 first true' do
46 | let(:features) { { f2: 'f2v1', f1: 'f1v3' } }
47 | it 'scores' do
48 | expect(score).to eq '-6.237581139073701E-4'
49 | end
50 | end
51 |
52 | context 'f1, f2, f4 first true' do
53 | let(:features) { { f2: 'f2v1', f1: 'f1v3', f4: 0.08 } }
54 | it 'scores' do
55 | expect(score).to eq '0.0021968294712358194'
56 | end
57 | end
58 |
59 | context 'f1, f2 first true f4 second true' do
60 | let(:features) { { f2: 'f2v1', f1: 'f1v3', f4: 0.09 } }
61 | it 'scores' do
62 | expect(score).to eq '-9.198573460887271E-4'
63 | end
64 | end
65 |
66 | context 'f1, f2, f3 first true, f4 second true' do
67 | let(:features) { { f2: 'f2v1', f1: 'f1v3', f4: 0.09, f3: 'f3v2' } }
68 | it 'scores' do
69 | expect(score).to eq '-0.0021187239505556523'
70 | end
71 | end
72 |
73 | context 'f1, f2 first true f3, f4 second true' do
74 | let(:features) { { f2: 'f2v1', f1: 'f1v3', f4: 0.09, f3: 'f3v4' } }
75 | it 'scores' do
76 | expect(score).to eq '-3.3516227414227926E-4'
77 | end
78 | end
79 |
80 | context 'f2 first true f1 second true' do
81 | let(:features) { { f2: 'f2v1', f1: 'f1v4' } }
82 | it 'scores' do
83 | expect(score).to eq '0.0011015286521365208'
84 | end
85 | end
86 |
87 | context 'f2 second true' do
88 | let(:features) { { f2: 'f2v4' } }
89 | it 'scores' do
90 | expect(score).to eq '0.0022726641744997256'
91 | end
92 | end
93 |
94 | context 'f2 none are true' do
95 | let(:features) { { f2: 'f2v7' } }
96 | let(:error) { 'Null tree: 2532, bad feature: f2' }
97 | it 'scores' do
98 | expect(Scoruby.logger).to receive(:error).with(error)
99 | expect(decision).to be_nil
100 | end
101 | end
102 | end
103 |
104 | context 'Score distribution' do
105 | let(:tree_file) { 'spec/fixtures/decision_tree.pmml' }
106 | let(:tree_xml) { Scoruby.xml_from_file_path(tree_file) }
107 | let(:decision_tree) { described_class.new(tree_xml.child) }
108 | let(:decision) { decision_tree.decide(ppd: ppd) }
109 | let(:score_distribution) { decision.score_distribution }
110 |
111 | context 'ppd 10' do
112 | let(:ppd) { 10 }
113 | let(:expected) do
114 | {
115 | '0' => 0.999513428516638,
116 | '1' => 0.000486571483361926
117 | }
118 | end
119 | it 'scores' do
120 | expect(score_distribution).to eq(expected)
121 | end
122 | end
123 |
124 | context 'ppd 20' do
125 | let(:ppd) { 20 }
126 | let(:expected) do
127 | {
128 | '0' => 0.999615579933873,
129 | '1' => 0.000384420066126561
130 | }
131 | end
132 |
133 | it 'scores' do
134 | expect(score_distribution).to eq(expected)
135 | end
136 | end
137 |
138 | context 'ppd 50' do
139 | let(:ppd) { 50 }
140 | let(:expected) do
141 | {
142 | '0' => 0.999710889179894,
143 | '1' => 0.000289110820105768
144 | }
145 | end
146 |
147 | it 'scores' do
148 | expect(score_distribution).to eq(expected)
149 | end
150 | end
151 | end
152 |
153 | context 'Score distribution' do
154 | let(:tree_file) { 'spec/fixtures/binary_split_decision_tree.pmml' }
155 | let(:tree_xml) { Scoruby.xml_from_file_path(tree_file) }
156 | let(:decision_tree) { described_class.new(tree_xml.child) }
157 | let(:features) do
158 | {
159 | ppd: ppd,
160 | business_traveler: business_traveler,
161 | total_nights: total_nights,
162 | days_to_booking: days_to_booking,
163 | percent_distance_avg_price: percent_distance_avg_price
164 | }
165 | end
166 | let(:decision) { decision_tree.decide(features) }
167 | let(:score_distribution) { decision.score_distribution }
168 |
169 | let(:ppd) { 10 }
170 | let(:business_traveler) { 0.3 }
171 | let(:percent_distance_avg_price) { 0.1 }
172 | let(:total_nights) { 2 }
173 | let(:days_to_booking) { 6 }
174 | let(:expected) do
175 | {
176 | '0' => 0.4507042253521127,
177 | '1' => 0.5492957746478874
178 | }
179 | end
180 |
181 | it 'scores' do
182 | expect(score_distribution).to eq(expected)
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/spec/fixtures/decision_tree.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/spec/fixtures/binary_split_decision_tree.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 2017-12-19T22:11:27Z
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/spec/fixtures/naive_bayes.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/spec/fixtures/cars_rf.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 2018-06-24 15:00:11
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
--------------------------------------------------------------------------------
/spec/fixtures/titanic_gbm.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 2018-03-08T11:57:48Z
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
--------------------------------------------------------------------------------
/spec/fixtures/titanic_gbm_4_2.pmml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 2017-06-24T20:16:48Z
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
946 |
947 |
948 |
949 |
950 |
951 |
952 |
953 |
954 |
955 |
956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 |
964 |
965 |
966 |
967 |
968 |
969 |
970 |
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 |
979 |
980 |
981 |
982 |
983 |
984 |
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 |
993 |
994 |
995 |
996 |
997 |
998 |
999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
1009 |
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1025 |
1026 |
1027 |
1028 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 |
1035 |
1036 |
1037 |
1038 |
1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
1051 |
1052 |
1053 |
1054 |
1055 |
1056 |
1057 |
1058 |
1059 |
1060 |
1061 |
1062 |
1063 |
1064 |
1065 |
1066 |
1067 |
1068 |
1069 |
1070 |
1071 |
1072 |
1073 |
1074 |
1075 |
1076 |
1077 |
1078 |
1079 |
1080 |
1081 |
1082 |
1083 |
1084 |
1085 |
1086 |
1087 |
1088 |
1089 |
1090 |
1091 |
1092 |
1093 |
1094 |
1095 |
1096 |
1097 |
1098 |
1099 |
1100 |
1101 |
1102 |
1103 |
1104 |
1105 |
1106 |
1107 |
1108 |
1109 |
1110 |
1111 |
1112 |
1113 |
1114 |
1115 |
1116 |
1117 |
1118 |
1119 |
1120 |
1121 |
1122 |
1123 |
1124 |
1125 |
1126 |
1127 |
1128 |
1129 |
1130 |
1131 |
1132 |
1133 |
1134 |
1135 |
1136 |
1137 |
1138 |
1139 |
1140 |
1141 |
1142 |
1143 |
1144 |
1145 |
1146 |
1147 |
1148 |
1149 |
1150 |
1151 |
1152 |
1153 |
1154 |
1155 |
1156 |
1157 |
1158 |
1159 |
1160 |
1161 |
1162 |
1163 |
1164 |
1165 |
1166 |
1167 |
1168 |
1169 |
1170 |
1171 |
1172 |
1173 |
1174 |
1175 |
1176 |
1177 |
1178 |
1179 |
1180 |
1181 |
1182 |
1183 |
1184 |
1185 |
1186 |
1187 |
1188 |
1189 |
1190 |
1191 |
1192 |
1193 |
1194 |
1195 |
1196 |
1197 |
1198 |
1199 |
1200 |
1201 |
1202 |
1203 |
1204 |
1205 |
1206 |
1207 |
1208 |
1209 |
1210 |
1211 |
1212 |
1213 |
1214 |
1215 |
1216 |
1217 |
1218 |
1219 |
1220 |
1221 |
1222 |
1223 |
1224 |
1225 |
1226 |
1227 |
1228 |
1229 |
1230 |
1231 |
1232 |
1233 |
1234 |
1235 |
1236 |
1237 |
1238 |
1239 |
1240 |
1241 |
1242 |
1243 |
1244 |
1245 |
1246 |
1247 |
1248 |
1249 |
1250 |
1251 |
1252 |
1253 |
1254 |
1255 |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 |
1262 |
1263 |
1264 |
1265 |
1266 |
1267 |
1268 |
1269 |
1270 |
1271 |
1272 |
1273 |
1274 |
1275 |
1276 |
1277 |
1278 |
1279 |
1280 |
1281 |
1282 |
1283 |
1284 |
1285 |
1286 |
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 |
1294 |
1295 |
1296 |
1297 |
1298 |
1299 |
1300 |
1301 |
1302 |
1303 |
1304 |
1305 |
1306 |
1307 |
1308 |
1309 |
1310 |
1311 |
1312 |
1313 |
1314 |
1315 |
1316 |
1317 |
1318 |
1319 |
1320 |
1321 |
1322 |
1323 |
1324 |
1325 |
1326 |
1327 |
1328 |
1329 |
1330 |
1331 |
1332 |
1333 |
1334 |
1335 |
1336 |
1337 |
1338 |
1339 |
1340 |
1341 |
1342 |
1343 |
1344 |
1345 |
1346 |
1347 |
1348 |
1349 |
1350 |
1351 |
1352 |
1353 |
1354 |
1355 |
1356 |
1357 |
1358 |
1359 |
1360 |
1361 |
1362 |
1363 |
1364 |
1365 |
1366 |
1367 |
1368 |
1369 |
1370 |
1371 |
1372 |
1373 |
1374 |
1375 |
1376 |
1377 |
1378 |
1379 |
1380 |
1381 |
1382 |
1383 |
1384 |
1385 |
1386 |
1387 |
1388 |
1389 |
1390 |
1391 |
1392 |
1393 |
1394 |
1395 |
1396 |
1397 |
1398 |
1399 |
1400 |
1401 |
1402 |
1403 |
1404 |
1405 |
1406 |
1407 |
1408 |
1409 |
1410 |
1411 |
1412 |
1413 |
1414 |
1415 |
1416 |
1417 |
1418 |
1419 |
1420 |
1421 |
1422 |
1423 |
1424 |
1425 |
1426 |
1427 |
1428 |
1429 |
1430 |
1431 |
1432 |
1433 |
1434 |
1435 |
1436 |
1437 |
1438 |
1439 |
1440 |
1441 |
1442 |
1443 |
1444 |
1445 |
1446 |
1447 |
1448 |
1449 |
1450 |
1451 |
1452 |
1453 |
1454 |
1455 |
1456 |
1457 |
1458 |
1459 |
1460 |
1461 |
1462 |
1463 |
1464 |
1465 |
1466 |
1467 |
1468 |
1469 |
1470 |
1471 |
1472 |
1473 |
1474 |
1475 |
1476 |
1477 |
1478 |
1479 |
1480 |
1481 |
1482 |
1483 |
1484 |
1485 |
1486 |
1487 |
1488 |
1489 |
1490 |
1491 |
1492 |
1493 |
1494 |
1495 |
1496 |
1497 |
1498 |
1499 |
1500 |
1501 |
1502 |
1503 |
1504 |
1505 |
1506 |
1507 |
1508 |
1509 |
1510 |
1511 |
1512 |
1513 |
1514 |
1515 |
1516 |
1517 |
1518 |
1519 |
1520 |
1521 |
1522 |
1523 |
1524 |
1525 |
1526 |
1527 |
1528 |
1529 |
1530 |
1531 |
1532 |
1533 |
1534 |
1535 |
1536 |
1537 |
1538 |
1539 |
1540 |
1541 |
1542 |
1543 |
1544 |
1545 |
1546 |
1547 |
1548 |
1549 |
1550 |
1551 |
1552 |
1553 |
1554 |
1555 |
1556 |
1557 |
1558 |
1559 |
1560 |
1561 |
1562 |
1563 |
1564 |
1565 |
1566 |
1567 |
1568 |
1569 |
1570 |
1571 |
1572 |
1573 |
1574 |
1575 |
1576 |
1577 |
1578 |
1579 |
1580 |
1581 |
1582 |
1583 |
1584 |
1585 |
1586 |
1587 |
1588 |
1589 |
1590 |
1591 |
1592 |
1593 |
1594 |
1595 |
1596 |
1597 |
1598 |
1599 |
1600 |
1601 |
1602 |
1603 |
1604 |
1605 |
1606 |
1607 |
1608 |
1609 |
1610 |
1611 |
1612 |
1613 |
1614 |
1615 |
1616 |
1617 |
1618 |
1619 |
1620 |
1621 |
1622 |
1623 |
1624 |
1625 |
1626 |
1627 |
1628 |
1629 |
1630 |
1631 |
1632 |
1633 |
1634 |
1635 |
1636 |
1637 |
1638 |
1639 |
1640 |
1641 |
1642 |
1643 |
1644 |
1645 |
1646 |
1647 |
1648 |
1649 |
1650 |
1651 |
1652 |
1653 |
1654 |
1655 |
1656 |
1657 |
1658 |
1659 |
1660 |
1661 |
1662 |
1663 |
1664 |
1665 |
1666 |
1667 |
1668 |
1669 |
1670 |
1671 |
1672 |
1673 |
1674 |
1675 |
1676 |
1677 |
1678 |
1679 |
1680 |
1681 |
1682 |
1683 |
1684 |
1685 |
1686 |
1687 |
1688 |
1689 |
1690 |
1691 |
1692 |
1693 |
1694 |
1695 |
1696 |
1697 |
1698 |
1699 |
1700 |
1701 |
1702 |
1703 |
1704 |
1705 |
1706 |
1707 |
1708 |
1709 |
1710 |
1711 |
1712 |
1713 |
1714 |
1715 |
1716 |
1717 |
1718 |
1719 |
1720 |
1721 |
1722 |
1723 |
1724 |
1725 |
1726 |
1727 |
1728 |
1729 |
1730 |
1731 |
1732 |
1733 |
1734 |
1735 |
1736 |
1737 |
1738 |
1739 |
1740 |
1741 |
1742 |
1743 |
1744 |
1745 |
1746 |
1747 |
1748 |
1749 |
1750 |
1751 |
1752 |
1753 |
1754 |
1755 |
1756 |
1757 |
1758 |
1759 |
1760 |
1761 |
1762 |
1763 |
1764 |
1765 |
1766 |
1767 |
1768 |
1769 |
1770 |
1771 |
1772 |
1773 |
1774 |
1775 |
1776 |
1777 |
1778 |
1779 |
1780 |
1781 |
1782 |
1783 |
1784 |
1785 |
1786 |
1787 |
1788 |
1789 |
1790 |
1791 |
1792 |
1793 |
1794 |
1795 |
1796 |
1797 |
1798 |
1799 |
1800 |
1801 |
1802 |
1803 |
1804 |
1805 |
1806 |
1807 |
1808 |
1809 |
1810 |
1811 |
1812 |
1813 |
1814 |
1815 |
1816 |
1817 |
1818 |
1819 |
1820 |
1821 |
1822 |
1823 |
1824 |
1825 |
1826 |
1827 |
1828 |
1829 |
1830 |
1831 |
1832 |
1833 |
1834 |
1835 |
1836 |
1837 |
1838 |
1839 |
1840 |
1841 |
1842 |
1843 |
1844 |
1845 |
1846 |
1847 |
1848 |
1849 |
1850 |
1851 |
1852 |
1853 |
1854 |
1855 |
1856 |
1857 |
1858 |
1859 |
1860 |
1861 |
1862 |
1863 |
1864 |
1865 |
1866 |
1867 |
1868 |
1869 |
1870 |
1871 |
1872 |
1873 |
1874 |
1875 |
1876 |
1877 |
1878 |
1879 |
1880 |
1881 |
1882 |
1883 |
1884 |
1885 |
1886 |
1887 |
1888 |
1889 |
1890 |
1891 |
1892 |
1893 |
1894 |
1895 |
1896 |
1897 |
1898 |
1899 |
1900 |
1901 |
1902 |
1903 |
1904 |
1905 |
1906 |
1907 |
1908 |
1909 |
1910 |
1911 |
1912 |
1913 |
1914 |
1915 |
1916 |
1917 |
1918 |
1919 |
1920 |
1921 |
1922 |
1923 |
1924 |
1925 |
1926 |
1927 |
1928 |
1929 |
1930 |
1931 |
1932 |
1933 |
1934 |
1935 |
1936 |
1937 |
1938 |
1939 |
1940 |
1941 |
1942 |
1943 |
1944 |
1945 |
1946 |
1947 |
1948 |
1949 |
1950 |
1951 |
1952 |
1953 |
1954 |
1955 |
1956 |
1957 |
1958 |
1959 |
1960 |
1961 |
1962 |
1963 |
1964 |
1965 |
1966 |
1967 |
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 |
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 |
1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 |
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 |
2000 |
2001 |
2002 |
2003 |
2004 |
2005 |
2006 |
2007 |
2008 |
2009 |
2010 |
2011 |
2012 |
2013 |
2014 |
2015 |
2016 |
2017 |
2018 |
2019 |
2020 |
2021 |
2022 |
2023 |
2024 |
2025 |
2026 |
2027 |
2028 |
2029 |
2030 |
2031 |
2032 |
2033 |
2034 |
2035 |
2036 |
2037 |
2038 |
2039 |
2040 |
2041 |
2042 |
2043 |
2044 |
2045 |
2046 |
2047 |
2048 |
2049 |
2053 |
2054 |
2055 |
2056 |
2057 |
2058 |
2059 |
2060 |
2061 |
2062 |
--------------------------------------------------------------------------------