環境
ubuntu 16.04 LTS
python 3.7.3
やること
Googleが運営しているmaterial.ioに最近追加されたData Visualizationのページを参考に、遊戯王カードに関するデータセットからmatplotlib(一部pandas)でチャートを作成していきます。
material.ioとData Visualizationについて
Googleが自社のマテリアルデザインに関する情報を統合的に公開しているもので、UIの開発ツールも提供されています。そこに最近追加されたのがこのData Visualizationに関するページです。
これが結構為になるので皆様と共有していきたい所存です。特に、Do/Don'tという形で具体的なチャートを分かりやすく例示していて、今回はこのDo/Don'tの一部を遊戯王データセットとmatplotlibを使って確認していこうと思います。
遊戯王データセットについて
今回は以下のデータを使わせてもらいます。
https://www.kaggle.com/tathor/yugioh-trading-cards-dataset
ygoprodeck APIに登録されているカードから6534枚のデータを抽出していて、古いものからサイバースなどマスタールール4のカードまで全般的に含まれています。とりあえずどんなデータか見てみましょう。ついでに今回使うライブラリもすべて読み込んでしまいます。jupyter-notebookで実行しているので注意してください。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set()
df = pd.read_csv('yugioh-trading-cards-dataset/card_data.csv', sep=',')
df.head(10)
注意すべきポイントは2つです。あとは見ればすぐに分かると思います。
①「Type」はモンスター・魔法・トラップ・トークンの分類で、モンスターのみノーマル、効果、リバースなど詳細な種類で記録されている
②「Race」はモンスター・トークンの場合は種族、魔法・罠の場合は通常・永続・カウンターといった種類が記録されている
それではDo/Don’tを参考にチャートを作成していきます。
①闇/光属性間でモンスターのレベルの割合を比較する
Do.
Use bar charts to show changes over time or differences between categories.
Don’t.
Don’t use multiple pie charts to show changes over time. It’s difficult to compare the difference in size across each slice of the pie.
こんな感じで、まずData VisualizationのページのDo/Don'tを引用し、次に遊戯王データセットによるチャートを作成します。ただし、Before/Afterにしたいので、順番はDon't/Doにしてます。
# データ抽出・カテゴリー変数の追加
df_Monster = df[df['Type'].str.match('.*Monster')]
df_Monster['Rank'] = '1 - 4'
df_Monster.loc[df_Monster['Level']>=5, 'Rank'] = '5 - 6'
df_Monster.loc[df_Monster['Level']>=7, 'Rank'] = '7 - 8'
df_Monster.loc[df_Monster['Level']>=9, 'Rank'] = '9 - 12'
x = df_Monster[df_Monster['Attribute']=='DARK']
y = df_Monster[df_Monster['Attribute']=='LIGHT']
# Don't
x = x.Rank.value_counts(True)
y = y.Rank.value_counts(True)
label = ['1 - 4', '5 - 6', '7 - 8', '9 - 12']
plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.title('DARK')
plt.pie(x, startangle=90, labels=label, wedgeprops={'linewidth': 1, 'edgecolor':"white"}, counterclock=False)
plt.subplot(1, 2, 2)
plt.title('LIGHT')
plt.pie(y, startangle=90, labels=label, wedgeprops={'linewidth': 1, 'edgecolor':"white"}, counterclock=False)
plt.axis('equal')
# Do
df_LD = pd.concat([x, y], axis=1)
df_LD.columns = ['LIGHT', 'DARK']
ax = df_LD.sort_index(ascending=False).plot.barh()
vals = ax.get_xticks()
ax.set_xticklabels(['{:3.0f}%'.format(x*100) for x in vals])
モンスターレベルを下級・中級・上級で分け、ちょっとグループが少ないので上級だけは7-8と9-12で分けました。それを闇/光属性で比較しています。
Don't
Do
期間やカテゴリーが異なるデータの間で各割合を比較する場合、円グラフだとそれぞれの変化や違いが分かりにくいから棒グラフを使おうということですね。この場合はグループの数が4つと多くないので円グラフの方でも大小関係はそれなりに読み取れますが、やはり棒グラフの方が確実です。
②地属性モンスターのレベルごとの攻撃力(平均値)を他属性モンスターと比較する
Do.
Use a combination of color highlights and neutral colors to provide contrast and emphasis.
Caution.
Many colors in a single chart can hinder focus.
今回はDo/Cautionです。場合によってはアリだけどあまり好ましくない例。
# Don't
for i in ['DARK', 'FIRE', 'EARTH', 'WIND', 'LIGHT', 'WATER', 'DIVINE']:
df_atr = df[df['Attribute']==i]
plt.plot(df_atr.groupby('Level').mean().ATK, label=i)
plt.legend()
# Do
for i in ['DARK', 'FIRE', 'WIND', 'LIGHT', 'WATER', 'DIVINE']:
df_atr = df[df['Attribute']==i]
plt.plot(df_atr.groupby('Level').mean().ATK, color='silver')
df_EARTH = df[df['Attribute']=='EARTH']
plt.plot(df_EARTH.groupby('Level').mean().ATK, color='g')
横軸をレベル、縦軸を攻撃力の平均値として属性ごとに算出しています。
Don't
Do
これは一目瞭然ですね。線の数が多いこともそうですが、特にレベル1から8まではほとんど重なっていて、全てに色をつけるとかなり分かりにくいです。もし1対多でなく多対多の比較を想定していたとしても、上のようなチャートになってしまう場合は方法を変えるべきでしょう。
③戦士族/魔法使い族間でレベルごとの攻撃力/守備力の平均値を比較する
Do.
Vary a line’s texture to represent different data types.
Don’t.
Don't use different colors to show periodical variation for the same data category.
# データ抽出
df_W = df[df['Race']=='Warrior']
df_S = df[df['Race']=='Spellcaster']
# Don't
plt.plot(df_W.groupby('Level').mean().ATK[:8], label='ATK:Warrior')
plt.plot(df_S.groupby('Level').mean().ATK[:8], label='ATK:Spellcaster')
plt.plot(df_W.groupby('Level').mean().DEF[:8], label='DEF:Warrior')
plt.plot(df_S.groupby('Level').mean().DEF[:8], label='DEF:Spellcaster')
plt.legend()
# Do
plt.plot(df_W.groupby('Level').mean().ATK[:8], label='ATK:Warrior', color='b')
plt.plot(df_S.groupby('Level').mean().ATK[:8], label='ATK:Spellcaster', color='darkorange')
plt.plot(df_W.groupby('Level').mean().DEF[:8], label='DEF:Warrior', color='b', linestyle='dashed')
plt.plot(df_S.groupby('Level').mean().DEF[:8], label='DEF:Spellcaster', color='darkorange', linestyle='dashed')
plt.legend()
「periodical variation」ということで、本来は複数のカテゴリーについて今月/前月というような異なる期間で比較する場合を想定していると思われますが、そのようなデータがないので強引に攻撃力/守備力、戦士族/魔法使い族で比較してます。ただしレベル12まで含むと違いがほとんど分からなくってしまうので、レベル8までにしています。
Don't
Do
全体的に散らばりが小さいので感動するほどの変化はありませんが、それでもパッとチャートを見たときに、線のテクスチャの違いからATKとDEFの分類がDon'tの場合よりもすぐに入ってくると思います。また種族で色を統一しているのでそのグルーピングも分かりやすくなります。
④通常魔法以外の魔法カードの割合を比較する
Do.
A bar chart starting at the zero baseline
Don’t.
Don’t start the baseline at values other than zero. This baseline starts at 20%, making the bar differences look more dramatic.
# データ抽出
df_Spell = df[df['Type']=='Spell Card']
z = df_Spell.Race.value_counts(True)
z = pd.DataFrame(z)
z = z.drop('Normal', axis=0)
z = z.drop('Ritual', axis=0)
# Don't
ax = z.plot.bar()
plt.ylim(0.08, 0.18)
vals = ax.get_yticks()
ax.set_yticklabels(['{:3.0f}%'.format(x*100) for x in vals])
ax.legend().set_visible(False)
# Do
ax = z.plot.bar()
plt.ylim(0, 0.18)
vals = ax.get_yticks()
ax.set_yticklabels(['{:3.0f}%'.format(x*100) for x in vals])
ax.legend().set_visible(False)
割合が大きすぎる通常魔法に加え割合が小さすぎる儀式魔法も、各割合を算出した後でチャートから除いています。
Don't
Do
y軸の一部を省略することで、より劇的な違いがあるように見せてしまうということですね。偏見で恐縮ですが、質の悪い不動産投資の資料で使われるイメージです。
⑤モンスター・魔法・罠・トークンの割合の比較
Do.
Support legibility by using a balanced number of axis labels.
Don’t.
Don’t overload the chart with numerous axis labels.
# データ抽出
df_Type = df.replace('.*Monster', r'Monster', regex=True).Type.value_counts()
# Don't
ax = df_Type.plot.bar()
ax.set_yticks(np.linspace(0, 5000, 11))
# Do
ax = df_Type.plot.bar()
ax.set_yticks(np.linspace(0, 5000, 6))
Don't
Do
y軸の目盛りのバランスについてです。大したtipsに感じないかもしれませんが、ここにも検討の余地があることを忘れないようにしましょう。
⑥種類ごとのモンスター数TOP10
Do.
Orient text horizontally on bar charts, rotating the bars if needed to make space.
Caution.
Don’t rotate bar labels, as it makes them difficult to read.
# データ抽出
df_M_Type = df_Monster.Type.value_counts()
df_M_Type = pd.DataFrame(df_M_Type)
# Don't
ax = df_M_Type[0:10].plot.bar()
ax.legend().set_visible(False)
# Do
ax = df_M_Type[0:10].sort_values('Type', ascending=True).plot.barh()
ax.legend().set_visible(False)
Don't
Do
x軸のラベルの文字が重なってしまうのを防ぐためにそれを回転させる場合があり、特にYYYY-MM-DDで表示されたラベルを左に45°回転させているチャートをよく見ます。しかし、xyを逆にして表現した方がラベルは圧倒的に読みやすいですね。今回作成した縦棒グラフはすべてラベルを縦にしているのですが、ラベルの数や各文字数が多い場合は、可能ならこのようにすべきでしょう。ちなみにXYZ Monsterは、最初XYZドラゴンキャノンとその仲間のこと(伝われ)を言っているのかと思いましたが、エクシーズのことなんですね。KONAMIのオシャレポイント。
以上で今回は終わりにします。引用元のData Visualizationには他にも色々なポイントがまとめられているのでぜひチェックしてみてください。