ジニ不純度の算出3 - Python, R, CoffeeScript

前々回前回 に続き、下記のようなプログラム言語でジニ不純度(ジニ係数)の算出処理を同様に実装してみました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20140622/

Python で実装

Python では itertools モジュールの groupbycombinations 関数が使えます。

groupbyHaskell と同様に隣り合う同じ値をグルーピングできます。 (今回のケースでは sorted でソートが必要)

groupby 結果の値部分(グルーピング部分)には直接 len 関数を使えないようなので list 関数でリスト化してから len を適用します。

また、combinations 関数を使用すると Scala の combinations と同様に要素の組み合わせを取得できます。(下記では AB、AC、BC の 3種類)

gini.py
from itertools import *

def size(xs):
    return float(len(xs))

# (a) 1 - (AA + BB + CC)
def giniA(xs):
    return 1 - sum(map(lambda (k, g): (size(list(g)) / size(xs)) ** 2, groupby(sorted(xs))))

def countby(xs):
    return map(lambda (k, v): (k, size(list(v))), groupby(sorted(xs)))

# (b) AB * 2 + AC * 2 + BC * 2
def giniB(xs):
    return sum(map(
        lambda ((xk, xv), (yk, yv)): xv / size(xs) * yv / size(xs) * 2, 
        combinations(countby(xs), 2)
    ))

vlist = ["A", "B", "B", "C", "B", "A"]

print giniA(vlist)
print giniB(vlist)
実行結果
> python gini.py

0.611111111111
0.611111111111

Python 3 で実行するには

Python 3.4 で実行するにはラムダ式と print のところを書き換える必要があります。

Python 3 のラムダ式ではタプル内の複数の要素を個別に引数として取得できないようなので、Python 2 のように lambda (k, v): ・・・ とは書けず、lambda x: ・・・ として個々の要素をインデックスで参照 (x[0] 等) する事になります。

gini3.py (Python 3.4)
・・・
# (a) 1 - (AA + BB + CC)
def giniA(xs):
    return 1 - sum(map(lambda x: (size(list(x[1])) / size(xs)) ** 2, groupby(sorted(xs))))
・・・
# (b) AB * 2 + AC * 2 + BC * 2
def giniB(xs):
    return sum(map(
        lambda x: x[0][1] / size(xs) * x[1][1] / size(xs) * 2, 
        combinations(countby(xs), 2)
    ))

vlist = ["A", "B", "B", "C", "B", "A"]

print(giniA(vlist))
print(giniB(vlist))
実行結果 (Python 3.4)
> python gini3.py

0.6111111111111112
0.611111111111111

R で実装

R では table 関数で要素毎のカウント値を取得でき、combn 関数で ScalaPython の combinations と同様の組み合わせを行列(matrix)として取得できます。

lapply の結果(リスト)には sum 関数を適用できないようなので Reduce を使って合計しています。

また、apply の第2引数を 2 とすれば列単位にデータを処理できます。

gini.R
# (a) 1 - (AA + BB + CC)
giniA <- function(xs) {
  1 - Reduce("+", lapply(table(xs), function(x) (x / length(xs)) ^ 2))
}

# (b) AB * 2 + AC * 2 + BC * 2
giniB <- function(xs) {
  sum(apply(combn(table(xs), 2), 2, function(x) (x[1] / length(xs)) * (x[2] / length(xs)) * 2))
}

list <- c("A", "B", "B", "C", "B", "A")

giniA(list)
giniB(list)
実行結果
・・・
> giniA(list)
[1] 0.6111111

> giniB(list)
[1] 0.6111111

備考

各処理の結果は下記のようになります。

table(list) の結果
> table(list)

list
A B C 
2 3 1
combn(table(list), 2) の結果
> combn(table(list), 2)

     [,1] [,2] [,3]
[1,]    2    2    3
[2,]    3    1    1

ちなみに、上記は以下のような組み合わせのカウント値です。

     [,1] [,2] [,3]
[1,] "A"  "A"  "B" 
[2,] "B"  "C"  "C" 

CoffeeScript で実装

CoffeeScript では、Underscore.js 等のライブラリを使用しない限り、グルーピングや組み合わせの処理を自前で実装する事になると思います。

gini.coffee
countBy = (xs) -> xs.reduce (acc, x) ->
    acc[x] ?= 0
    acc[x]++
    acc
, {}

sum = (xs) -> xs.reduce (acc, x) -> acc + x

# (a) 1 - (AA + BB + CC)
giniA = (xs) -> 1 - sum( (v / xs.length) ** 2 for k, v of countBy(xs) )

flatten = (xs) -> xs.reduce (x, y) -> x.concat y

combination = (xs) -> flatten( [x, y] for x in xs when x isnt y for y in xs )

# (b) BA + CA + AB + CB + AC + BC
giniB = (xs) -> sum( (x[1] / xs.length) * (y[1] / xs.length) for [x, y] in combination([k, v] for k, v of countBy(xs)) )

list = ["A", "B", "B", "C", "B", "A"]

console.log giniA(list)
console.log giniB(list)
実行結果
> coffee gini.coffee

0.6111111111111112
0.611111111111111