と。

Github: https://github.com/8-u8

2値変数だけのデータにk-meansをすると詰む

ブログ執筆から逃げるな

まだgitには上げてません。

さっき上げました♨

github.com

教師なし学習で大変に代表的な手法であるところのk-means(K平均法)ですが、
2値変数にたいして適用すると悲しいことが起きるので注意が必要です。

地味な設計

以下のようなデータを用意します。x1 から x3 までは、パラメータ $p$ をそれっぽく変えて、
rbinom から生成した2値データ、 x4 のみ、平均をそれっぽく変えた正規分布に従う乱数を生成したデータです。 x2x3 は、それぞれ x1 の値に依存して反応確率が変わります*1

library(ggplot2)
library(gridExtra)

set.seed(1234)
library(ggplot2)
library(gridExtra)

set.seed(1234)
x11 <- rbinom(30, 1, 0.5)
x12 <- rbinom(20, 1, 0.75)
x13 <- rbinom(50, 1, 0.2)

x21 <- ifelse(x11==1, rbinom(30, 1, 0.7), rbinom(30, 1, 0.1))
x22 <- ifelse(x12==1, rbinom(20, 1, 0.8), rbinom(20, 1, 0.05))
x23 <- ifelse(x13==1, rbinom(50, 1, 0.75), rbinom(50, 1, 0.25))

x31 <- ifelse(x11==1, rbinom(30, 1, 0.9), rbinom(30, 1, 0.1))
x32 <- ifelse(x12==1, rbinom(20, 1, 0.6), rbinom(20, 1, 0.04))
x33 <- ifelse(x13==1, rbinom(50, 1, 0.59), rbinom(50, 1, 0.01))

# 検証のために連続データもいれとこ。
x41 <- rnorm(30, 0, 1)
x42 <- rnorm(20, 5, 1)
x43 <- rnorm(50, -3, 1)

kmeans_data <- data.frame(
  id = paste0("id_", c(1:100)),
  x1 = c(x11, x12, x13),
  x2 = c(x21, x22, x23),
  x3 = c(x31, x32, x33),
  x4 = c(x41, x42, x43)
)

コードをみて分かるとおり、3つのクラスタに分かれてほしい気持ちがあふれています。
Spearmanの相関係数(離散に強いタイプ)も、以下の感じで、 x1 ~ x3 の間の相関は相対的に強めです。

          x1        x2        x3        x4
x1 1.0000000 0.7112844 0.6130185 0.4874672
x2 0.7112844 1.0000000 0.5072037 0.4667618
x3 0.6130185 0.5072037 1.0000000 0.4976532
x4 0.4874672 0.4667618 0.4976532 1.0000000

さらに言えば、ID昇順で分かれてくれると嬉しいですね。
まずは x1 から x3 だけを使ってk-meansを実施してみます。

kmeans関数がある

Rであれば stats::kmeans()でやってくれる。最高では?

k_means <- stats::kmeans(kmeans_data[,-c(1,5)], centers = 3)

結果はグラフでみてみましょう。 IDを横軸、各変数を縦軸に置いた時、ID方向に綺麗に色が分かれれば成功ですね。

f:id:kinuit:20200912022219p:plain
おや?

ダメそうです。ちなみに連続値 x4 があるといけます。

k_means <- stats::kmeans(kmeans_data[,-1], centers = 3)

f:id:kinuit:20200912024859p:plain
x4のおかげでしっかり分かれている、という印象。

なんで?

単純な話で、2値変数同士の散布図は、(0,0), (0,1), (1,0), (1,1)にしか点が置かれないので、
k平均法で重要な「重心」の計算が進まず、初期値から収束しないまま計算が終わってしまうからですね。それはそう。
でも周りだと意外とこれをやらかす人がいるので、世の中って怖いですよね。

ちなみに

「バイナリ距離で階層クラスタリングしたら割と行けちゃったりするのでは?」と思った

d_mat <- dist(kmeans_data[,-c(1,5)], method = "binary")
h_cluster <- hclust(d_mat, method = "average")

f:id:kinuit:20200912024211p:plain
デンドログラム的にはいい感じ

なんかデンドログラムはイケてるようにみえたんですが、やっぱりダメそう。

f:id:kinuit:20200912024245p:plain
お世辞にも綺麗なクラスタリングとは言えない模様

f:id:kinuit:20200912024554p:plain
x4を入れてみる。あれ、k-meansのほうよくね?

結論

2値データでクラスタリングをすると悲しいらしいね。
とはいえググる2値変数から上手いこと重心を計算してkmeansする方法などのレビューや
モデルベースでのクラスタリングなどがあるようで、研究は相応にされている模様。
上手い方法があれば教えてください。おわり。

*1:最初はデータ生成が独立なのでクラスタが本当に生成されない構造でした♡