アイカツ!シリーズの誕生祭ツイートTop15をバーチャートレースで競争させてみた

2020.12.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

データアナリティクス事業本部@札幌の佐藤です。

12月10日(木)からついにデータカードダス アイカツプラネット! 1弾の稼働が始まりましたね。

この弾からアイカツスターズ!から使用されていた筐体から、新しい筐体に代わり、アイカツ!カードからスイングに変更されました。
新しいアイカツ!が始まったという感じですね。

また新シリーズ『アイカツプラネット!』発表記念でアイカツ8という人気キャラクターでの限定ユニットも約4年ぶりに実施されました。
ある程度想像通りの結果だったなという印象でした。

そんな人気キャラクター、人気があるということはファンの人がたくさんいるので、誕生日のお祝いもたくさんあるだろう。

ということで2013年~2020年までの誕生日おめでとうツイートから、Bar Chart Raceを作成して誕生日が祝われる推移を見ていきたいと思います。

Bar Chart Raceとは

Bar Chart Raceは、英語そのままですが、時系列順に変化する棒グラフをアニメーションとして視覚的に表示するものになります。

私はグラフを見るのが好きなので、それが競争する+今回はそれがアイカツ!シリーズにかかわる内容なので、つまり大好きです。

Bar Chart Raceで有名なものはFlourish になると思います。

FlourishはBIツールのひとつで、Webブラウザ上からNoCodeでビジュアライゼーションができる点や、ほかのBIツールに比べアニメーション描画が強いのが特徴だと思います。

またinputとなるデータはエクセルが使用可能で、取込後の加工もエクセルのセルなのでエクセルが使い慣れている人はすぐに使用できるかなと思います。

個人使用は無料なので、BIに興味のある方は使ってみてもよいかもしれません。


ただ今回はFlourishは使用せずにPython(バージョン3.8.3)で実装(Jupyter notebook上で実装)していきます。

Pythonでの実装は、よくあるmatplotlibでの実装に加え、bar_chart_raceライブラリも存在しますのでそちらも使用していきます。

完成イメージ

このようなものができます。※matplotlibでの実装結果です

色は各キャラクターの属性を象徴する色です。
(キュート:ピンク、クール:青、ポップ:黄色、セクシー:紫、そのほか:グレー)

※そのほかは、アイカツスターズ!に登場したハルカ☆ルカのみです。一応誕生日設定がありますが、アニメ内のCGシーンおよびデータカードダス(ゲーム)には登場していないため属性がわからないためです。

当記事の話からずれますが、上で書いたアイカツ8 2020 の結果とこのグラフを比較すると若干結果が違います。

アイカツ!シリーズのランキングイベントはたまに実施されますが、そのたびに意外枠があるので面白いですね。
(アイカツ8 2020でのサプライズ枠は早乙女あこでした)

inputデータ

今回は以下のようなルールでツイッターの情報を取得している状態からスタートします。

  • 2013年~2020年3月までのツイッターでのハッシュタグ「#双葉アリア生誕祭」か「#双葉アリア生誕祭2020」(2020はその年の情報がセットされます)を対象としています。
    全キャラクターです。テレビアニメに登場していないキャラクターもハッシュタグがあれば集計しています。
    (4月はじまりの年度としているためです。2020年は花園きららの誕生日3月30日までが対象です。4月1日誕生日の大空あかりは翌年度になっているので対象外です)

  • ひとつのツイートでハッシュタグを両方セットしている場合は、ひとつのツイートとして集計しています。

  • 同一ユーザの複数ツイートも集計対象としていますが、リツイートは対象外にしています。

  • 取得しているツイートはキャラクターの誕生日前後1週間にしています。
    例えば、私の推し双葉アリアの誕生日は12月24日なので、その前後1週間12月17日~12月31日までが対象です。(誕生日に遅刻してくる勢への配慮)

    データとしては以下のようなものになります。レコード数は約59,000です。

import pandas as pd
df = pd.read_csv('XXXXX/HBD_tweet.tsv', sep='\t', index_col=0)
df


あとは各キャラクターの属性情報として、誕生日とシリーズの情報になります。

#各キャラのシリーズ情報・属性をread
df_series = pd.read_csv('XXXXX/series.tsv', sep='\t', index_col=0).reset_index()
df_series.head(10)

