俺のOneNote

俺のOneNote

データ分析が仕事な人のOneNote愛とか、分析小話とか。

横浜市のテイクアウト情報をダッシュボード化してみた件。

f:id:kopaprin:20200509144006j:plain

地元横浜市を応援したいっていうことで、横浜市が展開している以下サイトのデータをTableauダッシュボード化してみました。

www.city.yokohama.lg.jp

処理コードなどはこちらにまとめております。

github.com

基本的に、公開CSVを多少処理して先般記事のとおりジオコーディングしているだけです。

ほんとは料理ジャンルとか駅名とかいろいろ前処理できるといいのかもですが、 あまりにもパターンが多いので挫折。

現状の完成品はこちらです。

※5/9現在、スマホレイアウトしか対応してません。
 Desktopレイアウトとの共存は余裕あれば対応(練習)してみたいと思います。

Google Map API を利用した Python によるジオコーディング

いろんなデータを漁ってると、住所だけの情報を扱うことがでてきます。
よほど整備されたデータでない限り、緯度・経度なんてついてないことが多い。。。

私は地理データが大変苦手なんですが、どうしても正確な位置情報をダッシュボード上にプロットする必要があったので、
Pythonでジオコーディングします。

ググってみると色々やり方はあるようなのですが、
既にエラーで出来ないコードも多かったので、確実なGoogle Map APIgooglemapsを使った方法にします。

Google Map API の使用

GCPでGeocoding APIを取得するだけの作業です。
ソースは以下のような詳しいサイトがたくさんあるので、これらに従えば大丈夫です。

nendeb.com

取得できたらAPIキーをコピっておきます。

f:id:kopaprin:20200508005450p:plain

Python

利用するのは googlemaps です。

github.com

試しにマイホームタウンの横浜市役所の住所をぶちこんでみます。

#!pip install googlemaps
import googlemaps

googleapikey = 'ここにAPIキーを入れる'
gmaps = googlemaps.Client(key=googleapikey)

gmap_list = gmaps.geocode("横浜市中区港町1-1")

ll = gmap_list[0]["geometry"]["location"]
print("Latitude : ",ll["lat"])
print("Longitude : ",ll["lng"])

無事、緯度経度を取得できたようです。

f:id:kopaprin:20200508010312p:plain

実際に横浜市役所の位置か検証します。

先生!ばっちりでありんす!

これで適当にリストで住所をながせば、緯度経度情報が簡単に揃えられそうです。
APIの使用制限や料金はちゃんと検討しないと・・・。まだ詳しくないです。すみません。

ちょっとだけ、地理データと仲良くなれた気がしたデータ遊びでした。

Google Colaboratory からKaggle API を叩く

過去にQiitaに書いたことがありますが、
こちらにもメモとして再度検証、記録に残します。

Kaggle APIの公式リファレンスはこちら

github.com

Kaggle json のダウンロード

My Accountから、API Tokenを作成、kaggle.jsonをダウンロードします。

f:id:kopaprin:20200506105528j:plain

以下、google drive上に先ほどのjsonを格納

f:id:kopaprin:20200506105624j:plain

Colaboratory

以下、pipからkaggleモジュールをインポート

!pip install kaggle

google driveにマウント、必要があれば適当にカレントディレクトリを設定します。

import os
from google.colab import drive
drive.mount('/content/drive')
os.chdir("/content/drive/My Drive/hoge/hogehoge")

先ほど読み込んだjsonファイルからusernameとkeyを環境変数に読み込みます。

import os
import json
f = open("/content/drive/My Drive/kaggle.json", 'r')
json_data = json.load(f) 
os.environ['KAGGLE_USERNAME'] = json_data['username']
os.environ['KAGGLE_KEY'] = json_data['key']

以上で準備は終わりです。簡単でした。

api

上記処理で、あとはapiを叩くだけです。

現在開催されているコンペティションリストはこれ。

!kaggle competitions list

f:id:kopaprin:20200506111754p:plain

試しに現在開催されている以下コンペのデータをカレントディレクトリにダウンロードしてみます。

www.kaggle.com

Dataタブに表記されているapiを、colaboratoryにコピペするだけです。

f:id:kopaprin:20200506105600p:plain

!kaggle competitions download -c m5-forecasting-uncertainty

f:id:kopaprin:20200506111920p:plain

無事全ファイルダウンロードされていました。

また、この例ではデータはzip形式になっていますが、
pandas.read_csvはzipのまま読み込むことが可能です。

import pandas as pd
train = pd.read_csv("sales_train_validation.csv.zip")

print(train.shape)
display(train.head())

f:id:kopaprin:20200506112120p:plain

無事、ちゃんと読み込めていそうな感じでした。

appendix

