すのふら

すのふら

日々の備忘録

『Pythonで体験してわかるアルゴリズムとデータ構造』メモその4/バケットソートの制約を知ろう

Pythonで体験してわかるアルゴリズムとデータ構造』を読みながらアルゴリズムについて勉強する。

Pythonで体験してわかるアルゴリズムとデータ構造

Pythonで体験してわかるアルゴリズムとデータ構造

第4章にちょろっと書いてあったバケットソートについて書いていく。



バケットソートについて

バケツ数 k 個使った場合、オーダーはO(n + k)となり、ソートする要素数nとk を無関係にできる場合線形時間ソートとなるが、要素間の全順序関係を用いるソートとは異なり、キーの取りうる値がk種類である、という入力により強い制限を要求するソートである。
バケットソート - Wikipedia

ここでいうバケットとはバケツのこと。ビンソートともいう

事前に用意したバケツに数字を置いていくというのが特徴。

用意したバケツの番号へ同じ番号のデータを順番に渡してあげるため、最良計算量はO(n)。
数字同士の比較が存在しないため、その分処理が速いため早いソートになるが、制約が多め。

事前にバケツを用意する、つまり最初に渡される数字分の要素を用意してあげる必要性があるため、その時点でまずメモリを使用する。
そのため要素数が多すぎるとメモリを使い切ってしまう可能性がある。

また、特徴上渡す値は整数である必要があり、数字の重複が許されない。

数字の重複を許すバケットソートが分布数え上げソート。

バケットソートのイメージは以下

f:id:snofra:20190921020917p:plain



実装

分布数え上げソートで実装。

