#coding utf-8

require_relative 'dataset.rb'
require_relative 'learner.rb'

module MachineLearner
  
  # ナイブベイズ学習器
  class BayesLearner < Learner

    # コンストラクタ
    def initialize(alpha = 1)
      set_alpha(alpha)
    end

    # スムージングに利用するαを設定する
    def set_alpha(alpha)
      @alpha = alpha
    end
    
    # データを元に学習を行う
    # @param datas [Array<DataSet>] トレーニングデータの配列
    # @return [Array<Boolean>] 識別結果の配列
    def learn(datas, ds = nil)
      # 学習データ
      @training = datas

      # 学習データがHashか
      @hash_mode = @training[0].x.kind_of?(Hash)

      # 学習データの結果のリスト
      @ys = @training.map{|data| data.y }

      # 各種変数の初期化
      @fields = nil
      @candidate_x = @hash_mode ? {} : []
      @p_xi_count = {}
      @p_y_count = {}
    end
    
    # 識別を行う
    # @param x 特徴空間
    # @return [Fixnum] 識別結果
    def classify(xs)
      # 最も尤度の高い候補 y を探す
      kouho = classify_raw(xs).max{ |x, y| x[1] <=> y[1] }
      (kouho[1] == 0) ? nil : kouho[0]
    end
    
    def classify_raw(xs)
      candidate_y.map{|y| [y, p_y_x(y, xs)]}.to_h
    end
    
    # フィールド一覧
    def fields
      @fields ||= @training.map{|data| data.x.keys }.flatten.uniq.compact
    end
    
    # xの候補を列挙
    def candidate_x(field)
      @candidate_x[field] ||= @training.map{|data| data.x[field]}.uniq.compact
    end

    # 特定のyとxの各値の倍率を取得する
    def candidate_ratio_dev(yval)
      val = {}
      # 全体の中の、fieldの割合
      field_ratio = @ys.count(yval) / @ys.size.to_f
      fields.map do |f|
        # フィールドの各値の中の、fieldの割合
        t1 = @training.reduce(Hash.new) do |memo, ds|
          next memo if ds.x[f] == nil
          memo[ds.x[f]] ||= [0, 0]
          memo[ds.x[f]][0] += 1
          memo[ds.x[f]][1] += (ds.y == yval ? 1 : 0)
          memo
        end
        r = t1.reduce([0, 0]){|memo, (k, (v1, v2))|
          memo[0] += v1
          memo[1] += v2
          memo
        }
        field_ratio = r[1].to_f / r[0]

        [f, t1.map { |k, v| [ k,(v[1].to_f/v[0])/field_ratio ] }.to_h]
      end.to_h
    end

    def candidate_ratio(yval)
      fields.map {|f| [f, candidate_x(f).map {|i| [i, p_xi_y(i, f, yval)] }.to_h] }.to_h
    end
    private
  
    # P(Y | X) を計算
    def p_y_x(y, xs)
      # P(Y | X) =  P(Y) * product(P(Xi | Y))
      p = Math.log(p_y(y))
      xs.each_with_index do |xi, i|
        i, xi = xi if @hash_mode
        next if xi.nil?
        p += Math.log(p_xi_y(xi, i, y))
      end
      return Math.exp(p)
    end
  
    # P(Xi | Y) を計算(TODO:実際はP(Xi | Y)/P(Xi))
    def p_xi_y(x, i, y)
      total = count = 0
      @ys.each_with_index do |ysj, j|
        next if ysj != y
        total += 1
        count += 1 if @training[j].x[i] == x
      end
      #return (count + @alpha).to_f / (total + candidate_x(i).size * @alpha).to_f
      return ((count.to_f / total.to_f) + @alpha) / (p_xi(x, i) + @alpha * candidate_x(i).size)
    end
  
    # P(Y) を計算
    def p_y(y)
      @p_y_count ||= {}
      @p_y_count[y] = @ys.count(y)
      return (@p_y_count[y] + 1).to_f / (@ys.size + candidate_y.size).to_f
    end

    # P(Xi)を計算
    def p_xi(x, i)
      @p_xi_count ||= {}
      @p_xi_count[i] ||= {}
      @p_xi_count[i][x] ||= @training.count{|data| data.x[i] == x}
      return @p_xi_count[i][x].to_f / @training.size.to_f
    end
  
    # yの候補を列挙
    def candidate_y
      @candidate_y ||= @ys.uniq.compact
    end
  end
  
end

# vim: set et ts=2 sts=2 sw=2:

