├── 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 | 21 | 22 | 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 | [![Coverage Status](https://coveralls.io/repos/github/asafschers/scoruby/badge.svg?branch=master)](https://coveralls.io/github/asafschers/scoruby?branch=master) 3 | [![Gem Version](https://badge.fury.io/rb/scoruby.svg)](https://badge.fury.io/rb/scoruby) 4 | [![Build Status](https://travis-ci.org/asafschers/scoruby.svg?branch=master)](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 |
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 |
-------------------------------------------------------------------------------- /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 | 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 |
-------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /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 | 30 | 31 | 32 | 33 | 34 | -0.4732877044469254 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 | 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 | 2050 | 2051 | 2052 | 2053 | 2054 | 2055 | 2056 | 2057 | 2058 | 2059 | 2060 | 2061 |
2062 | --------------------------------------------------------------------------------