検索キーワードの、共起ネットワーク図を python で描画する


Google Search Console の キーワードを元に、共起ネットワーク図が、描画できるか試してみた結果を記載します。
結論を書きますと、共起ネットワーク図かはわかりませんが、それっぽい図の描画はできました。
以下は、TfidfVectorizer で作成した共起単語行列を元に出力した図になります。
keyword_network


共起ネットワーク図について

共起語については、以下に記載があります。
共起語SEOをもう一度解説してみる | 海外SEO情報ブログ
例えば、python だったら django とか flask 文書上一緒に現れてくることが多い単語と理解しました。
この共起語同士で関連性が高いものを集めて、その関連語に線を引いたものが共起ネットワーク図というのが、個人的な理解です。
一般的に、Web全体の中で共起するものなイメージですが、今回実装しているのは、Google Search Console の検索キーワードをinput に作成です。
これは、サイトが外[ここではGoogle]からどう見えているかを示すものかなと考えます。


処理の流れ

実装は python で作成しました。処理の流れは以下になります。

  1. キーワード文字列を取得
    Google Search Console からキーワードを取得します。
    キーワードのデータは、Search Analytics for Sheets - Google スプレッドシート アドオン を使って取得、スプレッドシートの取得には、gspread を使っています。

  2. 共起単語行列を作成する
    キーワードをINPUTに、共起単語行列を作成します。
    sklearn の CountVectorizer と、TfidfVectorizer を使った処理を作成しました。

  3. networkx で、ネットワーク図を描画
    networkx を使って、ネットワーク図を描画します。


前提

python の verison

% python -V
Python 2.7.10

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

スクリプトを動作させるのに必要なのは、以下になります。
実行する場合はあらかじめインストールお願い致します。

pip install networkx
pip install matplotlib
pip install sklearn

実装

以下作成したスクリプトになります。
2ファイルです。

  • search_console_plot_keyword_network.py

    # -*- coding: utf-8 -
    import networkx as nx
    import matplotlib.pyplot as plt
    import search_console_classifier_utils as utils
    
    
    def __create_keywords_data():
        keywords = []
    
        # CSVからキーワードを取得
        for line in utils.parse_report_tsv():
            keywords.append(line)
        return keywords
    
    
    def __get_co_occurrence_matrix_from(keywords):
        # -----------------------------------------------------
        # 以下、CountVectorizer で共起単語行列を作る
        # ----------------------------------
        # from sklearn.feature_extraction.text import CountVectorizer
        # count_model = CountVectorizer(ngram_range=(
        #     1, 1), stop_words=utils.stop_words)  # default unigram model
        # X = count_model.fit_transform(keywords)
    
        # # normalized co-occurence matrix
        # import scipy.sparse as sp
        # Xc = (X.T * X)
        # g = sp.diags(2. / Xc.diagonal())
        # Xc_norm = g * Xc
    
        # import collections
        # splited_keywords = []
        # for keyword in keywords:
        #     splited_keywords.extend(utils.split_keyword(keyword))
        # counter = collections.Counter(splited_keywords)
        # return Xc_norm, count_model.vocabulary_, counter
    
        # -----------------------------------------------------
        # 以下、TfidfVectorizer で共起単語行列を作る
        # ----------------------------------
        from sklearn.feature_extraction.text import TfidfVectorizer
        tfidf_vectorizer = TfidfVectorizer(ngram_range=(
            1, 1), stop_words=utils.stop_words, max_df=0.5, min_df=1, max_features=3000, norm='l2')
        X = tfidf_vectorizer.fit_transform(keywords)
        # normalized co-occurence matrix
        import scipy.sparse as sp
        Xc = (X.T * X)
        g = sp.diags(2. / Xc.diagonal())
        Xc_norm = g * Xc
    
        import collections
        splited_keywords = []
        for keyword in keywords:
            splited_keywords.extend(utils.split_keyword(keyword))
        counter = collections.Counter(splited_keywords)
        return Xc_norm, tfidf_vectorizer.vocabulary_, counter
    
    
    def main():
    
        # -------------------------
        # 1. キーワード文字列を取得
        # -------------------------
        keywords = __create_keywords_data()
    
        # -------------------------
        # 2. 共起単語行列を作成する
        # -------------------------
        Xc_norm, vocabulary, counter = __get_co_occurrence_matrix_from(keywords)
    
        # -------------------------
        # 3. networkx で、ネットワーク図を描画
        # -------------------------
        # 3-1.初期ノードの追加
        G = nx.from_scipy_sparse_matrix(
            Xc_norm, parallel_edges=True, create_using=nx.DiGraph(), edge_attribute='weight')
    
        # 3-2.nodeに、count にcount属性を設定
        value_key_dict = {}
        for key, value in vocabulary.items():
            count = counter.get(key, 0)
            nx.set_node_attributes(G, "count", {value: count})
            value_key_dict.update({value: key})
    
        # 3-3.エッジと、ノードの削除
        # 出現回数の少ないエッジを削除
        for (u, v, d) in G.edges(data=True):
            if d["weight"] <= 0.15:
                G.remove_edge(u, v)
    
        # 出現回数の少ないノードを除去
        for n, a in G.nodes(data=True):
            if a["count"] <= 125:
                G.remove_node(n)
    
        # 3-4 ラベルの張り替え、from_scipy_sparse_matrix 設定時はラベルとして1,2,3 等の数値が設定されている
        G = nx.relabel_nodes(G, value_key_dict)
    
        # 3-5 描画のために調整  
        # figsize で 図の大きさを指定
        plt.figure(figsize=(10, 10))
        # 反発力と吸引力の調整
        pos = nx.spring_layout(G, k=0.1)
        # ノードサイズの調整
        node_size = [d['count'] * 20 for (n, d) in G.nodes(data=True)]
        nx.draw_networkx_nodes(G, pos, node_color='lightgray',
                               alpha=0.3, node_size=node_size)
        # フォントサイズ、使用するフォントの設定
        nx.draw_networkx_labels(G, pos, fontsize=8,
                                font_family="IPAexGothic", font_weight="bold")
        # エッジの線の調整
        edge_width = [d['weight'] * 2 for (u, v, d) in G.edges(data=True)]
        nx.draw_networkx_edges(G, pos, alpha=0.4, edge_color='c', width=edge_width)
        # 枠線の表示/非表示 on:表示 off:非表示
        plt.axis("off")
        plt.show()
    
    
    if __name__ == '__main__':
        main()
    

  • search_console_classifier_utils.py

    # -*- coding: utf-8 -
    import gspread
    from __builtin__ import unicode
    from oauth2client.service_account import ServiceAccountCredentials
    from sets import Set
    from sklearn.feature_extraction import text
    
    extra_words = Set(['name', 'not', 'the', 'usr', 'you', 'version', 'this'])
    stop_words = text.ENGLISH_STOP_WORDS.union(extra_words)
    
    key_file = "your_api_key.json"
    scope = ['https://spreadsheets.google.com/feeds']
    
    
    def __check_stop_word(word):
        """
        stop word のチェック
        2文字以下の文字列を除去と、英語のstopwords を除去を行う
        """
        if word in stop_words:
            return False
        if len(word) <= 2:
            return False
        return True
    
    
    def split_keyword(text):
        """
        キーワードを区切り、stopwordsを除外する
        """
        keywords = text.split(" ")
        return [keyword for keyword in keywords if __check_stop_word(keyword)]
    
    # report csv を parse する
    def parse_report_tsv():
        lines = []
        row_count = 1
        credentials = ServiceAccountCredentials.from_json_keyfile_name(
            key_file, scope)
        gc = gspread.authorize(credentials)
        # スプレッドシート名は、Google Search Console Analyze シート名は[Merge] をOpen
        wks = gc.open("Google Search Console Analyze").worksheet("Merge")
        for line in wks.export(format='tsv').split("\n"):
            if row_count != 1:
                arr = line.split("\t")
                # キーワードカラムを取り出す
                lines.append(arr[1])
            row_count += 1
        return lines
    


