任意の単語間の相関係数を求めてみる 【Python & Google Trends】

Python

Python を使って、検索ボリュームの推移から任意の単語(検索ワード)間の相関係数を求めてみます。

以下の環境で動作確認をしています。
環境:
・ Windows パソコン、Python 3.X(Anaconda インストール済み)
・ ウェブサイト: Google トレンド
★ データの出力に pandas を使っています。

背景 ~ 検索ワード間の相関係数を求めてみる

Google トレンドを使うと、任意の検索ワードについて検索ボリュームの推移を調べることができます。得られたデータは簡単に CSV 形式でダウンロードできます。

すると、任意の2つの単語について、相関係数を求めることが可能になります。
たとえば、「ウクライナ」、「ロシア」の単語の例でいえば、2022年3月の同時期に検索ボリュームが急増しています。
これらの検索数の推移の相関を定量化できたすると、強い相関となるはずです。相関係数は 1.0 (100%)に近くなるはずです。

ということで、Google トレンドのデータから相関係数を求める Python のスクリプトをまとめ、公開しておくことにします。
任意のワードについて、どれほどの相関があるのか、相関がないのか、逆相関になっているのかなど、定量的に扱うことが可能になります。

ところで、以前、日経平均株価の推移について機械学習を行い、株価の予測を試みました。
株価のように社会的な影響を反映した指標を機械学習で扱う際に問題になるのは、つぎのような点です。
機械学習を行うにあたっては、学習用データが必要です。しかし、世界的に影響の大きな一過性の事件・出来事(コロナ、戦争など)があると、機械学習に用いる学習用データにその影響が反映されてしまいます。こうしたデータで機械学習を行うと、学習済みモデルが一過性の出来事の影響を受けてしまいます。この結果、機械学習のモデルが妥当なものとはいえなくなってしまいます。

株価のように様々な社会的な事象の影響を受ける時系列のデータについて、機械学習などプログラミングで扱おうとする際、再現性のない一過性の事件や出来事はいったいどのように扱うべきかが課題となります。
ひとつの切り口として、検索ボリュームのデータを利用すると、一過性の出来事であっても、その出来事と強く相関している指標や用語を抽出することが可能と考えられます。ソフトウェアで定量的に扱うことが可能になります。
たとえば、「コロナ」や「戦争」のような一過性の出来事について、検索キーワードの推移グラフを取得して、各経済指標、雇用統計、為替、株価推移などとの相関を評価することで、各出来事が各指標にどの程度影響を及ぼすのか、先行指標として妥当なものはどれか、など定量的な取り扱いが可能になると考えられます。

なお、Google トレンドを使ったのは、単なる一例です。
何らかの方法で類似のデータさえ得られれば、同様に、各項目間の相関係数などを求めることが可能です。

設定手順

① パソコン内に相関関係を分析するためのフォルダを作ります。
例: C:\user\correlation_coefficient1
② ①のフォルダの中に、さらにフォルダ “reference01” を作ります。
入手したデータと分析結果を入れるフォルダとします。
例: C:\user\correlation_coefficient1\reference01
③ ①のフォルダ内に correlation_coefficient1.py 等の名前でテキストファイルを作成し、以下のサンプルスクリプトをコピー&ペーストで貼りつけて保存します。

使い方