過去から記事を漁るといろいろでてきますので、お好きな方法で実装すればよろしいかと思います。

【Pythonメモ】Google ColaboratoryでKaggle APIを使うおまじないコード&作法 - Qiita

Easy way to use Kaggle datasets in Google Colab | Data Science and Machine Learning

Google Collaboratory で kaggle を扱う

Google Colab上でKaggleのデータをロード、モデル訓練、提出の全てを行う - Qiita

Ternary plot をTableauで実現する

Ternary Plot、3次元プロット?をTableauで実装してみます。

en.wikipedia.org

Tearnary Plotは3次元の構成要素を正三角形内にプロットするVizです。

Tableau Public に Tearnary Plot のサンプルが多々 Publish されているので、
それを見ていただくとイメージがつかめます。
僕もこれらを参考に、以下設計していきます。

public.tableau.com

データ

e-statから市区町村の年齢3階級別人口を取得しました。

この3カテゴリの構成比データを2次元座標データに加工し、 Tableau上でTernary Plotとして描写するのが本記事のゴールです。

www.e-stat.go.jp

上記e-statでデータを加工し、以下のデータを取得します。

f:id:kopaprin:20200504233108p:plain

余計な情報はcsv上で処理し、Tableauへ渡します。

Tableau上での描写

Tableauに渡した段階のデータはこちらになります。

f:id:kopaprin:20200504234814p:plain

ここから、以下のとおり構成比のメジャーを作成します。

f:id:kopaprin:20200505124617p:plain

このデータをどう2次元座標データにするかという問題には、wikipediaに答えがあります。

en.wikipedia.org

以下計算式により、3次元のa,b,cデータをデカルト座標にプロットできます。*1

f:id:kopaprin:20200505125040p:plain

TableauでX, Yのメジャーを以下のとおり作成します。

/// X
(1/2) * 
(
(2*[B:15-64]+[C:65]) /
([A:-15]+[B:15-64]+[C:65])
)
///Y
(SQRT(3)/2) *
(
[C:65] /
([A:-15]+[B:15-64]+[C:65])
)

あとはX, Yのメジャーを行列に配置し、 詳細に市町村名を配置、適当に都道府県等のディメンションで色分け、pointの大きさを人口にしてみます。

X,Yの軸の範囲は0~1に固定しておきましょう。 また、レポートの背景色・罫線などの情報は削除しておきます。

f:id:kopaprin:20200505140436p:plain

別にダッシュボードを作成、 背景に上記wikipediaで公開されているTernary Plotの画像を適当に拝借しましょう。

commons.wikimedia.org

設置した背景の上に作成したレポートを浮動オブジェクトで合わせます。

f:id:kopaprin:20200505140923p:plain

これで完成、全国市区町村の年齢3階級別構成比を2次元のTernary Plotで表現できました。

f:id:kopaprin:20200505141110g:plain

Public Garrary上の動かせるデータはこちらです。

public.tableau.com

appendix

正直、市区町村年齢構成の大勢はそこまで変わりませんので、
DataViz作品としては微妙でしたね。

もう少し時系列データで遷移を見たりすると面白いかもしれません。

3次元かつ、それぞれの軸が百分率で表現できる指標にすると、こちらのVizで表現できます。
機会があったら他の例もつくってみたいと思います。

*1:線形代数がダメなので、表現間違ってたらすみません。勉強しておきます

商品購買のアソシエーションルールをネットワークで可視化する

アソシエーション分析の概略

アソシエーション分析は、商品の何と何が一緒に買われやすいのか?を示すための分析手法です。
「アソシエーション分析」とか「アソシエーションルール」とかをググると大量の記事が出てきますので、詳細はそちらの記事を参考にしてください。*1

以下、本記事中で重要になる点だけ記載します。

支持度(support)
総購買ユーザーNのうち、商品Aと商品Bを同時に買っている人の割合です。

f:id:kopaprin:20200504011925p:plain

信頼度(confidence)
商品Aを買った人のうち、商品Bを購入した人の割合です。

f:id:kopaprin:20200504011950p:plain

リフト(lift)

商品Aを買った人のうち商品Bを購入する人の割合(信頼度)が、
そもそも総購買ユーザーNにおける商品Bの購入率より高いか低いか、を表します。

f:id:kopaprin:20200504012034p:plain

一般的には、ある程度の支持度がある購入パターンのみで線引きし、リフト値を見て商品ごとの連関強弱を計ります。

今回の目的は、アソシエーションルールをネットワークで可視化し、商品ごとの連関を分かりやすく表現できるか検証します。
ネットワークは、Pythonのnetworkxを利用し、Tableauで描写することにします。

SQLによるデータ抽出

