Python
自然言語処理
機械学習
GoogleNaturalLanguageAPI

インターン生が3日でアンケート結果を感情推定を用いて分析してみた


はじめに

どうも,@2357giです。

大学3年の夏休みの間株式会社TORICOにインターン生として参加させて頂きました。

そして、その中でも株式会社TORICOの運営する「スキマ」という漫画サービスのエンジニアとして働かせていただいています。

今回は、スキマにておこなったユーザーアンケートがスキマのDBに転がっていた熟成されていたので

これを使って自然言語処理を行ってみようと考えました。

インターン期間が残り2日なので機械学習のモデルを作ることを目的とせず、実際に生データを分析するということを目的に据え、モデリングを簡略化するためにGoogleのNaturalLanguageAPIを使用することにしました。

データ分析の経験が乏しく、稚拙な内容ですがどうぞよろしくお願いいたします。


今回の分析の目的

今回のアンケートは2018年2月にスキマの機能改善、大幅リニューアルの時に行ったアンケートであり、様々な機能に変更を加えました。

また、アンケートデータ自体にもテキスト記入欄が多く、

大量のテキストデータがあったので

「そのテキストがどの機能変更について言及しているのか、またユーザーはその機能変更についてどう思っているのか」

を分析しようと考えました。

具体的には、テキストを分析しどの機能変更について言及しているのかを予想し、

また「その機能変更についてどう思っているのか」という点は

Google Natural LanguageAPIを用いてテキストの感情分析を行い、算出されたスコアから導き出す方法をとりました。


私が使用するデータは個人情報がマスキングされたものを扱っています。

この記事の中で記述されているデータは全て実際のデータではなく、

私がそれっぽいものを考えたデータとなっています。



環境


  • macOS High Sierra 10.13.6

  • python 3.5.1

  • pandas-0.23.4

  • Google Natural Language API


使用データ

今回のアンケートは株式会社TORICOが運営しているサービス「スキマ」サービス全体を大きくリニューアルした際に取ったアンケートであります。

色々な機能を大きく変更したリニューアルでしたので、色々な機能に関する意見がいただけました。


データの観察, 整形

データ自体はサービスのDBに転がっていたのでSQLでサクッと引っ張りました。

CSVにて出力した感じがこんな感じになります。実際はもう少しカラムがあります。

id
rating
pros
cons
others
premium
gene

1
5
使いやすくなった
今の所何もなし
ジャンル分けがもう少し細かくして欲しい。
0
2

過去に読んだ作品を見返しやすくなった
待てば無料がわかりずらい

2715
4
全体的に使いやすくなった
反応が鈍くて間違って押してしまうことがある

1
0

premiumはスキマのプレミアムユーザー(月額課金ユーザー)か否かを表しています。1がプレミアムユーザーです。

geneは性別カラムになっており0が性別データ無し、1が男性2が女性となっております。

「ratingはサービスを5段階で評価してください」の5段階です。5が最高です。

この段階では「ユーザーがどの機能について言及しているか」を明確にするアンケート項目が無く、どの機能について言及しているかを導き出す必要がありました。


データ観察

pandasを用いてCSVをDataFrameにし、.info()で中の様子を見ます。


review.py

dp = pd.read_csv("data/question_text.csv")

print(dp.info())

# angeIndex: 2716 entries, 0 to 2715
# Data columns (total 12 columns):
# id 2716 non-null object
# pros 2716 non-null object
# cons 2715 non-null object
# others 810 non-null object
# premium 2714 non-null float64
# gene 2714 non-null float64
# dtypes: float64(2), int64(2), object(8)
# memory usage: 254.7+ KB



  • 自由記入欄だけ取り出し処理するつもりだったが、prosとconsもテキストデータとして使えそうなので、これら3つのカラムを1つに纏めて使うとよさそう。

  • ratingを残しておくことでAPIから吐き出したスコアとのズレが見えそう

  • genre, premiumのデータも残しておけば色分け散布図が楽しそう

  • premium, genreのfloat64が気に食わないですね。


テキストのみのカラムを作る

とりあえずテキストをひとまとめにし、欠損値の処理、改行の処理をします。

id
text
premium
gene
rating

1
つかいやすくなった
0
1

1
今のところ何も無し
0
2

1
ジャンル分けがもう少し細かくして欲しい
0
0

2
全体的に使いやすくなった
1
0

こんな感じにpros,cons,othersを合わせてひとつのカラムにしたのち、様子を見てみます。


formatcsv.py

import pandas as pd

