Skip to content

Instantly share code, notes, and snippets.

@medhiwidjaja
Last active August 29, 2015 13:55
Show Gist options
  • Save medhiwidjaja/8725026 to your computer and use it in GitHub Desktop.
Save medhiwidjaja/8725026 to your computer and use it in GitHub Desktop.
AnalysisMethods::Ahp module
# Copyright (c) 2012 Medhi Widjaja
require 'matrix'
# require 'analysis_methods/pairwise_comparison.rb'
# require 'analysis_methods/pairwise_rank_comparison.rb'
# Ahp Module contains the classes for modeling and calculating decision hierarchy using the AHP
# (Analytic Hierarchy Process) method.
# For background theory, see http://en.wikipedia.org/wiki/Analytic_hierarchy_process
#
module AnalysisMethods
# Ahp embodies a single node in the decision hierarchy.
# There are two methods to initialize it:
# 1) e.setup_ahp(matrix, n)
# matrix must be a square matrix of dimension n*n. matrix must be in the form of reciprocal matrix
# suitable for Ahp calculation. No checking is made whether the matrix is really reciprocal matrix.
# Example:
# matrix = Matrix[[1.0, 1/4.0, 4, 1/6.0], [4.0, 1.0, 4, 1/4.0], [1/4.0, 1/4.0, 1.0, 1/5.0], [6.0, 4.0, 5.0, 1.0]]
# e.setup_ahp(matrix, 4)
# 2) e.setup_ahp(ary, n)
# ary is an array containing the elements of the upper half above the diagonal of the matrix.
# n is the dimension of the square matrix.
# The number of elements in the array must be equal to ((n**2)-n)/2
# For example, the AhpNode above could also be initialized in the following way
# e.setup_ahp [1/4.0, 4, 1/6.0, 4, 1/4.0, 1/5.0], 4
#
# TODO: prone to rounding error because of Float's lack of precision. Consider using BigDecimal or Rational data type
#
module Ahp
extend ActiveSupport::Concern
include PairwiseComparable
included do
embeds_many :pairwise_comparisons, class_name: 'AnalysisMethods::Ahp::PairwiseComparable::PairwiseComparison', as: :pairwise_comparable
accepts_nested_attributes_for :pairwise_comparisons, allow_destroy: false
field :ahp_scale, type: String
field :c_r, type: Float
field :ahp_notes, type: String
end
attr_reader :matrix, :n
def setup_ahp(array, n)
if array.class == Matrix
raise ArgumentError.new("Matrix is not square") unless array.square?
@matrix = array
raise ArgumentError.new("Matrix size is different from the size argument") unless array.column_size == n
@n = n
elsif array.class == Array
# Accept array of numbers of each element of the upper half of the matrix above the diagonal line.
# Size of the array has to be equal to number_of_elements(n)
raise ArgumentError.new("Number of elements doesn't match the specified matrix size.") unless array.count == number_of_elements(n)
i = 0
matrix_array = []
(0..n-1).each do |row|
matrix_array << []
(0..n-1).each do |col|
if row == col
matrix_array[row][col] = 1.0
elsif row < col # above the diagoal
matrix_array[row][col] = array[i]
i += 1
else # row > col
matrix_array[row][col] = 1.0/matrix_array[col][row]
end
end
end
@matrix = Matrix.build(n) { |row, col| matrix_array[row][col] }
@n = n
else
raise ArgumentError.new("Not a matrix or an array.")
end
end
def priority_vector(mode=:distributive)
l, idx = lambda_max
v = @matrix.eigen.eigenvectors[idx].collect { |i| i.abs }
case mode
when :distributive
sum = v.sum
vector = v.map { |i| i / sum }
when :ideal
max = v.max
ideal_v = v.map { |i| i / max }
sum = ideal_v.sum
vector = ideal_v.map { |i| i / sum }
end
vector
end
def pv(mode=:distributive)
priority_vector(mode)
end
# The largest lambda value corresponding to the eigenvectors
# Returns the largest lambda and the row index needed to get the corresponding eigenvector
def lambda_max
dmax = 0.0
idx = 0
lambdas = @matrix.eigen.eigenvalues
(0..lambdas.size-1).each do |i|
d = lambdas[i]
if d.real? && dmax < d.abs
dmax = d.abs
idx = i
end
end
[dmax, idx]
end
def lm
lambda_max.first
end
def inconsistency_index
(lm - @n) / (@n - 1)
end
def ci
self.inconsistency_index
end
def inconsistency_ratio
random_index = [0.0, 0.0, 0.58, 0.9, 1.12, 1.24, 1.32, 1.41, 1.45, 1.49, 1.52, 1.54, 1.56, 1.58, 1.59 ]
if @n > 2
inconsistency_index / random_index[@n-1]
else
0.0
end
end
def cr
self.c_r
end
def save_cr
self.update_attribute :c_r, self.inconsistency_ratio
end
# Pretty print the matrix. For debugging purpose only.
def ppmatrix
(0..@n-1).each {|i| print "[ "; (0..@n-1).each {|j| printf(" %0.03f ", @matrix.row(i)[j])}; print " ]\n"}
end
def eigenvectors
@matrix.eigen.eigenvectors
end
def save_ahp_scores
# TODO: Store numbers as BigDecimal or Rational Number
entries = self.pairwise_comparisons.map {|pw| pw.comparison_value }
self.setup_ahp entries, self.scorables.size
self.save_cr
weights = self.pv.to_a.reverse
scorables = self.scorables
scorables.each do |scorable|
weight = weights.pop
self.save_score weight: weight,
weight_n: weight,
scorable: scorable,
with_respect_to: evaluable,
eval_method: AnalysisMethods::Base::EVALUATION_METHODS['Pairwise']
end
end
private
# Number of elements in strict upper half of the matrix required to create a reciprocal matrix
def number_of_elements(n)
((n**2)-n)/2
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment