Python
MongoDB
Twitter
自然言語処理
statistics

スタバのTwitterデータをpythonで大量に取得し、データ分析を試みる その3

More than 3 years have passed since last update.

スタバTwitterシリーズも第3回となりました。下記は今までの投稿です。


その1:Twitter REST APIsでデータを取り込みmongoDBにインポート
http://qiita.com/kenmatsu4/items/23768cbe32fe381d54a2

その2:取得したTwitterデータからスパムの分離
http://qiita.com/kenmatsu4/items/8d88e0992ca6e443f446

その3:ある日を境にツイート数が増えたわけは?(今回)
http://qiita.com/kenmatsu4/items/02034e5688cc186f224b

その4:Twitterにひそむ位置情報の視覚化
http://qiita.com/kenmatsu4/items/114f3cff815aa5037535


さて、この前回の時系列ツイート数グラフをよく見ると3/18からグラフが全体的に上に持ち上がっているように見えないでしょうか?この日から何ががあったのではないかと思うので、これを分析してみようと思います。

timeseries_no_spam-compressor.png

ツイートの内容に何か変化がないかをツイート本文から分析する、ということを試みます。
まず考え方として、ツイートを3/18 0:00を境に前後に分割します。
で、その前後でつぶやきに含まれる単語で増加率が高い単語が何か、というのをあぶり出すことをやってみたいと思います。

しばらくデータ処理の説明になるので、結果を先に見たい人は3. 分析結果に飛んでください。

1.単語の分かち書き

ツイート本文を統計的に処理するにあたり、単語の分かち書きという処理が必要となります。英語の文書であれば単語毎に間にスペースが入っているので、スペース区切りで単語を特定できるのですが、日本語は膠着語と呼ばれる区切りが無い言語のためこの分かち書きが必要です。
そこで、かの有名なMeCabを導入して分析に使いたいと思います。

1-1.MeCabの導入

mecab-ipadic-neologdとは、

mecab-ipadic-neologd は、多数のWeb上の言語資源から得た新語を
追加することでカスタマイズした MeCab 用のシステム辞書です。

とあるように、Web上をクローリングしてデータ収集して新語を取り入れた、@overlastさんが作成されたMeCab辞書です。Tweetデータなどは従来の単語辞書には含まれていなかった単語が多く含まれているので、この記事を書く直前にこのような辞書がリリースされたのはラッキーでした!
ご本人が第47回R勉強会@東京のセッションで説明されていた通り、超簡単に導入できるので、皆さんも是非^^

MeCab自体のインストールと、この新語対応の辞書mecab-ipadic-neologdのインストールも
https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md
にインストール方法の記載があるので、ここを参考にMeCabとその新語対応辞書mecab-ipadic-neologdをインストールします。

1-2.mecab-pythonの導入

https://code.google.com/p/mecab/downloads/list
(※Google Code のダウンロード機能が 2014/1 で終了したようなので下記からDLください)

Google Drive Mecab Python download
ここからダウンロードしてインストールします。

$ python setup.py build
$ python setup.py install

基本的に回答してこの2つのコマンドでインストールされるはず。

1-3.ツイートを形態素に分ける

PythonからMeCabの軌道を試してみましょう。mecab-ipadic-neologdを導入しているのでTaggerの生成時に引数でそのインストール先ディレクトリを指定しています。

import MeCab as mc
t = mc.Tagger('-Ochasen -d /usr/local/Cellar/mecab/0.996/lib/mecab/dic/mecab-ipadic-neologd/')
sentence = u'今日は良い天気ですが、雨ですね。キャラメルマキアートがうまいです。'
text = sentence.encode('utf-8') 
node = t.parseToNode(text)   # 先頭行はヘッダのためスキップ

while(node):
    if node.surface != "":
        print node.surface +"\t"+ node.feature
    node = node.next
    if node is None:
        break

下記のように文章を単語に分けてくれて、品詞もつけてくれます。ちゃんと"キャラメルマキアート"という固有名詞も拾ってくれます!