dp = pd.read_csv('data/questions_text.csv',
usecols=['pros', 'cons', 'その他', 'premium', 'gene', 'rating'])

pros_df = dp[['pros', 'premium', 'gene', 'rating']].rename(columns={'pros': 'text'})
cons_df = dp[['cons', 'premium', 'gene', 'rating']].rename(columns={'cons': 'text'})
others_df = dp[['その他', 'premium', 'gene', 'rating']].rename(columns={'その他': 'text'})

df = pd.concat([pros_df, cons_df, others_df])

df = df.dropna().replace('\n', '', regex=True)

import numpy as np

df.premium = df.premium.astype(np.int64)
df.gene = df.gene.astype(np.int64)

print(df.info())

# <class 'pandas.core.frame.DataFrame'>
# Int64Index: 6237 entries, 0 to 2715
# Data columns (total 4 columns):
# text 6237 non-null object
# premium 6237 non-null int64
# gene 6237 non-null int64
# rating 6237 non-null int64
# dtypes: int64(3), object(1)
# memory usage: 243.6+ KB
# None


文章だけになりました。pandasはカラムに一つでも欠損値があるとそのカラムは浮動小数点として処理されます。

なので、premiumとgeneがfloat64になっていました。

後々何か起こりそうなので処理し、ついでにこのタイミングで欠損値とテキスト内の改行も処理しました。

いい感じですね。

欠損値もなく型も揃っています。


頻出する単語を整理する

ただ、このまま言語処理にかけると大量の「特に無し」や「なし」などのワードも漁ってしまうので、

まずはそれらを処理します。

def val_count():

return print(df.text.value_counts()) # .value_counts()でユニークな頻出するあたいの個数を返す

# 特になし     379
# なし 158
# 特にない 135
# ない 106
# 見やすくなった 59
# とくになし 48
# 特になし。 37
# 見やすい 30

最初は閾値を決め、それ以上頻出する単語は削除しようとしたのですが、

「みやすくなった」や「使いづらくなった」などのワードも削除するのがいいのか判断しきれないので、とりあえず「なし」や「同上」などを削ることに。

removewords = ("ここに除外するテキストjj")

def remove_word(words):
return [df.drop(df.index[df.text == i], inplace=True) for i in words]

removewordsの中身は手打ちです。正気か?

恥ずかしいのでここには載せません。

いい感じに整形できたので書き出します。

df.to_csv("format_question.csv")


どの機能のリニューアルについて言及しているのか調べる

次に、この文章はどの機能について言及しているのかを見分けるための作業をしていきたいと思います。

判断方法は

この単語が含まれている文章はこの機能について言及している

という感じでやっていきます。

csvファイルに新しいカラムを用意して、どれについて言及しているかのカラムを設置しようと思います。

id
text
premium
gene
rating
func

1
つかいやすくなった
0
1
4

1
今のところ何も無し
0
2
2

1
ジャンル分けがもう少し細かくして欲しい
0
0
2
'ジャンル'

1
チケット制がわかりにくい
0
0
2
'チケット'

2
全体的に使いやすくなった
1
0
4

こんな感じに言及する昨日ごとにタグ付けしていきたいと思います。

まずはどの機能について言及しているのかをアンケート結果を眺めつつ見ていきたいと思います。

words_coin = ('coin', 'コイン', 'こいん')

words_ticket = ('ticket', 'チケット', 'ちけっと')
words_free = ('ただ', 'タダ', '無料')
words_wait4free = ('待てば無料', '待つと無料')
words_contents = ('作品', '漫画', '本', 'まんが', 'マンガ')
words_search = ('検索', 'サーチ', '探')
words_review = ('レビュー', 'れびゅー', 'review')
words_screen = ('画面')
words_system = ('システム', 'しすてむ')
words_renewal = ('リニューアル', 'りにゅーある', 'renewal')
words_tag = ('タグ', 'たぐ')

だいたいこのぐらいに分ければいい感じになりそうなので、表記ブレを考慮しつつタグとして使用するキーワードをまとめていきます。

ここが一番処理の方法悩みます どうすんねんこれ・・・

とりあえずインターネットとかいうジャングルをさまよってDataframeのまま1行ずつ処理するとメチャクチャ遅いので、

pandas.dataframeからnumpy.ndarrayに変換して1行ずつ処理する感じにしました。


ans_df = pd.read_csv('data/format_question.csv', index_col=0)

ans_np = np.asarray(ans_df)
new_row = np.array([0, 0, 0, 0, 0])

for row in ans_np:
text = row[0]

have_tag = False

