Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Explorer PCA

livebooks/explorer/pca.livemd

Explorer PCA

Mix.install([
  {:explorer, "~> 0.9"},
  {:nx, "~> 0.9"},
  {:kino, "~> 0.14"},
  {:kino_vega_lite, "~> 0.1"}
])

Alias

alias Explorer.DataFrame
alias Explorer.Series
require Explorer.DataFrame

Load Datasets

Explorer に用意されているワインのデータセットを使用する

元データはこちら

> These data are the results of a chemical analysis of wines grown in the same region in Italy but derived from three different cultivars. The analysis determined the quantities of 13 constituents found in each of the three types of wines.

このデータは、イタリアの同じ地域で栽培された、3種類の異なる品種に由来するワインの化学分析の結果です。 この分析では、3種類のワインに含まれる13種類の成分の量を測定しています。

> The attributes are (dontated by Riccardo Leardi, riclea ‘@’ anchem.unige.it ) > > 1. Alcohol > 2. Malic acid > 3. Ash > 4. Alcalinity of ash > 5. Magnesium > 6. Total phenols > 7. Flavanoids > 8. Nonflavanoid phenols > 9. Proanthocyanins > 10. Color intensity > 11. Hue > 12. OD280/OD315 of diluted wines > 13. Proline

属性は以下の通りです(Riccardo Leardi, riclea@anchem.unige.it による寄贈)。

  1. アルコール
  2. リンゴ酸
  3. 灰分
  4. 灰のアルカリ度
  5. マグネシウム
  6. 総フェノール量
  7. フラバノイド
  8. 非フラバノイドフェノール類
  9. プロアントシアニン
  10. 色の濃さ
  11. 色相
  12. 希釈ワインのOD280/OD315
  13. プロリン
wine_df = Explorer.Datasets.wine()

データフレームをテーブル表示する

Kino.DataTable.new(wine_df)

クラスが3種類であることを確認する

wine_df
|> DataFrame.distinct(["class"])
|> DataFrame.pull("class")

分析に利用するため、 class 以外の列名を取得しておく

cols =
  wine_df
  |> DataFrame.discard("class")
  |> DataFrame.names()

Describe

Python における pandas の DataFrame.describe を再現する

各項目毎に集計値を取得し、テーブル表示する

  • count: データ数
  • mean: 平均
  • std: 標準偏差
  • min: 最小値
  • 25%: 1/4 分位数
  • 50%: 中央値
  • 75%: 3/4 分位数
  • max: 最大値

データのおおよその傾向を知ることができる

wine_df
|> DataFrame.describe()
|> Kino.DataTable.new()

Histogram

ヒストグラム = 度数分布図 を作成する

各値がどのように分布しているか(バラついているか)が可視化される

get_values = fn df, col ->
  df
  |> DataFrame.pull(col)
  |> Series.to_list()
end
histgram = fn df, col ->
  x = get_values.(df, col)

  VegaLite.new(width: 200, height: 200, title: col)
  |> VegaLite.data_from_values(x: x)
  |> VegaLite.mark(:bar)
  |> VegaLite.encode_field(
    :x,
    "x",
    type: :quantitative,
    bin: %{maxbins: 20},
    title: col
  )
  |> VegaLite.encode_field(
    :y,
    "x",
    type: :quantitative,
    aggregate: :count
  )
end

histgram.(wine_df, "alcohol")

すべての列に対してヒストグラムを作成する

cols
|> Enum.map(fn col ->
  histgram.(wine_df, col)
end)
|> Kino.Layout.grid(columns: 3)

クラス(ワインの品種)毎の分布を見る

all_class_histgram = fn df, col ->
  x = get_values.(df, col)
  color = get_values.(df, "class")

  VegaLite.new(width: 200, height: 200)
  |> VegaLite.data_from_values(x: x, color: color)
  |> VegaLite.mark(:bar, opacity: 0.5)
  |> VegaLite.encode_field(
    :x,
    "x",
    type: :quantitative,
    bin: %{maxbins: 20},
    title: co
  )
  |> VegaLite.encode_field(
    :y,
    "x",
    type: :quantitative,
    aggregate: :count
  )
  |> VegaLite.encode_field(
    :color,
    "color",
    type: :nominal
  )
end