def bucket_sort(lst):
    bucket = [None for _ in range(max(lst)+1)]
    print(bucket)
    # バケットにデータを格納していく
    # input側で重複した値を存在した時は加算する
    for i in lst:
        if bucket[i] == None:
            bucket[i] = i
        else:
            bucket[i] += i
    
    print(bucket)
    # 書き戻し作業
    j = 0
    for bucket_num, lst_num in enumerate(bucket):
        if lst_num == None:
            continue
        if lst_num == 0:
            lst[j] = lst_num
            j += 1
            continue
        
        print("要素番号, データ", bucket_num, lst_num)
        
        for k in range(lst_num // bucket_num):
            # 要素番号よりも格納データのデータが大きい場合、重複したデータのためバケットの番号をセット
            if lst_num > bucket_num:
                lst_num = bucket_num
            lst[j] = lst_num
            print(lst)
            j += 1
            
lst = [1, 2, 3, 4, 5, 3, 2, 1, 7, 0]
print("処理前", lst)
print()
bucket_sort(lst)
print()
print("処理後", lst)
処理前 [1, 2, 3, 4, 5, 3, 2, 1, 7, 0]

[None, None, None, None, None, None, None, None]
[0, 2, 4, 6, 4, 5, None, 7]
要素番号, データ 1 2
[0, 1, 3, 4, 5, 3, 2, 1, 7, 0]
[0, 1, 1, 4, 5, 3, 2, 1, 7, 0]
要素番号, データ 2 4
[0, 1, 1, 2, 5, 3, 2, 1, 7, 0]
[0, 1, 1, 2, 2, 3, 2, 1, 7, 0]
要素番号, データ 3 6
[0, 1, 1, 2, 2, 3, 2, 1, 7, 0]
[0, 1, 1, 2, 2, 3, 3, 1, 7, 0]
要素番号, データ 4 4
[0, 1, 1, 2, 2, 3, 3, 4, 7, 0]
要素番号, データ 5 5
[0, 1, 1, 2, 2, 3, 3, 4, 5, 0]
要素番号, データ 7 7
[0, 1, 1, 2, 2, 3, 3, 4, 5, 7]

処理後 [0, 1, 1, 2, 2, 3, 3, 4, 5, 7]

参照

バケットソート

バケットソート/ビンソート : アルゴリズム

分布数え上げソート : アルゴリズム

www.youtube.com

不遇キャラこそ目立つときだ!アイカツ!アイドル総選挙中間発表から傾向を見る

アイカツオンパレード!』開始を記念して、現在稼働中の「データカードダス アイカツフレンズ!かがやきのドレス」3弾でアイドル総選挙が開催中!

www.aikatsu.com

投票は1プレイごとに1回与えられていて、投票は筐体からではなくマイページにログインして行う形になる。

投票数1位のキャラクターは新しいプレミアムドレスのカードが収録される。

投票できるのは、以下の56人。
f:id:snofra:20190908233846p:plain*1

その結果は10月上旬に発表されるんだけど
先週、中間発表でトップ20が公開

f:id:snofra:20190908220414p:plain
©BNP/BANDAI, DENTSU, TV TOKYO

この結果から、どういうキャラクターを投票する傾向があるのかを考えてみる。


人気キャラvsプレミアムドレス欲しいキャラ

このランキングを見る限り、「人気が高いキャラクター」と「アニメの登場回数のわりにプレミアムレアドレスに恵まれなかったキャラクター」というのが言えるのでは?

人気が高いキャラをどう確認するのかというので、『一番くじ アイカツ!』のアイドル総選挙結果と、アイカツオンパレード!宣伝動画の各キャラクター再生数ランキングの結果から推測する。

プレミアムレアドレスに恵まれなかったのかというのは、公式のカードリストのコーデ数から推測する。

なお『アイカツフレンズ!』のキャラクターは両方のランキングに含まれていないので、コーデ数だけで推測。

f:id:snofra:20190908221304p:plain

これを見ると、藤堂ユリカ様や星宮いちご、夏樹みくるなど、総選挙系上位陣、つまり人気の高いキャラクターが想定通りランクインされていることが分かる。

これらのキャラクターは、今後の総選挙でも安定して上位にいると思う。
ある程度の固定客が見込めるキャラクターだと言えるため、今後のグッズ展開などは見込めるのかなと思える。


このランキングではそれに混じるように、プレミアムレア欲しい枠として、トップ陣にいるキャラクターがいる。
ココや七倉小春、早乙女あこなどがそれにあたり、総選挙順位も高くないので、順位だけで見るとなぜいるのかわからないようなキャラクターになる。

キャラクターの作中の立場を考えると、欲しいと思うファンの気持ちが分かる。

ココは、作中での立場上そもそもアイカツカード自体が存在しない。

早乙女あこは『アイカツスターズ!』で第26代S4という四ツ星学園のトップ4におり、アニメ中でも出番が多いキャラクターにもかかわらず、「データカードダス アイカツスターズ!」ではプレミアムドレス・スタープレミアムドレス数が0。
データカードダス アイカツフレンズ!」でようやく収録という、なかなかにかわいそうなキャラクターだったりする。

七倉小春も『アイカツスターズ!』の中盤以降のストーリーでは重要な役回りではあったが、物語上虹野ゆめのフォローに回っていることも多く、「データカードダス アイカツスターズ!」ではプレミアムドレス・スタープレミアムドレス数が0。


ただ、このランキングが難しいのは、藤堂ユリカ様や夏樹みくるのような、人気は高いけど実はプレミアムレア数も少ないのもいるということだと思う。

必ずしも人気キャラクターはプレミアムレアドレスも優遇されているわけではないということになるのと同時に、人気かつプレミアムレア不遇枠に、プレミアムレア不遇枠がランキング上で勝っていくのは非常に難しいということになる。

ただ、このプレミアムレア不遇枠が次のコンテンツの売上に貢献するひとつの材料になる可能性も十分あるのでは?と判断される可能性がある。

勝ち目ないとは思いつつも投票をしたほうが良いのかなーって思う。


次にランキングされた20人の属性の割合を見てみる。

最初に今回投票可能な56人の属性別キャラクター数は以下となっている。
f:id:snofra:20190908234133p:plain

これを見る限り、「アイカツ!あかりジェネレーション」のキュートとクールの数が少ないが、それ以外はシリーズ大体同じくらい人数いるということが分かる。

全体を見ても、クールのキャラクターが多いくらいで極端に多い少ないはないように思える。


じゃあ今回のトップ20にランキングしたキャラクターの属性別のキャラクター数も確認し見てみる。

f:id:snofra:20190908234920p:plain

56人に比べて、セクシー属性のキャラクター数が少し少ないか?と思える程度で大差ないように見える。


それぞれを円グラフ上でチェックしてみる。

f:id:snofra:20190908235248p:plain

f:id:snofra:20190908235252p:plain


ここを見てもトップ20はセクシー属性少ないかな?って思えるくらいでそんなに全体から大きくずれるような割合になっていない。


最後に

アイドル総選挙は9月17日(火)23:59まで開催しているので、是非データカードダス アイカツフレンズ!をプレイして、プレイした後は総選挙に1票をお願いします!

投票するためのマイページはこちらになります。

mypage.aikatsu.com


俺も霧矢あおいを1位に持っていきたいので、できる範囲で投票頑張っていきます!

*1:ココは属性がいまいちわからないのでその他とした

2018年「アイカツ!」シリーズの売上が良かったのか?決算短信から確認してみる

先日kasumiさんの同人誌『ゼロからわかるプリキュアの数字』を読んだため、勉強もかねてバンダイナムコ決算短信アイカツ!」シリーズ分をまとめてみる。

www.bandainamco.co.jp

まとめる期間は決算短信に初めて出てきた2014年3月期 第4四半期から2019年3月期 通期まで。
具体的な期間としては2013年1月から2019年3月の結果となる。

※途中で決算短信上掲載されていない2018年3月期 第1四半期から第3四半期は翌年の情報から確認し、埋められないところについては前半期で埋めているため正確性はちょっと落ちます。

今回は全体の内容に言及しつつ、今回の2018年の結果について思うことを記載する。


2018年度の売上高結果

アイカツ!」シリーズの2018年度の売上は30億円。
前年度を比較すると約83.3%成長。
つまり前年に比較すると売上が落ちている
ことになる。
(トイホビーの売上は19億円)

各年度別売上高

f:id:snofra:20190721232458p:plain

f:id:snofra:20190722000211p:plain

集計対象としては、具体的にコンテンツ名が表示されているコンテンツ全体とトイホビー(玩具、カプセルトイ、カード、菓子・食品、アパレル、生活用品、プラモデル、景品、文具)としている。

アイカツ!」シリーズはトイホビーのトイホビーだけではなく、
CDやBlu-ray Disc等の映像音楽についてもバンダイナムコグループのため、決算短信の売上高の対象となっている。


シリーズ別売上高

バンダイナムコ公式の集計ではありません。
※シリーズで開始時期が異なるので、四半期単位でまとめています。

f:id:snofra:20190721234015p:plain

f:id:snofra:20190722001902p:plain


売上高は計画に通り

1年の計画に対して売上がどうだったかを見てみる。
f:id:snofra:20190722002204p:plain
この図は1年間の計画(通期計画)に対して、売上高(実績値)が達成できたのかどうかを表しています。
赤線は各四半期ごとの売上高、緑色の棒グラフは累計値です。
通期計画が下方・上昇修正しているのは売上高の好調不調により調整されるものです。


面白いなと思うのが2013年度の売上が伸びたが、2014年度はこの売上は続かないだろうと第1四半期計画の時点で下方修正されているところ。
そこから2015年度まで大きく下方修正していく。

2015年度は「アイカツ!あかりジェネレーション」時期で、2014年度から2015年度第1四半期計画の計画からも刷新したほうが良いという判断を下したのではないかと推測できる。

逆に2016年度の『アイカツスターズ!』の期待値が高く、またスマホゲーム『アイカツ! フォトonステージ!!』がサービス開始していたり、ある程度V字回復していくのではと考えられていたよう。
残念ながら結果としては経営層が期待する結果に着地することができなかった。


2017年度は通期まで決算短信に出てこなかったため、コアコンテンツとして単体で報告すべき優先度が下がっているように思える。
それがイコールコンテンツがなくなるとは考えにくいが、ファンとしては決算短信に名前が出てきたほうが安心する。


2018年度は決算短信に再び記載されるようになった。
計画値としてはほぼ横ばいで想定通りな感じだったが、最終的に5億円下方修正して計画達成。

アイカツ!5周年イヤーであり、5周年にちなんだアイテムや『アイカツ!シリーズ 5thフェスティバル!!』が開催されていたり、過去ファンに向けた施策を打っていたが結果としては振るわずということだろうか。

毎年開催されていた「アイカツ!ミュージックフェスタ」が開催されなかった。歌唱担当=声優になり現行シリーズの曲ストックがないという問題もあるように思える。


来年度の計画値は2018年度と同じと見越している。
現在『アイカツフレンズ!』第2シーズン「かがやきのジュエル」編がデータカードダスとアニメで展開されているが、これらの売上は現状維持になると思われる。

また、決算短信のIPクリエイション事業にIP創出強化の言及があるので、『バンダイナムコエンターテインメントフェスティバル』などイベントで「アイカツ!」シリーズが展開されることが多いのかなと思う。

【技術編】アイカツ!シリーズのCD売上から見えてくるもの

先週書いたアイカツ!のCD売上分析で使用したpytonコードをメモとして記載しておく。 分析自体については以下を参照ください。
snofra.hatenablog.com

今回のデータ分析について


f:id:snofra:20180611001537p:plain
今回はデータ量も大したことなかったので手動でデータをcsvに加工して作業をしていた。
JINの投稿データは収集に1時間かかってマジクソと思ったんだけど、クローラ作るほど何度もJINは利用しないので頑張った。

データの加工はpandasで簡単にやりつつ、描画はbokehにしてpandasのdataframeをそのまま取り込むことにした。

実装量はかなり少ない。変に自前でサマリする処理加えるよりも楽だったというのが正直なところ。


実装


ロード部

import pandas as pd
import numpy as np
import math
# bokeh系のライブラリ
from bokeh.plotting import figure, output_file, show
from bokeh.io import output_notebook
from bokeh.models import ColumnDataSource,  Range1d, LinearAxis
output_notebook()

# csvファイルのロード
# アイカツ!のCD売上
df_aikatsu_cd = pd.read_csv('C:/xxxx/cd_sales.csv', parse_dates=[2], engine='python', encoding="utf-8")
# googleTrends
df_trend = pd.read_csv('C:/xxxx/multiTimeline.csv', parse_dates=[0], engine='python', encoding="utf-8")
# JINのアイカツでヒットした投稿数
df_jin = pd.read_csv('C:/xxxx/jin.csv', parse_dates=[0], engine='python', encoding="utf-8")

sales_day_all = df_aikatsu_cd.fillna('0')


加工部

# 発売月単位でサマリする
df = pd.DataFrame({'発売月' : sales_day_all["発売日"].dt.strftime('%Y-%m'),
                   '初動枚数' : sales_day_all.fillna('0')["初動枚数"].str.replace(',','').astype('int'),
                   '累計枚数' : sales_day_all.fillna('0')["累計枚数"].str.replace(',','').astype('int'),
                   '売上高' : (sales_day_all.fillna('0')["セールス(円)"].str.replace(',','').astype('int') /10000).round(0) # 売上高(万円)単位で出すため除算
                  })

df_groupby = df.groupby("発売月",as_index=False).sum()

# 発売年単位でサマリする
df_groupby_year = pd.DataFrame({'発売年' : sales_day_all["発売日"].dt.strftime('%Y'),
                                '初動枚数' : sales_day_all.fillna('0')["初動枚数"].str.replace(',','').astype('int'),
                                '累計枚数' : sales_day_all.fillna('0')["累計枚数"].str.replace(',','').astype('int'),
                                '売上高' : (sales_day_all.fillna('0')["セールス(円)"].str.replace(',','').astype('int')/100000000).round(4) # 売上高(億円)単位で出すため除算 
                               })

df_groupby_year = df_groupby_year.groupby("発売年",as_index=False).sum()

# googleTrendsを月単位でサマリ
df_trend_month = pd.DataFrame({'月' : df_trend["週"].dt.strftime('%Y-%m'),
                               'トレンド数' : df_trend.fillna('0')["トレンド数"].astype('int')
                             })

df_groupby_trend = df_trend_month.groupby("月",as_index=False).sum()

# googleTrendsを年単位でサマリ
df_trend_year = pd.DataFrame({'年' : df_trend["週"].dt.strftime('%Y'),
                             'トレンド数' : df_trend.fillna('0')["トレンド数"].astype('int')
                             })

df_groupby_trend_year = df_trend_year.groupby("年",as_index=False).sum()


# JINのアイカツでヒットした投稿数のサマリ
df_jin = pd.DataFrame({'投稿月' : df_jin["投稿日"].dt.strftime('%Y-%m'),
                       'count' : df_jin.fillna('0')["count"].astype('int')
                      })

df_groupby_jin = df_jin.groupby("投稿月",as_index=False).sum()

年単位、月単位のサマリはDataframeでやるのが一番簡単かなということで、Pandasのdataframeのgroupbyとsum()で一気に集計している。

bokehの表示上の都合で、売上高の桁調整をここでやる。 0が多すぎるとうまく表示されない。

pandasのdataframeの一部カラムだけ表示したいなーと思って調べたので、今回は特に使うことないがメモだけしておく。

# pandasのdataframeの一部のカラムだけ表示する
print(df.loc[:,['発売月','初動枚数','累計枚数']])

f:id:snofra:20180610015241p:plain
こんな感じで表示される。


曲線近似の算出

# 曲線近似の算出
# 累計枚数
regression = np.polyfit(df_groupby.index, df_groupby['累計枚数'], 1)

print(df_groupby.index*regression[0] + regression[1])

# 売上高
regression_sales_amount = np.polyfit(df_groupby.index, df_groupby['売上高'], 1)

print(df_groupby.index*regression_sales_amount[0] + regression_sales_amount[1])

曲線近似の算出はnumpyとscipyがあるようだけど、今回はnumpyで実装。
ailaby.com
qiita.com

正直、曲線にする必要は全然なかった。
グラフのプロットはprint文のとおり、CD売上月とCDの累計売上数or売上高で1次関数で算出している。


グラフのplot

時系列データを使ったCD売上推移

# CD売上推移のグラフplot
df_groupby["発売月"] = pd.to_datetime(df_groupby["発売月"])

# CD売上のplot
p = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title="CD売上推移")
p.line(df_groupby["発売月"], df_groupby['累計枚数'], line_width=3.5, color="red", alpha=0.5)
p.circle(df_groupby["発売月"], df_groupby['累計枚数'], fill_color="white", line_color="red", size=10)

# 曲線近似のplot
p.extra_y_ranges = {"graph2": Range1d(start=0, end=11200)}
p.line(df_groupby["発売月"], df_groupby.index*regression[0] + regression[1], line_width=1,color="blue", alpha=0.5, y_range_name="graph2")

show(p)

グラフのX軸と、y軸の説明、グラフの凡例は多少加工しているが以下のような感じで出力される。
f:id:snofra:20180529000900p:plain

かなりシンプルに実装できる。 Dataframeをそのままグラフにplotできるbokehだからこそのシンプルさだと思う。

苦戦したのはX軸をうまく読んでくれないというのがあった。
というのも時系列データにする場合、型をちゃんと日付型にする必要があったんだけど、daraframeでは文字列型だったから。
日付型にto_datetimeしてから再度入れなおすという対応をした。

盛り上がっていた時期を調べるグラフ

df_groupby_trend["月"] = pd.to_datetime(df_groupby_trend["月"])
p = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title="盛り上がっていた時期は?")

# CD売上のplot
p.line(df_groupby["発売月"], df_groupby['累計枚数'], line_width=3.5, color="red", alpha=0.5)
p.circle(df_groupby["発売月"], df_groupby['累計枚数'], fill_color="white", line_color="red", size=10)

# googletrendsのplot
p.extra_y_ranges = {"graph2": Range1d(start=1, end=500)}
p.line(df_groupby_trend["月"], df_groupby_trend['トレンド数'], line_width=3.5, color="green", alpha=0.5, y_range_name="graph2")