#各キャラの誕生日をread
df_hbd = pd.read_csv('XXXXX/birthday.tsv', 
                     sep='\t', index_col=0,
                     dtype = {'charactor':'str', 'month':'str', 'day':'str'}
                     ).reset_index()
df_hbd.head(10)

各キャラのシリーズ情報・属性

各キャラの誕生日

前処理

描画する前のデータの加工処理になります。
Pythonですべて加工しているので、ここが一番大変なところになります。

RDBSを利用しているのであれば、そちらで加工したほうが早いと思います。

累積和の取得

# ツイートをキャラクター別、年別で集計してカラム名を変更する
df = pd.DataFrame(df.groupby(['charactor', 'year'], as_index=False).size()).reset_index()
df.rename(columns={0:'tweet_num'}, inplace=True)

# ツイートデータと誕生日データを結合
df = pd.merge(df, df_hbd, on='charactor')
# 各年の誕生日が時系列データとなるので、誕生日を作成する。
df['birthday'] = df['year'].astype(str) + '-' + df['month'] + '-' + df['day'] 
# ツイート数の累計を出すためにダミーをセット
df['total'] = 'total'

# 誕生日時点でのツイート総数(累積和)を求める
df_totw = df.groupby(by=['total','birthday']).sum().groupby(level=[0]).cumsum().reset_index()
df_totw.rename(columns={'tweet_num':'total_tweets'}, inplace=True)
df_totw = df_totw.drop(['total','year'], axis=1)

誕生日時点でのツイートの累積和をdf.groupby(by=['total','birthday']).sum().groupby(level=[0]).cumsum()で求めていきますが、今持っている情報のみでは実施できません。

集約できる情報は、項目「年」と「誕生日」なので、年別で誕生日別に集約はできますが、全体2013年~2020年までの集約はできません。

そのため、集約用に事前にダミーのカラムを用意しています。


df.groupby(by=['year','birthday']).sum() の実行結果を見ればわかりますが、一番左側が年でまとまっているため、これで累積和を求めると年単位になります。


df.groupby(by=['total','birthday']).sum() の実行結果を見ると、一番左がすべてまとまっているのでこれで累積和を求めると全体の累積が求められます。

欠損値の補充

def get_prev_tweets(index):
    # 処理インデックス番号にすでに値があればそれを使用する
    if pd.isnull(tmp_df.at[index, 'tweet_cumsum']):
        # 0番目の場合は前のインデックスがないのでスキップ
        if index == 0:
            tmp_df.at[index, 'tweet_cumsum'] = 0
            return
        tmp_df.at[index, 'tweet_cumsum'] = tmp_df.at[index -1, 'tweet_cumsum']
        return
    else:
        tmp_df.at[index, 'tweet_cumsum']=tmp_df.at[index, 'tweet_cumsum']
        return
# ツイートのキャラクター別、年別での累積和を求める
df_cmsm = df.groupby(by=['charactor','year']).sum().groupby(level=[0]).cumsum().reset_index()
df_cmsm.rename(columns={'tweet_num':'tweet_cumsum'}, inplace=True)

# 欠損埋め
df_merge = pd.DataFrame()
# 全体の情報とツイートの累積和を結合する。
df =pd.merge(df, df_cmsm, on=['charactor', 'year'], how='inner')

for i in df['charactor'].drop_duplicates():
    # ツイート数の累積和を完全外部結合することで、時系列情報に対して、それぞれの誕生日のツイート情報が紐づく。ただしそれ以外は欠損
    tmp_df = pd.merge(df[df['charactor']==i], df_totw, on='birthday', how='outer').sort_values('birthday').reset_index(drop=True)
    # 欠損埋め
    tmp_df.loc[tmp_df['tweet_cumsum'].isnull(), 'birthday'].index.map(get_prev_tweets)
    # 欠損している情報のキャラクター名称と年を埋める
    tmp_df['charactor'] = tmp_df['charactor'].dropna(how='all').drop_duplicates().values[0]
    tmp_df['year'] = tmp_df['birthday'].str[:4]
    df_merge = pd.concat([df_merge, tmp_df])

# シリーズファイルと結合
df_merge =pd.merge(df_merge, df_series, on=['charactor'], how='inner')

Bar Chart Raceをするためには時系列データがなければならないのですが、誕生日という仕様上、人間である限り誕生日は年に1度しか来ません。(姫石らき誕生日増幅バグを除く)
そのため、ほかの人の誕生日ではツイート数がNULLになります。(白百合さくや・かぐやの双子を除く)

