LinkedIn Visualizer ~Dash & Plotlyを用いて LinkedIn データ可視化 Web アプリを作る~

# 1. はじめに

今回、plotly、dash というライブラリを用いて  LinkedIn データを可視化する Web アプリを作っていこうと思います。

完成イメージは下の画像のような感じです。
要は、
1. ユーザーがアップロードしたデータに応じて
2. plotlyでグラフを書き
3. グラフをユーザーに返してあげる
ような Dash アプリケーションを作ろうというものです。
![Screenshot 2021-05-04 150028.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/4fe98e2b-b80a-2ab3-4999-01fc4bb002cc.png)



https://linkedin-visualizer.herokuapp.com/ にて試していただくこともできます。

サンプルデータは [こちら](https://linkedin-visualizer.s3.eu-north-1.amazonaws.com/Connections_sample.csv) をお使いください。
LinkedInアカウントをお持ちの方は、([取得方法](https://www.linkedin.com/help/linkedin/answer/66844/export-connections-from-linkedin?lang=en)) に沿って Connections.csv というファイルを取得すると、ご自身のデータを可視化することができます。


# 2. Plotly、Dashについて
## 2.1 Plotly
Document から引用します

> Plotly's Python graphing library makes interactive, publication-quality graphs. Examples of how to make line plots, scatter plots, area charts, bar charts, error bars, box plots, histograms, heatmaps, subplots, multiple-axes, polar charts, and bubble charts. (https://plotly.com/python/)          

注目すべきは interactive というところ。
例えばマウスをホバーするとその時点の数字としてのデータが表示されたり、
<img width=100% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/72b7fbcd-797c-d7e0-c3e9-4e2f2624694d.png>
ズームしたりできます。
<img width=100% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/2071cd48-811a-0a5c-275d-eebb97683958.png>

実は、Dash Core Componentというモジュールを用いると、スライサーをつけたり pull down をつけたりと、もう少し高度なことができます。

## 2.2 Dash

これも Document から引用します。

> Dash is a productive Python framework for building web analytic applications.
Written on top of Flask, Plotly.js, and React.js, Dash is ideal for building data visualization apps with highly custom user interfaces in pure Python. It's particularly suited for anyone who works with data in Python.
Through a couple of simple patterns, Dash abstracts away all of the technologies and protocols that are required to build an interactive web-based application. Dash is simple enough that you can bind a user interface around your Python code in an afternoon.
Dash apps are rendered in the web browser. You can deploy your apps to servers and then share them through URLs. Since Dash apps are viewed in the web browser, Dash is inherently cross-platform and mobile ready.

この中で、"interactive web-based application" を簡単に構築できて、サーバーにデプロイすれば他の人とも作成物を共有できる、ということが書かれています。

ここに書かれていることを信じてやってみます。
今回はインタラクティブ性を「**ユーザーがアップロードしたデータに応じたグラフのレンダリング**」として与えることとし、
アプリケーションは Heroku サーバにデプロイすることとします。

# 3. 開発
ということで、作っていきます。
掲載するソースコードの全体は https://github.com/camsenec/linkedIn-visualization にあります。


実装は、
1. ユーザーがアップロードしたデータに応じて
2. plotlyでグラフを書き
3. グラフをユーザーに返してあげる
という流れで進めます。

使用したモジュールとそのバージョンは以下です。必要に応じてインストールしてください。

```text
numpy==1.20.1
dash==1.20.0
plotly==4.14.3
pandas==1.2.2
dash_html_components==1.1.3
dash_core_components==1.16.0
dash_table==4.11.3
```

## 3.1 準備
とりあえずDashアプリケーションを実行してみましょう。

```python:app.py
import dash
import dash_html_components as html

app = dash.Dash(__name__)
app.layout=html.P("Hello World")

if __name__ == "__main__":
    app.run_server(debug=True, use_reloader=True)
```
というファイルを作成したうえで実行です。


```bash
python app.py
```

流れは、
1. Dashアプリケーションのインスタンスを作って
2. レイアウトを定義して
3. 実行する
という形です。


コマンドライン上に


```
Dash is running on http://127.0.0.1:8050/
```
といった表示がされるのでアクセスしてみましょう。すると、こんなページが見えるはずです。
![Screenshot 2021-05-04 102621.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/dac6a9e0-0e2a-ea75-9777-303cc6776b67.png)

このapp.layoutの値を変えれば、好きなようにhtmlレイアウトを構成できます。
ということで、こんな感じにしましょう。

```python:app.py
# Initiate Application Instance
app = dash.Dash(__name__)
app.title = "LinkedIn Visualizer"

# HTML document that is initially displayed, which consists of a title area and an input form)
app.layout = html.Div(children=[ 
    # title area
    html.Div(
          children=[
              html.H1(
                  children="LinkedIn Visualizer", className="header-title"
              ),
              html.P(
                  children="Analyze your LinkedIn Data",
                  className="header-description",
              ),
          ],
                  className="header",
        ),
])
if __name__ == "__main__":
    app.run_server(debug=True, use_reloader=True)
```
CSSファイルやJSファイルは、assetsというディレクトリを作成しその配下に置くことで自動的に読み込まれます ( https://dash.plotly.com/dash-enterprise/static-assets )。

今回作るアプリのassetsディレクトリ内ファイルは、簡単な `style.css` のみで、
https://github.com/camsenec/linkedIn-visualization/tree/master/assets
にあります。

`assets/style.css` を配置するとこんな感じになります。
![Screenshot 2021-05-04 111212.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/2dd17c60-862a-d81e-c676-e71e5499e92c.png)


ではこれより機能実装していきましょう。

## 3.2 機能別実装
### 3.2.1 ユーザーのファイルアップロード
ファイルをアップロードするためのフォームには、`dash_core_components.Upload()` を用いることができます。 今回は [Documentation で例示されているフォーム](https://dash.plotly.com/dash-core-components/upload) をほぼそのまま利用します。

```python:app.py
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import visualizer

# Initiate Application Instance
app = dash.Dash(__name__)
app.title = "LinkedIn Visualizer"

# HTML document that is initially displayed, which consists of a title area and an input form)
app.layout = html.Div(children=[
    # title area
    html.Div(
          children=[
              html.H1(
                  children="LinkedIn Visualizer", className="header-title"
              ),
              html.P(
                  children="Analyze your LinkedIn Data",
                  className="header-description",
              ),
          ],
                  className="header",
        ),

    # input form
    dcc.Upload(
        id='upload-data',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select your Connections.csv')
        ]),
        style={
            'width': '100%',
            'height': '60px',
            'lineHeight': '60px',
            'borderWidth': '1px',
            'borderStyle': 'dashed',
            'borderRadius': '5px',
            'textAlign': 'center',
            'margin': '10px'
        },
        # Allow multiple files to be uploaded
        multiple=True
    ),
    # a hidden html element
    html.Div(id='output-data-upload'),
])
```

次に、アップロードされたファイルを処理する関数を書きます。この関数はコールバックとして実装し、アップロードフォームに紐づけます。具体的には以下のようにします。これも[Documentation で例示されているもの](https://dash.plotly.com/dash-core-components/upload) をほぼそのまま利用しています。

```python:app.py
@app.callback(Output('output-data-upload', 'children'),
              Input('upload-data', 'contents'),
              State('upload-data', 'filename'),
              State('upload-data', 'last_modified'))
def update_output(list_of_contents, list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            visualizer.parse_contents(c, n, d) for c, n, d in
            zip(list_of_contents, list_of_names, list_of_dates)]
        return children
```

簡単に解説すると、この関数は

- `id=output-data-upload`で参照される HTML element の `children` （=アップロードフォーム)　に入力されたファイルを受け取り

- contents と filename を `State` としてもち

-  `id=output-data-upload`で参照される HTML element の `children` というフィールドに返り値を代入する

ということをします。つまり、`dcc.Upload()` 内の

```python
    # a hidden html element
    html.Div(id='output-data-upload')
```
が、ユーザーのファイルアップロードに応じて

```python
    # a hidden html element
    # children is the return value of upload_output function
    html.Div(id='output-data-upload', children=children)
```
と形を変えます。この children に HTML element を与えてあげることで、動的なビューを生成することができます。

このあとのステップは
2. plotlyでグラフを書き
3. グラフをユーザーに返してあげる
という形ですが、これまで行ってきたことを考えると、

**plotlyでグラフを書いて、HTML フォーマットでレイアウトして、そのレイアウトを返却する関数** 
を書けばよさそうです。その返り値が `html.Div(id='output-data-upload', children=children)` の children に代入され、グラフビューを生成することになります。

### 3.2.2 plotlyでグラフを書く
Plotlyでのグラフの書き方そのものについてはこの記事では言及しません。[公式ページ](https://plotly.com/python/)に様々な例が紹介されており、それぞれコードを参照することができます。

また、Qiita上にも

- [[Python] Plotlyでぐりぐり動かせるグラフを作る](https://qiita.com/inoory/items/12028af62018bf367722)
- [JavaScriptとQiitaAPIで取得した人気タグ情報をplotlyで散布図表示してみる](https://qiita.com/ranchi1977/items/73f1346cf15798f52444)

といった記事を見つけることができました。


今回は、Connections.csvという　以下のような形式のデータ（[取得方法](https://www.linkedin.com/help/linkedin/answer/66844/export-connections-from-linkedin?lang=en) ）を用います。

```txt:Connections.csv
First Name,Last Name,Email Address,Company,Position,Connected On
Mcqueen, Mejiro, mcqueen@mejiro.com, Mejiro Shouji, Race Horse, 04-May-21
...

```
このデータをもとに、

- コネクション数の変化を表すグラフ
- つながっている人の所属分布
- つながっている人が所属している企業
- つながっている人が働いているポジション

を図示することにします。

```python:graph.py
import plotly.graph_objects as go 
import plotly.express as px
import datetime

# Changes of the number of connections
def trend(connections_df):
    # This format list is prepared for the format variety 
    # of LinkedIn Connection.csv dataset. 
    format_cand = ["%d %b %y", "%d %b %Y", "%d-%b-%y", "%d-%b-%Y"]
    for format in format_cand:
        try:
            connections_df["Connected On"] = connections_df["Connected On"].apply(
                lambda x: datetime.datetime.strptime(x, format).strftime("%Y-%m-%d"))
        except:
            print("format error")
            continue
        else:
            break
    
    df = connections_df.groupby(by="Connected On").count().reset_index()
    df["count"] = 0
    for i, index in enumerate(df.index):
        if i == df.index[0]:
            df.loc[index,"count"] = df.loc[index,"First Name"]
        else:
            df.loc[index,"count"] = df.iloc[i-1]["count"] + df.loc[index,"First Name"]
            

    fig = go.Figure()
    fig.add_trace(go.Scatter(x=df["Connected On"], 
                        y=df["count"]))
    fig.update_layout(title='Changes of your number of Connections',
                    xaxis_title='Connected On',
                    yaxis_title='Number')
    return fig

# Distribution of companies connected people work at
def company_hist(connections_df):
    fig  = px.histogram(connections_df, x = "Company")
    fig.update_layout(title='Distribution of companies your connected people work')
    return fig

# Companies where connected people work
def company_treemap(connections_df):
    df_by_company = connections_df.groupby(by="Company").count().reset_index().sort_values(
        by="First Name", ascending=False).reset_index(drop=True)
    company_treemap = px.treemap(df_by_company[:100], path=["Company"],
                    values="First Name",
                    labels={"First Name": "Count"})
    company_treemap.update_layout(title='Companies where your connected people work')
    return company_treemap

# Job Positions of connected people
def position_treemap(connections_df):
    df_by_position = connections_df.groupby(by="Position").count().reset_index().sort_values(
        by="First Name", ascending=False).reset_index(drop=True)
    position_treemap = px.treemap(df_by_position[:100], path=["Position"],
                    values="First Name",
                    labels={"First Name": "Count"})
    position_treemap.update_layout(title='Job Positions of your connected people') 
    print(type(position_treemap))
    return position_treemap
```

### 3.2.3 グラフをユーザーに返す
最後にステップ 2 で定義した関数を呼び出し、ページ内に HTML Element として組み込んでいきます。`parse_contents()` という関数はファイルアップロードと同時に呼び出されるコールバック関数 `update_output()` 内で呼び出していた関数です。

ここではまず、 csv ファイルを pandas Dataframe として読み込み、そのDataframeを引数として`graph.py`で定義した関数を呼び出しています。帰り値である `plotly.graph_objects` を `dash_core_components.Graph` でラップしてあげることでグラフを HTML Element として組み込むことができます。

```python:visualizer.py
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import graphs

import base64
import datetime
import io

def parse_contents(contents, filename, date):
    content_type, content_string = contents.split(',')
    
    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV file
            # Sometimes, LinkedIn dataset has "Notes" at the beginning
            # of the csv file. In this case, the "Notes" is removed.
            if "Notes:" == decoded.decode('utf-8')[0:6]:
                connections_df = pd.read_csv(io.StringIO(decoded.decode('utf-8').split('\n\n')[1]))
            else:
                connections_df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
        else:
           return html.Div([
            'This file extension is not supported'
        ])
    except Exception as e:
        print(e)
        return html.Div([
            'There was an error processing this file.'
        ])

    return html.Div(
    children=[
        html.Div(
            children=[
                html.Div(
                    children=dcc.Graph(
                      figure=graphs.trend(connections_df)
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        figure=graphs.company_hist(connections_df)
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        figure=graphs.company_treemap(connections_df)
                    ),
                    className="card",
                ),
                html.Div(
                    children=dcc.Graph(
                        figure=graphs.position_treemap(connections_df)
                    ),
                    className="card",
                ),
            ],
            className="wrapper",
        ),
    ]
)
```

## 3.3 テスト
最後に、

```bash
python app.py
```
と実行しローカルホストにもう一度アクセスしてみましょう。
すると、以下のようなページが見えるはずです。
![home.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/86e3b03a-42c5-9a71-e9a6-0c3f5a045b12.png)

ここに、[サンプルデータ](https://linkedin-visualizer.s3.eu-north-1.amazonaws.com/Connections_sample.csv)を投げてみましょう。4つのグラフが表示されれば成功です。
うまくいかない場合や、私のコードに誤りがある場合はコメントいただけると幸いです。
![Screenshot 2021-05-04 150301.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1440431/5a21d146-b6f3-99ab-b440-a68e13f26c02.png)

# 4. まとめ
今回は、Dash と Plotly を用いて LinkedIn のデータを可視化するアプリを作ってみました。

他にも、
1. ユーザーがアップロードしたデータに応じて
2. plotly でグラフを書き
3. グラフをユーザーに返してあげる
という流れで、
あらゆるデータセットから、あらゆる可視化をおこなうことができます。