show(p)

これもグラフのX軸と、y軸の説明、グラフの凡例は多少加工しているが以下のような感じで出力される。
f:id:snofra:20180530002546p:plain

JINとの比較グラフ

df_groupby_jin["投稿月"] = pd.to_datetime(df_groupby_jin["投稿月"])
p = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title="GoogleTrendsの検索数vsJINの記事数")

# googletrendsのplot
df_groupby_trend["月"] = pd.to_datetime(df_groupby_trend["月"])
p.line(df_groupby_trend["月"], df_groupby_trend['トレンド数'], line_width=3.5, color="green", alpha=0.5)

# JINの投稿数のplot
p.extra_y_ranges = {"graph2": Range1d(start=0, end=20)}
p.line(df_groupby_jin["投稿月"], df_groupby_jin['count'], line_width=3.5, color="blue", alpha=0.5, y_range_name="graph2")
p.add_layout(LinearAxis(y_range_name="graph2"), 'right')

show(p)

基本は前のものと同じなんだけど、このグラフはグラフの右側にメモリを表示するようにした。メモリがあったほうが分かりやすいかなという判断。
p.add_layout(LinearAxis(y_range_name="graph2"), 'right')


折れ線グラフを棒グラフにしたパターンも試してみているので、メモしておく

df_groupby_trend["月"] = pd.to_datetime(df_groupby_trend["月"])
df_groupby_jin["投稿月"] = pd.to_datetime(df_groupby_jin["投稿月"])
p = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title="GoogleTrendsの検索数vsJINの記事数")

# googletrendsのplot
p.line(df_groupby_trend["月"], df_groupby_trend['トレンド数'], line_width=3.5, color="green", alpha=0.5)

# JINの投稿数のplot
p.extra_y_ranges = {"graph2": Range1d(start=0, end=20)}
p.vbar(x=df_groupby_jin["投稿月"], width=1, bottom=0,
       top=df_groupby_jin['count'], color="blue", y_range_name="graph2")

show(p)

その場合はこのようにplotされる。
f:id:snofra:20180610024701p:plain

相関係数の算出

# 相関係数の算出
correlation = np.corrcoef(df_groupby_trend_year["トレンド数"].values.tolist(), df_groupby_year["累計枚数"].values.tolist())
print(correlation[0,1])

相関係数を出すために各dataframeをarrayにしている。 月単位も年単位も同じやろってことで年単位にしてる。

numpy.corrcoef — NumPy v1.14 Manual

f:id:snofra:20180610233329p:plain
相関係数0.75はまあまあ相関しているのかなーっと。

売上高の算出

p = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title="CD売上高推移")

# 売上高のplot
p.line(df_groupby["発売月"], df_groupby['売上高'], line_width=3.5, color="red", alpha=0.5)
p.circle(df_groupby["発売月"], df_groupby['売上高'], fill_color="white", line_color="red", size=10)

# 曲線近似のplot
p.extra_y_ranges = {"graph2": Range1d(start=1000, end=2000)}
p.line(df_groupby["発売月"], df_groupby.index*regression_sales_amount[0] + regression_sales_amount[1], line_width=1,color="graph2", alpha=0.5, y_range_name="foo")

show(p)

グラフの実装は前と同じなので、言うことは特にない。
f:id:snofra:20180605001140p:plain


売上高の算出

# 年別売上高
#売上高でソートしてdataframeのindex振り直し
df_groupby_year_tmp = df_groupby_year.sort_values('売上高').reset_index(drop=True)

# 売上高のplot
p = figure(plot_width=400, plot_height=500, title="年別売上高")

p.hbar(y=df_groupby_year_tmp.index, height=0.5, left=0,
       right=df_groupby_year_tmp['売上高'], color="firebrick")

show(p)

plotするときにソートができそうになかったので、事前にソートを実施。
その時にdataframeのindexを振りなおさないと、結局plotしたときにソートが意味なくなってしまうので、dataframeのindexを振りなおして、y軸にindexを設定している。

ただこれをやるとy軸のメモリの凡例が0~6になるという問題があって、俺は結局グラフの加工でカバーすることにした。
うまい方法ないのかなー。
f:id:snofra:20180605004057p:plain


実装

簡単なplotなのでそんなに実装に時間はかかっていないけど、加工するのめんどい。
けど、加工したほうが見やすいっていうのをここのブログ見て理解した。

speakerdeck.com

shinyorke.hatenablog.com

今まではplotしてはいできたって、実装視点でしか見てなかったんだけど、実際はあれじゃわかりにくかったよなーと。
グラフ中に分かりやすく書いてあげるってのが必要だと思ってた。
気づかせてくれてありがとうございますっていう気持ち。

【技術編】『ラジカツスターズ!』全102回とコーナー全364問から見えてくるもの

先週書いた『ラジカツスターズ!』の分析で使用したpytonコードをメモとして記載しておく。 snofra.hatenablog.com

実装


ロード部

import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
import numpy as np
import datetime
%pylab inline --no-import-all

