Youtubeでよく見かける「棒グラフがぬるぬる動くやつ」を作ってみた

# はじめに
Youtubeでよく見かける、棒グラフがぬるぬる動くやつ。
[Flourish](https://flourish.studio/) というサービスで作成できるようなのですが、
Pythonの [bar_chart_race](https://www.dexplo.org/bar_chart_race/) というライブラリでも作成できるようなので試してみました。

# 環境
Google Colaboratory の無料版です。
環境構築不要で気軽にコードを試すことができるので、技術を新しく学習する際にいつも重宝しています。

また、Google Colaboratory にはいわゆる「90分ルール」と「12時間ルール」というものが存在し、本記事のコードとは別にそちらの対策も必要になります。
詳しくは下記の記事が参考になるかと思います。

https://qiita.com/enmaru/items/2770df602dd7778d4ce6


# 題材
Qiitaサービス開始（2011/09/16）から今日に至るまでに投稿された全記事について、
記事に付けられたタグを集計してランキング形式にします。
データの取得には QiitaAPI を利用しておりますが、本記事ではQiitaAPIについての解説は割愛します。

# 成果物
<iframe width="560" height="315" src="https://www.youtube.com/embed/3ZBVVcRM2LE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 実装
以下のコードをそれぞれ別のセルで実行していけば成果物の動画が出力されます。

①ライブラリ導入
②CSV出力準備
③データ取得・CSV出力
④Google Drive マウント
⑤CSV読込・データ整形・グラフ出力


## ①ライブラリ導入
```python
!pip install bar_chart_race
!pip install japanize-matplotlib
```
Google Colaboratory で matplotlib で使用する際、日本語が文字化けしてしまします。
それを防ぐ目的として japanize-matplotlib を導入しています。


## ②CSV出力準備
```python
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials
import pandas as pd


# Google Drive 認証
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

data = []
data.append({'date': 'test', 'tag': 'test'})
df = pd.DataFrame(data)
filename = 'test.csv'
df.to_csv(filename)

# ファイル出力テスト
upload_file = drive.CreateFile()
upload_file.SetContentFile(filename)
upload_file.Upload()
```

上記コードを実行すると Google Cloud SDK からGoogleアカウントへのアクセスを求められます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1532684/d31e8248-aa16-fc0d-ba33-ac4ea0d4d55e.png)
リンクを押下し、画面に従って認証を行うとコードが発行されるので、Google Colaboratory に戻り「Enter verification code」にコードを入力し、Enterキー押下で認証が行われ、Google Drive に「test.csv」が作成されています。


## ③データ取得・CSV出力
```python
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials
import json
import time
import datetime
import requests
import pandas as pd
import bar_chart_race as bcr
import csv


# ログを記録
def record_log(msg):
    t_delta = datetime.timedelta(hours=9)
    JST = datetime.timezone(t_delta, 'JST')
    dt = datetime.datetime.now(JST)
    print(dt.strftime('[%Y-%m-%d %H:%M:%S] ') + msg)


# 取得データをCSV形式で GoogleDrive に出力
def output_csv(start, end, df):
    t_delta = datetime.timedelta(hours=9)
    JST = datetime.timezone(t_delta, 'JST')
    dt = datetime.datetime.now(JST)
    filename = dt.strftime('%Y%m%d%H%M%S') + start + 'to' + end + '.csv'

    df.to_csv(filename)

    auth.authenticate_user()
    gauth = GoogleAuth()
    gauth.credentials = GoogleCredentials.get_application_default()
    drive = GoogleDrive(gauth)

    upload_file = drive.CreateFile()
    upload_file.SetContentFile(filename)
    upload_file.Upload()

URL = "https://qiita.com/api/v2/items?"
TOKEN = {QiitaAPIのアクセストークン}
header = {
    'Authorization': 'Bearer {}'.format(TOKEN),
}

# カウント用変数
num = 0

# この期間に作成されたQiitaの記事情報を取得
start = '2011-09-14'
end = '2018-12-31'
# start = '2018-12-31'
# end = '2022-02-01'

# 半月ごとの日付をリスト化
date_list = [d.strftime('%Y-%m-%d') for d in pd.date_range(start, end, freq='SM')]
start_list = date_list[:-1]
end_list = date_list[1:]

# 結果格納用
data = []

record_log('データ取得処理開始')

for i in start_list:
    num += 1
    # 日付のリストから検索の開始日と終了日を取得
    start_date = start_list[num - 1]
    end_date = end_list[num - 1]
    page = 0

    record_log('取得開始：' + start_date + ' ～ ' + end_date)

    for j in range(100):
        page += 1
        query = "&query=created:>=" + start_date + "+created:<" + end_date
       
        # APIコール
        response = requests.get(URL + "page=" + str(page) + "&per_page=100" + query, headers=header)
        status = response.status_code

        while status != 200:
            # APIコール回数が上限（1時間あたり1000回）に達したらステータスコード 403 が返ってくるので、
            # 解除されるまでは5分おきにAPIをコールする
            record_log('APIコール回数が制限に達しました。解除されるまでは5分おきにAPIをコールします。')
            time.sleep(300)
            response = requests.get(URL + "page=" + str(page) + "&per_page=100" + query, headers=header)
            status = response.status_code

        # 記事を取得できなくなったら次の期間へ
        if not len(response.json()):
            record_log(str(page) + 'ページ目で記事を取得できなくなったため、次の期間に移ります')
            break

        # 記事を取得できた場合、年月とタグを格納する
        for item in response.json():
            for tag in item.get('tags'):
                data.append({
                    'date': item.get('created_at')[:7].replace('-', '/'),
                    'tag': tag.get('name')
                })
    record_log('取得終了：' + start_date + ' ～ ' + end_date)

record_log('データ取得処理終了')

# 取得データを Dateframe に変換し、年月およびタグごとの記事数を集計
df = pd.DataFrame(data)
df = df.groupby(['date', 'tag']).size().reset_index(name='count')

# データを横持ちにする
# fill_value=0 で NaN を 0 に置換
df = df.pivot_table(index='date', columns='tag', values='count', fill_value=0)

# CSV出力
output_csv(start, end, df)
```
全期間のデータを一度に取得しようとするとうまくいかなかったため、
「2011-09-14 ～ 2018-12-31」と「2018-12-31 ～ 2022-02-01」に分けています。


## ④Google Drive マウント
```python
# Google Drive 認証
!pip install -U -q PyDrive

from google.colab import drive
drive.mount('/content/drive')
```
上記コード実行により、現在のランタイムから Google Drive 上のファイルを操作できるようになります。


## CSV読込・データ整形・グラフ出力
```python
import pandas as pd
import bar_chart_race as bcr
import japanize_matplotlib


# 保存したCSVを読み込んで結合
df1 = pd.read_csv('drive/MyDrive/202201172000122011-09-14to2018-12-31.csv', index_col=0)
df2 = pd.read_csv('drive/MyDrive/202201180009192018-12-31to2022-02-01.csv', index_col=0)
df = pd.concat([df1, df2]).fillna(0)

# 累計を取得
df = df.cumsum()

# 各月においてグラフに表示されるタグを取得する
dispCols = []
for index, row in df.iterrows():
    rank10Value = row.sort_values(ascending=False).iloc[10]
    dispCols += row.index.values[row.values >= rank10Value].tolist()

# 削除対象のタグ（すべてのタグと各月の10位以内のタグの差分）を取得する
deleteCols = list(set(df.columns.values) - set(dispCols))
df = df.drop(deleteCols, axis=1)

dispCols = []
for index, row in df.iterrows():
    rank10Value = row.sort_values(ascending=False).iloc[10]
    dispCols += row.index.values[row.values >= rank10Value].tolist()
deleteCols = list(set(df.columns.values) - set(dispCols))
df = df.drop(deleteCols, axis=1)

# グラフ描画
bcr.bar_chart_race(
    df=df,
    n_bars=10,
)
```
ポイントは、全期間において10位以上になることがないタグ（＝グラフに一度も表示されることのないタグ）を削除している点です。
これをしないと最後のグラフ描画処理の途中でエラーが発生したり、処理が著しく遅くなったりしました。

# おわりに
普通のグラフとは視覚的に一風変わっているので、プレゼン用の資料などでも効果的に使えるかもしれません。
データ取得の部分を変更すればそれ以外の部分は流用できそうなので、また何か興味のある題材を考え付いたら作成してみたいと思います。