自身のお仕事にも活かせるよう購買データを使います。
postgresqlが提供してくれているDVDレンタルのサンプルデータを使用することにします。 ただし、バスケット(同レシート内購買)単位での分析は難しいので、ユーザー単位で集計することとします。

サンプルデータとER図はここで確認できます。

www.postgresqltutorial.com

f:id:kopaprin:20200502112927p:plain

これを自身のDB環境に入れます。

以下、SQLで上記指標に必要なA∩B, A, B, Nを集計します。*2

with item as (
    select
        inventory_id
        , title
        , name 
    from

--- 中略------------------------------------------

    inner join(
        select
            tran.title
            , count (distinct customer_id) as "B"
        from
            tran
        group by
            tran.title
    ) as target_table
    on fromto.target_item = target_table.title
    
    ;

※正直汚いSQLです。すみません。
 気になる方は以下GitHubをご確認いただければと思います。

github.com

こんな感じのテーブルに仕上がりました。

f:id:kopaprin:20200504010456p:plain

ここから、support, confidence, liftの算出とネットワーク座標の生成はPythonに渡します。

Python・networkxによる node座標データの作成

ここは、先般のブログどおり、netowrokxでTableauに取り込むテーブルデータを準備します。

kopaprin.hatenadiary.jp

まず、support , confidence , lift を計算します。 さらにAandBの出現UUが少ない組み合わせは除外してしまいましょう。

import pandas as pd
edge_df = pd.read_csv("output.csv")
# 以下、support , confidence , liftを計算します。
edge_df["support"] = edge_df["AandB"] / edge_df["N"]
edge_df["confidence"] = edge_df["AandB"] / edge_df["A"]
edge_df["lift"] = edge_df["confidence"] / ( edge_df["B"] / edge_df["N"] )
# supportが低い組み合わせを除外します。
edge_df = edge_df[edge_df["AandB"]>3]
display(edge_df)
print(edge_df.shape)

本来はconfidenceとliftの両方を考察すべきですが、
一旦liftだけをweightとしてみました。

edge_df_lift = edge_df[["source_item","target_item","confidence"]]
col_name = ["source", "target","weight"]
edge_df_lift.columns = col_name


import networkx as nx

def make_network(df):
  G = nx.from_pandas_edgelist(df,edge_attr=True)
  pos = nx.spring_layout(G)
  edges = G.edges()
  weights = [G[u][v]['weight'] for u,v in edges]
  nx.draw(G, pos, edges=edges, width=weights, node_size=10, node_color="c",with_labels=False)

  node = []
  x = []
  y = []
  for k,v in pos.items():
    node.append(k)
    x.append(v[0])
    y.append(v[1])

  node_df=pd.DataFrame({
      "item":node,
      "X":x,
      "Y":y
  })

  return node_df 

node_df = make_network(edge_df_lift)

・・・一応netowrokxでの画像出力していますが、完全に意味不明な感じになっています。

f:id:kopaprin:20200504020725p:plain

結果が見える戦いをするのはつらいですが、Tableau化まで以前と同じ手順で進めます。

result_df_1 = pd.merge(edge_df_lift,node_df,how="left",left_on="source",right_on="item")
result_df_2 = pd.merge(edge_df_lift,node_df,how="left",left_on="target",right_on="item")
result_df = pd.concat([result_df_1, result_df_2])
result_df["edge_name"] = result_df["source"] + "_" + result_df["target"]

Tableau上での可視化

手順は上にあげた過去記事とおりですので省略します。

なんとなく、lift(weight)が明らかに高い組み合わせもありますが、全体のネットワークの傾向としては解釈が難しいですね。

networkx側でもう少し手を加えるべきなのでしょうか・・・?
そもそも購買データをネットワークグラフにするのが適切ではない・・・?
あるいはサンプルデータだからランダマイズされてるとか・・・?

技術・知識的な未熟さが原因か、そもそもデータの話かは検証する余地がありそうですが、 面白い試みだったかなぁと思います。

*1:Albertの記事が大変分かりやすかったです。

*2:ここではユニークユーザー数ベースの集計値です。もしかしたら単純なトランザクションのほうがよかったかも?

【随時更新】誰にも教えたくないBIダッシュボードデザインに便利なサイト集を晒す

デザイン・分析アイデア

public.tableau.com

powerbi.microsoft.com

d3js.org

www.storytellingwithdata.com

datavizproject.com

data-viz-lab.com

From data to Viz | Find the graphic you need

デザイン素材

icooon-mono.com

free-line-design.com

kage-design.com

unsplash.com

www.pakutaso.com

www.irasutoya.com

stories.freepik.com

配色ツール

https://color.adobe.com/ja/create/color-wheel/color.adobe.com

coolors.co

brandpalettes.com

データ

www.kaggle.com

https://archive.ics.uci.edu/ml/index.phparchive.ics.uci.edu

www.data.go.jp

www.e-stat.go.jp

appendix

気になったものなど、随時更新していきます。

ネットワークグラフをつくるためのTableauとPython

Tableauでネットワークグラフを可視化しようとすると、座標データをどう用意するかが一番ネックになると思います。
現状、Tableau内でネットワークグラフを自動生成する機能は無さそうです。

ので、この辺の処理はPythonを噛ませると楽にできるので、ちゃんとできるか試行します。

networkxによる処理

そもそもnetworkxを使って完結しちゃえばいいじゃん、という話ではありますが、
TableauはじめBIプラットフォーム上で表現できれば色々使用用途が高いです。

まずはリファレンスに従い、乱数を固定しておいたほうが良さそうです。

networkx.github.io

import random
random.seed(246) 
import numpy
numpy.random.seed(4812)

pandas.DataFrameで以下のようなサンプルデータを用意。

import pandas as pd
edge_df = pd.DataFrame({
    "source":["A","B","B","C","A","C","C","A","B","F","E","A","H"],
    "target":["B","C","D","D","E","E","F","G","G","G","H","I","I"],
    "weight":[0.6,0.1,0.2,0.9,0.8,0.7,0.5,0.3,0.8,0.1,0.7,0.4,0.8]
})

networkxを使用して、任意のアルゴリズムによるノードの座標を取得しておきます。

import networkx as nx
G = nx.from_pandas_edgelist(edge_df,edge_attr=True)
pos = nx.spring_layout(G)

この座標情報をテーブル化してTableauに読ませることになります。

なお、普通に描写するとこんな感じ。

edges = G.edges()
weights = [G[u][v]['weight'] for u,v in edges]
nx.draw(G, pos, edges=edges, width=weights, node_size=400, node_color="c",with_labels=True,font_weight="bold")

f:id:kopaprin:20200501153406p:plain

エッジのweight以外も属性をつけることが可能です。
その場合、上記の例ではinput_dfに別カラムをつけ、edge_attr=TrueをしておけばOK。

これと同じネットワークグラフをTableauで描写するのが目的です。 posはdict型なので、テーブル化しておきます。

print(type(pos))
print(pos)
>>> <class 'dict'>
>>> {'A': array([ 0.05494571, -0.43277963]), 'B': array([0.38390294, 0.05184413]), 'C': array([-0.08115845,  0.52158239]), 'D': array([-0.08585107,  0.92455717]), 'E': array([-0.38630969, -0.13289213]), 'F': array([0.5411944 , 0.85869962]), 'G': array([ 0.75986786, -0.13710168]), 'H': array([-0.73922077, -0.65390988]), 'I': array([-0.44737093, -1.        ])}
import pandas as pd
node = []
x = []
y = []
for k,v in nodePos.items():
  node.append(k)
  x.append(v[0])
  y.append(v[1])

node_df=pd.DataFrame({
    "item":node,
    "X":x,
    "Y":y
})

f:id:kopaprin:20200501154446p:plain

作成したノードの座標情報データnode_dfと、
最初につくったエッジの情報データedge_dfをTableauへ送り込む用に加工します。

result_df_1 = pd.merge(edge_df,node_df,how="left",left_on="source",right_on="item")
result_df_2 = pd.merge(edge_df,node_df,how="left",left_on="target",right_on="item")
result_df = pd.concat([result_df_1, result_df_2])
result_df["edge_name"] = result_df["source"] + "_" + result_df["target"]

edge_dfを縦積みにして、source,targetそれぞれのX座標,Y座標をjoinします。
おまけにfrom-toを表すedge_nameをつけておきます。

f:id:kopaprin:20200501173837p:plain

よーし、準備OK!
result_dfをTableauに持っていくぞー。

Tableauによる表現

まずはX,Y座標によるscatter plotにして二重軸にします。
X,Y座標のメジャーを「合計」にしないように注意。
「平均」とか「最大」とかを選びましょう。

f:id:kopaprin:20200501175501p:plain

1つめの軸はディメンションのedge_nameを詳細にドラッグ(itemよりも上の階層)してマークの種類は「線」、
edgeの太さや色はweightでお好きに加工。
edgeの完成です。

f:id:kopaprin:20200501175817p:plain

2つめの軸のマークは「点」、詳細はitemだけでOKで、適当にラベルとか大きさを調整します。 これでnodeの完成。

f:id:kopaprin:20200501175941p:plain

あとは二重軸を同期して完成です~

f:id:kopaprin:20200501180039p:plain

無事、networkxと同じ描写ができましたね。
BIに載せることができると、表示範囲のフィルタリング等がしやすいので重宝しそうです。
データも最低限source, targetの情報さえあれば描写できちゃいますしね。