#csvファイルのロード
df = pd.read_csv('C:/Users/xxxx/radikatsu_stars_list.csv', index_col=0, parse_dates=[1], engine='python')
df = df.fillna(0)
radikatsu = df.loc[0:] # todo 正解数の抽出できなかったので加工


データ加工部

on_air = radikatsu.index # 放送回数
series_correct_num = radikatsu['正解数'].astype(int) # 正解数全量
series_question_num = radikatsu['出題数'].astype(int) # 出題数全量

# 正解数と出題数をそれぞれ累計
list_cumulative_correct = [] # 正解数
list_cumulative_question = [] # 出題数
list_cumulative_correct_rate = [] # 正解率
cumulative_correct = 0
cumulative_question = 0
cumulative_correct_rate = 0

for x, y in zip(series_correct_num, series_question_num):
    cumulative_correct = cumulative_correct + x
    cumulative_question = cumulative_question + y
    # 暫く0が続くのでその場合は処理を行わない
    if x != 0 and y !=0:
        cumulative_correct_rate = (cumulative_correct / cumulative_question) * 100
    list_cumulative_correct.append(cumulative_correct) # 正解数累計
    list_cumulative_question.append(cumulative_question) # 問題数累計
    list_cumulative_correct_rate.append(cumulative_correct_rate) # 正解率累計

# 各累計数をDataFrameにする
cumulative = pd.DataFrame(
    {'01.on_air': on_air,
     '02.question': list_cumulative_question,
     '03.correct': list_cumulative_correct,
     '04.rate': list_cumulative_correct_rate})
print(cumulative)
cumulative.to_csv("cumulative.csv")

こんな感じで出力 f:id:snofra:20180409003650p:plain


データ加工部(メンバー別)

def member_answer(member):

    #担当回チェック
    list_question = [] #問題数
    list_correct = [] #正解数
    list_date = [] #放送年月
    list_cast = [] #パーソナリティ回数
    bfr_date = None
    question_num = 0
    answer_num = 0
    cast_num = 0
    len_cur = len(radikatsu) 
    # DataFrameの行単位でループ
    for index, i in enumerate(radikatsu.iterrows()):
        # tupleの値部分の取得
        series_row = i[1]

        target = series_row['公開日'].strftime("%Y-%m")

        # パーソナリティ回だった場合
        if series_row[member] == 1:
            # 同年月だった場合累計
            if target == bfr_date:
                question_num = question_num + series_row['出題数']
                answer_num = answer_num + series_row['正解数']
                cast_num = cast_num + 1

        # 1行目もしくは年月が切り替わった場合
        if target != bfr_date and bfr_date is not None:
            list_date.append(bfr_date)
            list_question.append(question_num)
            list_correct.append(answer_num)
            list_cast.append(cast_num)
            # パーソナリティ回だった場合、その年月を設定しなおす。
            if series_row[member] == 1:
                question_num = series_row['出題数']
                answer_num = series_row['正解数']
                cast_num = 1
            else:
                question_num = 0
                answer_num = 0
                cast_num = 0

        bfr_date = target

        # 最終行の追加
        if len_cur == index + 1:
            list_date.append(target)
            list_question.append(question_num)
            list_correct.append(answer_num)
            list_cast.append(cast_num)

    # 年月別の問題/回答数
    member_correct = pd.DataFrame(
        {'01.year': list_date,
         '02.question': list_question,
         '03.correct': list_correct,
         '04.parsonarty': list_cast})
    print(member_correct)
    member_correct.to_csv("member_correct" + member + ".csv")
    
    # 問題数/正解数/正解率の最大値の取得
    question_crr = 0
    answer_crr = 0
    cast_crr = 0
    for x, y, z in zip(list_question, list_correct, list_cast):
        question_crr = question_crr + x
        answer_crr = answer_crr + y
        cast_crr = cast_crr + z
    crr_ans_rate = (answer_crr / question_crr) * 100
   
    # jupiterを日本語対応していないので、適当にタイトルを設定
    if member == "るか":
        title = "Ruka's Question/Correct num"
    elif member == "みき":
        title = "Miki's Question/Correct num"
    elif member == "かな":
        title = "Kana's Question/Correct num"
    elif member == "みほ":
        title = "Miho's Question/Correct num"
    elif member == "ななせ":
        title = "Nanase's Question/Correct num"
    elif member == "せな":
        title = "Sena's Question/Correct num"
    else:
        title = "Rie's Question/Answer num"
        
    # X軸表示用に年月分連番を設定しておく
    serial_no = [index + 1 for index, i in enumerate(list_date)]
    
    # plot
    plt.figure(figsize=(20, 10), dpi=100, linewidth = 100)
    ax = plt.subplot()
    ax.bar(serial_no, list_question, color='#44A5CB', align="center", label="Question num") # 問題数
    ax.bar(serial_no, list_correct, color='#EDAD0B', align="center", label="Correct num") # 正解数
    plt.ylabel("Question/Correct num", fontsize=15)
    ax.legend(loc=2) # 凡例
    plt.title(title, fontsize=15)
    plt.yticks( np.arange(0, 20, 1) )
    plt.xticks(serial_no, list_date, rotation = 90)
    plt.savefig(member + '.png') # グラフのダウンロード
    plt.show()

    return question_crr, answer_crr, crr_ans_rate, cast_crr


メンバー別の表示

list_question_cum = []
list_answer_cum = []
list_crr_ans_rate = []
list_cast_cum =[]

# るか
question_ruka, answer_ruka, rate_ruka, cast_ruka = member_answer('るか')
list_question_cum.append(question_ruka)
list_answer_cum.append(answer_ruka)
list_crr_ans_rate.append(rate_ruka)
list_cast_cum.append(cast_ruka)
# みき
question_miki, answer_miki, rate_miki, cast_miki = member_answer('みき')
list_question_cum.append(question_miki)
list_answer_cum.append(answer_miki)
list_crr_ans_rate.append(rate_miki)
list_cast_cum.append(cast_miki)
# かな
question_kana, answer_kana, rate_kana, cast_kana = member_answer('かな')
list_question_cum.append(question_kana)
list_answer_cum.append(answer_kana)
list_crr_ans_rate.append(rate_kana)
list_cast_cum.append(cast_kana)
# みほ
question_miho, answer_miho, rate_miho, cast_miho = member_answer('みほ')
list_question_cum.append(question_miho)
list_answer_cum.append(answer_miho)
list_crr_ans_rate.append(rate_miho)
list_cast_cum.append(cast_miho)
# ななせ
question_nanase, answer_nanase, rate_nanase, cast_nanase = member_answer('ななせ')
list_question_cum.append(question_nanase)
list_answer_cum.append(answer_nanase)
list_crr_ans_rate.append(rate_nanase)
list_cast_cum.append(cast_nanase)
# せな
question_sena, answer_sena, rate_sena, cast_sena = member_answer('せな')
list_question_cum.append(question_sena)
list_answer_cum.append(answer_sena)
list_crr_ans_rate.append(rate_sena)
list_cast_cum.append(cast_sena)
# りえ
question_rie, answer_rie, rate_rie, cast_rie = member_answer('りえ')
list_question_cum.append(question_rie)
list_answer_cum.append(answer_rie)
list_crr_ans_rate.append(rate_rie)
list_cast_cum.append(cast_rie)