# ---------------------------
# 読み込むキーワードの順序は要変更
#
# ---------------------------

for word_coin in words_coin:
if str(word_coin) in str(text):
n_row = np.insert(row, 4, 'coin')
new_row = np.vstack((new_row, n_row))
have_tag = True
break

if have_tag is True:
continue

.
.
.
.
.

for word_tag in words_tag:
if str(word_wait4free) in str(text):
n_row = np.insert(row, 4, 'wait4free')
new_row = np.vstack((new_row, n_row))
have_tag = True
break

if have_tag is True:
continue

continue

new_row = np.delete(new_row, [0], axis=0)
data_df = pd.DataFrame(new_row, columns=['text', 'premium', 'gene', 'rating', 'tag'])

data_df.premium = data_df.premium.astype(np.int64)
data_df.gene = data_df.gene.astype(np.int64)
data_df.rating = data_df.rating.astype(np.int64)

print(data_df.info())

ごめんなさい 許してください 後生です

クソコードを書いてしまったのでもうこのコードが表に出ることはないでしょう。

最初はワードリストをdistで作り、forの部分も関数化しようと考えていたのですが、あまりにもうまくできずにハゲてしまい愚行に及びました。

関数化の考え方がまだいまいち身についてないです、精進ですね。。。


Google natural Language APIにつっこむ

pandasのdataframeで1行ずつ見てnatural language apiに飛ばすと、

pandasの使用上メチャクチャ重くなってしまうので、

np.arrayであるこの段階で感情分析の処理を作ることにしました。


APIを使う部分

テキストを渡すとスコアを返してくれる物を作って、

def emotext(text):

url = 'https://language.googleapis.com/v1/documents:analyzeSentiment?key=AIzaSyC8cjSpZ4b5KPKHdcc-nhFldd-aVN-tX9Q'
header = {'Content-Type': 'application/json'}
body = {
"document": {
"type": "PLAIN_TEXT",
"language": "JA",
"content": text
},
"encodingType": "UTF8"
}
response = requests.post(url, headers=header, json=body).json()
score = response['documentSentiment']['score']
return score

np.arrayで処理している部分で呼び出します

    for word_coin in words_coin:

if str(word_coin) in str(text):
n_row = np.insert(row, 4, 'coin')
# new_row = np.vstack((new_row, n_row))
emo = emotext(text)
nn_row = np.insert(n_row, 5, emo)
new_row = np.vstack((new_row, nn_row))
have_tag = True
break

data_df.head()

スクリーンショット 2018-09-14 12.18.16.png

いい感じにスコアが追加できてますね。

textカラムが用済みなので削除して、emoカラムの型をいい感じにして出力します

data_df.emo = data_df.emo.astype(np.float64)

data_df.to_csv("data/emo_data.csv")


出力されたデータを元に分析する

出力されたデータをシャッフルしたのち、適当に抽出します。

csv = 'emo_data.csv'

df = pd.read_csv(csv, index=0)
df = df.sample(frac=1)

df = df[0:150]

出力したデータをmatplotlibやseaborn, PixieDustを用いて観察し、考察します。

一番面白い部分なのですが, 会社内のデータをここにあげるわけにも行かないので...

sns.distplot(df[df["tag"]=="contents"].emo,kde=True,rug=True,color='yellow')

sns.distplot(df[df["tag"]=="ticket"].emo,kde=True,rug=True, color='blue')
sns.distplot(df[df["tag"]=="free"].emo,kde=True,rug=True, color='green')
sns.distplot(df[df["tag"]=="wait4free"].emo,kde=True,rug=True, color='red')
plt.show()

このような感じで各タグごとに色分けしてグラフ表示することで、

各変更点に対してユーザーがどのような感情を抱いたのかを分析して行きます。


反省と振り返り

今回は時間がなくAPIを使いましたが、

今度はjanomeやMecabなどを使って機械学習部も作りたいと思いました。

また、学習にかける文章をもっと整形する必要がありました。

スキマ固有の「待つと無料」などと言ったキーワードが感情分析のスコアに左右してしまったのでは無いかと考えられるので、左右しない当たり障りない言葉へと変更する必要がありました。

また、「このテキストがどの機能について言及しているか」を考える部分も、

特定テキストが含まれているか否かだけで判断するのは雑すぎました。もうすこし上手な方法でやる必要がありました。

また、pandasやnumpyのndarrayにとても手を焼いてしまったので、勉強する必要がありますね。

ただ、データをビジュアライズして要因を考察する作業はとても楽しいので、

また面白そうなデータを見つけて分析したいと思います。