<<<output>>>
今日  名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
良い  形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ
天気  名詞,一般,*,*,*,*,天気,テンキ,テンキ
です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
が 助詞,接続助詞,*,*,*,*,が,ガ,ガ
、 記号,読点,*,*,*,*,、,、,、
雨 名詞,一般,*,*,*,*,雨,アメ,アメ
です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
ね 助詞,終助詞,*,*,*,*,ね,ネ,ネ
。 記号,句点,*,*,*,*,。,。,。
キャラメルマキアート  名詞,固有名詞,一般,*,*,*,キャラメル・マキアート,キャラメルマキアート,キャラメルマキアート
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
うまい   形容詞,自立,*,*,形容詞・アウオ段,基本形,うまい,ウマイ,ウマイ
です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。 記号,句点,*,*,*,*,。,。,。

で、この形態素解析をツイート本文に適用します。本文をMeCabにかけて、今回は名詞・動詞・形容詞・副詞のみを抽出、それぞれをツイートのレコードに列追加していきます。(※正確にはmongoDBではレコードはドキュメント、列はフィールドです)助詞、助動詞等を抜いているのですが、これらは本文の内容を表すような特徴をそこまで表現していないと思いましたので除外しています。

# mecab 形態素分解
def mecab_analysis(sentence):
    t = mc.Tagger('-Ochasen -d /usr/local/Cellar/mecab/0.996/lib/mecab/dic/mecab-ipadic-neologd/')
    sentence = sentence.replace('\n', ' ')
    text = sentence.encode('utf-8') 
    node = t.parseToNode(text) 
    result_dict = defaultdict(list)
    for i in range(140):  # ツイートなのでMAX140文字
        if node.surface != "":  # ヘッダとフッタを除外
            word_type = node.feature.split(",")[0]
            if word_type in ["名詞", "形容詞", "動詞"]:
                plain_word = node.feature.split(",")[6]
                if plain_word !="*":
                    result_dict[word_type.decode('utf-8')].append(plain_word.decode('utf-8'))
        node = node.next
        if node is None:
            break
    return result_dict

下記で全Tweetデータに対して形態素に分けていく処理を行います。

for d in tweetdata.find({},{'_id':1, 'id':1, 'text':1,'noun':1,'verb':1,'adjective':1,'adverb':1}):
    res = mecab_analysis(unicodedata.normalize('NFKC', d['text'])) # 半角カナを全角カナに

    # 品詞毎にフィールド分けして入れ込んでいく
    for k in res.keys():
        if k == u'形容詞': # adjective  
            adjective_list = []    
            for w in res[k]:                adjective_list.append(w)
            tweetdata.update({'_id' : d['_id']},{'$push': {'adjective':{'$each':adjective_list}}})
        elif k == u'動詞': # verb
            verb_list = []
            for w in res[k]:
                verb_list.append(w)
            tweetdata.update({'_id' : d['_id']},{'$push': {'verb':{'$each':verb_list}}})
        elif k == u'名詞': # noun
            noun_list = []
            for w in res[k]:                noun_list.append(w)
            tweetdata.update({'_id' : d['_id']},{'$push': {'noun':{'$each':noun_list}}})
        elif k == u'副詞': # adverb
            adverb_list = []
            for w in res[k]:
                adverb_list.append(w)
            tweetdata.update({'_id' : d['_id']},{'$push': {'adverb':{'$each':adverb_list}}})
    # 形態素解析済みのツイートにMecabedフラグの追加
    tweetdata.update({'_id' : d['_id']},{'$set': {'mecabed':True}})


2. CountVectorizerで単語を数える

Scikit-learnというpythonの機械学習ライブラリにCountVectorizerというものがあるのですが、単語数を数えるのにこれを使ってみたいと思います。

2-1. CountVectorizerの使い方の簡単な例

3つの文章を例にとってCountVectorizerでどういうことができるのかを説明します。CountVectorizerはfit関数に渡された文字列データに含まれる単語を単語毎にその出現回数をカウントして、それをベクトルとして表現してくれるクラスです。文字での説明より、下記のコードを見たほうが早いかもしれませんw