# メンバー別の問題数/正解数/正解率
crr_ans_rate = pd.DataFrame(
    {'01.member': ['るか', 'みき', 'かな', 'みほ', 'ななせ', 'せな', 'りえ'],
     '02.question': list_question_cum,
     '03.answer': list_answer_cum,
     '04.correct_rate': list_crr_ans_rate,
     '05.parsonaroty': list_cast_cum
    })
print(crr_ans_rate)
crr_ans_rate.to_csv("crr_ans_rate.csv")

メンバー別、月別で問題数と正解数を作ったんだけど。分析するにはいまいち言うこともないし、微妙じゃね?ということで結局載せなかった。

f:id:snofra:20180409004757p:plain f:id:snofra:20180409004841p:plain f:id:snofra:20180409004806p:plain f:id:snofra:20180409004815p:plain f:id:snofra:20180409004823p:plain f:id:snofra:20180409004829p:plain f:id:snofra:20180409004835p:plain


せなとみほの正解数推移比較

# せな
list_sena = []
# DataFrameの行単位でループ
for index, i in enumerate(radikatsu.iterrows()):
    # tupleの値部分の取得
    series_row = i[1]

    target = series_row['公開日'].strftime("%Y-%m")

    # パーソナリティ回だった場合
    if series_row['せな'] == 1:
        list_sena.append(series_row['正解数'])

# みほ
list_miho = []
# DataFrameの行単位でループ
for index, i in enumerate(radikatsu.iterrows()):
    # tupleの値部分の取得
    series_row = i[1]

    target = series_row['公開日'].strftime("%Y-%m")

    # パーソナリティ回だった場合
    if series_row['みほ'] == 1:
        list_miho.append(series_row['正解数'])

# seabornでせなとみほの結果をカーネル密度推計でplot
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(20, 10), dpi=100, linewidth = 100)
plt.xticks([0,1,2,3,4,5])
plt.tick_params(labelsize=18)
plt.xlabel('Answer', fontsize=18)

sns.distplot(pd.DataFrame({'sena':list_sena}), rug=True, hist=False, color = 'pink', kde_kws={'label':'sena'})

sns.distplot(pd.DataFrame({'sena':list_miho}), rug=True, hist=False, color = 'purple', kde_kws={'label':'miho'})
plt.legend(fontsize=18)
plt.savefig('sena_vs_miho.png')

できた画像を多少加工しているけど、こんな感じのグラフができる。 f:id:snofra:20180403000309p:plain

みほ、せなコンビで今度ライブをやるとのことで、ラジカツ優秀勢きたなって思いました(小並感)


メンバー別の表示

# メンバー別質問数と正解数、正解率
appr_member = [1,2,3,4,5,6,7] # X軸の表示用

# plot
plt.figure(figsize=(20, 10), dpi=100, linewidth = 100)
ax = plt.subplot()
ax.bar(appr_member, list_question_cum, color='#44A5CB', align="center", label="Question num") # 問題数
ax.bar(appr_member, list_answer_cum, color='#EDAD0B', align="center", label="Correct num") # 正解数
ax.legend(loc=2) # 凡例
plt.yticks( np.arange(0, max(list_question_cum)+3, 10) )
plt.xticks(appr_member, ['ruka', 'miki', 'kana', 'miho', 'nanase', 'sena', 'rie'], rotation = 90, fontsize=15)
plt.ylabel("Question num", fontsize=15)

ax2 = ax.twinx()
ax2.plot(appr_member, list_crr_ans_rate, linewidth=5, marker='o', markersize=10, color='#C7243A') # 正解率
plt.ylabel("Correct answer rate", fontsize=15)
plt.savefig('Correct answer rate.png') # グラフのダウンロード
plt.show()

すでに用意していたのをベースに作業していたんだけど、ここはmatplotlibよりもbokehにしようと思ってた。
だけど、bokehだとグラフを複数使用したときのY軸が左右にでなくて、右のY軸にメモリなきゃ雰囲気グラフやんと思って、結局matplotlibのままでいくことにした。

ちなみにこんなグラフが出る。
f:id:snofra:20180402010014p:plain


番組への貢献度表示

# 番組への貢献度をbokehでプロット
from bokeh.plotting import figure, output_file, show
from bokeh.io import output_notebook
from bokeh.models import ColumnDataSource, LabelSet, Range1d
output_notebook()

source = ColumnDataSource(data=dict(ans=list_crr_ans_rate,
                                    cast=list_cast_cum,
                                    names=['るか', 'みき', 'かな', 'みほ', 'ななせ', 'せな', 'りえ'],
                                    colors=['orange', 'green', '#ef5285', 'purple', 'blue', 'pink', '#00b9f1']))

p = figure(title = "誰が番組に貢献したか", x_range=Range1d(30, 40),y_range=Range1d(10, 40))
p.xaxis[0].axis_label = 'パーソナリティ回数'
p.yaxis[0].axis_label = '正解率(%)'

p.scatter(x='cast', y='ans', size=8, source=source, color='colors')
labels = LabelSet(x='cast', y='ans', text='names', level='glyph',
              x_offset=5, y_offset=5, source=source, render_mode='canvas')
p.add_layout(labels)

show(p)

こんな感じのグラフが出る。 f:id:snofra:20180402005825p:plain

matplotlibとどう違うんだってのも見てみたけど、実装の楽さや見栄え見てもbokehがいいね。
matplotlibはDataFrame使えないし、色をこまめに変えたい場合実装行数が多くなるのが難点。

matplotlibでの散布図実装

お試しでやってみたので併せて載せておく。

# dataFrameの作成
cumulative = pd.DataFrame(
    {'names': ['るか', 'みき', 'かな', 'みほ', 'ななせ', 'せな', 'りえ'],
     'ans': list_crr_ans_rate,
     'cast': list_cast_cum,
     'colors': ['orange', 'green', '#ef5285', 'purple', 'blue', 'pink', '#00b9f1']})

# メンバー別にパージしておく
ruka = cumulative[0:1]
miki = cumulative[1:2]
kana = cumulative[2:3]
miho= cumulative[3:4]
nanase = cumulative[4:5]
sena = cumulative[5:6]
rie = cumulative[6:7]

# plot
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.scatter(ruka["cast"], ruka["ans"], c=ruka["colors"])
ax.annotate("ruka",xy=(ruka["cast"], ruka["ans"]),size=10)

ax.scatter(miki["cast"], miki["ans"], c=miki["colors"])
ax.annotate("miki",xy=(miki["cast"], miki["ans"]),size=10)

ax.scatter(kana["cast"], kana["ans"], c=kana["colors"])
ax.annotate("kana",xy=(kana["cast"], kana["ans"]),size=10)

ax.scatter(miho["cast"], miho["ans"], c=miho["colors"])
ax.annotate("miho",xy=(miho["cast"], miho["ans"]),size=10)

ax.scatter(nanase["cast"], nanase["ans"], c=nanase["colors"])
ax.annotate("nanase",xy=(nanase["cast"], nanase["ans"]),size=10)

ax.scatter(sena["cast"], sena["ans"], c=sena["colors"])
ax.annotate("sena",xy=(sena["cast"], sena["ans"]),size=10)

ax.scatter(rie["cast"], rie["ans"], c=rie["colors"])
ax.annotate("rie",xy=(rie["cast"], rie["ans"]),size=10)
ax.set_xlabel('parsonality num')
ax.set_ylabel('correct per')

plt.savefig('matplotlib.png') # グラフのダウンロード
plt.show()

f:id:snofra:20180409010928p:plain
これだったらbokehでいいかなー

ラジカツスターズ!のコーナーのメンバー別正解数を図で確認する

f:id:snofra:20170415025404p:plain

pythonの勉強も兼ねて、『私達、AIKATSU☆STARS!です!!スターズ!!!』で誰が1回の放送で安定して回答しているのかを見てみる。

『私達、AIKATSU☆STARS!です!!スターズ!!!』というコーナーは、ひとつに質問に対して回答を合わせるという趣旨のもの。


条件

  • 『私達、AIKATSU☆STARS!です!!スターズ!!!』の正解数は、前回の情報を元に抽出。(49回目まで)
  • コーナー自体やっていない場合(初回など)は計算から除外


図にするとよりわかりやすい「るか」と「みほ」

細かいことを言う前に結果。

f:id:snofra:20170604012230p:plain

この図はカーネル密度推定で算出。まあ気にしなくていいっす。
とにかくこの図で各メンバーが1回のコーナーで何回正解できているんだ?ってのを示してる。
このグラフで言うと、一番わかりやすいのが「るか」と「みほ」


圧倒的に答えられない「るか」と、確実に1回は正解する「みほ」

この図から見ても、るかは回答数が0でコーナーが終わるのがかなり多く、このコーナーの正当率を引き下げている要因と言っても仕方ないかと。

まあ、るかの回答は大分アレでしたし
将棋の駒一つ挙げろって言われてキングとか、ピザに乗っているものでピザソースとか、正解数0が多いのも納得ですわ。

逆にみほは回答数0が少なく、確実に1問は正解する傾向が強いけど、2問以上の爆発的な回答はしない傾向にあるよう。
正解数3~4の数が極端に少ない*1

爆発的な回答をするのは「せな」と「ななせ」になるよう。

せなは比較的安定して正解しているのと、ななせは回答数が2問目で落ち込んで、それを超えるとブーストして正解数が上がってくる。

ラジオでも正解数が増えるとどんどんいこうみたいなスタイルになるのが、ななせの特徴のように感じている(勝手に)ので、その結果が表れているのかなあ。


自分の備忘録

ここから細かい話。
統計とかの話なので興味あればレベル。自分の備忘録として。

今回、この図を出すにあたり、カーネル密度推定を使用したんだけど、それって何ぞやってところをメモ。

ウィキペディアにも書いているんだけど、まあ俺のようなアホにはちょっと何言ってんのかよくわからん。
カーネル密度推定 - Wikipedia

ざっくりいうと、ヒストグラムの棒グラフを滑らかにしたもの。

でもって、ヒストグラムについてはここを見てもらうと分かりやすい
ヒストグラムはある程度で区切ってその区切った範囲でどのくらい数があるのかなってこと。

今回で言うと、1回のコーナーで回答数が何回だったの?ってのを示したかった。
正直ヒストグラムでやる必要はないわな。0~4という区切るのにしても少なすぎるし。

カーネル密度推定がヒストグラムの棒グラフを滑らかにしたものなら、そもそもヒストグラムのままでもいいじゃんみたいな感じある。

だけど、ヒストグラムでやると棒が重なるので視覚的に難がある。
f:id:snofra:20170604013528p:plain

箱ひげ図はあまりなじみがないだろうし、このメンバーの感じだとちょっと説明しにくい。
f:id:snofra:20170604013710p:plain
正直この結果で何を書けばいいんだくらいのヤツ。

ということで、カーネル密度推定で表示してみようと思った次第。

ソースコード

さらにメモ用でソースコードを記載。jupyter notebookから書いているので、ほかので流用する場合はもうちょっと何かいる。

# もろもろ宣言
import pandas as pd
from pandas import Series
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
 
#csvファイルの読込。カラムは左からname/on_air/Question/Ans
df = pd.read_csv('C:/Users/xxxxx/yyyy.csv')
 
#取り込んだcsvファイルの中からAns列だけ取ってくる関数
def getdf(name):
   getAns =Series(df[df['name'] == name].Ans, name='Answer')
   return getAns
 
#各メンバーのコーナー1回での回答数を取得
RukaAnsSe=getdf('ruka')
MikiAnsSe=getdf('miki')
KanaAnsSe=getdf('kana')
MihoAnsSe=getdf('miho')
NanaseAnsSe=getdf('nanase')
SenaAnsSe=getdf('sena')
RieAnsSe=getdf('rie')
 
