2019年11月20日水曜日

PlotlyでStack OverflowのDeveloper Surveyを可視化する

Pythonで共起ネットワークを作成するでは、PythonライブラリのMatplotlibとNetworkXで夏目漱石の『こころ』のテキストをもとに共起ネットワークを作成した。作成したネットワーク図は画像なので、単純に表示して見るだけなのだが、PythonライブラリのPlotlyを使うとインタラクティブな可視化ができる。

今回は、KH CoderでStack OverflowのDeveloper Surveyを可視化するで利用したStack Overflow Annual Developer Surveyのデータを使って、インタラクティブな共起ネットワークを作成する。


環境


Windows10(1903)のWSL(Ubuntu 18.04)とJupyter Notebookを使用。




必要ライブラリなどのインストール


Plotlyはpipでインストールできる。


他にNetworkXとGraphvizも使う。インストールはPythonで共起ネットワークを作成するを参照。


データのクリーニングとJaccard係数の算出


使用するデータはStack Overflow Annual Developer Surveyで、2019年のデータ(developer_survey_2019.zip)をダウンロードして解凍し、survey_results_public.csvをJupyter Notebookの作業ディレクトリに置いておく。KH CoderでStack OverflowのDeveloper Surveyを可視化すると同じように、過去に経験のあるプログラミング言語やデータベース、フレームワークなどの開発で使われる技術に関する質問の回答結果を使用する。データクリーニングのためのコードを流用するのでsurvey_loader.pyとして保存しておく。以下は使用する質問項目。

  • LanguageWorkedWith(プログラミング言語、スクリプト、マークアップ言語)
  • DatabaseWorkedWith(データベース)
  • PlatformWorkedWith(プラットフォーム)
  • WebFrameWorkedWith(ウェブフレームワーク)
  • MiscTechWorkedWith(その他の技術)
  • DevEnviron(開発環境)

それから、「;」区切りの回答を回答者ごとにリストにして、技術ペアごとのJaccard係数を算出する。方法はPythonで共起ネットワークを作成すると同じ関数を使用するので、Gistの全体コードをcoonetwork_sample.pyとして保存しておく。survey_loader.pyとcoonetwork_sample.pyはJupyter Notebookの作業ディレクトリに置いておく。


ネットワーク図の作成


回答ペアごとのJaccard係数を算出したら、NetworkXとGraphvizでネットワークのノードとエッジの配置を決め、そのデータを使ってPlotlyで可視化する。NetworkXで決めたノードとエッジの配置をもとにPlotlyで可視化する方法がNetwork Graphs in Pythonにあるので、これを参考にした。コードは以下の通り。
import os
import numpy as np
import pandas as pd

import networkx as nx
from networkx.drawing import nx_agraph
from plotly import graph_objs as go

# Stack Overflow Annual Developer Surveyのデータをクリーニングする関数をインポート
from survey_loader import to_df, clean_df

# Jaccard係数を算出する関数をインポート
from coonetwork_sample import bform2pair, pair2jaccard

