【保存版】ビジネスで使える13のデータ分析手法を分かりやすく解説 を眺めていて、コレスポンデンス分析という分析手法があることを知りました。
Google Analytics から取得したデータを元にコレスポンデンス分析をできないか調べたところ、R での参考記事を見つけたので、python で同じようなことができるか試してみました。
結果を以下に記載します。
参考
- esafak/mca: Multiple correspondence analysis
- Googleアナリティクスとコレスポンデンス分析を用いた年齢別のユーザー像の捉え方 | トライフィールズ
- ワインの味を分析してリア充達のクリスマスディナーを台無しにしよう - Qiita
- 【保存版】ビジネスで使える13のデータ分析手法を分かりやすく解説
分析して得る情報
記事のカテゴリ、記事を読んだ人の興味を持っているジャンルの傾向の情報
python で コレスポンデンス分析を行うライブラリ
python でコレスポンデンス分析を実行できるライブラリには以下があります。
今回はワインの味を分析してリア充達のクリスマスディナーを台無しにしよう - Qiita を参考に、mca
を使ってみます。
-
コレスポンデンス分析 : 分析技術とインテリジェンス
Orange の Orange.projection.correspondence を使って、コレスポンデンス分析を実行しています。 -
MaxHalford/prince: Python factor analysis library (PCA, CA, MCA, FAMD)
一般的な因子分析を行うライブラリ だと、README.md
に記載があります。 -
TomAugspurger/skmca: A scikit-learn compatible implementation of MCA
prince
のラッパーで、scikit-learn と同じようなインターフェースでコレスポンデンス分析を実装できます。 -
esafak/mca: Multiple correspondence analysis
Pandas のデータフレームを使用できる MCAライブラリにです。skmca
でも Pandas のデータフレームは使えます。
ライブラリのインストール
必要なライブラリのインストールをします。
mca のインストール
コレスポンデンス分析を行うesafak/mca: Multiple correspondence analysis というライブラリをインストールします。
-
pip install
python3 -m pip install --user mca
-
Output
Collecting mca Using cached mca-1.0.2.tar.gz Requirement already satisfied: scipy in /Users/xxxxxx/Library/Python/3.6/lib/python/site-packages (from mca) Requirement already satisfied: numpy in /Users/xxxxxx/Library/Python/3.6/lib/python/site-packages (from mca) Requirement already satisfied: pandas in /Users/xxxxxx/Library/Python/3.6/lib/python/site-packages (from mca) Requirement already satisfied: pytz>=2011k in /Users/xxxxxx/Library/Python/3.6/lib/python/site-packages (from pandas->mca) Requirement already satisfied: python-dateutil>=2 in /usr/local/lib/python3.6/site-packages (from pandas->mca) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.6/site-packages (from python-dateutil>=2->pandas->mca) Building wheels for collected packages: mca Running setup.py bdist_wheel for mca ... done Stored in directory: /Users/xxxxxx/Library/Caches/pip/wheels/85/0d/24/fe459ababfb49e7669f16d9607247c055d25537c0f5c7c0d93 Successfully built mca Installing collected packages: mca Successfully installed mca-1.0.2
その他のライブラリのインストール
gspread
、oauth2client
をインストールします。
python3 -m pip install gspread --user
python3 -m pip install oauth2client --user
python3 -m pip install df2gspread --user
データの入手
Google Analytics Spreadsheet Add-on | Analytics Implementation Guides and Solutions | Google Developers で Google Analytics からデータを取得します。
以下のMetrics と Dimensions を指定します。
Metrics
- ga:pageviews
Dimensions
- ga:dimension6 (ブログ記事のカテゴリ)
- ga:interestAffinityCategory
- ga:pagePath
手順
以下の流れで、可視化まで行います。
- データの取得
- データ加工
mca
の呼び出し- 可視化
1. データの取得
Google スプレッドシートからデータを、Pandas のデータフレームとして取得します。
from oauth2client.service_account import ServiceAccountCredentials
def download_as_df(sheet_id, wks_name):
"""
Google Spread Sheet からデータを取得、pandas data frame として返す。
"""
from df2gspread import gspread2df as g2d
# key_file 以下の指定方法だと、notebook と同じディレクトリにあるキーファイルを取得しています。
key_file = "spreadsheet_api_key.json"
scope = ['https://spreadsheets.google.com/feeds']
credentials = ServiceAccountCredentials.from_json_keyfile_name(key_file, scope)
df = g2d.download(sheet_id, wks_name=wks_name, col_names=True, row_names=False, credentials=credentials, start_cell = 'A15')
return df
# ファイルのダウンロード
df = download_as_df("1ruwQJIt35zFDXrcFlibwqJcMn1QJwtoJsTBQw7L-1WQ","ユーザの好みと、ページカテゴリの関係")
# データ出力
df
ga:dimension6 | ga:interestAffinityCategory | ga:pagePath | ga:pageviews | |
---|---|---|---|---|
0 | Elasticsearch | Lifestyles & Hobbies/Business Professionals | /blog/Implement-Paging-with-Elasticsearch-Java... | 29 |
1 | Elasticsearch | Lifestyles & Hobbies/Green Living Enthusiasts | /blog/Implement-Paging-with-Elasticsearch-Java... | 58 |
2 | Elasticsearch | Lifestyles & Hobbies/Shutterbugs | /blog/Implement-Paging-with-Elasticsearch-Java... | 24 |
... | ... | ... | ... | ... |
569 | python | Technology/Technophiles | /blog/pycharm-terminal-からpythonスクリプトを実行できるようにする/ | 98 |
599 rows × 4 columns
2.データ加工
データ加工として、以下を実施します。
* Affinity Category の 日本語化 と大カテゴリの切り出し。
* 大カテゴリを元にデータを再集計する。
* クロス集計表に変換する。
# Affinity Category の日本語化をするファンクションの定義
import requests
translation_dictionary = {}
def create_dictionary():
r = requests.get("https://gist.githubusercontent.com/kemsakurai/6c287eab5aa45415bf5c50df1e61a47f/raw/3ea1cf361a8914ad20c70ac46e8473e2590597e9/AffinityCategoryTranslation.tsv")
lines = r.text.split("\n")
index = 0
for line in lines:
index = index + 1
if index == 1:
continue
elems = line.split("\t")
translation_dictionary.update({elems[0]: elems[1]})
def translate_affinity_category(english_string):
results = []
for elem in english_string.split("/"):
results.append(translation_dictionary.get(elem, "undefined"))
return '/'.join(results)
# 辞書の初期化
create_dictionary()
# 大カテゴリのみを切り出し
df["ga:interestAffinityCategory"] = [i.split("/")[0] for i in df["ga:interestAffinityCategory"]]
# 切り出したカテゴリを日本語に変換
df["ga:interestAffinityCategory_ja"] = [translate_affinity_category(i) for i in df["ga:interestAffinityCategory"]]
# 後続の集計処理がうまく計算できないので、整数に変換する
df["ga:pageviews"] = df["ga:pageviews"].astype(int)
# 結果の出力
df
ga:dimension6 | ga:interestAffinityCategory | ga:pagePath | ga:pageviews | ga:interestAffinityCategory_ja | |
---|---|---|---|---|---|
0 | Elasticsearch | Lifestyles & Hobbies | /blog/Implement-Paging-with-Elasticsearch-Java... | 29 | ライフスタイル&趣味 |
1 | Elasticsearch | Lifestyles & Hobbies | /blog/Implement-Paging-with-Elasticsearch-Java... | 58 | ライフスタイル&趣味 |
2 | Elasticsearch | Lifestyles & Hobbies | /blog/Implement-Paging-with-Elasticsearch-Java... | 24 | ライフスタイル&趣味 |
3 | Elasticsearch | Media & Entertainment | /blog/Implement-Paging-with-Elasticsearch-Java... | 51 | メディア&エンターテイメント |
4 | Elasticsearch | Media & Entertainment | /blog/Implement-Paging-with-Elasticsearch-Java... | 46 | メディア&エンターテイメント |
5 | Elasticsearch | Media & Entertainment | /blog/elasticsearch-の-kuromoji-plugin-が削除されてin... | 13 | メディア&エンターテイメント |
... | ... | ... | ... | ... | ... |
598 | データ分析 | Technology | /blog/mac-os-siera-に-superset-をインストールする/ | 14 | 技術 |
599 rows × 5 columns
# 不要なカラムを削除
del df["ga:interestAffinityCategory"]
del df["ga:pagePath"]
# 再集計
df = df.groupby(['ga:dimension6','ga:interestAffinityCategory_ja'])['ga:pageviews'].sum().reset_index()
# ソート
df = df.sort_values(by="ga:pageviews",ascending=False)
# 結果の出力
df
ga:dimension6 | ga:interestAffinityCategory_ja | ga:pageviews | |
---|---|---|---|
200 | python | メディア&エンターテイメント | 1316 |
... | ... | ... | ... |
177 | msites;資格取得 | ホーム&ガーデン | 15 |
219 rows × 3 columns
# クロス集計
df = df.pivot_table(values='ga:pageviews', index='ga:dimension6', columns='ga:interestAffinityCategory_ja', aggfunc = 'sum')
# クロス集計結果を整数に変換
df = df.fillna(0).astype(int)
# 結果の出力
df
ga:interestAffinityCategory_ja | スポーツ&フィットネス | ニュース&政治 | フード&ダイニング | ホーム&ガーデン | メディア&エンターテイメント | ライフスタイル&趣味 | 技術 | 旅行 | 自動車&輸送 | 買い物好き | 銀行&金融 |
---|---|---|---|---|---|---|---|---|---|---|---|
ga:dimension6 | |||||||||||
Elasticsearch | 31 | 0 | 0 | 0 | 110 | 111 | 126 | 11 | 0 | 73 | 0 |
Elasticsearch;java | 26 | 16 | 0 | 18 | 42 | 70 | 48 | 15 | 0 | 28 | 0 |
Google Apps Script | 17 | 61 | 15 | 19 | 112 | 80 | 63 | 17 | 0 | 34 | 14 |
Google Spread Sheet | 121 | 236 | 83 | 83 | 507 | 418 | 286 | 75 | 11 | 211 | 50 |
Jupyter Notebook | 14 | 34 | 0 | 0 | 18 | 32 | 33 | 0 | 0 | 21 | 0 |
Lombok;findbugs;java | 0 | 0 | 0 | 0 | 11 | 21 | 24 | 0 | 0 | 13 | 0 |
MongoDB | 0 | 12 | 0 | 0 | 0 | 14 | 12 | 0 | 0 | 13 | 0 |
SEO;python;データ分析 | 24 | 30 | 0 | 22 | 22 | 97 | 58 | 23 | 0 | 29 | 25 |
SonarQube | 16 | 0 | 0 | 0 | 16 | 47 | 35 | 0 | 0 | 21 | 0 |
SonarQube;python | 11 | 25 | 0 | 17 | 25 | 50 | 37 | 11 | 0 | 21 | 0 |
Web API;python | 28 | 76 | 16 | 29 | 157 | 133 | 100 | 22 | 0 | 56 | 18 |
amp | 0 | 0 | 0 | 0 | 0 | 0 | 13 | 0 | 0 | 12 | 0 |
django | 76 | 194 | 37 | 45 | 408 | 306 | 288 | 37 | 0 | 164 | 35 |
django;mezzanine | 0 | 0 | 0 | 17 | 0 | 15 | 40 | 0 | 0 | 21 | 0 |
django;mezzanine;python | 13 | 0 | 0 | 0 | 0 | 44 | 32 | 0 | 0 | 16 | 0 |
django;postgresql | 0 | 14 | 0 | 0 | 13 | 43 | 30 | 0 | 0 | 16 | 0 |
eclipselink;java | 0 | 0 | 0 | 11 | 0 | 24 | 17 | 0 | 0 | 15 | 0 |
findbugs | 0 | 0 | 0 | 0 | 0 | 13 | 17 | 0 | 0 | 17 | 0 |
gradle | 22 | 54 | 0 | 23 | 89 | 94 | 68 | 14 | 0 | 36 | 0 |
jackson;java | 33 | 93 | 16 | 40 | 141 | 159 | 126 | 22 | 0 | 78 | 15 |
java | 160 | 270 | 85 | 86 | 644 | 543 | 422 | 63 | 0 | 260 | 54 |
java;pmd | 26 | 71 | 0 | 0 | 88 | 92 | 86 | 0 | 0 | 43 | 0 |
java;rest;wicket | 0 | 0 | 0 | 0 | 0 | 0 | 36 | 0 | 0 | 21 | 0 |
java;wicket | 0 | 0 | 0 | 0 | 48 | 41 | 42 | 0 | 0 | 33 | 0 |
javascript | 0 | 22 | 0 | 18 | 118 | 73 | 63 | 0 | 0 | 33 | 0 |
linux;python | 0 | 0 | 0 | 0 | 17 | 20 | 23 | 0 | 0 | 23 | 0 |
memcached | 54 | 111 | 26 | 33 | 169 | 174 | 135 | 27 | 0 | 74 | 31 |
mezzanine;python | 0 | 0 | 0 | 0 | 0 | 43 | 42 | 0 | 0 | 32 | 0 |
msites;資格取得 | 18 | 0 | 0 | 15 | 30 | 48 | 43 | 13 | 0 | 25 | 0 |
pmd | 0 | 0 | 0 | 0 | 0 | 21 | 35 | 0 | 0 | 17 | 0 |
postgresql | 31 | 65 | 16 | 26 | 113 | 117 | 94 | 18 | 0 | 49 | 16 |
python | 250 | 565 | 110 | 194 | 1316 | 918 | 773 | 129 | 13 | 476 | 71 |
python;データ分析 | 26 | 0 | 0 | 0 | 45 | 71 | 53 | 0 | 0 | 29 | 0 |
spring-boot | 0 | 0 | 0 | 0 | 15 | 56 | 45 | 0 | 0 | 24 | 0 |
wicket | 0 | 0 | 0 | 0 | 0 | 0 | 13 | 0 | 0 | 0 | 0 |
データ分析 | 0 | 0 | 0 | 0 | 0 | 14 | 14 | 0 | 0 | 0 | 0 |
3.mca の呼び出し
クロス集計データの準備ができたので、mca を呼び出します。
# コレスポンデンス分析のライブラリ
import mca
# コレスポンデンス分析
ncols = df.shape[1]
# Benzécri? 補正するかどうか、データ構造の問題か、True だとエラーとなったため、False で設定
mca_ben = mca.MCA(df, ncols=ncols, benzecri=False)
mca_ben.fs_r(N=2)
array([[ 4.04399401e-01, -1.52979611e-01],
[ 1.32693892e-01, 2.68842331e-01],
[ -2.23112167e-01, 3.42928950e-02],
[ -1.59749683e-01, 2.37024719e-02],
[ 1.30182305e-01, -5.47186735e-02],
[ 6.84718573e-01, -1.58233299e-01],
[ 4.44952860e-01, 4.10821376e-03],
[ 2.85366767e-02, 6.05926046e-01],
[ 5.39503680e-01, 3.58668344e-02],
[ 8.45671201e-02, 3.06657490e-01],
[ -1.22543395e-01, 4.62416906e-02],
[ 1.23966422e+00, -2.25021302e-01],
[ -5.67431872e-02, -6.78476641e-02],
[ 7.72658336e-01, 4.27289369e-01],
[ 7.21808152e-01, 2.17287190e-01],
[ 4.30533385e-01, -4.64137005e-02],
[ 6.54749307e-01, 4.96416905e-01],
[ 1.02923931e+00, -3.24243400e-02],
[ -1.24779452e-02, 5.91214264e-02],
[ -2.55507110e-02, 1.04271517e-01],
[ -7.17114562e-02, -2.35859666e-02],
[ 8.42241200e-02, -1.78772790e-01],
[ 1.24523117e+00, -2.02165463e-01],
[ 4.49509345e-01, -3.42900571e-01],
[ 5.81775959e-02, -2.59105041e-01],
[ 6.35473732e-01, -2.65732128e-01],
[ -1.23921506e-01, 1.17432604e-01],
[ 9.62526283e-01, 4.09888658e-02],
[ 2.68648888e-01, 3.02809495e-01],
[ 1.02700453e+00, 7.05955985e-04],
[ -7.98232747e-02, 1.09810136e-01],
[ -1.03646230e-01, -6.40915368e-02],
[ 3.98004987e-01, -7.22225097e-02],
[ 7.19449170e-01, -3.80962202e-02],
[ 1.26361260e+00, -1.26698070e-01],
[ 8.72559804e-01, 1.77653717e-01]])
mca_ben.fs_c(N=2)
array([[-0.09804034, 0.11634891],
[-0.30316912, -0.00814008],
[-0.43927978, -0.03347052],
[-0.16271903, 0.36136428],
[-0.19793054, -0.17165508],
[ 0.11685867, 0.06648366],
[ 0.30667071, -0.01747563],
[-0.26910795, 0.33520229],
[-0.53301903, -0.1729311 ],
[ 0.29456212, -0.0457295 ],
[-0.39228832, 0.36158167]])
import pandas as pd
# 表頭の座標を書き出す
result_row = pd.DataFrame(mca_ben.fs_r(N=2))
result_row.index = list(df.index)
result_row
0 | 1 | |
---|---|---|
Elasticsearch | 0.404399 | -0.152980 |
Elasticsearch;java | 0.132694 | 0.268842 |
Google Apps Script | -0.223112 | 0.034293 |
Google Spread Sheet | -0.159750 | 0.023702 |
Jupyter Notebook | 0.130182 | -0.054719 |
Lombok;findbugs;java | 0.684719 | -0.158233 |
MongoDB | 0.444953 | 0.004108 |
SEO;python;データ分析 | 0.028537 | 0.605926 |
SonarQube | 0.539504 | 0.035867 |
SonarQube;python | 0.084567 | 0.306657 |
Web API;python | -0.122543 | 0.046242 |
amp | 1.239664 | -0.225021 |
django | -0.056743 | -0.067848 |
django;mezzanine | 0.772658 | 0.427289 |
django;mezzanine;python | 0.721808 | 0.217287 |
django;postgresql | 0.430533 | -0.046414 |
eclipselink;java | 0.654749 | 0.496417 |
findbugs | 1.029239 | -0.032424 |
gradle | -0.012478 | 0.059121 |
jackson;java | -0.025551 | 0.104272 |
java | -0.071711 | -0.023586 |
java;pmd | 0.084224 | -0.178773 |
java;rest;wicket | 1.245231 | -0.202165 |
java;wicket | 0.449509 | -0.342901 |
javascript | 0.058178 | -0.259105 |
linux;python | 0.635474 | -0.265732 |
memcached | -0.123922 | 0.117433 |
mezzanine;python | 0.962526 | 0.040989 |
msites;資格取得 | 0.268649 | 0.302809 |
pmd | 1.027005 | 0.000706 |
postgresql | -0.079823 | 0.109810 |
python | -0.103646 | -0.064092 |
python;データ分析 | 0.398005 | -0.072223 |
spring-boot | 0.719449 | -0.038096 |
wicket | 1.263613 | -0.126698 |
データ分析 | 0.872560 | 0.177654 |
# 表側の座標を書き出す
result_col = pd.DataFrame(mca_ben.fs_c(N=2))
result_col.index = list(df.columns)
result_col
0 | 1 | |
---|---|---|
スポーツ&フィットネス | -0.098040 | 0.116349 |
ニュース&政治 | -0.303169 | -0.008140 |
フード&ダイニング | -0.439280 | -0.033471 |
ホーム&ガーデン | -0.162719 | 0.361364 |
メディア&エンターテイメント | -0.197931 | -0.171655 |
ライフスタイル&趣味 | 0.116859 | 0.066484 |
技術 | 0.306671 | -0.017476 |
旅行 | -0.269108 | 0.335202 |
自動車&輸送 | -0.533019 | -0.172931 |
買い物好き | 0.294562 | -0.045729 |
銀行&金融 | -0.392288 | 0.361582 |
4.可視化
matplotlib で グラフを描画します。
# 作図用ライブラリ
import matplotlib.pyplot as plt
import matplotlib
%matplotlib inline
plt.rcParams['font.family'] = 'IPAPGothic' #全体のフォントを設定
plt.rcParams["figure.figsize"] = [20, 12] # グラフのサイズを指定
plt.rcParams['font.size'] = 12 #フォントサイズを設定 default : 12
plt.rcParams['xtick.labelsize'] = 10 # 横軸のフォントサイズ
plt.rcParams['ytick.labelsize'] = 10 # 縦軸のフォントサイズ
matplotlib.font_manager._rebuild()
import random as rnd
# 図の設定(任意)
plt.figure(figsize=(10,10))
plt.rcParams["font.size"] = 10
<matplotlib.figure.Figure at 0x1089c03c8>
# 表頭をプロット
plt.scatter(result_col[0], result_col[1], s=100, marker="o")
# ラベル付け
cnt = 0
for label in list(result_col.index):
r = rnd.random() * 0.1
plt.text(result_col.iloc[cnt, 0]+r, result_col.iloc[cnt, 1]+r, label)
plt.plot([result_col.iloc[cnt, 0]+r, result_col.iloc[cnt, 0]], [result_col.iloc[cnt, 1]+r, result_col.iloc[cnt, 1]])
cnt += 1
# 表側をプロット
plt.scatter(result_row[0], result_row[1], s=100, marker="o")
# ラベル付け
cnt = 0
for label in list(result_row.index):
r = rnd.random() * 0.1
plt.text(result_row.iloc[cnt, 0]+r, result_row.iloc[cnt, 1]+r, label)
plt.plot([result_row.iloc[cnt, 0]+r, result_row.iloc[cnt, 0]], [result_row.iloc[cnt, 1]+r, result_row.iloc[cnt, 1]])
cnt += 1
結果の分析
点と点の距離が近いものは、実際に似通った傾向が出ているデータです。
どんな傾向が出ているかをグラフ上に書き込みました。
表頭のグラフ
上記、丸をつけた箇所の説明です。
-
緑色
ページの種類に因らず、PageView数が多いグループになります。 -
赤色
PageView数が少ないグループになります。自動車&輸送
系のカテゴリに興味がある人は、ページのアクセスが偏っていました。 -
黄色
PageView数は少ない。python 関係のページにアクセス数が多いグループのようでした。
表側のグラフ
上記丸をつけた箇所の説明です。
-
赤丸
PageViewが多く、ユーザーのアフィニティカテゴリに偏りが少なかったグループになります。 -
緑色
PageViewはそこそこで、ユーザーのアフィニティカテゴリに偏りがあったグループになります。 -
紫色
PageViewが少ない、ユーザーのアフィニティカテゴリに偏りがあったグループになります。
アフィニティカテゴリの偏りは、緑色のグループとは異なります。
まとめ
Google Analytics のデータを python でコレスポンデンス分析してみました。
以下まとめます。
- mca を使うことで、python での コレスポンデンス分析は実施可能だった。
- カスタムディメンションを予め送信しておくことで、独自指標での分析ができる。
- 図の解釈を人間が行わないといけないので、ラベルとして実データを表示できるインタラクティブなグラフのほうが分析しやすいかもしれない。
以上です。
コメント