説明

以下、スクリプトの処理の説明になります。

1. キーワード文字列を取得 について

キーワード文字列を、スプレッドシートから取得しています。
tsv そのままでも読み込めますが、個人的な好みでスプレッドシートから読み込むようにしました。
tsv でもcsv でもキーワードのリストを読み込めれば動作しますので、
スプレッドシートから読み取る必要がなければ、書き換えてください。

2. 共起単語行列を作成処理について

  • CountVectorizer と TfidfVectorizer
    CountVectorizer、TfidfVectorizer でそれぞれ作成しました。
    どちらもそれっぽい図が出力されたので、うまくいっているのではないかと思います。
    CountVectorizer、TfidfVectorizer の説明は、テキスト分類問題その1 チュートリアル|ビッグデータ大学(β) がわかりやすかったです。
    共起単語行列の作成方法は、python - word-word co-occurrence matrix - Stack Overflow を参考に作成しました。
    CountVectorizer、TfidfVectorizer の出力結果もそれほど変わらないですが、個人的にはTfidfVectorizer の出力結果のほうがしっくりきました。
    ただ、TfidfVectorizer に与えているパラメータはstopwords以外、意味もわからず設定しているのでそのあたりチューニングの余地はあるのかと思います。

  • stopword
    stopwords は、キーワードとして検索されているのが英単語がほとんどだったので、sklearn.feature_extraction.text の ENGLISH_STOP_WORDS と 個人的に気になった単語を以下のようにunion してそれを使用しました。

    extra_words = Set(['name', 'not', 'the', 'usr', 'you', 'version', 'this'])
    stop_words = text.ENGLISH_STOP_WORDS.union(extra_words)
    

  • メソッドの戻り値
    戻り値についてですが、共起単語行列Xc_norm と ラベルと単語の紐付け辞書vocabulary_ 以外に、単語数を返すようにしました。
    これは、後続処理でNode の大きさを単語数により指定したためです。
    CountVectorizer、TfidfVectorizer の 戻りで代替となる値をとれればそれを使うのですが、見つけられませんでした。

3. networkx で、ネットワーク図を描画

[Python]NetworkXでQiitaのタグ関係図を描く - Qiita を参考に作成しました。
変えたところについて説明します。

  • 3-1.初期ノードの追加
    Xc_norm を Loop で設定しようかと思ったのですが、なかなかcsr_matrix を変換する実装サンプルが見つからず、メソッドとして from_scipy_sparse_matrix が存在したので、そちらを使うようにしました。
    以下を参考にしました。
    python - Transform csr_matrix into networkx graph - Stack Overflow
    from_scipy_sparse_matrix — NetworkX 1.11 documentation

  • 3-2.nodeに、count にcount属性を設定
    Nodes に 文字のカウント数設定しています。
    Nodeサイズをこれを元に設定します。

        count = counter.get(key, 0)
        nx.set_node_attributes(G, "count", {value: count})
    
    ここでついでに、value と、key を 入れ替えています。
    value_key_dict.update({value: key})
    

  • 3-3.エッジと、ノードの削除
    出現回数の少ない、エッジと、ノードの除去をしています。
    ここは、サイトのアクセス数で変わってくるのかと思います。

        # 出現回数の少ないエッジを削除
        for (u, v, d) in G.edges(data=True):
            if d["weight"] <= 0.15:
                G.remove_edge(u, v)
    
        # 出現回数の少ないノードを除去
        for n, a in G.nodes(data=True):
            if a["count"] <= 125:
                G.remove_node(n)
    

  • 3-4 ラベルの張り替え
    value と key を入れ替えた dictionary を使って、ノードに名称を設定します。

    G = nx.relabel_nodes(G, value_key_dict)  
    

説明は以上です。


TODO

動くものを実装することはできました。 以下 TODO 事項になります。


参考

以下、文中に登場する以外で参考にした記事になります。

以上です。

コメント

カテゴリー