Last active
August 29, 2015 13:55
-
-
Save medhiwidjaja/8725026 to your computer and use it in GitHub Desktop.
AnalysisMethods::Ahp module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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