#カーネル密度推定を算出してグラフに書いて保存
plt.figure(figsize=(20, 10), dpi=100, linewidth = 100)
plt.xticks([0,1,2,3,4,5])
plt.tick_params(labelsize=18)
plt.xlabel('Answer', fontsize=18)
sns.distplot(RukaAnsSe,
      rug=True,
      hist=False,
      color = 'skyblue',
      kde_kws={'label':'Ruka'})
sns.distplot(MikiAnsSe,
      rug=True,
      hist=False,
      color = 'green',
      kde_kws={'label':'Miki'})
sns.distplot(MihoAnsSe,
      rug=True,
      hist=False,
      color = 'indigo',
      kde_kws={'label':'Miho'})
sns.distplot(KanaAnsSe,
      rug=True,
      hist=False,
      color = 'orange',
      kde_kws={'label':'Kana'})
sns.distplot(NanaseAnsSe,
      rug=True,
      hist=False,
      color = 'dodgerblue',
      kde_kws={'label':'Nanase'})
sns.distplot(SenaAnsSe,
      rug=True,
      hist=False,
      color = 'hotpink',
      kde_kws={'label':'Sena'})
sns.distplot(RieAnsSe,
      rug=True,
      hist=False,
      color = 'darkblue',
      kde_kws={'label':'Rie'})
plt.legend(fontsize=18)
plt.savefig('rajikatsu1.png')
 
 
#以下説明用
#ヒストグラムを作る
plt.figure(figsize=(20, 10), dpi=100, linewidth = 100)
plt.xticks([0,1,2,3,4,5])
#normedでヒストグラムを1になるように積分する(正規化処理を加える)
plt.hist(RukaAnsSe, bins=4, color='skyblue', normed=True, alpha=0.5, label='ruka')
plt.hist(MikiAnsSe, bins=4, color='green', normed=True, alpha=0.3, label='Miki')
plt.savefig('rajikatsu2.png')
 
#箱ひげ図
plt.figure(figsize=(20, 10), dpi=100, linewidth = 100)
sns.boxplot(data=[RukaAnsSe, MikiAnsSe, MihoAnsSe, KanaAnsSe, NanaseAnsSe, SenaAnsSe, RieAnsSe])
plt.xticks([0,1,2,3,4,5,6],['Ruka','Miki','Miho','Kana','Nanase','Sena','Rie'], fontsize=18)
plt.ylabel('Answer', fontsize=18)
plt.yticks([-1,0,1,2,3,4,5],['-1','0','1','2','3','4','5'], fontsize=18)
plt.savefig('rajikatsu3.png')

参考サイト

https://matplotlib.org/2.0.0/examples/color/named_colors.html
http://seesaawiki.jp/met-python/d/matplotlib#content_2_8
http://qiita.com/supersaiakujin/items/be4a78809e7278c065e6
http://qiita.com/Tatejimaru137/items/4ee6a73114d07d85bfd7

https://matplotlib.org/api/figure_api.html

*1:コーナー1回の総質問数が少ないからかと思ったけどそんなことない

アイカツ!声優38人はアイマス系列作品・グランブルーファンタジーに出ているのか

f:id:snofra:20160410164330j:plain

少し前に「アイカツ!」シリーズ出演声優は、「アイドルマスター」シリーズ作品に参戦していることが多いみたいな話があって、本当にそうなのかと思ったので確認してみる。
アニメも始まった『グランブルーファンタジー』ではどうなのかも確認してみる*1


分析内容について

Wikipediaの「アイカツ!の登場人物一覧」および「アイカツスターズ!の登場人物」のデータカードダスでのプレイヤーキャラクターを演じている声優が対象

・対象声優のWikipedia内の役から、「アイドルマスター」シリーズ作品及び『グランブルーファンタジー』の出演作品を確認

・諸星 すみれさん、田所 あずささん、大橋 彩香さん、上田 麗奈さんについては、『アイカツ!』を集計対象とする


アイカツ!」シリーズ出演声優のキャラ一覧

f:id:snofra:20170423163113p:plain

ぱっと見「アイドルマスター」シリーズも『グランブルーファンタジー』も共にそんなに出ている人が多いわけではなさそうに見える。

アイカツスターズ!』の声優は新人が多いからか、「アイドルマスター」シリーズおよび『グランブルーファンタジー』共に出ていないよう。


アイドルマスター」シリーズに出ている人の大半はシンデレラガールズに出ている

f:id:snofra:20170423163129p:plain

アイカツ!」シリーズ出演声優の大半は「アイドルマスター」シリーズに出ていないようで、まだまだ50%にも満たしていない状況。
また既にアイドルマスターで声を貰っている人のほとんどが『アイドルマスター シンデレラガールズ』に出ている。

この差はそもそものキャラクター数の違いで、シンデレラガールズが圧倒的にキャラ数が多いのでこの状況になっている。
今回以降もこの比率は変わらないんじゃないかと思う。


グランブルーファンタジー』に出ている人はほとんどプレイヤーキャラクター

f:id:snofra:20170423163140p:plain

グランブルーファンタジー』だけに注目し、出演しているのかいないのかを確認。
やってない間にアイマスコラボイベやら、新キャラやらでキャラクター数も増えたので、出演比率は50%。

プレイアブルキャラクターに昇格したキャラクターも結構いたので、NPCも減っている。
ジオラはプレイアブルは難しいような気もするが、「舞い歌う五花」の続編があればある、のか?


アイドルマスター」シリーズと『グランブルーファンタジー』共に出ている人はほとんどいない

f:id:snofra:20170423163152p:plain

アイドルマスター」シリーズと『グランブルーファンタジー』共に出ている人は8人しかいないので、ほとんどの人がどちらか片方にしか出ていないか両方とも出ていない。

アイカツ!」シリーズ出演声優は「アイドルマスター」シリーズと『グランブルーファンタジー』のどちらかには出ているよう。
アイドルマスター シンデレラガールズ』はまだまだ声のないキャラクターが多いので、そこに入ってくる可能性はかなり大きいと思う。

グラブルでのSoleil

f:id:snofra:20170423165344p:plain

*1:単に俺がグラブルプレイヤーだからなだけです