# 3つの文章 (1つの文が1つのツイート本文と思ってください)
data=["This is a pen.",
      "This is also a pen. Pen is useful.",
      "These are pencils.",
     ]

c_vec   = CountVectorizer()           # CountVectorizerオブジェクトの生成
c_vec.fit(data)                       # 対象ツイート全体の単語の集合をセットする
c_terms = c_vec.get_feature_names()   # ベクトル変換後の各成分に対応する単語をベクトル表示
c_tran  = c_vec.transform([data[1]])  # 2つ目の文章の数を数える

print c_terms
print data[1]
print c_tran.toarray()

下記がアウトプット結果です。
1行目にget_feature_names()関数で生成されたベクトルが表示されています。1つ目の成分はalsoのカウント数、2つ目の成分はareのカウント数…のように解釈します。fit()関数に渡したデータ一式に含まれる単語がuniqueになって入っています。
2行目はtransform()関数に渡したカウントしたい対象の文章です。
3行目が欲しかった単語毎のカウント数です。このように"This is also a pen. Pen is useful."という文章にはalsoが1つ、areは無し、isが2つ、penも2つ入っている、と言ったように単語数をカウントした数字が入っています。これはテストなのでベクトルの成分が8個しかないですが、このあと行うTweetデータに対してこの処理を行うと45001個の成分を持つベクトルが生成されます。30万件のツイートにユニークワードが4万5千個あるということですね。

<<<output>>>

[u'also', u'are', u'is', u'pen', u'pencils', u'these', u'this', u'useful']
This is also a pen. Pen is useful.
[[1 0 2 2 0 0 1 1]]

2-2. TweetデータをCountVectorizerにかける

では、Tweetデータで同じことをしていきたいと思います。

まずは必要なライブラリのインポート、DBへの接続と、Utility的な関数の宣言です。

from sklearn.feature_extraction.text import CountVectorizer
from pymongo import Connection
import numpy as np
from collections import defaultdict
import sys, datetime

connect = Connection('localhost', 27017)
db = connect.starbucks
tweetdata = db.tweetdata


def str_to_date_jp_utc(str_date):
    if str_date is not None:
        return datetime.datetime.strptime(str_date,'%Y-%m-%d %H:%M:%S') - datetime.timedelta(hours=9)
    else:
        return None

次にMeCabで分かち書きをした文字列を取得する関数です。これはmongoDBに蓄積されたTweetデータから指定した期間の物を取り出し、MeCabで単語化されたものをリストに集約してそれを返します。

# mecabで分解した単語を連結して文字列化する。
def get_mecabed_strings(from_date_str=None, to_date_str=None,include_rt=False):
    tweet_list = []
    tweet_texts = []

    from_date = str_to_date_jp_utc(from_date_str)
    to_date = str_to_date_jp_utc(to_date_str) 

    # 取得対象期間の条件設定
    if (from_date_str is not None) and (to_date_str is not None):
        query = {'created_datetime':{"$gte":from_date, "$lt":to_date}}
    elif (from_date_str is not None) and (to_date_str is None):
        query = {'created_datetime':{"$gte":from_date}}
    elif (from_date_str is None) and (to_date_str is not None):
        query = {'created_datetime':{"$lt":to_date}}
    else:
        query = {}

    # spam除去
    query['spam'] = None

    # リツイートを含むか否か
    if include_rt == False:
        query['retweeted_status'] = None
    else:
        query['retweeted_status'] = {"$ne": None}

    # 指定した条件のツイートを取得
    for d in tweetdata.find(query,{'noun':1, 'verb':1, 'adjective':1, 'adverb':1,'text':1}):
        tweet = ""
        # Mecabで分割済みの単語をのリストを作成
        if 'noun' in d:
            for word in d['noun']:
                tweet += word + " "
        if 'verb' in d:
            for word in d['verb']:
                tweet += word + " "
        if 'adjective' in d:
            for word in d['adjective']:
                tweet += word + " "
        if 'adverb' in d:
            for word in d['adverb']:
                tweet += word + " "
        tweet_list.append(tweet)
        tweet_texts.append(d['text'])
    return {"tweet_list":tweet_list,"tweet_texts":tweet_texts}

上記関数を利用して何か変化がありそうだった3/18 0:00の前後でそれぞれ単語のリストを取得します。今回リツイートは除外しています。

# "2015-03-18 00:00:00"以前
ret_before = get_mecabed_strings(to_date_str="2015-03-18 00:00:00")
tw_list_before = ret_before['tweet_list']

# "2015-03-18 00:00:00"以降
ret_after = get_mecabed_strings(from_date_str="2015-03-18 00:00:00")
tw_list_after= ret_after['tweet_list']

# 全期間
ret_all = get_mecabed_strings()
tw_list_all = ret_all['tweet_list']

ここから先は先ほど説明したCountVectorizerの出番です。

c_vec = CountVectorizer(stop_words=[u"スタバ"])  # 「スタバ」は全Tweetに含まれるので除外
c_vec.fit(tw_list_all)                          # 全Tweetに含まれる単語の集合をここでセット
c_terms = c_vec.get_feature_names()             # 各ベクトル要素に対応する単語を表すベクトル

# 期間の前後でひとまとまりと考え、transformする
transformed = c_vec.transform([' '.join(tw_list_before),' '.join(tw_list_after)])

はい、これで単語を数えることができました。ここからがやりたかったことですが、この期間の前後で増えている単語が何か、を表示させていきます。

# afterからbeforeを引くことで増分をsubに代入
sub = transformed[1] - transformed[0]

# トップ50がどの位置にあるかを取り出す
arg_ind = np.argsort(sub.toarray())[0][:-50:-1]

# トップ50の表示
for i in arg_ind:
    print c_vec.get_feature_names()[i]

3. 分析結果

長かったですが、やっとたどり着きました!
下記が期間の前後で増加率の高い単語トップ50です。
「カウント順位」は単純に前後での該当単語が出現するツイート数の差分ですが、前後のツイート数が違うので、率を算出してランキングしています。
(表が見にくくてスミマセン・・・)

順位 単語 出現率
カウント カウント
順位
before after
出現 出現しない 出現率 出現 出現しない 出現率
1 新作 7.75% 7,816 1 2,446 119,717 2.00% 10,262 94,928 9.76%
2 飲む 3.34% 2,203 3 9,455 112,708 7.74% 11,658 93,532 11.08%
3 アーモンドミルク 2.23% 2,266 2 561 121,602 0.46% 2,827 102,363 2.69%
4 ハニー 1.38% 1,420 4 263 121,900 0.22% 1,683 103,507 1.60%
5 美味しい 1.24% 726 9 4,167 117,996 3.41% 4,893 100,297 4.65%
6 新しい 1.21% 1,174 6 748 121,415 0.61% 1,922 103,268 1.83%
7 アーモンド 1.21% 1,230 5 290 121,873 0.24% 1,520 103,670 1.45%
8 http 1.02% -3,605 - 33,705 88,458 27.59% 30,100 75,090 28.61%
9 クランチ 0.90% 932 7 139 122,024 0.11% 1,071 104,119 1.02%
10 with 0.86% 869 8 287 121,876 0.23% 1,156 104,034 1.10%
11 おいしい 0.80% 592 10 1,772 120,391 1.45% 2,364 102,826 2.25%
12 フラペチーノ 0.73% 378 12 2,775 119,388 2.27% 3,153 102,037 3.00%
13 今日 0.68% -185 - 6,497 115,666 5.32% 6,312 98,878 6.00%
14 甘い 0.67% 547 11 1,121 121,042 0.92% 1,668 103,522 1.59%
15 やつ 0.56% 268 18 2,291 119,872 1.88% 2,559 102,631 2.43%
16 https 0.45% 57 75 3,023 119,140 2.47% 3,080 102,110 2.93%
17 みる 0.42% -14 - 3,255 118,908 2.66% 3,241 101,949 3.08%
18 商品 0.40% 301 13 829 121,334 0.68% 1,130 104,060 1.07%
19 うまい 0.39% 274 17 982 121,181 0.80% 1,256 103,934 1.19%
20 くる 0.34% -740 - 7,878 114,285 6.45% 7,138 98,052 6.79%
21 ちゃ 0.30% 284 16 228 121,935 0.19% 512 104,678 0.49%
22 問題 0.30% 298 14 102 122,061 0.08% 400 104,790 0.38%
23 人種 0.28% 291 15 29 122,134 0.02% 320 104,870 0.30%
24 期間限定 0.26% 233 19 264 121,899 0.22% 497 104,693 0.47%
25 はちみつ 0.23% 233 20 81 122,082 0.07% 314 104,876 0.30%
26 飲める 0.23% 53 81 1,338 120,825 1.10% 1,391 103,799 1.32%
27 すぎる 0.22% -199 - 3,104 119,059 2.54% 2,905 102,285 2.76%
28 開始 0.22% 221 21 75 122,088 0.06% 296 104,894 0.28%
29 ハチミツ 0.22% 220 22 64 122,099 0.05% 284 104,906 0.27%
30 自宅 0.20% 200 25 103 122,060 0.08% 303 104,887 0.29%
31 印象 0.20% 179 26 208 121,955 0.17% 387 104,803 0.37%
32 届ける 0.20% 201 23 31 122,132 0.03% 232 104,958 0.22%
33 購入 0.19% 176 27 204 121,959 0.17% 380 104,810 0.36%
34 新サービス 0.19% 201 24 0 122,163 0.00% 201 104,989 0.19%
35 line 0.18% 141 33 378 121,785 0.31% 519 104,671 0.49%
36 買う 0.18% -316 - 3,660 118,503 3.00% 3,344 101,846 3.18%
37 交換 0.17% 161 30 153 122,010 0.13% 314 104,876 0.30%
38 感じ 0.16% 59 72 791 121,372 0.65% 850 104,340 0.81%
39 行く 0.16% -1,843 - 14,473 107,690 11.85% 12,630 92,560 12.01%
40 プーさん 0.16% 165 28 13 122,150 0.01% 178 105,012 0.17%
41 キャンペーン 0.16% 159 31 33 122,130 0.03% 192 104,998 0.18%
42 昨日 0.15% -9 - 1,232 120,931 1.01% 1,223 103,967 1.16%
43 ceo 0.15% 161 29 8 122,155 0.01% 169 105,021 0.16%
44 latte 0.15% -78 - 1,704 120,459 1.39% 1,626 103,564 1.55%
45 一言 0.15% 134 35 162 122,001 0.13% 296 104,894 0.28%
46 呼び方 0.15% 142 32 97 122,066 0.08% 239 104,951 0.23%
47 ビスケット 0.14% 136 34 87 122,076 0.07% 223 104,967 0.21%
48 家族 0.14% 123 39 167 121,996 0.14% 290 104,900 0.28%
49 微妙 0.14% 131 36 86 122,077 0.07% 217 104,973 0.21%
50 美味い 0.13% 93 47 322 121,841 0.26% 415 104,775 0.39%

「新作」、「アーモンドミルク」など 3/18から始まったアーモンドミルクラテに関するツイートが増加していたのですね!

トップ10の単語を並べ替えると「新作 アーモンドミルク with ハニー クランチ 飲む 美味しい」です!どうやら好評のようです(^ー^*)これ、狙って作ったデータではないのですが、キレイに結果が出ました。

「アーモンドミルク ラテ with ハニー クランチ」はこれです。



「新作」がダントツトップなのは、「スタバの新作飲みたい!」みたいに具体的な名称「アーモンドミルクラテ」がでてこないツイートが結構多かったことが理由のようです。

次に上位の単語のうち「飲む」を除いた"新作", "アーモンドミルク","ハニー", "アーモンド", "新しい"が含まれるツイート数を集計してみます。

def is_include_word_list(text, word_list,f):
    for word in word_list:
        if text.find(word) > -1:
            return True
    return False


date_dict = defaultdict(int)

word_list = [u"新作", u"アーモンドミルク",u"ハニー", u"アーモンド", u"新しい", "with", u"クランチ"]

with open('armond.txt','w') as f:
    for d in tweetdata.find({'spam': None, 'retweeted_status': None},{'created_datetime':1,'text':1}):
        str_date = date_to_Japan_time(d['created_datetime']).strftime('%Y\t%m/%d %H %a') 

        text = d['text']
        if is_include_word_list(text, word_list,f):
            date_dict[str_date] += 1
            # マッチした対象をファイルに書き出す(for 検証用)
            ret_str = str_date +' '+ text.replace('\n', ' ')+'\n'
            f.write(ret_str.encode('utf-8'))

print "date_dict", len(date_dict)
print "階級数:", len(date_dict)
print "日付" + "\t\t\t" + "# of Tweet"
keys = date_dict.keys()
keys.sort()
for k in keys:
    print k  + "\t" +  str(date_dict[k]) 

結果をプロットしたのが下記のグラフです。紫の線がリストアップした単語を含むツイート数です。

listed-compressor.png

この紫の数を、リツイート含まないツイート数(青い線)から引いてあげると…

listed_diff-compressor.png

ほとんど山が平らになりました!3/18日以降に増えていた単語は先ほどリストアップしたものでだいたい説明がつくようです。

でも、毎週のツイート数サイクルがこんなに安定しているのはなんででしょうかね、大数の法則ってこと(・ω・)? この謎を解ける人がいたら是非教えて欲しいです。

このスタバTweet分析もあと、1回か2回くらい続く予定です。
なんか、統計学的に難しいモデルとか使えないかと思って始めたのですが、データ眺めて加工するだけでも結構面白いことができた気がします。
次回はもう少し検定とか推定とかそんな話をしたいかなと思っています。もう少しお付き合いいただければ幸いです。

使用したコードの全体はgistにあります。
https://gist.github.com/matsuken92/72a0da8d9b42bed28e61

APPENDIX:期間前後の単語の出現確率の検定

3/18 0:00の前後、各単語の出現有無を2x2のマトリクスにして考えると下記のようになります。これはカイ二乗適合度検定が行える形ですね。beforeとafterで本当に出現率に差があったのかどうか、を検定してみます。

単語:
アーモンドミルク
出現 出現
しない
Before 561 121,602 122,163
After 2,827 102,363 105,190
3,388 223,965 227,353

下記がカイ二乗検定をかけた結果の表です。No.39を除き全て有意な結果が出ているので、やはり3/18以前と以降で出現率に変化があったと考えて良さそうです。No.39「行く」ですが、今回の分析対象としては、この単語が増えたと言えないことにさほど不都合はないかなと思いますので、良い結果かと思います。

順位 単語 chi2 p-val 結果
1 新作 6437.337845 0 有意
2 飲む 749.5007818 5.15E-165 有意
3 アーモンドミルク 1910.25734 0 有意
4 ハニー 1275.398025 2.51E-279 有意
5 美味しい 227.0216513 2.66E-51 有意
6 新しい 717.7324129 4.17E-158 有意
7 アーモンド 1042.145835 1.24E-228 有意
8 http 29.34665299 6.05E-08 有意
9 クランチ 871.5525659 1.50E-191 有意
10 with 667.7020689 3.16E-147 有意
11 おいしい 200.486648 1.64E-45 有意
12 フラペチーノ 116.9898183 2.89E-27 有意
13 今日 49.35872884 2.13E-12 有意
14 甘い 207.6474174 4.48E-47 有意
15 やつ 83.8410031 5.36E-20 有意
16 https 44.31937829 2.79E-11 有意
17 みる 35.19560616 2.98E-09 有意
18 商品 103.1110711 3.17E-24 有意
19 うまい 87.88641968 6.93E-21 有意
20 くる 10.3550916 0.001291181 有意
21 ちゃ 155.9814931 8.54E-36 有意
22 問題 224.6028811 8.96E-51 有意
23 人種 288.2657541 1.19E-64 有意
24 期間限定 110.5936405 7.26E-26 有意
25 はちみつ 174.3777441 8.19E-40 有意
26 飲める 24.391691 7.86E-07 有意
27 すぎる 10.62339463 0.001116659 有意
28 開始 166.5727271 4.15E-38 有意
29 ハチミツ 173.6897162 1.16E-39 有意
30 自宅 130.4736816 3.23E-30 有意
31 印象 83.82905236 5.39E-20 有意
32 届ける 184.6605771 4.65E-42 有意
33 購入 82.49458545 1.06E-19 有意
34 新サービス 231.4807808 2.83E-52 有意
35 line 48.21110495 3.83E-12 有意
36 買う 6.279272571 0.012215823 有意
37 交換 81.93429413 1.41E-19 有意
38 感じ 20.1122379 7.30E-06 有意
39 行く 1.355305558 0.244352729 有意でない
40 プーさん 167.4411384 2.68E-38 有意
41 キャンペーン 136.6923979 1.41E-31 有意
42 昨日 12.43299481 0.000421815 有意
43 ceo 170.5916319 5.49E-39 有意
44 latte 8.815476835 0.002986861 有意
45 一言 61.49900119 4.43E-15 有意
46 呼び方 82.67724362 9.66E-20 有意
47 ビスケット 81.23778177 2.00E-19 有意
48 家族 53.7386464 2.29E-13 有意
49 微妙 77.40857135 1.39E-18 有意
50 美味い 29.5887298 5.34E-08 有意

カイ二乗適合度検定を行ったコードは下記です。

data = np.array([[2446,119717,10262,94928],
[9455,112708,11658,93532],
[561,121602,2827,102363],
[263,121900,1683,103507],
[4167,117996,4893,100297],
[748,121415,1922,103268],
[290,121873,1520,103670],
[33705,88458,30100,75090],
[139,122024,1071,104119],
[287,121876,1156,104034],
[1772,120391,2364,102826],
[2775,119388,3153,102037],
[6497,115666,6312,98878],
[1121,121042,1668,103522],
[2291,119872,2559,102631],
[3023,119140,3080,102110],
[3255,118908,3241,101949],
[829,121334,1130,104060],
[982,121181,1256,103934],
[7878,114285,7138,98052],
[228,121935,512,104678],
[102,122061,400,104790],
[29,122134,320,104870],
[264,121899,497,104693],
[81,122082,314,104876],
[1338,120825,1391,103799],
[3104,119059,2905,102285],
[75,122088,296,104894],
[64,122099,284,104906],
[103,122060,303,104887],
[208,121955,387,104803],
[31,122132,232,104958],
[204,121959,380,104810],
[0,122163,201,104989],
[378,121785,519,104671],
[3660,118503,3344,101846],
[153,122010,314,104876],
[791,121372,850,104340],
[14473,107690,12630,92560],
[13,122150,178,105012],
[33,122130,192,104998],
[1232,120931,1223,103967],
[8,122155,169,105021],
[1704,120459,1626,103564],
[162,122001,296,104894],
[97,122066,239,104951],
[87,122076,223,104967],
[167,121996,290,104900],
[86,122077,217,104973],
[322,121841,415,104775]])

from scipy.stats import chi2_contingency as chi2_con
for i, d in zip(range(len(data)), data):
    chi2, p, dof, ex = chi2_con(d.reshape(2,2))
    print i+1, " chi2", chi2, "  p-val", p, u"有意" if p < 0.05 else u"有意でない"

参考

MeCab:形態素解析ライブラリ
http://mecab.googlecode.com/svn/trunk/mecab/doc/index.html

MeCabの新語辞書:Neologism dictionary for MeCab
https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md

Scikit-leaern:Python用機械学習ライブラリ
http://scikit-learn.org/stable/