all_class_histgram.(wine_df, "alcohol")
cols
|> Enum.map(fn col ->
  all_class_histgram.(wine_df, col)
end)
|> Kino.Layout.grid(columns: 3)

flavanoidscolor_intensity あたりが、クラスによって分布の頂点が違います

この辺りの値で大体クラス分類ができそうなことが分かります

Scatter

散布図を作成する

項目間の関連具合を見ることができる

scatter = fn df, x_col, y_col ->
  x = get_values.(df, x_col)
  y = get_values.(df, y_col)
  class = get_values.(wine_df, "class")

  VegaLite.new(width: 200, height: 200)
  |> VegaLite.data_from_values(x: x, y: y, class: class)
  |> VegaLite.mark(:point)
  |> VegaLite.encode_field(:x, "x",
    type: :quantitative,
    scale: [domain: [Enum.min(x), Enum.max(x)]],
    title: x_col
  )
  |> VegaLite.encode_field(:y, "y",
    type: :quantitative,
    scale: [domain: [Enum.min(y), Enum.max(y)]],
    title: y_col
  )
  |> VegaLite.encode_field(:color, "class", type: :nominal)
end

alcoholmalic_acid の散布図は完全にバラバラになっている

クラス毎にもバラツキが多い

scatter.(wine_df, "alcohol", "malic_acid")

flavanoidscolor_intensity の散布図は特定の箇所に集中している

クラス毎にも明確に分布が別れている

scatter.(wine_df, "flavanoids", "color_intensity")

Scatter Matrix

散布図行列を作成する

各項目の組み合わせで、同項目の場合はヒストグラム、別項目の場合は散布図を出す

graphs =
  cols
  |> Enum.map(fn col_1 ->
    h_graphs =
      cols
      |> Enum.map(fn col_2 ->
        cond do
          col_1 == col_2 ->
            all_class_histgram.(wine_df, col_1)

          true ->
            scatter.(wine_df, col_1, col_2)
        end
      end)

    VegaLite.new(width: 200 * Enum.count(cols), height: 200)
    |> VegaLite.concat(h_graphs, :horizontal)
  end)

VegaLite.new(width: 200 * Enum.count(cols), height: 200 * Enum.count(cols))
|> VegaLite.concat(graphs, :vertical)

Standardization

データを標準化する

データの大きさ、バラツキを統一するため、各値から平均を引いて標準偏差で割る

相関行列を計算しやすくするために標準化しておく

standardize = fn df, column ->
  mean =
    wine_df
    |> DataFrame.pull(column)
    |> Series.mean()

  std =
    wine_df
    |> DataFrame.pull(column)
    |> Series.standard_deviation()

  df
  |> DataFrame.mutate_with(fn in_df ->
    %{column => Series.subtract(in_df[column], mean)}
  end)
  |> DataFrame.mutate_with(fn in_df ->
    %{column => Series.divide(in_df[column], std)}
  end)
end
standardized_wine_df =
  cols
  |> Enum.reduce(wine_df, fn col, standardized_df ->
    standardize.(standardized_df, col)
  end)

standardized_wine_df
|> Kino.DataTable.new()

Correlation matrix

相関係数 = 項目間の関係性の強さ

相関行列 = 各項目の組み合わせに対して相関係数を算出したもの

標準化している場合、 相関行列 = 分散共分散行列

行列計算を行うため、データフレームをテンソルに変換する

df_to_tensor = fn df ->
  df
  |> DataFrame.names()
  |> Enum.map(fn col ->
    standardized_wine_df
    |> DataFrame.pull(col)
    |> Series.to_tensor()
  end)
  |> Nx.concatenate()
  |> Nx.reshape({DataFrame.n_columns(df), DataFrame.n_rows(df)})
end

standardized_wine_tensor =
  standardized_wine_df
  |> DataFrame.select(cols)
  |> df_to_tensor.()
  |> Nx.transpose()

分散共分散行列は (標準化した行列T・標準化した行列) / データ数 で求めることができる

covariance_tensor =
  standardized_wine_tensor
  |> Nx.transpose()
  |> Nx.dot(standardized_wine_tensor)
  |> Nx.divide(DataFrame.n_rows(standardized_wine_df))

テンソルをデータフレームに変換してテーブル表示する

add_cols_label = fn list, cols_ ->
  [{"x", cols_} | list]
end

