はじめに
2020年も遂に大晦日がやってきました。
年の瀬に記事を投稿したいと思います。
最後の記事は自分が楽しめる題材にしようということで、
最近ハマりにハマっている、チェンソーマンのツイートを分析しました!!
最終回とかアニメ化とか色々あったので分析したら面白そうでは?と思いました
(アニメ化&2部、おめでとうございます!!!🎉🎉🎉)
ほんとに色々と初心者レベルなのでミスも目立ちましたので、優しい心で閲覧していただけるとありがたいです笑
コメント、ウェルカムです!ぜひともお願いします!👍
作業内容の概要
1. DB作成
- 毎日およそ20000~30000ツイートを収集。今回は12月9日から12月29日までのツイートを定期収集
- 収集したツイートの結合。データフレーム形式
- ツイートの修正
- 修正後のデータフレームをPostgreSQL上に保存
2. 分析(メイン)
- 総ツイート数、いいね数、リツイート数
- いいね、リツイートの各々の数が最も多いツイート
- 日別ツイート数
- 高頻度の単語上位30個表示
1. DB作成
ツイート収集
こちらの神記事を参考(というより同じ...)に、大量のツイートを収集しました。
検索キーワードはチェンソーマン exclude:retweets
です。
収集状況は、18日までは1時間に2000ツイート収集、19日以降は12時間におよそ20000ツイート収集しています。
色々手探りだったのもあり、途中データが欠損だったりしました...
ツイート結合
収集したツイートデータを結合します。
ツイートの修正
ノイズになりそうな単語(主にメンション用の@)の除去を行いました。
ノイズ除去は、TwitterAPIでツイートと一緒に取得されるentities
カラムやdisplay_text_range
カラムを活用
############## 抜粋! ##############
array = []
# entitiesから@~~やURLやハッシュタグの情報を抽出して、arrayに格納
# (URLとハッシュタグは`display_text_range`で除去できるけど、このときは気付かなかった)
for l in range(len(df_c['entities'][num]["hashtags"])):
array.append("#"+df_c['entities'][num]["hashtags"][l]["text"])
array.append("#"+df_c['entities'][num]["hashtags"][l]["text"])
for l in range(len(df_c['entities'][num]["urls"])):
array.append(df_c['entities'][num]["urls"][l]["url"])
スクリプト
全部まとめて載せてます。後日、綺麗にまとめます。
全部みる
############ ツイート収集 ############
def job():
"""
main内で繰り返すプログラム
"""
NOW = datetime.datetime.now()
print("start!", NOW)
# 検索キーワード リツイートを除く
keyword = "チェンソーマン exclude:retweets"
# 保存ディレクトリ 先に作っておく
DIR = 'chainsaw/'
Consumer_key = 'your info'
Consumer_secret = 'your info'
Access_token = 'your info'
Access_secret = 'your info'
url = "https://api.twitter.com/1.1/search/tweets.json"
twitter = OAuth1Session(Consumer_key, Consumer_secret, Access_token, Access_secret)
# 収集に使うパラメタ
max_id = -1
count = 100
params = {'q' : keyword, 'count' : count, 'max_id' : max_id, 'lang' : 'ja', 'tweet_mode' : 'extended'}
df_out = pd.DataFrame()
while(True):
if max_id != -1: # ツイートidを既に格納したツイートよりも遡る
params['max_id'] = max_id - 1
req = twitter.get(url, params = params)
if req.status_code == 200: # 正常に取得できていれば
search_timeline = json.loads(req.text)
if search_timeline['statuses'] == []: # 全ツイートを取り終えたら
print("break", datetime.datetime.now())
break
for tweet in search_timeline['statuses']:
# DFへ格納
df = pd.DataFrame(tweet.values(), index=tweet.keys()).T
df["created_at"] = pd.to_datetime(df['created_at']) + datetime.timedelta(hours=9)
df["created_at"] = datetime.datetime.strftime(df["created_at"][0], "%Y/%m/%d %a %H:%M:%S")
df_out = df_out.append(df)
max_id = search_timeline['statuses'][-1]['id']
else: # アクセス頻度制限に引っかかった場合15分待つ
print("Total", df_out.shape[0], "tweets were extracted", sep=" ")
print("break", datetime.datetime.now())
break
df_out = df_out.reset_index(drop=True)
thistime = datetime.datetime.strftime(NOW, '%Y-%m-%d-%H-%M-%S')
nexttime = NOW+datetime.timedelta(hours=3)
nexttime = datetime.datetime.strftime(nexttime, '%Y-%m-%d-%H-%M-%S')
df_out.to_csv(DIR + thistime + keyword[:8] +".csv", index=False, encoding="utf-8-sig")
print(thistime, "Total", df_out.shape, "tweets were extracted!! next start at", nexttime)
def main():
print("12時間おきに実行、1秒停止という間隔でデータを収集!")
schedule.every(12).hours.at("06:00").do(job)
while True:
schedule.run_pending()
sleep(1)
if __name__ == '__main__':
main()
############# 結合! ############
PATH1 = glob.glob('your tweet data path')
df_chain = pd.DataFrame()
for i, path in enumerate(tqdm(PATH1)):
try:
chain1 = pd.read_csv(PATH1[i])
df_chain = pd.concat([df_chain, chain1])
if df_chain.duplicated().sum() != 0:
print("ファイル名", path, "重複: ",df_chain.duplicated().sum())
df_chain.drop_duplicates(keep='first', inplace = True)
except EmptyDataError as e: # インプット(収集したツイートが記載されたファイル)が空の場合に発生するエラーをスキップ
print("エラーキャッチ", e, "ファイル名", path)
df_chain.to_csv("save name", index=False, encoding="utf-8-sig") # csvとして保存
engine=create_engine("postgresql://[your username]@localhost:5432/chainsaw") # postgres上にDBをアップロード
df_chain.to_sql('chainsaw_20201229', con=engine, if_exists='replace', index=False)
############# テキスト修正と保存! ############
def make_fixed_text(df):
df_c = df[["created_at","full_text","display_text_range","entities","retweet_count","favorite_count"]].copy()
df_c["created_at"] = pd.to_datetime(df_c["created_at"])
df_c["display_text_range"] = df_c["display_text_range"].apply(eval) # カラムの型がtextなら実行。それ以外は不要。
df_c['entities'] = df_c['entities'].apply(eval) # カラムの型がtextなら実行。それ以外は不要。
LEN = len(df_c)
df_c.loc[:,"delete_text"]="" # 除去用のカラム
df_c["fixed_text"]=df_c["full_text"].copy() # 修正後のテキスト格納用のカラム
for num in range(LEN):
array = []
# hashtags
for l in range(len(df_c['entities'][num]["hashtags"])):
array.append("#"+df_c['entities'][num]["hashtags"][l]["text"])
array.append("#"+df_c['entities'][num]["hashtags"][l]["text"])
for l in range(len(df_c['entities'][num]["urls"])):
array.append(df_c['entities'][num]["urls"][l]["url"])
if array != []:
df_c["delete_text"][num]=array
for i in range(len(df_c)):
words = df_c["full_text"][i][df_c["display_text_range"][i][0]:df_c["display_text_range"][i][1]]
if (len(df_c["delete_text"][i]) > 0):
words_re = re.compile("|".join(re.escape(w) for w in df_c["delete_text"][i]))
words_fix = re.sub(words_re, '', words)
df_c["fixed_text"][i] = words_fix
else:
df_c["fixed_text"][i] = words
df_chain = pd.read_sql('chainsaw_20201229', con=engine)
df_chain2 = make_fixed_text(df_chain)
df_chain2.to_sql('chainsaw_20201229_fixed_text', con=engine, if_exists='replace', index=False, dtype={'entities': sqlalchemy.types.JSON})
2. 分析(メイン)
DB概要
まずはDBの概要です!📖📖📖
-
ツイート取得期間:12月9日~29日
- 色々あって取得ツイート数は一定ではない。プログラムの実行し忘れ等でデータの欠損もあり。信頼性は低いほうですが目を瞑ります...お許しを...
-
shape: 671084 rows x 8 cols
-
size: 352Mb
-
colsについて
- created_at: APIで取得可能。ツイートの投稿時刻
- full_text: APIで取得可能。ツイート文
- display_text_range: APIで取得可能。ハッシュタグやurlを除いた、ツイート本文の範囲
- entities: APIで取得可能。ツイートの属性。(display_text_rangeあればノイズの除去できたので、不要説濃厚)
- retweet_count: APIで取得可能。リツイート数
- favorite_count: APIで取得可能。いいね数
- delete_text: 自作。ノイズ除去対象の文章
- fixed_text: 自作。修正後のツイート文
以上です。
それでは、本題の分析をしていきましょう!👉👉👉👉👉👉
総ツイート数、いいね数、リツイート数
def calc_num(df):
num_tweet = len(df)
num_favorite = df["favorite_count"].sum()
num_retweet = df["retweet_count"].sum()
print("総ツイート数", num_tweet)
print("総いいね数", num_favorite)
print("総リツイート数", num_retweet)
結果は以下のとおりです
総ツイート数 671084
総いいね数 5757392
総リツイート数 1033805
いいね、リツイートの各々の数が最も多いツイート
def show_tweet_with_specific_conditions(df):
max_favorite = df["favorite_count"].max()
max_retweet = df["retweet_count"].max()
print("最大いいね数", max_favorite, "ツイート", df["fixed_text"][df["favorite_count"]==max_favorite])
print("最大リツイート数", max_retweet, "text", df["fixed_text"][df["retweet_count"]==max_retweet])
結果は以下のとおりです
最大いいね数 56186 ツイート 'ジャンプフェスタの番組ご視聴頂き有難うございました。\n\n『チェンソーマン』10巻の書影はこちらになります。\n\n1月4日発売予定でございます❗️\n\n是非に、皆様ご予約をお願い出来たら、幸いです‼️ https://t.co/m4iOxwmEHP'
最大リツイート数 15676 ツイート 'ジャンプフェスタの番組ご視聴頂き有難うございました。\n\n『チェンソーマン』10巻の書影はこちらになります。\n\n1月4日発売予定でございます❗️\n\n是非に、皆様ご予約をお願い出来たら、幸いです‼️ https://t.co/m4iOxwmEHP'
いいね数とリツイート数大賞は同ツイートになりました笑
ん?このツイート、見たことある気がします........
これだーー!!!
担当者様のツイートがいいね数とリツイート数共に一位でございました!おめでとうございます!!🎉
マキマさん表紙、エロコワカッコイイ..
日別ツイート数
def visualize_tweet_per_day(df, savename):
mini_day = df.groupby(pd.Grouper(key="created_at", freq="D"))
l_day = []
num_day = []
for i, df_mini in mini_day:
l_day.append(i.date())
num_day.append(len(df_mini))
plt.figure(figsize=(20, 10))
plt.tick_params(labelsize=15)
sns.barplot(x=l_day,y = num_day,color='pink')
plt.xticks(rotation=15)
plt.title('Number of tweets per day',size=20)
plt.savefig('./日別ツイート数_'+savename+'.png', bbox_inches='tight')
plt.show()
最初の方では、12月14日でツイート数が40000超えとかなり多いですね。
この日、チェンソーマン最終回&重大発表の日です。💣
ビッグイベントがあったので、前後の日と比較して多いんですねぇ
14日以降は17日~21日まで40000超えです。
一番多い19日に何があったかというと、**ジェンプフェスタの日です。**🎇🎇
タツキ先生ご出演&10巻発表など新規情報盛り沢山の日でしたのでツイート数が一気に伸びたようです。ちなみに上記写真のツイートも19日です。
ただ、私のツイート収集方法が途中で変わったり、重複削除の方法が少し間違ってたり(「課題」参照)したので、結果は怪しいところもあり、です。
頻出単語の上位30個を表示
def show_frequent_words(df, savename):
# 一つの長文にする
all_text = ""
for w in df.fixed_text:
all_text = all_text + w
# 「チェンソーマン」除外
at2 = all_text.replace("チェンソーマン","")
# 形態素解析(名詞&固有名詞)
m = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
node = m.parseToNode(at2)
words=[]
while node:
hinshi1 = node.feature.split(",")[0]
hinshi2 = node.feature.split(",")[1]
if (hinshi1 in ["名詞"]) and (hinshi2 in ["固有名詞"]):
origin = node.feature.split(",")[6]
words.append(origin)
node = node.next
# 頻出単語の算出
c = collections.Counter(words)
sns.set(context="talk")
fig = plt.subplots(figsize=(15, 15))
sns.countplot(y=words,order=[i[0] for i in c.most_common(30)])
plt.savefig('./頻出単語_'+savename+'.png', bbox_inches='tight')
plt.show()
頻出単語の上位30個をグラフ化しました
眺めてみると結構面白いですねぇ😲
いくつかノイズが入っていますが(「*」や「???」)、ほかはまともな単語では?!
ノイズを除いてみると、**上位に「最終回、マキマ、デンジ、生姜焼き」等の最終局面のキーワードがズラリと並んでいますね笑
他には「アニメ化、MAPPA、重大発表、1位、2部」**といった最近話題になったイベントのキーワードも散見されますね
結構、他の作品名も上位に並んでいて興味深かったです。(呪術廻戦とかドロヘドロとかはアニメ関連で出てきたっぽいですね、あと鬼滅の刃も上位。)
個人的には「レゼ」がランキング入りしていて嬉しかったです。チェンソーマンキャラクター内では4位なのでやっぱり人気なんですねぇ。頼む2部で再登場してくれ...!!!🙏
※検索キーワードは「チェンソーマン」にしているため、**本来の頻出単語一位は「チェンソーマン」です。**ただそれだと当然の結果過ぎてなんとも微妙なので今回は省きました。
課題
たっくさんありますが、今回は3つ(たいして考えていない)に絞って紹介します
-
ツイート収集方法
一週間分のテキストを一括取得したいです。
前は一週間分のテキストデータを取れたと思うのですが、うまくいかないです。
今はmax20000ツイートしか取れないです。 -
データベース作成時の重複処理
はい、普通にしくってました。
今回は、とりあえずテキストデータをがっちゃんこしていって、その後にデータフレーム上で全カラムで重複する行を消すという方法だったのですが、
取得時間によって、テキストは同じでもいいね数やリツイート数が違うということをすっかり忘れていました。
つまり、テキストの重複が発生していました。 -
ストップワードの除去
頻出単語の上位に*とか???というのはいかんですね笑
なんかいい感じに除去してくれる方法ないかなぁ
感想
自分なりに何がやりたいか考えてから作業することが一番ですね。
結構夢中になって作業できました。
来年もまだまだコロナの影響は続きますが、
たまには息抜きで娯楽を楽しみ、来年も頑張っていきましょう!
未来最高!未来最高!(アニメ化&2部&10巻、楽しみなんじゃあ...)
ではでは👏
2021年もよろしくお願いします!
おまけ
後日更新予定