概要
- 以前 紹介した
streamR
を利用して, 選挙前11日分のツイートを取得していた. - しかし活用方法が分からないので簡単な集計結果のみ公開
ggplot2
,dplyr
などを使ってグラフにまとめる例を示す
streamR でツイートを集める
1時間単位でツイートを収集し, tweet_201412DD_HHMM.json
というファイル名で保存するコードを書いた. HHMM は収集開始時刻である. これをバックグラウンドで流していた. streamR
は Twitter API にアクセスしてツイートを取得するだけなので, 負荷は小さい. また, ダウンロードしたデータをそのままファイルに書き込むため, メモリを圧迫することもなく, 容量の十分にあるディスクに保存するように設定しておけば問題ない.
以下が, 実際に使用したコードである. ただし, 実際に使用する場合は, ROauth
オブジェクトの用意と, パスの適宜変更が必要である. ROauth
オブジェクトの作り方は, 前回を参照.
######################################## # streamR.auto.R # fetch tweets about voting automatically ####################################### library(twitteR) library(streamR) Sys.setlocale("LC_TIME", "en_US.utf-8") load("source/twit.oauth.RData") # ROauth オブジェクト読み込み registerTwitterOAuth(twit.oauth) setwd("/media/HOGEHOGE") # 保存先ディレクトリ名 times <- 24 # 何時間取得するか for (i in 1:times ){ print( paste("Start at", Sys.time(), paste(i, "/", times, sep=""), sep=" ")) filterStream(file.name = paste("tweet", format(x = Sys.time(), format = "%Y%m%d_%H%M"), ".json", sep=""), track ="選挙, 衆院選, 党" , timeout=3600, oauth=twit.oauth, language = "ja") print(paste("Done at", Sys.time(), sep=" ") ) }
上記のコードでは, 24回繰り返し取得するため, 1回実行すればまる1日ぶんのツイートを取得できる. ここでは, 「選挙」「衆院選」「党」のいずれかの語を含むツイートのみ抽出した. 自然言語処理には詳しくないので, 他にもっと良い方法があったかもしれないが, 思いつかなかったのでこうした.
この streamR.auto.R を, ターミナルを開いてRのバッチモードで以下のように実行した.
R --no-save < .../streamR.auto.R
ただし, ...
には適当なパスを入れている. --no-save
は終了時にワークスペースを保存しない, というオプションで, バッチモードで 実行する際にはこれか, --save
, --vanilla
のいずれかを必ず指定しなければならない.
なお, これは UNIXの場合で, Windows ではこの方法ではできないようだ. 未確認だが, windowsからバッチ処理でRを利用する方法 に Windows でバッチモードを実行する方法が書かれており, Rterm.exe
を実行すればいいらしい.
さて, 自分はPCを1つしか持ってないので, 24時間ごとにこれを手動で実行していたが, 漏れ無く取得したければ, times
を増やしたり, シェルの方でループさせるようにするなど変更すればいい.
データの概観
このようにして, 12月3日から12月13日までのツイートを取得した.
まず, R にデータを読み込み, 集計する前の前処理について. 集計したツイートは .json ファイルとして保存しておいたので, これを parseTweets()
関数で順に読み込んでいった. この関数で, .json ファイルをデータフレーム形式に変換できる. データの中には, ツイートの投稿時刻が created_at
という変数で含まれている. 読み込んだ直後は単なる文字列なので, POSIXct
オブジェクトに変換する.
Sys.setlocale(category = "LC_TIME", locale = "en_US.UTF-8") df$POSIXt <- as.POSIXlt(df$created_at, format="%a %b %d %H:%M:%S %z %Y", tz = "Asia/Tokyo") df$Date <- as.Date(df$POSIXt) df$wkday <- weekdays(df$Date, abbreviate = T) df$weekend <- ifelse(df$wkday %in% c("Sat", "Sun"), "土日", "平日")
created_at
は, "Sun Dec 14 00:00:00 +0000 2014" のように, 曜日が英語で書かれているが, as.POSIClt()
関数*1が判断できる曜日や月の文字は, システムロケールに依存する. これを読んでいる人間の多くは日本語になってるだろうから, このままでは読み込めず, 欠損扱いになる. よって, Sys.setlocale()
で英語に設定しなおしている. また, この時刻は全て UTC で記録されている*2.
これで時刻を得られたので, そこから日付と曜日, 土日判定の変数も作成している (ただし土日判定は, 今回は結局使用しなかった).
次に, ツイートが RT かどうかを判定する変数を作成する. retweeted
という変数が, そのツイートがリツイートかどうかのフラグに見えるが, 公式によると, "Indicates whether this Tweet has been retweeted by the authenticating user." つまり, 認証済ユーザにリツイートされたかどうか, という情報らしい. 実際, 17万件以上あるレコードで, これが TRUE であるツイートは1つも見つからなかった. そこで, リツイート回数を表す retweet_count
からリツイートかどうかを判定する変数を作成した.
df$is.RT <- df$retweet_count > 0
このようにして加工したデータフレームを元に, 「日付ごとのツイート数」「日付ごとのツイートしたユーザのユニーク数」「24時間のツイート数の推移」を作成した. 作成には, dplyr
, ggplot2
, scales
, RColorBrewer
のパッケージを使用した.
表1
表2
表3
表1から, 選挙日が近づくにつれてツイート数が増加していることが分かる. ユニークユーザ数を表して表2と比べても, スパムのように同一のアカウントが大量に投稿しているために発生している現象ではないことが分かる*3. また, 通常のツイートよりも, RT 数のの伸びが大きいことも分かる.
最後に表3だが, 深夜から明け方にかけてのツイートが1日で最も少ないという結果になった. 【分析】日本人らしさが判明! Twitterの曜日別・時間帯別のツイート数の統計 by Twitter4J その2 による時間帯別の日本語ツイート数の分布からも逸脱していない*4.
結論
というわけで, 入手できたデータは今のところ特におかしな点がみられない, ということが分かった. つまり特に新発見はなかった. 自然言語処理とかテキストマイニングとかそういう知識があまりないので, そのへんを調べつつなんとかこのデータを料理できないかこれから試行錯誤する.
表作成に使用したパッケージの解説
dplyr の使用例
dplyr
は便利な集計用パッケージである. 例として, 日付ごとに RT のみのユーザーを数える処理を紹介する. つまり, 1. ツイート全体からRTのみを抜き出し, 2. 日付ごとにグループ化し, 3. グループ化ごとに, ユーザID の重複を削除 (ユニーク化)する. これを dplyr でおこなうと,
df.uu.RT <- select(.data = df, user_id_str, Date, is.RT ) %>% filter(is.RT ==T ) %>% group_by(Date) %>% summarise(user_id=n_distinct(user_id_str))
だけで完了できる. しかも plyr
で同様の加工をおこなう場合より高速であるという (dplyr最強伝説). 特徴として, 処理をUNIXシェルのパイプのように, %>%
でつなげることができる. 上記の場合, まず, select()
で 日付, ユーザーID, RT判定フラグ, 週末フラグだけを抜き出して, filter()
で RT のみ抽出, group_by()
で日付, RT判定でグループ化, そして summarise()
内で n_distinct()
関数を用いてグループごとにユーザーIDをユニークにしている. SQL でいうなら,
select Date, is.RT, DISTINCT user_id_str as user_id from df where is_RT = TRUE group by Date;
といったところだろうか. 実際, dplyr を使ったコードは SQL と直感的に類似していて, とっつきやすさがある.
なお, ユーザーID はuser_id_str
であり, id_str
はツイート固有のIDであるので注意が必要だ.
ggplot2 と scales
今回利用した ggplot2 の機能は, ほとんどが以前書いた[R] 都知事選挙を題材に学ぶ ggplot2 の作例で書いたことなので, 多くは説明しないが, あらたに付け加えることとして, scales
パッケージのりようが挙げられる. このパッケージは, ggplot でグラフの体裁, 特にスケールを整える際に役立つ. たとえば, 今回はグラフの縦軸の数値に, 3桁刻みのコンマを入れたが, これは scales パッケージの comma_format()
関数を利用している. 横軸は日付だが, これを MM/DD (曜日) と表したいなら, date_format("%m/%d (%a)")
とすればよい. 前回は scale_*()
関数の labels=
に, 手入力の目盛りのベクトルを代入したり, 自作の関数を与えたりしていたが, そのような作業の多くはこのパッケージの関数で代用できる.
使用したコード全体像
以下には, 今回は掲載しなかった, 各日付ごとの, 時間別ツイート数の推移のグラフ, 期間中のユーザー毎のツイート数の分布を作成するコードも含まれている.
################################################################### # Vote analysis on the House of Representatives of JPN at 14DEC2014 # # ill-identified, 19DEC2014 ################################################################### library(streamR) # ver. 0.2.1 setwd("/media/ks/BackUpHDD/TEMP") # ツイートを保存しておいた外付けHDD # 時刻読み込みのため時刻のロケール変更 # 日本語ロケールの状態で英語の曜日を読み込むオプションはない? Sys.setlocale(category = "LC_TIME", locale = "en_US.UTF-8") # Ubuntu用. Windows では locale の値が異なる # ディレクトリには .json ファイルしかない. flist <- list.files(include.dirs = T ) temp <- parseTweets(flist[1]) for (i in flist[-1]){ print(i) temp <- rbind(temp, parseTweets(i)) } df <- temp; rm(temp) unique(df$time_zone) # 海外タイムゾーンが結構ある sort(table(df$time_zone), decreasing = T) sort(table(df$country), decreasing = T) # データの仕様は https://dev.twitter.com/overview/api/tweets を参照. # "created_at" (投稿時間) は UTC df$POSIXt <- as.POSIXlt(df$created_at, format="%a %b %d %H:%M:%S %z %Y", tz = "Asia/Tokyo") df$Date <- as.Date(df$POSIXt) # rewteeted が認証済ユーザでしか判定されないらしいのでRT回数を使ってフラグを作り直す df$is.RT <- df$retweet_count > 0 # 一旦保存 save(df, file = "tweet_df.RData") load(file = "tweet_df.RData") # longitude, latitude を numeric に変換 df$lon <- as.numeric(df$lon); df$lat <- as.numeric(df$lat) # しかし位置情報を公開してるのは数件しか無い summary(df[,c("lon","lat")] ) # 2日深夜から開始して中途半端なので, 3日以降 13日までのみ集計対象に. 14日も 0:30で停止 df <- df[as.Date("2014-12-03") <= df$Date & df$Date <= as.Date("2014-12-13"),] library(ggplot2) # ver. 1.0.0 library(scales) # ver. 0.2.4 library(RColorBrewer) # ver. 1.1-2 library(dplyr) # ver. 0.3.0.2 # 表示のため, ロケールを日本語に戻す Sys.setlocale(category = "LC_TIME", "ja_JP.UTF-8") # Table 1: count num of tweets per date # 日付ごとの行数をカウント df.tw <- select(.data= df, Date, is.RT) %>% group_by(Date, is.RT) %>% summarise(count=n()) ggplot(data=df.tw) + geom_bar(aes(x=Date, y=count, fill=is.RT, group=is.RT), stat="identity", binwidth=1) + theme_bw() + labs(title="日毎のツイート数", x="日付", y="ツイート数") + theme(axis.title.y = element_text(angle = 0, vjust = 1), axis.text.x=element_text(angle = -45, hjust = -.1, vjust = 1), legend.title=element_blank(), legend.position= "bottom") + scale_fill_brewer(palette = "Dark2", labels = c("通常tw (非公式RT含む)", "RT")) + scale_y_continuous(labels= comma_format()) + scale_x_date(labels = date_format("%m/%d (%a)"), breaks = date_breaks("1 day")) # Table 2: count num of unique user per date # 日付ごとに id を一意にしてカウント. RT しかしないユーザも集計 df.uu <- select(.data=df, user_id_str, Date ) %>% group_by(Date) %>% summarise(user_id=n_distinct(user_id_str)) df.uu$RT <- F df.uu.RT <- select(.data = df, user_id_str, Date, is.RT ) %>% filter(is.RT ==T ) %>% group_by(Date) %>% summarise(user_id=n_distinct(user_id_str)) df.uu.RT$RT <- T df.uu <- rbind(df.uu, df.uu.RT) ggplot(data=df.uu) + geom_bar(aes(x=Date, y=user_id, group=RT, fill=RT, group=RT), position="dodge", stat="identity") + theme_bw() + labs(title="日毎に最低1回ツイートしたユーザ数", x="日付", y="ユーザ数\n(人)") + theme(axis.title.y = element_text(angle = 0, vjust = 1), axis.text.x=element_text(angle = -45, hjust = -.1, vjust = 1), legend.title=element_blank(), legend.position= "bottom") + scale_fill_brewer(palette = "Dark2", labels = c("全体", "RTのみ")) + scale_y_continuous(labels= comma_format()) + scale_x_date( labels =date_format("%m/%d (%a)"), breaks= date_breaks("1 day") ) # Table 3: num of tweet per hour df.hour <- select(.data=df, POSIXt, Date, is.RT) df.hour$hour <- format(df.hour$POSIXt, format = "%H") df.hour$POSIXt <- NULL # dplyr は POSIXlt 型を扱えないので削除 df.hour.2 <- group_by(.data=df.hour, hour, is.RT) %>% summarise(count=n()) df.hour.3 <- group_by(.data=df.hour, Date, hour, is.RT) %>% summarise(count=n()) ggplot(df.hour.2) + geom_histogram(aes(x=hour, y=count, fill =is.RT, group=is.RT ), stat="identity") + theme_bw() + labs(title = "時刻毎の分布", x="時間", y="総ツイート数") + theme(axis.title.y = element_text(angle = 0, vjust = 1), axis.text.x=element_text(angle = -45, hjust = -.1, vjust = 1), legend.title=element_blank(), legend.position= "bottom") + scale_fill_brewer(palette = "Dark2", labels = c("通常tw (非公式RT含む)", "RT")) for (i in seq.Date(from = as.Date("2014-12-02"), to = as.Date("2014-12-13"), by="day")){ print( ggplot(df.hour.3[df.hour.3$Date==i,]) + geom_histogram(aes(x=hour, y=count, group=is.RT, fill=is.RT), stat="identity") +labs(title=as.Date(i, origin=as.Date("1970-01-01") ) ) ) } # Table 4A: count num of tweets per account # ユーザごとのツイート数をカウントし, ツイート数で度数集計 df.uu.tw <- select(.data = df, is.RT, user_id_str) %>% group_by(is.RT, user_id_str) %>% summarise(count = n()) ggplot(df.uu.tw) + geom_histogram(aes(x=count, group=is.RT, fill=is.RT), stat="bin", binwidth=5) + theme_bw() + theme(axis.title.y = element_text(angle = 0, vjust = 1), axis.text.x=element_text(angle = -45, hjust = -.1, vjust = 1), legend.title=element_blank(), legend.position= "bottom") + scale_fill_brewer(palette = "Dark2", labels = c("通常tw (RT含む)", "RT") ) + labs(x="ツイート数", y = "人数 (人)", title="ユーザ毎のツイート数の分布") # 度数表 table(df.uu.tw$count) # Table 4B: trimmed qt <- quantile(df.uu.tw$count, prob=c(.01, .99)) ggplot(df.uu.tw[qt[1] <= df.uu.tw$count & df.uu.tw$count <= qt[2],]) + geom_histogram(aes(x=count, fill=is.RT, group=is.RT), binwidth=5 ) + theme_bw() + theme(axis.title.y = element_text(angle = 0, vjust = 1), axis.text.x=element_text(angle = -45, hjust = -.1, vjust = 1), legend.title=element_blank(), legend.position= "bottom") + scale_fill_brewer(palette = "Dark2", labels = c("通常tw (RT含む)", "RT") ) + labs(x="ツイート数", y = "人数 (人)", title="ユーザ毎のツイート数の分布 (両側1%トリム)") # 度数表 table(df.uu.tw$count[qt[1] <= df.uu.tw$count & df.uu.tw$count <= qt[2]]) # count num of tweets per account, day df.tw.uu.day <- select(.data=df, Date, is.RT, user_id_str) %>% group_by(Date, is.RT, user_id_str) %>% summarise(tw.uu.day = n() ) %>% group_by(Date, is.RT, tw.uu.day) %>% summarise( count=n() ) for (i in seq.Date(from = as.Date("2014-12-02"), to = as.Date("2014-12-13"), by="day") ){ print(ggplot(df.tw.uu.day[df.tw.uu.day$Date==i,]) + geom_histogram(aes(x=tw.uu.day, y=log(count, 2), fill=is.RT, group= is.RT ), stat="identity") + labs(title=as.Date(i, origin = as.Date("1970-01-01") ) ) ) } # To Be Continued...
参考文献
- てぃー (2014) 『【分析】日本人らしさが判明! Twitterの曜日別・時間帯別のツイート数の統計 by Twitter4J その2』 http://teapipin.blog10.fc2.com/blog-entry-290.html
- dichika (2014) 『dplyr最強伝説』 東京で尻を洗う http://d.hatena.ne.jp/dichika/20140103/p1
- teramonagi (2009) 『windowsからバッチ処理でRを利用する方法』 My Life as a Mock Quant http://d.hatena.ne.jp/teramonagi/20091126/1259244903
- ill-identified (2014a) 『[R] 都知事選挙を題材に学ぶ ggplot2 の作例』 http://ill-identified.hatenablog.com/entry/2014/02/22/231809
- --- (2014b) 『[R] [Twitter] streamR パッケージで Twitter Streaming API にアクセスしてみた』 http://ill-identified.hatenablog.com/entry/2014/01/27/220607
R 関連
- Pablo Barbera (2014). streamR: Access to Twitter Streaming API via R. R package version 0.2.1. http://CRAN.R-project.org/package=streamR
- Wickham. ggplot2: elegant graphics for data analysis. Springer New York, 2009.
- Hadley Wickham (2014). scales: Scale functions for graphics.. R package version 0.2.4. http://CRAN.R-project.org/package=scales
- Erich Neuwirth (2014). RColorBrewer: ColorBrewer Palettes. R package version 1.1-2. http://CRAN.R-project.org/package=RColorBrewer
- Hadley Wickham and Romain Francois (2014). dplyr: A Grammar of Data Manipulation. R package version 0.3.0.2. http://CRAN.R-project.org/package=dplyr
*1:後で気づいたのだが, この後で使う dplyr パッケージの関数は POSIXlt オブジェクトを扱えない. よって, 実際は後の処理を考えると as.POSIXct() を使う方が便利である
*2: 公式: https://dev.twitter.com/overview/api/tweets より
*3:今回は掲載しなかったが, ユーザごとのツイート数の分布も確認している
*4:こちらのツイートはつぶやき内容に関係なく全体の1%のサンプリングである.