def build_interactive_network(G, pos, pr_values):
    # Plotlyで共起ネットワークを可視化
    # nodeの大きさと色をページランクアルゴリズムによる重要度(pr_values)により変える
    
    # edgeデータの作成
    edge_x = []
    edge_y = []
    for edge in G.edges():
        x0, y0 = pos[edge[0]]
        x1, y1 = pos[edge[1]]
        edge_x.append(x0)
        edge_x.append(x1)
        edge_x.append(None)
        edge_y.append(y0)
        edge_y.append(y1)
        edge_y.append(None)

    edge_trace = go.Scatter(
        x=edge_x, y=edge_y,
        line=dict(width=0.5, color='#888'),
        hoverinfo='none',
        mode='lines')

    # nodeデータの作成
    node_x = []
    node_y = []
    for node in G.nodes():
        x, y = pos[node]
        node_x.append(x)
        node_y.append(y)

    node_trace = go.Scatter(
        x=node_x, y=node_y,
        text=list(G.nodes()),
        textposition='top center',
        mode='markers+text',
        hoverinfo='text',
        marker=dict(
            showscale=True,
            # colorscale options
            #'Greys' | 'YlGnBu' | 'Greens' | 'YlOrRd' | 'Bluered' | 'RdBu' |
            #'Reds' | 'Blues' | 'Picnic' | 'Rainbow' | 'Portland' | 'Jet' |
            #'Hot' | 'Blackbody' | 'Earth' | 'Electric' | 'Viridis' |
            colorscale='Rainbow',
            reversescale=False,
            color=[],
            size=10,
            colorbar=dict(
                thickness=15,
                title='Page Ranking',
                xanchor='left',
                titleside='right'
            ),
            line_width=2))

    # nodeの色、サイズ、マウスオーバーしたときに表示するテキストの設定
    node_adjacencies = []
    node_text = []
    for node, adjacencies in enumerate(G.adjacency()):
        # nodeに接続するedgeの数
        node_adjacencies.append(len(adjacencies[1]))
        node_text.append('{} connection(s)'.format(len(adjacencies[1])))

    node_trace.marker.color = pr_values
    node_trace.marker.size = [value*1000 for value in pr_values]
    node_trace.hovertext = node_text
    
    # ネットワーク図の可視化
    fig = go.Figure(data=[edge_trace, node_trace],
                 layout=go.Layout(
                    showlegend=False,
                    hovermode='closest',
                    margin=dict(b=20,l=5,r=5,t=40),
                    xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
                    yaxis=dict(showgrid=False, zeroline=False, showticklabels=False))
                    )
    fig.show()
    
def build_network(jaccard_dict):
    # 回答ペアごとのJaccard係数をもとにnodeとedgeのレイアウトを決めてネットワーク図を可視化

    G = nx.Graph()

    #  接点/単語(node)の追加
    # ソートしないとネットワーク図の配置が実行ごとに変わる
    nodes = sorted(set([j for pair in jaccard_dict.keys() for j in pair]))
    G.add_nodes_from(nodes)

    print('Number of nodes=', G.number_of_nodes())

    #  線(edge)の追加
    for pair, coef in jaccard_dict.items():
        G.add_edge(pair[0], pair[1], weight=coef)

    print('Number of edges=', G.number_of_edges())

    # nodeの配置方法の指定
    seed = 0
    np.random.seed(seed)
    # k = node間反発係数
    #pos = nx.spring_layout(G, k=0.3, seed=seed)

    # できるだけnodeが重ならないようにする(Graphvizを使う)
    pos = nx_agraph.graphviz_layout(
        G,
        prog='neato',
        args='-Goverlap="scalexy" -Gsep="+6" -Gnodesep=0.8 -Gsplines="polyline" -GpackMode="graph" -Gstart={}'.format(seed))

    # ページランクアルゴリズムによる重要度
    pr = nx.pagerank(G)

    # 共起ネットワークの可視化
    build_interactive_network(G, pos, list(pr.values()))

def main():
    df = to_df()
    df = clean_df(df)

    df['tech'] = df[df.columns[1:]].apply(lambda row: ';'.join(row).split(';'), axis=1)
    print(df['tech'].head())
    
    # Jaccard係数の計算
    pair_count = bform2pair(df['tech'].tolist(), min_cnt=20)
    jaccard_dict = pair2jaccard(pair_count, df['tech'].tolist(), edge_th=0.4)
    
    # 共起ネットワーク作成
    build_network(jaccard_dict)
    
if __name__ == '__main__':
    main()


結果


Jupyter Notebookでコードを実行すると以下のような共起ネットワーク図が作成された。

さらに、メニューがあってズームなどができるし、マウスオーバーでテキストなどを表示できる。



0 件のコメント:

コメントを投稿