④ Google Trends (以下)にアクセスします。
・ Google Trends: https://trends.google.com/trends/explore?date=today%205-y&geo=JP
★ エラーが出る場合は、何度か、ブラウザ上で再表示をさせると表示されるようです。
いくつか検索を実行すると、サーバー側で表示を拒否することがあるようです。
⑤ 任意のキーワードを1つ決め、検索キーワードの欄に入力し、検索します。
例:コロナ
★ 検索時、検索期間は「過去5年間」となるように設定してください。
たまたまプログラムを5年分のデータ用に作っているためです。設定が異なると正常に計算できません。
⑥ 結果(検索数の推移のグラフ)が表示されたら「人気度の動向」の欄の右側にある、”下向き矢印のアイコン”(csv)をクリックして、CSV ファイルをダウンロードしてください。
⑦ ⑤~⑥を繰り返し、任意のキーワード、興味のあるキーワードについて、同様にデータをダウンロードしてください。
例: テレワーク、旅行、Python、Tensorflow、…
★ 複数のキーワードを設定できるようになっていますが、検索するキーワードはダウンロードごとに1つのみ(他は空欄)としてください。Python で読み取る際、データのフォーマットを統一するためです。
★ 1つの CSV ファイルが1つのキーワードに対応することになります。
★ Google トレンドのサイトの仕様により、ダウンロード時のファイル名は 同一名称となっています。しかし、ダウンロード時、同一名のファイルがあると、ブラウザの機能によって自動で付番が付加されると思います。
★ また、各検索ワードでのダウンロードは、同時期にしてください。年月日が同一となっているときの値を取得して、相関係数を計算します。時期が重なっていないと、取得できるデータの個数が変動するためです。
⑧ ⑤~⑦でデータのダウンロードが終わったら、ダウンロードした CSV ファイルをすべて、②のフォルダ reference01 に移してください。
★ Windows パソコンの場合、エクスプローラーを開くと、「ダウンロード」フォルダ(”C:\Users\(ユーザー名)\downloads” )があり、この中に CSV ファイル(multiTimeline….csv といった名前のファイル)があると思います。
⑨ CSV ファイルの準備ができたら、コマンドプロンプト(または、Anaconda Prompt)を起動して、③の Python のスクリプトを実行してください。

例: python C:\user\correlation_coefficient1\correlation_coefficient1.py

→ ②のフォルダの中に結果(以下の HTML ファイル)が出力されたら成功です!
ブラウザでダブルクリックして、内容を確認してください。

・ 01_correlation_coefficients1.html
すべてのワードの組み合わせについて、相関係数の高いものを抽出し、順に並べたもの
・ 02_correlation_coefficients1.html
各ワードごとに、相関係数が高いワードを並べたもの
・ 03_detail1.html
計算に用いた内部の全データを書き出したもの
・ 04_detail2.html
上記の 03 を相関係数でソートしたもの
逆相関となっているワードの組み合わせについてもすべて出力したもの

相関係数の計算結果の見方

・ reference01 フォルダに入れた検索ワードのすべての組み合わせについて、相関係数を求め、ランキングをとっています。HTML ファイルをダブルクリックして、参照してみてください。
・ 相関係数(correlation coefficient)は、-1.0 から 1.0 の範囲の値をとります。
値が大きいほど高い相関があることになります。相関が最も高いとき、値は 1.0 となります。
値がマイナスになっているときは、逆相関になっていることを示しています。
完全な逆相関となると -1.0 となります。
相関が完全にないとき、0.0 になります。
一例として、-0.6 ~ +0.6 程度(?)の値となっているときは、あまり相関していない、等とみてよいと思います。単語の母集団にもよると思いますが、いくらか相関係数を調べてみて、妥当な範囲を設定してみてください。
・ 01_correlation_coefficient1.html は、検索ワードの組み合わせについて、相関係数の高いワードの組み合わせをランキング形式で表示しています。
また、各ワードについて、かならず1つはリストに入っているようにしています。
加えて、一例として、相関係数が 0.7 以上となるものについては、各ワードについて合計5個まで、リストに入れるようにしています。抽出条件は、プログラムを修正することで変更可能です。
・ 各単語ごとに相関している単語を調べたい場合は、02_correlation_coefficient1.html を参照してください。
一例として、相関係数 0.5 以上となっているものを抽出しています。
・ 03_detail1.html、04_detail1.html は、上記の2つのデータを求めるために使った、元データです。N 個の単語をダウンロードしたとき、N個× N個の単語の組み合わせが考えられます。この N個×N個の組み合わせ(マトリックス)について、内部のデータをそのまま出力しています。

相関を調べてみた具体例

冒頭の画像では、キーワードをランダムに入力し、Google Trends のデータをダウンロードして相関係数を求めてみました。
具体的には、以下の2つのグループを作り、それぞれに関係するキーワードでの Google トレンドのデータをフォルダに入れ、相関係数を求めてみました。
グループ1:ニュースでよく出てくる単語(ウクライナ、ロシア、日経平均、ダウなど)
グループ2:ソフトウェア・機械学習の関連用語(JavaScript、PHP、Python、OpenCV、Tensorflow、Keras など)

計算結果は、冒頭の画像のとおりです。
・ 過去5年の検索ボリュームの推移で相関を求めると、グループ1では「ウクライナ」と「ロシア」の相関係数がトップとなっており、96% 程度となっています。
ウクライナで戦争が始まったと同時に多くの人が検索したため、相関係数が高くなったものと考えられます。他に、「天然ガス」なども、これらの用語と高い相関があります。
・ グループ2では、「JavaScript」と「PHP」の組み合わせがトップとなっており、相関係数が 96% 程度となっています。
他に、「C#」と「Linux」も相関が高くなっています。
推測ですが、これらの用語は、ソフトウェアの開発中に同時期に検索される傾向があるようです。そのため、トレンドが似通っていると考えられます。
他にも、機械学習系の用語などについて調べています。結果は、冒頭の画像のようになりました。
ブームが過ぎ去ると、過ぎ去った用語間での相関係数が高まるため、任意のジャンルでのトレンドを分析することも可能と考えられます。

相関係数の定義のまとめ

相関係数の定義について、まとめておきます。

n1 組のデータ (x1, y1), (x2, y2), (x3, y3), … (xn1, yn1) が観察されたとき、以下を定義します。
$$ <x> = \frac{1}{n1}\sum_{i=1}^{n1}x_i $$
$$ <y> = \frac{1}{n1}\sum_{i=1}^{n1}y_i $$
$$ S_{xy} = \frac{1}{n1}\sum_{i=1}^{n1}(x_i-<x>)(y_i-<y>) $$
<x>、<y>は、平均であって、\(S_{xy}\) は、共分散です。
共分散の定義で、y を x と置くと、また、x を y と置くと、分散の定義となり、以下が得られます。
$$ S_{xx} = \frac{1}{n1}\sum_{i=1}^{n1}(x_i-<x>)^2 $$
$$ S_{yy} = \frac{1}{n1}\sum_{i=1}^{n1}(y_i-<y>)^2 $$
上記の分散のルートを取ると、標準偏差となります。

相関係数は、以下で定義されます。
$$ r_{xy} = \frac{S_{xy}}{\sqrt{S_{xx}}\sqrt{S_{yy}}}$$
分子は共分散とし、分母は2つの変数の標準偏差とします。
ここでもし、x=y とおくと、相関係数は 1.0 となることがわかります。
また、上記の式の分子分母に共分散、分散の式を代入すると、各冒頭部分の 1/n1 はキャンセルします。
したがって、以下でプログラムを作るとき、相関係数は1/n1 の部分を省いて記述すればよいことになります。

スクリプトの説明

・ スクリプトの冒頭では、pandas など必要なパッケージを読み込み、各関数を定義しています。
・ スクリプトを実行すると、まず、get_all_trends1() 関数を実行します。
この関数で、reference01 フォルダの中に入っている CSV ファイルを探しにいき、リストを取得します。
・ この CSV ファイルのそれぞれを読み取って、各ワード間の相関係数を求めます。
・ Google トレンドからダウンロードした CSV ファイルには、冒頭に検索ワードと、検索ボリュームの推移が日付つきで入っています。
そこで、get_all_trends1() の末尾で、取得した[ 単語(検索ワード)、プロファイル(検索ボリュームの推移データ)、CSV ファイル名 ] をリストに入れて出力します。
・ つぎに symmetric_matrix1() と normalize1() の2つの関数で、各単語のプロファイルから相関係数を求めています。
相関係数の計算は、上の定義をそのままプログラムに直しています。
単語数を N 個とすると、相関係数は NxN の正方行列の形で書けるため、各要素を定義に従って計算しています。
たとえば、2つの単語が同一となるときは、行列の対角要素に対応しています。相関係数を求めると、行列の対角要素は 1.0 となります。行列の各要素のデータは 03_detail1.html を参照してください。(行列とはいっても、実際は、行番号、列番号を入れた1次元のリストの形でデータを格納しています。)
・ 以降の関数は、結果を表示するための関数です。相関係数の高いワードの組み合わせなどについてリストを作ってソート等を実行し、HTML ファイル形式で出力しています。
・ 相関係数の計算結果を利用する場合は、同リストを流用することで応用が可能です。

うまく動いたら

・ うまく動いたら、興味のあるキーワードについて、同様に Google トレンドのデータをダウンロードして、相関を求めてみてください。
たとえば、「雨」、「晴天」などの天候と弁当などの商品名の相関を調べれば、天候と売り上げの相関を調べていくことも可能かと思います。
・ プログラミング言語や機械学習の新技術についても、相関関係を調べていくことが可能です。技術のはやりすたりを把握することも可能だと思います。
・ また、出力したデータから、逆相関になっている用語の組み合わせを調べることも可能です。
たとえば、「コロナ」の用語が増えると、旅行関連のキーワードが減るなど、逆相関となる用語の組み合わせを把握することも可能です。

注意点
・ なお、今回のスクリプトでは相関係数を求めています。相関関係と因果関係は異なるため、注意が必要です。
たとえば、相関しているからといって因果関係があると判断してしまうと間違いの原因になります。
因果関係については、相関係数とは明確にわけて別のロジックで確認する必要があります。
・ また、相関係数を求めるにあたり、2つの要素が持つ値(各単語の頻度)については、分布が1次関数の関係で近似できるという前提を置いています。前提を置いた上での計算結果ですので、ご理解ください。

関連事項のまとめ
・ 相関係数を簡単に覚えておくのであれば、(相関係数)=(共分散)/(それぞれの標準偏差)と覚えておけばよいです。
・ 標準偏差 σ (シグマ)については、68 – 95 – 99.7 ルールがよく知られています。
たとえば、数学、製造業、品質管理の分野では、正規分布の ±1σ,±2σ,±3σ の範囲に、68%、95%、99.7% が入ることを覚えると思います。
・ 資産運用の分野では、標準偏差1シグマ(σ) ≒ リスク ≒ ボラリティ、という表現を使うとのことです。
たとえば、株式の利回り3%、リスク5%とすると、計算上、68%の確率で、3%±5% = -2% ~ 8% の範囲に入ると見積もられる。安全をみて、2σ、3σを取るならレンジを2倍、3倍取っておけばよいということになります。

まとめ

Google トレンドのデータを使って、相関係数を求める Python のスクリプトをまとめました。
相関係数についてネット検索をしたところ、式が間違っているものや内容をブラックボックスにしたものがかなり多いようです。そこで、計算式についてもまとめ、プログラムと対応させる形で公開しておくことにします。

他にも類似の分析などをしています。関心のある方は、関連リンクなども参照してみてください。

関連リンク
・ 検索方法のまとめ 【Google】
・ 機械学習で株価予測 【Python】
・ フルーツでの物体検出をやってみた 【YOLOv5】
・ 円周率を求めてみる 【Python】
・ ネイピア数を求めてみる 【Python】
・ 
WordPress で数式を扱う 【LaTeX】

サンプルスクリプト correlation_coefficient1.py


import os 
import glob as gb1 
import pandas as pd1 
import copy as cp1 
from operator import itemgetter 

def read1( file1 ): 
    with open( file1, 'r', encoding='utf-8' ) as f1: 
#   with open( file1, 'r', encoding='ansi' ) as f1: 
#   with open( file1, 'r', encoding='shift_jis' ) as f1: 
        str1 = f1.read()
    return str1 

def write1( file1, str1 ): 
    with open( file1, 'w', encoding='utf-8' ) as f1: 
        f1.write( str1 ) 
    return 0 

def write_html1( file1, a0, a1 ):                     # write html 
    df1 = pd1.DataFrame( a1 ) 
    if len( a0 ) >= len( a1[0] ): 
        df1.columns = a0[0:len(a1[0])] 
    df1.to_html( file1 ) 

def get_a1( file1 ): 
    a1 = read1( file1 ).strip().split( "\n" ) 
    return a1 

def get_trend0( file1 ): 
    a1 = get_a1( file1 )                              # original file 
    str1 = a1[2].split( "," )[1].split( ":" )[0]      # word 
    for i1 in range( 3 ): 
        del a1[0] 
    return [ str1, a1 ] 

def get_trend1( file1 ): 
    str1, a1 = get_trend0( file1 ) 
    for i1 in range( len( a1 ) ): 
        a2 = a1[i1].replace( "-", "" ).split( "," )   # date1, count1 
        v1 = float( a2[1].replace( "1 未満", "0" ) ) 
        a1[i1] = [ a2[0], v1 ]                        # [ date1, count1 ] 
    return [ str1, a1 ]                               # [ word, profile ] 

def get_all_trends1( pattern1 ): 
    files1 = gb1.glob( pattern1 ) 
    a1 = [] 
    for file1 in files1: 
        a2 = get_trend1( file1 ) 
        if len( a2[1] ) > 200:                        # 5 years data 
            a3 = file1.split( "\\" ) 
            file2 = a3[ len(a3)-1 ]                   # filename 
            a1.append( [ a2[0], a2[1], file2]  )      # [ word, profile, filename1 ] 
    a1.sort() 
    return a1 

def symmetric_matrix1( a0 ): 
    a1 = cp1.deepcopy( a0 ) 

    a2 = [] 
    for i1 in range( len( a1 ) ): 
        a3 = a1[i1]                                   # [ word, profile, filename1 ] 
        ave1 = 0.0 
        a4 = a3[1]                                    # profile 
        for i2 in range( len( a4 ) ): 
            ave1 = ave1 + a4[i2][1] 
        ave1 = float( ave1/len( a4 ) ) 
        for i2 in range( len( a4 ) ): 
            a4[i2][1] = a4[i2][1] - ave1 
        a2.append( [a3[0], a4, a3[2]] )               # [ word, profile, filename ] 
    a1 = a2 

    a2 = [] 
    for i1 in range( len( a1 ) ): 
        str1 = a1[i1][0]                              # word 1 
        a3 = a1[i1][1]                                # profile 1 
        file1 = a1[i1][2]                             # filename 1 
        for i2 in range( i1, len( a1 ) ): 
            str2 = a1[i2][0]                          # word 2 
            a4 = a1[i2][1]                            # profile 2 
            file2 = a1[i2][2]                         # filename 2 
            sum1 = 0.0 
            for i3 in range( len( a3 ) ): 
                str3 = a3[i3][0]                      # date 1 
                for i4 in range( len( a4 ) ): 
                    str4 = a4[i4][0]                  # date 2 
                    if str3 == str4:                  # same date 
                        sum1 = sum1 + a3[i3][1] * a4[i4][1] 
                        break 
            a2.append( [ str1, str2, sum1, i1, i2, file1, file2] ) 
            if i1 != i2: 
                a2.append( [ str2, str1, sum1, i2, i1, file2, file1] ) 
    a2.sort( key=itemgetter(4) )                      # for symmetric matrix 
    a2.sort( key=itemgetter(3) ) 
    return a2 