その場合、ある人の時点のツイート数が見えなくなってしまうので、欠損値を前レコードで補填する必要があります。

以下が例です。双葉アリアが12月24日、音城ノエルが12月25日、藤堂ユリカ様が12月26日がそれぞれ誕生日です。
欠損を埋めていきますが、データ数を揃えるために0は落としていないです。



具体的には関数get_prev_tweets で処理しています。

index番号を渡しひとつ前のデータを取得します。

index番号を使用しているのは、前のレコードを取得するために使える情報が誕生日しかなく、365日分誕生日が存在していないため前のレコードを取得するには情報が不足しているからです。

「年度」など連番で存在しているカラムがあれば、それを減算して取得してもいいと思います。


増幅したデータとシリーズファイルと結合すると最終的に以下のようなデータができます。
後続で不要な項目は補充していません。dropしてしまってもよいかと思いますが、後続影響がないのでそのままにしています。

matplotlibで描画

FuncAnimation を使うくらいでそのほかは通常のmatplotlibの描画実装と変わりません。

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import matplotlib.animation as animation
def transform_color(all_names):
    dict_att_color = {'キュート': '#ffc0cb', 'クール': '#87cefa', 'ポップ': '#ffff00', 'セクシー': '#dda0dd', 'そのほか': '#d3d3d3'}
    # 属性に対して色を割り当てていく
    hex_colors = [dict_att_color[df_series[df_series['charactor']==i]['attribute'].values[0]] for i in all_names]
    # キャラクターと属性の辞書を作る
    return dict(zip(all_names, hex_colors))
def draw_barchart(date):
    # 上位15人を取得
    df_view = df_merge[df_merge['birthday'].eq(date)].sort_values(by='tweet_cumsum', ascending=True).tail(15)
    ax.clear()

    # キャラクターと属性のマッピング表を作る
    normal_colors = transform_color(df_merge['charactor'].unique().tolist())
    # 横棒グラフの設定
    ax.barh(df_view['charactor'], df_view['tweet_cumsum'], color = [normal_colors[x] for x in df_view['charactor']])

    # バー内の設定
    dx = df_view['tweet_cumsum'].max() / 100
    for i, (value, name, series) in enumerate(zip(df_view['tweet_cumsum'], df_view['charactor'], df_view['series'])):
        ax.text(value-dx, i, name, size=12, weight=800, ha='right', va='bottom')
        ax.text(value-dx, i-.33, series, size=8, weight=800, ha='right', va='bottom')
        ax.text(value+dx, i, f'{value:,.0f}',  size=10, ha='left',  va='center')

    # バーの詳細設定
    total=df_view['total_tweets'].values[0]
    ax.text(1, 0.4, date[:7], transform=ax.transAxes, color='#766712', size=56, ha='right', weight=500)
    ax.text(1, 0.3, f'Total - {total:,.0f}', transform=ax.transAxes, color='#777777', size=20, ha='right', weight=500)
    ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:,.0f}'))
    ax.xaxis.set_ticks_position('top')
    ax.tick_params(axis='x', colors='#777777', labelsize=12)
    ax.set_yticks([])
    ax.margins(0, 0.01)
    ax.grid(which='major', axis='x', linestyle='-')
    ax.set_axisbelow(True)
    ax.text(0, 1.12, 'アイカツ!シリーズ誕生祭ツイート数', transform=ax.transAxes, size=24, weight=600, ha='left')
    plt.box(False)
    plt.tight_layout()
fig, ax = plt.subplots(figsize=(15, 8))
animator = animation.FuncAnimation(fig, draw_barchart, frames=df_totw['birthday'].tolist(), interval=500)
animator.save('aikatsu_barchartrace_matplotlib.mp4', writer='ffmpeg', fps=5)

FuncAnimation ですが関数draw_barchart に対し、誕生日の配列を渡すことで配列順で処理します。

前処理の最終的な結果(上記の画像)から対象の誕生日のデータを取得して描画していくということを繰り返します。

速度は引数interval で調整します。

FuncAnimation でのポイントは、ax.clear() で前のデータを削除する点です。

これを実装しないと前のデータが消えずその上に新しいグラフが描画されるので、きちんとしたアニメーションになりません。

mp4で保存するにはffmpegが必要なので適宜インストールしてください。

bar_chart_raceで描画

bar_chart_raceライブラリは、そのライブラリ名の通りBar Chart Raceに特化したライブラリです。

