Twitterの感情分析を用いてビットコイン価格予測モデルを構築してみた


この記事はプログラミング初心者文系の私がAidemyさんデータ分析コースの最終成果物として、Twitterで影響のある人物の投稿を感情分析し、ビットコイン価格に影響があるのかについて分析したものです。

最近、Web3（Web3.0）という言葉を耳にすることが増えたという人も多いと思います。
Web3（Web3.0）とは、これまで情報を独占してきた GAFAM や巨大企業に対して、テクノロジーを活用して分散管理することで情報の主権を民主的なものにしようという概念で、暗号資産やメタバースがその例です。

これからはWeb3（Web3.0）の活用により、5大企業 GAFAM （Google、Amazon、Facebook、Apple、Microsoft）により独占されている権力の個人分散を目指す時代になると言われていますが、この分散を可能とするのが、ブロックチェーン技術です。

これはビットコインやイーサリアムなどの暗号通貨で広く使われている技術です。今後、価格が10万ドルになるとも予測されているビットコインですが、影響力のある著名人のTwitterでの発言によって価格が大きく変動することもあり、株式投資よりボラティリティーが大きいことが知られています。

そこで今回は、テスラCEOのイーロンマスク氏のツイートを感情分析して記録してみようと思いました。


#目次
[1.はじめに](#1-はじめに)
[2.事前準備（データの前処理）](#2-事前準備（データの前処理）)
[3.ツイートの取得](#3-ツイートの取得)
[4.感情分析](#4-感情分析)
[5.機械学習用のデータ準備](#5-機械学習用のデータ準備)
[6.特徴量の作成](#6-特徴量の作成)
[7.予測モデルの作成と予測精度の検証](#7-予測モデルの作成と予測精度の検証)
[8.おわりに](#8-おわりに)

# 1. はじめに
【実行したこと】
イーロンマスク氏のツイートから感情分析を行って、ビットコインの価格にどのくらい影響力があったのかについて予測精度を求めた。

【実行環境】
・Macbook Air
・google colaboratory

【方法】
2021年9月〜2022年3月において、2日間ごとのビットコイン（価格）とPN値（感情）の変化をもとに次の日のビットコインの価格がプラスになるのかマイナスになるのかを調べた。

【予測精度の結果】
予測モデル(ロジスティック回帰、SVM、ランダムフォレスト)を構築して予測した結果
ロジスティック回帰・ランダムフォレストの数値が一番大きくなったが、0.5以下の結果になった為、イーロンマスク氏のツイートだけでは、良い予測が得られなかった。

【改善点】
・データについて
イーロンマスクのツイートを取得したが、そのデータだけでは良い結果は得られなかった。
別の著名人の場合ではどうなるのか検証する必要がある。
また金融経済などの他の要素もビットコイン価格へ影響するため、他のデータを追加して検証する必要がある。
・期間について
2021年9月1日から2022年3月31日を期間として指定したが、ツイートでは長期間の影響はない可能性があるので、短期間で調べる必要がある。
・モデルについて
ロジスティック回帰、SVM、ランダムフォレストではなくディープラーニング(LSTM、RNN)で検証する必要がある。


# 2. 事前準備（データの前処理）

ビットコイン価格データのCSVを準備して、pandasで扱えるようにします。
CSVをデータフレームにして、必要のない列を削除し'日付け'と'終値'列のみのデータにします。
そして、'日付け'列をインデックスにセットして、pd.to_datetime(df_chart.index, format='%Y年%m月%d日')でデータタイム型へ変更してから期間を指定します。
終値'列も、df_chart['終値'].str.replace("," , "").astype(float)で、float型に変更しています。

``` python:
#ビットコインの価格データを準備
df_chart = pd.read_csv("/content/drive/MyDrive/成果物/Bitcoin - ビットコイン過去データ - Investing.com (2).csv")

#データフレームから必要のない列を削除
df_chart = df_chart.drop(['始値', '高値', '安値', '出来高', '変化率 %'], axis=1)

#'日付け'をインデックスにセット
df_chart = df_chart.set_index('日付け')

#インデックスの形式をデータタイム型に変更
df_chart.index = pd.to_datetime(df_chart.index, format='%Y年%m月%d日')

#期間を指定する
df_chart = df_chart.query('"2021-09-01" <= 日付け <= "2022-03-31"').sort_values(by='日付け',ascending=True)

#’終値’をfloat型に変更
df_chart['終値'] = df_chart['終値'].str.replace("," , "").astype(float)

```
![E42774FC-9BEA-4A28-AA0A-47963D9F8284.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/b3786f04-aec3-110c-41ea-6b92c0a1f0ed.jpeg)


# 3. ツイートの取得

Twitterからツイートを取得するために、TwitterAPIを用いました。
TwitterAPIを使用するためには、Twitterの開発者プラットフォームから利用申請が必要になります。
（申請の際に、利用目的を英語で説明する必要があります。）
申請方法の詳細については割愛させて頂きます。

参考：
Twitterの開発者プラットフォームのURL
https://developer.twitter.com/en



``` python:

#TwitterAPIを用いてTwitterからあるアカウントの過去のツイートを取得
import tweepy
import csv

consumer_key = ''       # Consumer Key
consumer_secret = ''    # Consumer Secret
access_key = ''       # Access Token
access_secret = ''      # Accesss Token Secert

# Tweepy設定
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_key, access_secret)
api = tweepy.API(auth)

# wait_on_rate_limit_notify = Tweepyがレート制限の補充を待っているときに通知を出力するかどうか
api = tweepy.API(auth,wait_on_rate_limit=True, wait_on_rate_limit_notify=True)

#ツイートを取得
tweet_data = []

tweets = tweepy.Cursor(api.user_timeline,screen_name = "@elonmusk", include_rts=False, exclude_replies = True)
for tweet in tweets.items():
   tweet_data.append([tweet.id,tweet.created_at,tweet.text.replace('\n',''),tweet.favorite_count,tweet.retweet_count])
   tweeted_at = tweet.created_at + timedelta(hours=9)

labels = ['id', '日付け', 'ツイート', 'いいね数', 'リツイート数']

df = pd.DataFrame(tweet_data, columns=labels)
df.head()

```
![DB7EF172-6865-493D-9F64-33B4CE07E599.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/1a24c39f-8706-7035-eaf1-b628b82ec334.jpeg)

Twitterからツイートを取得していきますが、その際にアクセストークンが必要となります。
（Twitterの開発者プラットフォームから利用申請が完了すると取得できます。）
アクセストークンを設定し、tweepy.Cursorを用いて取得対象のID(@xxxx)を記述します。
今回は最初にID、日付け、ツイート、いいね数、リツイート数を取得します。

そして、ID、日付け、ツイート、いいね数、リツイート数を列名にしてデータフレームを作成します。

これでツイート取得からデータフレームの作成が完了しました。

# 4. 感情分析

次に、前項で取得したツイートデータを整理して、テキスト処理に使えるライブラリである
TextBlobを使用し感情分析を行います。

ここでTextBlobでアクセスできる機能の一部を次に示します。

・テキストのトークン化
・POSタグ付け
・分類
・スペル修正
・感情分析

TextBlobを使用してツイートのpolarity(極性)を[-1.0, 1.0]の浮動小数点の範囲で出力しました。

今回は、英語のツイートの感情分析するので、日本語のツイートの際に使用するMecabなどのモジュールは使用しませんでした。

日本語のツイートを用いて感情分析する場合は、単語の形態素解析を細かくしたり、辞書をダウンロードするというコーディングをしなくてはなりませんが、英語の感情分析をする場合はTextBlobを使用すれば、簡単に極性や主観の数値を出力することができます。

参考：データサイエンス用のライブラリトップ５
https://webbigdata.jp/ai/post-8586

``` python:
df = df.drop(['id', 'いいね数', 'リツイート数'], axis=1)

df['日付け'] = df['日付け'].dt.strftime('%Y-%m-%d')

df.set_index('日付け', inplace=True)

df_filterd = df.query('"2021-09-01" <= 日付け <= "2022-03-31"').sort_values(by='日付け',ascending=True)

#データフレームをcsvデータとして保存
df_filterd.to_csv("filtered_tweets.csv")

files = glob.glob("tweets.csv")
for file in files: # 実行時間の都合上、読み込むファイルを制限しています。
    with open(file, "r", encoding="utf-8") as f:
            # ファイルの内容を改行ごとに分けて読み込む
      lines = f.read().splitlines() 

import nltk
from nltk.corpus import stopwords
from collections import Counter

nltk.download('stopwords')

# Tweetを整理する
def tweet_to_words(raw_tweet):
    
    # a~zまで始まる単語を空白ごとに区切ったリストをつくります
    letters_only = re.sub("[^a-zA-Z@]", " ",raw_tweet) 
    words = letters_only.lower().split()
    
    # '@'と'flight'が含まれる文字とストップワードを削除します
    stops = set(stopwords.words("english"))  
    meaningful_words =  [w for w in words if not w in stops and not re.match("^[@]", w) and not re.match("flight", w) ] 
    return( " ".join(meaningful_words)) 

cleanTweet = df_filterd['ツイート'].apply(lambda x: tweet_to_words(x))
print(df_filterd['ツイート'])

from textblob import TextBlob
## create function to get polarity
def getPolarity(text):
    return TextBlob(text).sentiment.polarity
## create two new column
df_filterd['極性']=df_filterd['ツイート'].apply(getPolarity)
## show new Dataframe
df_filterd
```
![FD7FE895-B96D-4C0D-A996-105F44937AAB.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/796cd2d2-e00d-35c3-3d52-783a9c6e38d4.jpeg)

また、同じ日に複数回のツイートをしている場合があるので、以下コードでインデックスにgroupby()を用いてグループ分けをし、日付けごとの極性値の平均を取ります。

``` python:
df_per_day = df_filterd.groupby(df_filterd.index).mean()
df_per_day.index  = pd.to_datetime(df_per_day.index, format='%Y-%m-%d')
df_per_day
```
![940DE66E-C56D-4F1F-9965-7BD741F108D6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/495bde69-b4cc-ece5-fdfe-57a3d4ab67df.png)


# 5. 機械学習用のデータ準備

そして、機械学習で使用できるデータの準備をしていきます。
ビットコインの株価と感情分析の数値のデータを「日付け」をインデックスにして内部結合します。

``` python:
# df_filteredとdf_chartを、日付けをキーにして内部結合
df_table = pd.merge(df_per_day, df_chart, how="inner", on = "日付け")
df_table
```
![AA36B8F4-A951-4716-9609-166B2CA46A33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/cadb5b22-8a4a-46b1-e48f-220de198c806.png)


内部結合させたデータを用いて、機械学習用のデータを作成します。

``` python:
'''
訓練データ・テストデータに分割
'''
# XにPN値を、yに終値を格納
X = df_table.values[:, 0]
y = df_table.values[:, 1]

# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0, shuffle=False)
```

機械学習用のデータを作成する際、scikit-learnのtrain_test_split()関数を使用しますが、
以下でtrain_test_split()関数について、説明します。

・train_test_splitとは
list　・　numpy.array　・　pandasのDataFrameなどを、学習用のtrainデータとtestデータに分割してくれる関数です。手元のデータを【A】訓練データ【B】テストデータに分けて、【A】訓練データを元に【B】テストデータを検証します。

・割合、個数を指定: 引数test_size, train_size
train_test_split()関数の引数test_sizeでテストデータ（返されるリストの2つめの要素）の割合または個数を指定できます。デフォルトはtest_size=0.25で25%がテストデータ、残りの75%が訓練データとなります。
今回は、test_size=0.2と指定したので、20%がテストデータ、80%が訓練データとなっています。
引数train_sizeで訓練用の割合・個数を指定することもでき、test_sizeと同様に、0.0 ~ 1.0の割合か、個数を指定します。

・シャッフルするかを指定: 引数shuffle
デフォルトでは要素がシャッフルされて分割されますが、引数shuffle=Falseとするとシャッフルされずに先頭から順番に分割されます。

・乱数シードを指定: 引数random_state
random_stateとはデータを分割する際にデータのランダムな行の順番を固定する引数です。
機械学習のモデルの性能を比較するような場合、どのように分割されるかによって結果が異なってしまうため、乱数シードを固定して常に同じように分割されるようにする必要があるので、引数random_state=0を設定します。
testデータが実行する度に異なると、ハイパーパラメータのチューニングが無意味になったり、複数モデルを作って多数決を取る際(アンサンブル法)に予測結果が異様に高い精度を出す現象を引き起こします。

次に、標準化を行います。

分析対象となるデータに偏りが多い場合、標準化を行う必要がありますが、今回は前処理で標準化を行っています。
「特徴量が取りうる値の範囲を変える・異なる特徴量同士の尺度を統一すること」を特徴量のスケーリングと言います。

そして標準化したデータをもとに、df_train・df_testの２つのデータフレームを作成します。

``` python:
# 訓練データとテストデータの標準化
X_train_std = (X_train - X_train.mean()) / X_train.std()
X_test_std = (X_test - X_train.mean()) / X_train.std()

# df_trainというテーブルを作りそこにindexを日付け、カラム名をpn値、終値にして出力
df_train = pd.DataFrame(
    {'pn': X_train_std,
     '終値': y_train},
    columns=['pn', '終値'],
    index=df_table.index[:len(X_train_std)])

# テストデータについても同様にdf_testというテーブルを作成
df_test = pd.DataFrame(
    {'pn': X_test_std,
     '終値': y_test},
    columns=['pn', '終値'],
    index=df_table.index[len(X_train_std):])
```

1日ごとの変化を可視化するために、訓練データの数だけPN値・ビットコイン価格の変化を算出し、時間軸を設定してPN値と株価の変化をプロットします。

``` python:
'''
PN値とビットコイン価格の変化をプロット
'''
# 日付を格納
exchange_dates = []

# 1日ごとのpn値の差分を格納する準備
pn_rates_diff = []

# 1日ごとの株価の差分を格納する準備
exchange_rates_diff = []

prev_pn = df_train['pn'][0] # typeはfloat
prev_exch = float(df_train['終値'][0])


# 訓練データの数だけPN値（極性値）・ビットコイン価格の変化を算出
for i in range(len(X_train_std)):
    time = df_train.index[i]   # 日付け
    pn_val = df_train['pn'][i]   # 訓練データのPN値
    exch_val = float(df_train['終値'][i])  # 訓練データのビットコイン価格の終値

    exchange_dates.append(time)  # 日付
    pn_rates_diff.append(pn_val - prev_pn)   # PN値（極性値）の変化
    exchange_rates_diff.append(exch_val - prev_exch)   # ビットコイン価格の変化
    
    # 前日のPN値（極性値）、終値を更新
    prev_pn = pn_val
    prev_exch = exch_val

# 時間軸を設定してPN値とビットコイン価格の変化をプロット
fig = plt.figure(figsize=(6, 8))
# サブプロットの設定
ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)
# PN値（極性値）の設定
x_val = exchange_dates
y_val = pn_rates_diff
ax1.set_title("PN Value (diff)")
ax1.plot(x_val,y_val)
ax1.grid(True)
plt.setp(ax1.get_xticklabels(), rotation=30)
# ビットコイン価格の終値の設定
y_val = exchange_rates_diff
ax2.set_title("Closing Value (diff)")
ax2.plot(x_val,y_val)
ax2.grid(True)
plt.setp(ax2.get_xticklabels(), rotation=30)
plt.show()
```

![6733ED02-E20B-4B72-B10F-87BC5C02F036.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/07a87255-9300-7f95-1ffe-1cc11911b754.png)


# 6. 特徴量の作成とラベリング

①2日間ごとのPN値とビットコイン価格の変化データ(訓練データ)の作成
②2日間ごとのPN値とビットコイン価格の変化データ(テストデータ)の作成
をしていきます。

①2日間ごとのPN値とビットコイン価格の変化データ(訓練データ)の作成をします。
作成した訓練データに対して直近2日間の変化量（pn値、ビットコイン価格）を特徴量とし、i日目のビットコイン価格がプラスなら１、マイナスなら０とラベリングします。

``` python:

'''
2日間ごとのPN値とビットコインの変化を表示
'''
INPUT_LEN = 2
data_len = len(pn_rates_diff)

# 説明変数を格納する準備
tr_input_mat = []
# 目的変数を格納する準備
tr_angle_mat = []

# 直近2日間なので、INPUT_LENから開始
for i in range(INPUT_LEN, data_len):
    tmp_arr = []
    # i日目の直近2日間の株価とネガポジの変化を格納
    for j in range(INPUT_LEN):
        tmp_arr.append(exchange_rates_diff[i-INPUT_LEN+j])
        tmp_arr.append(pn_rates_diff[i-INPUT_LEN+j])   
    tr_input_mat.append(tmp_arr)

    # i日目のビットコイン価格の上下（プラスなら1、マイナスなら0）を目的変数に格納する
    if exchange_rates_diff[i] >= 0:
        tr_angle_mat.append(1)      # プラスの場合の処理
    else:
        tr_angle_mat.append(0)      # マイナスの場合の処理

# numpy配列に変換して結果を代入
train_feature_arr = np.array(tr_input_mat)
train_label_arr = np.array(tr_angle_mat)

# train_feature_arr, train_label_arrを出力して、2日間ごとのビットコインとPN値の変化を把握します。
# train_feature_arrは直近２日間のビットコイン価格とPN値の変化
print(train_feature_arr)
# train_label_arrはビットコインの上下（プラスなら1、マイナスなら0）
print(train_label_arr)

```

・1日ごとのPN値とビットコイン価格の差分を取得

・2日間ごとのPN値とビットコイン価格の変化データの取得結果(一部)
![45D69218-D321-4E0B-A71B-B33DE3154683.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/9bfb424d-c2bf-c11e-00fe-81526ee01d4d.png)

・ビットコイン価格の上下ラベル作成
![62B9DA4C-7A8D-4184-B86B-D7FC27F868B5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/621cacbb-59e9-d6af-01fd-2ddb09c4eb3d.png)


テストデータも訓練データと同様にして、
②2日間ごとのPN値とビットコイン価格の変化データ(テストデータ)の作成をします。


# 7. 予測モデルの作成と予測精度の検証

``` python:

from typing import Text
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
import glob
from datetime import datetime,timedelta
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
```
予測モデルの作成にあたり、ロジスティック回帰・ランダムフォレスト・SVCを用います。
fit関数を使って、モデルを構築しモデルの予測精度を出力します。

``` python:

# train_feature_arr, train_label_arr,test_feature_arr, test_label_arrを特徴量にして、予測モデル(ロジスティック回帰、SVM、ランダムフォレスト)を構築し予測精度を計測してください。
for model in [LogisticRegression(), RandomForestClassifier(n_estimators=200, max_depth=8, random_state=0), SVC()]:
    # fit関数を使って、モデルを構築
    model.fit(train_feature_arr, train_label_arr)

    print("--Method:", model.__class__.__name__, "--")
    # モデルの予測精度を出力
    print("Cross validatin scores:{}".format(model.score(test_feature_arr,test_label_arr)))
```
![94486C2A-8359-4A72-98B0-E424B4C6B5B3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661028/cc855658-2306-e0c4-12bb-8baa7cdcdb64.png)

結果は、冒頭でも記載した通り、ロジスティック回帰・ランダムフォレストの数値が一番大きくなったが、0.5以下となっていて予測精度は高くないため、ツイートの感情とビットコイン価格の相関関係は低い結果となりました。
予測精度を高くするためには、他にも特徴量を増やしてみる必要がありそうです。

# 8. おわりに

アイデミー様のデータ分析コースを受講し、Pythonの基礎から機械学習（教師なし学習・教師あり学習）やディープラーニングなどを学び、未経験からでも感情分析ができるようになりました。
受講終了後は、この講座で培ったPythonの知識を生かしてマーケティング領域の仕事に携わっていきたいと考えています。

文系でプログラミング未経験でも機械学習を学んでみたいという方は、是非アイデミー様で学んでみてください。

最後に、稚拙な質問にも丁寧に回答して頂きましたチューターの先生の皆様、スタッフの方々に感謝申し上げます。