def normalize1( a1 ): 
    a2 = cp1.deepcopy( a1 ) 
    n0 = int( len( a2 ) ** 0.5 )
    for i1 in range( n0 ): 
        diag1 = float( a2[i1*n0+i1][2] ) 
        val1 = float( diag1 ** 0.5 ) 
        for i2 in range( n0 ): 
            a2[i1*n0+i2][2] = float( a2[i1*n0+i2][2] / val1 ) 
        for i2 in range( n0 ): 
            a2[i2*n0+i1][2] = float( a2[i2*n0+i1][2] / val1 ) 
    return a2 

def remove_diagonal_elements1( a1 ): 
    a2 = cp1.deepcopy( a1 ) 
    a3 = [] 
    for i1 in range( len( a2 ) ):  
        if a2[i1][0] != a2[i1][1]:  
            a3.append( [ a2[i1][0], a2[i1][1], a2[i1][2] ] ) 
    return a3 

def remove_triangle_matrix1( a1 ): 
    a2 = cp1.deepcopy( a1 ) 
    a3 = [] 
    for i1 in range( len( a2 ) ): 
        if a2[i1][0] < a2[i1][1]: 
            a3.append( [ a2[i1][0], a2[i1][1], a2[i1][2] ] ) 
    return a3 

def ranking_list1( a1, n0 ): 
    a2 = cp1.deepcopy( a1 ) 
    a2.sort( key=itemgetter(2) )                      # coefficients -1.0 to +1.0 
    a2.reverse() 
    a2.sort( key=itemgetter(0) )                      # word 1 
    a3 = [] 
    count1 = 0 
    str1 = "" 
    for i1 in range( len( a2 ) ): 
        str2 = str1 
        str1 = a2[i1][0] 
        if str1 != str2:                              # first element 
            count1 = 1 
        else: 
            count1 = count1 + 1 
        if count1 <= n0: a3.append( a2[i1] ) return a3 def correlated_list1( a1, th1 ): a2 = cp1.deepcopy( a1 ) a3 = [] count1 = 0 str1 = "" for i1 in range( len( a2 ) ): str2 = str1 str1 = a2[i1][0] if str1 != str2: count1 = 1 else: count1 = count1 + 1 if count1 == 1: a3.append( a2[i1] ) elif a2[i1][2]**2 >= th1**2: 
            a3.append( a2[i1] ) 
    return a3 

path1 = os.path.dirname(__file__) + "/reference01/"  
pattern1 = path1 + "multiTimeline*.csv"

a1 = get_all_trends1( pattern1 ) 
a1 = symmetric_matrix1( a1 ) 
a1 = normalize1( a1 ) 

a0 = ["word_1", "word_2", "correlation_coefficient1", "n1", "n2", "file_name_1", "file_name_2" ] 
file1 = path1 + "03_detail1.html" 
write_html1( file1, a0, a1 ) 

a2 = cp1.deepcopy( a1 ) 
a2.sort( key=itemgetter(2) ) 
a2.reverse() 
file1 = path1 + "04_detail1.html" 
write_html1( file1, a0, a2 ) 


a2 = remove_triangle_matrix1( a1 ) 
a2 = ranking_list1( a2, 5 ) 
a2 = correlated_list1( a2, 0.7 ) 

a2.sort( key=itemgetter(0) ) 
a2.reverse() 
a2.sort( key=itemgetter(2) ) 
a2.reverse() 

file1 = path1 + "01_correlation_coefficients1.html" 
write_html1( file1, a0, a2 ) 


a3 = remove_diagonal_elements1( a1 ) 
a3 = ranking_list1( a3, 5 ) 
a3 = correlated_list1( a3, 0.5 ) 

a3.sort( key=itemgetter(2) ) 
a3.reverse() 
a3.sort( key=itemgetter(0) ) 

file1 = path1 + "02_correlation_coefficients1.html" 
write_html1( file1, a0, a3 ) 
タイトルとURLをコピーしました