裏でmatplotlibを使用しているので結局はmatplotlibをラップしているだけです。

PandasのDataFrameを渡すだけで、matplotlibのような面倒な実装不要で簡単にBar Chart Raceが作成できます。

作成にあたりffmpegが必要なので適宜インストールしてください。

pip install bar_chart_race でインストールしてください。

完成イメージ

今回はmatplotlibと比較するため同じように実装しています。

ETL処理

描画するにあたりmatplotlibとは取り込むinput情報のデータの持ち方が違うのが注意です。

通常のDWH構成では行単位でデータがデータベースに存在していることが多いと思いますので、データベースからbar_chart_raceでの描画の間に変換するためのETLが必要になります。

今回は、上で実施した前処理の結果に対して、ETL処理を実施します。

df_etl = pd.DataFrame()
for i, name in enumerate(df_merge['charactor'].drop_duplicates()):

    df_merges = df_merge[df_merge['charactor']==name].sort_values('birthday').reset_index(drop=True)
    # 前処理で不要な項目は落とす
    df_merges=df_merges.drop(['charactor','year','tweet_num','month','day','total','total_tweets','series','attribute'], axis=1)
    # 列名をキャラクター名に修正
    df_merges.rename(columns={'tweet_cumsum':name}, inplace=True)
    # birthday列は一つあればいいのでループ1回目以外は落とす
    if i != 0:
        df_merges=df_merges.drop(['birthday'], axis=1)

    # 列単位で結合する
    df_etl = pd.concat([df_etl, df_merges], axis=1)

df_etl=df_etl.set_index('birthday')

処理すると以下のような形になります。

描画する

import bar_chart_race as bcr
def transform_color(all_names):
    dict_att_color = {'キュート': '#ffc0cb', 'クール': '#87cefa', 'ポップ': '#ffff00', 'セクシー': '#dda0dd', 'そのほか': '#d3d3d3'}

    # 属性に対して色を割り当てていく
    hex_colors = [dict_att_color[df_series[df_series['charactor']==i]['attribute'].values[0]] for i in all_names]

    # キャラクターと属性の辞書を作る
    return dict(zip(all_names, hex_colors))
def summary(values, ranks):
    total_deaths = int(values.sum())
    s = f'Total - {total_deaths:,.0f}'
    return {'x': .99, 'y': .05, 's': s, 'ha': 'right', 'size': 8}
# キャラクターと属性のマッピング表を作る
normal_colors = transform_color(df_merge['charactor'].unique().tolist())

# 描画
bcr.bar_chart_race(df=df_etl, 
                   cmap=[normal_colors[x] for x in df_merge['charactor'].unique()],
                   title='アイカツ!シリーズ誕生祭ツイート数',
                   period_summary_func=summary,
                   n_bars=15)

matplotlibに比べて描画に必要な情報はかなり少ないです。

最低限、変数df があればよいと思います。

棒グラフの色情報は変数cmap を使用します。カラーマップはmatplotlibと同じです。
今回はカスタムしています。

グラフ中の「Total - XXXX」の数値は変数period_summary_func にセットします。
今回はDataFrameの行を集計する処理を入れて、ツイート数累積和を求めています。
matplotlibでは事前に用意しなければならなかったので、楽ですね。

今回は横棒グラフですが、縦棒グラフにすることもできます。
デフォルトは横棒グラフなので、orientation='v'を設定してください。

詳細はチュートリアルを参照していただければと思います。

最後に

今回はmatplotlibとbar_chart_raceライブラリで同じような実装をしてみました。
アニメーションはbar_chart_raceライブラリのほうがよさそうですが、データの持ち方に工夫が必要です。

matplotlibは日本語が豆腐になる問題もありますのでPython初心者でBar Chart Raceをしたい場合は本質ではないところで躓く可能性があります。

状況次第で見極めていただければと思います。

また、Pythonで実装するのかFlourishで実装するのかという点においても、状況で見極めていただければと思います。


データカードダス アイカツプラネット! 1弾12月10日(木)から稼働中です!
このご時世で筐体フルチェンジで挑んでいるので、ゲームセンターなどに行った場合はぜひプレイしてみてください!

テレビ放送2021年1月10日(日)7:00から放送開始です!
こちらも併せて是非ご覧いただければと思います。

参照

Bar Chart Race in Python with Matplotlib

データフレームの中の一個前の列を参照して、次の列の演算を行う