みなさん、こんにちは!!
こちらは「ABEJAアドベントカレンダー2020」の2日目の記事です。
ABEJAではデータサイエンティストをしているので、最近話題になっていたtwitterのデータを扱って遊んでみました。
なお、仕事の内容とは一切関係ありません!(キリッ
きっかけ
ちょうど先週末にTwitterでこんなツイートがあり、トレンドに上がっていました。
「思い出のアニメ」を3つ選ぶなら
— Netflix Japan Anime (@NetflixJP_Anime) November 28, 2020
あなたはどのアニメを選びますか——?
1⃣ @NetflixJP_Animeをフォロー
2⃣ #わたしのアニメ歴3選 で
ツイートすると…
おすすめアニメをリプライでお届け💌
名作・新作アニメが登場する
#ネトフリ 特別アニメーションも公開中✨#ネトフリアニメ pic.twitter.com/48aflo1saS
Netflixの公式アカウントでの施策(?)で、ハッシュタグ「#わたしのアニメ歴3選」をつけてツイートをすると、オススメのアニメを紹介してくれます。
アニメの紹介は以下のような形ですね。
ツイートありがとうございます✨
— Netflix Japan Anime (@NetflixJP_Anime) November 29, 2020
素敵なアニメ歴をお持ちのあなたに
是非ともオススメしたい #ネトフリ 配信中のアニメは…
『Re:ゼロから始める異世界生活 新編集版』https://t.co/6X1LKWDLKf
もしまだ観たことがないなら要チェック👀#ネトフリアニメ pic.twitter.com/npacWVcAi4
フォロワーの推しアニメが分かったり、どんなアニメをおすすめしてくれるんだろう?となったりで私自身も楽しみました。
ただ、3つに絞り切れるのはかなり難しかったです...(結果何回もツイートしてしまいました)
企画に関する憶測
これは完全に憶測ですが、今回の企画はTwitter上で楽しんでもらいつつも、ここで集まったツイートをNetflixのラインナップの更新やレコメンドに活かされるんじゃないかと思っています。
例えばレコメンドで言うと、ツイートを分析することで、アニメAを好きな人はアニメBも好きな傾向があるというのが分かります。
これが分かることで、Netflix上でのあなたにオススメの番組というのが、より刺さるものになるかもしれません。
もしそうだとしたら、企画からデータの収集、サービスの改善までの流れが面白いですよね!
ちなみに漫画で実際にこういったデータの集め方をしておられる方もいます。
※上記はあくまで私の憶測であり、ただの盛り上げ企画かもしれませんし、実際はわかりません。
せっかくなので分析してみた
ということで、せっかくの面白いデータなのでハッシュタグのついたツイートを収集して分析してみました!
3選にどんなアニメがたくさん入っているのか?、どのアニメ同士が好まれやすいのか?等、いろいろできそうです。
ちなみにNetflixのアカウントからオススメされる作品については今回触れないでおこうと思います。
「勘のいいガキは嫌いだよ」とか言われたくないので
前処理
思った以上に大変でした....
SNSの文章解析をした方あるあるかもですが、「あなたはどのアニメを選びますかーー?」と言われているだけであり、フォーマットは決まっていません。
なので、当然みんな自由にツイートしています。
改行して1行に1つのタイトルにしてツイートしている人もいれば、「AもいいけどBもいいんだよなあ」とツイートしている人もいたり、そもそもハッシュタグに関する感想だけでアニメのタイトルを挙げていない人もいたり。。。
また、アニメの名前も正式名称で答える人もいれば略称の人もいたりと様々です。
真面目にやるとキリがないので以下のような対処をしました。
データの選定
- 重複データを削除
- ツイート内容が完全に重複するものは、さくらやBOT等の可能性も考えて1つにしました
- 公式アカウントのツイートを削除
- ハッシュタグにはNetflix公式のツイートも含まれるのでこれは除きました
- Amazonのアフィリエイトリンクが含まれるものを削除
- 意外とこれが多くてビックリしました。
- ちょうどブラックフライデーと被っていたこともあり、トレンドにあやかってアフィリエイトから買い物をしてもらおうとしている人がいたのでしょうか
- リンクがありつつもアニメを答えているものもありますが、真面目に回答しているのかは怪しいので除きました
テキストの前処理
ツイート毎に以下のような前処理をしました。
速度重視で細かくは見ていません!
def split_data(content):
# ハッシュタグの除去
content = content.replace("#わたしのアニメ歴3選", "")
# 絵文字の除去
content = "".join(["" if c in emoji.UNICODE_EMOJI else c for c in content])
# URLの削除
content = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", content)
# 連続改行を1つに
content = re.sub(r"\n+", "\n", content)
# 改行毎に1つのタイトルとして改行で区切る
token_list = content.split("\n")
def preprocess_per_token(token):
# タイトルの前にある先頭文字を消去
token = re.sub("^\s?[..,•・・123❶❷❸①②③123#○*↓↓]+", "", token)
# 公式アカウントへのメンションを消去
token = token.replace("@NetflixJP_Anime", "")
# 以下、空白や記号への処理
token = token.replace(" ", " ")
token = re.sub(r"^\s?", "", token)
token = re.sub(r"\s?$", "", token)
token = re.sub(r"[!!]+$", "", token)
return token
# 行区切りしたものへの前処理
token_list = [preprocess_per_token(t) for t in token_list]
token_list = [t for t in token_list if len(t) > 0]
# 多数含まれる共通ワードを削除
drop_words = (
"です",
# 以下略....
)
token_list = [t for t in token_list if t not in drop_words]
token_list = [rename_dict[t] if t in rename_dict else t for t in token_list]
return token_list
タイトルの名寄せ
これが一番面倒でした。
1つのアニメでも本当にたくさんの表現があります。
ここでの方針としては、以下のように名寄せしました。
- 基本は手動での名寄せ
- Wikipediaや外部辞書との突合, 編集距離とかも考えましたが本題ではないし大した量ではないので手作業しました
- ある程度件数があるものを手動でチェック
- 少数派は無視します、ごめんなさい。
- 複数パターン書かれているものがあった場合、一方に寄せるように辞書定義してそれで名寄せする
- シリーズものは自明に一つでない限り、一つにまとめたりはしない
- 例:ガンダムシリーズ、Fateシリーズ等
表記揺れのあるものを、せっかくなのでいくつか紹介します。
STEINS;GATE(シュタインズゲート)
ちゃんと正式な名称にしていたり、カタカナだったり、シュタゲだったりですね...
rename_dict = {
"Steins Gate": "STEINS;GATE",
"Steins;Gate": "STEINS;GATE",
"シュタゲ": "STEINS;GATE",
"シュタインズゲート": "STEINS;GATE",
"シュタインズ・ゲート": "STEINS;GATE",
}
やはり俺の青春ラブコメはまちがっている。
漢字だったり、句読点がついていたり、、そして「俺ガイル」とか知らないと無理ですよね。。。笑
rename_dict = {
"やはり俺の青春ラブコメは間違っている。": "やはり俺の青春ラブコメはまちがっている。",
"やはり俺の青春ラブコメは間違っている": "やはり俺の青春ラブコメはまちがっている。",
"やはり俺の青春ラブコメはまちがっている": "やはり俺の青春ラブコメはまちがっている。",
"俺ガイル": "やはり俺の青春ラブコメはまちがっている。",
}
上記のようなタイトルごとの辞書がまとめて、最終的に辞書を1つ作り、それで寄せました。
人気ランキング
ここからが本編です。
名寄せ後のタイトルの数でのTOP20です。
タイトル | 件数 | |
---|---|---|
1 | カードキャプターさくら | 2683 |
2 | 鋼の錬金術師 | 2571 |
3 | 新世紀エヴァンゲリオン | 2545 |
4 | コードギアス 反逆のルルーシュ | 2302 |
5 | 銀魂 | 1709 |
6 | 魔法少女まどか☆マギカ | 1535 |
7 | けいおん | 1513 |
8 | ヴァイオレット・エヴァーガーデン | 1361 |
9 | セーラームーン | 1158 |
10 | 涼宮ハルヒの憂鬱 | 1133 |
11 | STEINS;GATE | 1128 |
12 | ソードアートオンライン | 1114 |
13 | ラブライブ | 1031 |
14 | 夏目友人帳 | 1013 |
15 | カウボーイビバップ | 965 |
16 | 家庭教師ヒットマンREBORN | 952 |
17 | CLANNAD | 934 |
18 | ハイキュー | 878 |
19 | 進撃の巨人 | 877 |
20 | デジモンアドベンチャー | 876 |
第1位は「カードキャプターさくら」でした!!
男女問わず多くの世代から支持を集めたのでしょうか(適当)
上位陣はたしかに有名で人気どころの作品が多く上がっていますね。
大ヒット映画上映中の「鬼滅の刃」はランクインしていないのは少し意外でした。(33位でした)
このハッシュタグのツイートをした人たちは元々アニメ好きが多く、鬼滅の刃はアニメ好き以外の層にも届いてそこで人気なのではないかなと個人的には思っています。
Anime2Vec
ランキングを作るだけでは面白くありません。
せっかく頑張って前処理したので、もう少し遊んでみます。
今度はWord2VecならぬAnime2Vecを作ってみます。(適当に命名)
※Anime2Vecとググると、他にもAnime2Vecという名の記事やリポジトリが出てきました12が、中身を見る限りそれとは別物のAnime2Vecです。
よくあるのは作品の説明文からのベクトル化や視聴履歴からベクトル化があげられますが、そういったものと比較すると実際にユーザが特に好きな作品として作られたデータなので、よりユーザの好みの関係性がベクトル化できることを期待しています。
実装
基本的にはWord2Vecのアルゴリズム(Skip-gram/CBOW)を活用します。
作品一つずつを単語、一人のユーザが選んだ複数の作品の並びを文として、Word2Vecの学習を行ってみます。
アルゴリズムをざっくり説明すると、「作品Aと作品Bが好きな人はどの作品が好きか?」というのを簡易的なニューラルネットワークで学習していくことで、作品ごとのベクトル表現を得られるという手法です。
ただし通常の文章と違い、並び順にはあまり意味がないと思われる(好きな順にしているなら別ですが)ので、並び順は元々のものとユーザ毎にシャッフルしたものを足し合わせたものを使ってみます。
上記の前処理をすることで以下のような二重リストを用意できると思います。
二重リストのイメージ
[[ユーザ1のお気に入り作品1, ユーザ1のお気に入り作品2, ユーザ1のお気に入り作品3],
[ユーザ2のお気に入り作品1, ユーザ2のお気に入り作品2],
....
]
実際のリスト
>>> title_sentence_list[:10]
[['がっこうぐらし', 'ソードアートオンライン'],
['転生したらスライムだった件', 'ご注文はうさぎですか?', 'オーバーロード'],
['魔法少女まどか☆マギカ', 'ヴァイオレット・エヴァーガーデン'],
['盾の勇者の成り上がり', 'ノーゲーム・ノーライフ'],
['ポケットモンスター', '日常', 'Fateシリーズ'],
['攻殻機動隊', '狼と香辛料', 'オーバーロード', 'とらドラ', 'ブラックラグーン'],
['プリパラ', 'おそ松さん'],
['STEINS;GATE', 'CLANNAD', 'ヴァイオレット・エヴァーガーデン'],
['けいおん', '魔法少女まどか☆マギカ', 'ソードアートオンライン'],
['ヒカルの碁', 'ハイキュー']]
gensim3を使って以下のように学習させます。
コードはシンプルで簡単ですね!
import random
from gensim.models import word2vec
# ユーザごとにshuffleしたリストを作成
shuffled_sentence_list = [random.shuffle(sentence) for sentence in title_sentence_list]
# 元のリストとshuffleしたリストを合わせる
train_sentence_list = title_sentence_list + shuffled_sentence_list
# word2vecのパラメータ
w2v_params = {
"size": 64,
"iter": 10,
"seed": 2020,
"min_count": 1,
"workers": 1
}
# word2vecのモデル学習
model = word2vec.Word2Vec(title_sentence_list, **w2v_params)
これで学習は完了しました。
あとは以下のような形で各単語(アニメタイトル)のベクトルを取り出せます。
>>> model.wv["ポケットモンスター"]
array([-0.01666164, 0.24612345, 0.3171107 , -0.15554003, -0.08064189,
-0.42377824, 0.21372049, -0.5168727 , 0.55211025, -0.5834911 ,
-0.4355979 , 0.49262026, 0.11949556, 0.14716184, 0.4397376 ,
-0.5329746 , -0.1554945 , 0.24064176, 0.30889234, -0.44138482,
0.34901175, 0.2948994 , -0.02266309, 0.40219036, -0.39607367,
-0.26639333, 0.1566538 , -0.19332902, -0.04197901, -0.10609102,
-0.12176993, 0.031034 , 0.2160505 , 0.04975408, 0.15695912,
0.02971208, -0.07666103, 0.17774554, 0.0010377 , -0.26392284,
0.09105589, -0.3196749 , -0.02015243, -0.58814514, 0.07015242,
-0.43509468, -0.08375397, 0.50323445, 0.23665963, -0.01442516,
0.1666219 , -0.15285441, -0.21557336, -0.20237112, 0.00421314,
-0.09445517, 0.14319909, 0.07673828, 0.07542856, -0.03090033,
-0.14497752, -0.7117398 , 0.35385424, -0.43985626], dtype=float32)
作品同士の近さを見てみる
gensimのword2vecは近い単語(タイトル)を出すのも簡単にできるので、これで学習がうまく行っているのかを確認してみます。
銀魂
>>> model.wv.most_similar("銀魂")
[('ぬらりひょんの孫', 0.9865400791168213),
('NARUTO', 0.9803722500801086),
('鬼灯の冷徹', 0.9770888090133667),
('黒執事', 0.9760582447052002),
('弱虫ペダル', 0.9749394655227661),
('青の祓魔師', 0.9718550443649292),
('黒子のバスケ', 0.9685133695602417),
('SKETDANCE', 0.9676659107208252),
('ワールドトリガー', 0.9671770334243774),
('ヘタリア', 0.9636682868003845)]
けいおん
>>> model.wv.most_similar("けいおん")
[('涼宮ハルヒの憂鬱', 0.9750423431396484),
('らき☆すた', 0.967164933681488),
('結城友奈は勇者である', 0.958282470703125),
('ゼロの使い魔', 0.9454314112663269),
('俺の妹がこんなに可愛いわけがない', 0.9427845478057861),
('BanG Dream', 0.9403954744338989),
('灼眼のシャナ', 0.9401265978813171),
('ラブライブ!サンシャイン', 0.9370994567871094),
('魔法少女まどか☆マギカ', 0.9366708993911743),
('ゆるゆり', 0.9349873065948486)]
ポケットモンスター
>>> model.wv.most_similar("ポケットモンスター")
[('妖怪ウォッチ', 0.9508553743362427),
('プリキュア', 0.9429980516433716),
('アンパンマン', 0.9242094159126282),
('遊戯王', 0.9194732904434204),
('イナズマイレブン', 0.9176000356674194),
('名探偵コナン', 0.917043924331665),
('ケロロ軍曹', 0.9138047695159912),
('プリキュアシリーズ', 0.9057837724685669),
('メジャー', 0.8937799334526062),
('ふたりはプリキュア', 0.8787042498588562)]
!!!!!
全体的に良い感じですね!!!!!
ちゃんと近しい作品が上位に来ているのが分かります。
(ポケモンのところに色んなプリキュアがあるのは名寄せ漏れです...)
「けいおん」の近くに「ハルヒ」や「らきすた」があるのとか良い感じですね。
(ちなみにもう「けいおん」の放送から10年経ってるんですね...)
可視化してみる
t-SNEで2次元に圧縮して作品の近さを可視化してみます。
どうでしょうか?
ちょっと小さくて見にくいですが、個人的には似た作品同士がかなり近くにあるように思えます。
NextStep
今回はここまでとしますが、
- 実際にこれを活用したレコエンドエンジンを作ってみる、
- ユーザごとの他のツイートも併せて分析する
なども面白いかもしれません。
まとめ
今回はTwitterでトレンドになっていた「わたしのアニメ歴3選」を活用してランキングを作ったり、ベクトル化して遊んでみました。
技術的に何かすごいことをしている訳ではないですが、twitter上の企画からこういったデータが取得できるのは非常に面白いですよね。今後もこういった企画が増えるのかもしれません。
というわけで、私は自分の好きなアニメに近いまだ見ぬアニメでも見て12月を過ごしたいです....良いお年を