class Symian::TransitionMatrix

Attributes

transition_probabilities[R]

this is mostly for testing purposes

Public Class Methods

new(input) click to toggle source
# File lib/symian/transition_matrix.rb, line 13
def initialize(input)
  # allow filename, string, and IO objects as input
  if input.kind_of?(String)
    if File.exists?(input)
      input = File.new(input, 'r')
    else
      input = StringIO.new(input.strip.split("\n").collect{|l| l.strip }.join("\n"))
    end
  else
    raise RuntimeError unless input.respond_to?(:read)
  end

  @transition_probabilities = {}

  # process escalation matrix
  headers = nil
  CSV.parse(input.read, :headers => :first_row) do |row|
    headers ||= row.headers
    @sg_names ||= headers[1..-2]

    # make sure that support groups do not include the "In" virtual support group
    raise RuntimeError if @sg_names.include?("In")

    # make sure that last support group is the "Out" virtual support group
    raise RuntimeError unless headers[-1] == "Out"

    sg_name = row[0]  # the first row element is the support group name

    # make sure support group name is valid
    raise RuntimeError unless sg_name == "In" or @sg_names.include?(sg_name)

    # make sure we are not overwriting existing data
    raise RuntimeError if @transition_probabilities[sg_name]
    @transition_probabilities[sg_name] = []

    # prepare corresponding row in transition matrix
    2.upto(row.length) do |i|
      escalations = Integer(row[i-1]) # raises ArgumentError in case of errors
      if escalations > 0
        @transition_probabilities[sg_name] << { :sg_name => headers[i-1],
                                                :escalations => escalations }
      end
    end

    # calculate normalized probabilities
    normalize_probabilities(@transition_probabilities[sg_name])
  end

  # check that we have transition probabilities for each support group
  [ "In", *@sg_names].each do |name|
    raise RuntimeError unless @transition_probabilities.has_key?(name)
  end

  # TODO: make seeding of this thing configurable...
  @rng = ERV::RandomVariable.new(:distribution => :uniform, :min_value => 0.0, :max_value => 1.0)
end

Public Instance Methods

escalation(from) click to toggle source
# File lib/symian/transition_matrix.rb, line 71
def escalation(from)
  # raise error if source support group does not exist
  raise ArgumentError unless tps = @transition_probabilities[from]

  # get random value
  x = @rng.next

  # return name of first support group whose (cumulative)
  # transition probability is larger than x
  tps.each do |el|
    return el[:sg_name] if el[:probability] > x
  end

  # the destination support group was not found
  raise RuntimeError
end
merge(sg1_name, sg2_name, new_name=nil) click to toggle source
# File lib/symian/transition_matrix.rb, line 89
def merge(sg1_name, sg2_name, new_name=nil)
  # raise error if support groups do not exist
  raise RuntimeError unless sg1_probs = @transition_probabilities.delete(sg1_name) and
                            sg2_probs = @transition_probabilities.delete(sg2_name)

  new_sg_name = new_name || "Merge_of_%s_and_%s" % [ sg1_name, sg2_name ]

  # recalculate escalations to new sg
  @transition_probabilities.each do |k,v|

    # add escalation information for new group
    escalations = 0
    v.each do |el|
      if el[:sg_name] == sg1_name or el[:sg_name] == sg2_name
        escalations += el[:escalations]
      end
    end

    v << { :sg_name => new_sg_name,
           :escalations => escalations }

    # remove old escalation information
    v.delete_if {|el| el[:sg_name] == sg1_name or el[:sg_name] == sg2_name }

    # recalculate normalized probabilities
    normalize_probabilities(v)
  end

  # update @sg_names
  @sg_names[@sg_names.index(sg1_name)] = new_name
  @sg_names.delete(sg2_name)

  # recalculate escalations from new sg
  total_escalation_info = sg1_probs + sg2_probs
  @transition_probabilities[new_sg_name] = []
  [ @sg_names, "Out" ].flatten!.each do |name|
    escalations = total_escalation_info.inject(0) do |sum,el|
      sum + (el[:sg_name] == name ? el[:escalations] : 0)
    end

    if escalations > 0
      @transition_probabilities[new_sg_name] << { :sg_name => name,
                                                  :escalations => escalations }
    end
  end

  # recalculate normalized probabilities
  normalize_probabilities(@transition_probabilities[new_sg_name])
end
to_s() click to toggle source
# File lib/symian/transition_matrix.rb, line 140
def to_s
  lines = [ "From/To,#{@sg_names.join(',')},Out" ]
  [ "In", *@sg_names ].each do |input_sg|
    escalations = [ @sg_names, "Out" ].flatten!.map do |output_sg|
      @transition_probabilities[input_sg].map{|x| x[:sg_name] == output_sg ? x[:escalations] : nil }.compact.first || 0
    end
    lines << "#{input_sg},#{escalations.join(',')}"
  end
  lines.join("\n")
end

Private Instance Methods

normalize_probabilities(probability_vector) click to toggle source
# File lib/symian/transition_matrix.rb, line 153
def normalize_probabilities(probability_vector)
  # calculate total escalations
  total_escalations = probability_vector.inject(0) { |sum,el| sum += el[:escalations] }

  # probability values are cumulative
  cumulative_escalations = 0
  probability_vector.each do |el|
    cumulative_escalations += el[:escalations]
    el[:probability] = cumulative_escalations.to_f / total_escalations.to_f
  end

  # just in case...
  probability_vector[-1][:probability] = 1.0
end