covariance_df =
  cols
  |> Stream.with_index()
  |> Enum.map(fn {col, index} ->
    {col, Nx.to_flat_list(covariance_tensor[index])}
  end)
  |> add_cols_label.(cols)
  |> DataFrame.new()

covariance_df
|> Kino.DataTable.new(keys: ["x" | cols])

直感的に分かりづらいため、ヒートマップにして表示する

heatmap =
  cols
  |> Stream.with_index()
  |> Enum.map(fn {col_1, index_1} ->
    cols
    |> Stream.with_index()
    |> Enum.map(fn {col_2, index_2} ->
      covariance =
        covariance_tensor[index_1][index_2]
        |> Nx.to_number()

      %{
        x: col_1,
        y: col_2,
        covariance: covariance
      }
    end)
  end)
  |> List.flatten()
VegaLite.new(width: 300, height: 300)
|> VegaLite.data_from_values(heatmap)
|> VegaLite.mark(:rect)
|> VegaLite.encode_field(:x, "x", type: :nominal)
|> VegaLite.encode_field(:y, "y", type: :nominal)
|> VegaLite.encode_field(
  :fill,
  "covariance",
  type: :quantitative,
  scale: [
    domain: [-1, 1],
    scheme: :blueorange
  ]
)

Eigenvalues and Eigenvectors

PCA のために固有値、固有ベクトルを求める

{eigenvals, eigenvecs} = Nx.LinAlg.eigh(covariance_tensor)

Contribution rate

寄与率: 各主成分が、全体のうちどれくらいの情報を占めているか

寄与率 = 各固有値 / 総分散

総分散 = 項目数

get_contribution_rate = fn index ->
  Nx.to_number(eigenvals[index - 1]) / Nx.size(eigenvals)
end

contribution_rate_list =
  1..Nx.size(eigenvals)
  |> Enum.to_list()
  |> Enum.map(fn index ->
    get_contribution_rate.(index)
  end)

累積寄与率 = 当該成分までの合計寄与率

cumulative_contribution_rate_list =
  1..Nx.size(eigenvals)
  |> Enum.map(fn index ->
    contribution_rate_list
    |> Enum.slice(0, index)
    |> Enum.sum()
  end)

寄与率と累積寄与率をテーブル表示する

contribution_rate_df =
  %{
    "PC" => 1..Nx.size(eigenvals),
    "contribution_rate" => contribution_rate_list,
    "cumulative_contribution_rate" => cumulative_contribution_rate_list
  }
  |> DataFrame.new()

contribution_rate_df
|> Kino.DataTable.new()

累積寄与率を折れ線グラフで表示する

第1主成分、第2主成分で寄与率の半数以上を占めている

VegaLite.new(width: 300, height: 300)
|> VegaLite.data_from_values(
  x: 0..Nx.size(eigenvals),
  y: [0 | cumulative_contribution_rate_list]
)
|> VegaLite.mark(:line)
|> VegaLite.encode_field(
  :x,
  "x",
  type: :quantitative,
  title: "PC"
)
|> VegaLite.encode_field(
  :y,
  "y",
  type: :quantitative,
  title: "Cumulative Contribution Rate"
)

Dimensional compression

次元圧縮 = 各項目の値を主成分にまとめる

固有ベクトルの1番目 = 第1主成分得点

固有ベクトルの2番目 = 第2主成分得点

eigenvecs = Nx.transpose(eigenvecs)

w1 = eigenvecs[0]
w2 = eigenvecs[1]

IO.inspect(w1)
IO.inspect(w2)

w = Nx.stack([w1, w2], axis: 1)

標準化したデータと射影行列の内積を取ることで、第1主成分、第2主成分が取得できる

pca_wine_tensor =
  standardized_wine_tensor
  |> Nx.dot(w)
  |> Nx.transpose()

クラスと第1主成分、第2主成分でデータフレームを作る

classes = get_values.(wine_df, "class")

pca_wine_df =
  %{
    class: classes,
    pc1: Nx.to_flat_list(pca_wine_tensor[0]),
    pc2: Nx.to_flat_list(pca_wine_tensor[1])
  }
  |> DataFrame.new()

pca_wine_df
|> Kino.DataTable.new()

散布図にすると、クラス毎にまとまっていることが分かる

scatter.(pca_wine_df, "pc1", "pc2")