モザイクプロット#
準備#
Import#
Show code cell content
# warningsモジュールのインポート
import warnings
# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Show code cell content
# itertoolsモジュールのインポート
# 効率的なループを実行するためのイテレータビルディングブロックを提供
# これにより、データのコンビネーションや順列などを簡潔に表現できる
import itertools
# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path
# typingモジュールからListのインポート
# 型ヒントとして利用
from typing import List
# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np
# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd
# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px
# plotly.graph_objectsのインポート
# goという名前で参照可能
# 低レベルのインターフェイスを提供し、より詳細なカスタマイズが可能
import plotly.graph_objects as go
# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure
変数#
Show code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../../data/gm/input")
# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = (
DIR_CM.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "mosaic"
)
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = (
DIR_AN.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "mosaic"
)
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = (
DIR_GM.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "mosaic"
)
Show code cell content
# 読み込み対象ファイル名の定義
# マンガ作品とマンガ作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"
# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"
# アニメ作品と原作者の対応関係に関するファイル
FN_AC_ACT = "an_ac_act.csv"
# アニメ各話に関するファイル
FN_AE = "an_ae.csv"
# ゲームパッケージとプラットフォームの対応関係に関するファイル
FN_PKG_PF = "gm_pkg_pf.csv"
Show code cell content
# 可視化に関する設定値の定義
# 「年代」の集計単位
UNIT_YEARS = 10
Show code cell content
# plotlyの描画設定の定義
# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Show code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
0: "月",
1: "火",
2: "水",
3: "木",
4: "金",
5: "土",
6: "日",
}
Show code cell content
# 質的変数の描画用のカラースケールの定義
# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
"#000000", # 黒 (Black)
"#E69F00", # 橙 (Orange)
"#56B4E9", # 薄青 (Sky Blue)
"#009E73", # 青緑 (Bluish Green)
"#F0E442", # 黄色 (Yellow)
"#0072B2", # 青 (Blue)
"#D55E00", # 赤紫 (Vermilion)
"#CC79A7", # 紫 (Reddish Purple)
]
関数#
Show code cell source
def show_fig(fig: Figure) -> None:
"""
所定のレンダラーを用いてplotlyの図を表示
Jupyter Bookなどの環境での正確な表示を目的とする
Parameters
----------
fig : Figure
表示対象のplotly図
Returns
-------
None
"""
# 図の周囲の余白を設定
# t: 上余白
# l: 左余白
# r: 右余白
# b: 下余白
fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))
# 所定のレンダラーで図を表示
fig.show(renderer=RENDERER)
Show code cell content
def add_years_to_df(
df: pd.DataFrame, unit_years: int = UNIT_YEARS, col_date: str = "date"
) -> pd.DataFrame:
"""
データフレームにunit_years単位で区切った年数を示す新しい列を追加
Parameters
----------
df : pd.DataFrame
入力データフレーム
unit_years : int, optional
年数を区切る単位、デフォルトはUNIT_YEARS
col_date : str, optional
日付を含むカラム名、デフォルトは "date"
Returns
-------
pd.DataFrame
新しい列が追加されたデータフレーム
"""
# 入力データフレームをコピー
df_new = df.copy()
# unit_years単位で年数を区切り、新しい列として追加
df_new["years"] = (
pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
)
# 'years'列のデータ型を文字列に変更
df_new["years"] = df_new["years"].astype(str)
return df_new
Show code cell content
def create_mosaicplot(
df: pd.DataFrame,
x: str,
y: str,
color: str,
width: str,
text: str,
color_discrete_sequence: List[str] = OKABE_ITO,
) -> go.Figure:
"""
指定されたDataFrameを元にモザイクプロットを作成する関数
Parameters
----------
df : pd.DataFrame
プロットに使用するデータが含まれるDataFrame
x : str
x軸に表示するデータのカラム名
y : str
y軸に表示するデータのカラム名
color : str
グループ分けの基準となるデータのカラム名
width : str
各バーの幅を表すデータのカラム名
text : str
各バーに表示するテキストのデータのカラム名
color_discrete_sequence : List[str], optional
使用する色のリスト デフォルトはOKABE_ITOのカラーパレット
Returns
-------
go.Figure
作成されたモザイクプロットのFigureオブジェクト
"""
# 空のFigureオブジェクトを作成
fig = go.Figure()
# color列に登場するユニークな要素に対し、色をマッピング
unique_keys = df[color].unique()
color_map = {
name: color for name, color in zip(unique_keys, color_discrete_sequence)
}
# color列のユニークな要素ごとにDataFrameをフィルタリング
for i, name in enumerate(unique_keys):
df_tmp = df[df[color] == name].reset_index(drop=True)
# 幅をwidth列から抽出
widths = df_tmp[width]
# バーの位置を計算し、プロットに追加
# 幅が変わるようxの値を調整
fig.add_trace(
go.Bar(
name=name,
x=df_tmp[width].cumsum() - widths,
y=df_tmp[y],
text=df_tmp[text],
width=widths,
offset=0,
marker_color=color_map[name],
)
)
# 最初の要素を用いて、X軸ラベルの設定値を作成
if i == 0:
# 各「棒」の中央に配置されるように座標を計算
tickvals = df_tmp[width].cumsum() - df_tmp[width] / 2
ticktext = df_tmp[x].unique()
# x軸の表示範囲を決定するために利用
x_max = df_tmp[width].sum()
# x軸の目盛りの位置、テキスト、表示範囲を設定
# 「棒」の太さの合計値を1としたとき、左右に0.1ずつ余白が残るように調整
fig.update_xaxes(
tickvals=tickvals, ticktext=ticktext, title=x, range=[-x_max * 0.1, x_max * 1.1]
)
# y軸のタイトルを設定
fig.update_yaxes(title=y)
# プロットのレイアウトを設定、凡例タイトルも指定
fig.update_layout(barmode="stack", legend_title=color)
return fig
Show code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
"""
DataFrameをCSVファイルとして指定されたディレクトリに保存する関数
Parameters
----------
df : pd.DataFrame
保存対象となるDataFrame
dir_save : Path
出力先ディレクトリのパス
fn_save : str
保存するCSVファイルの名前(拡張子は含めない)
"""
# 出力先ディレクトリが存在しない場合は作成
dir_save.mkdir(parents=True, exist_ok=True)
# 出力先のパスを作成
p_save = dir_save / f"{fn_save}.csv"
# DataFrameをCSVファイルとして保存する
df.to_csv(p_save, index=False, encoding="utf-8-sig")
# 保存完了のメッセージを表示する
print(f"DataFrame is saved as '{p_save}'.")
可視化例#
マンガデータ#
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)
df_ce = pd.read_csv(DIR_CM / FN_CE)
Show code cell content
# df_ceに年代情報を追加するための関数add_years_to_dfを適用
df_ce = add_years_to_df(df_ce)
# yearsとccidのユニークな組み合わせを持つデータフレームを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てている
df_cc_years = df_ce.drop_duplicates(subset=["years", "ccid"], ignore_index=True)[
["ccid", "years"]
]
Show code cell content
# df_cc_crtとdf_cc_yearsをccidを基準にして右結合(right join)し、df_mergeを作成
# これにより、df_cc_crtのデータにdf_cc_yearsのyears情報が組み合わされる
df_merge = pd.merge(df_cc_crt, df_cc_years, on="ccid", how="right")
# df_mergeをマンガ雑誌名とyearsごとにグループ化し、作者のユニーク数を集計
# nunique()を使用して各グループ内のユニークな作者数をカウント
df_cm = (
df_merge.groupby(["mcname", "years"])["crtid"].nunique().reset_index(name="n_crt")
)
# years別の比率を計算するための一時的なDataFrameを作成し、マージ
df_tmp = df_cm.groupby("years")["n_crt"].sum().reset_index(name="years_total")
df_cm = pd.merge(df_cm, df_tmp, how="left", on="years")
# years別のマンガ作者数のシェアを計算
df_cm["ratio"] = df_cm["n_crt"] / df_cm["years_total"]
# 可視化した際の補足情報として、小数点以下2桁で丸めたシェアを計算
df_cm["text"] = df_cm["ratio"].apply(lambda x: f"{x: .2}")
# 列名をわかりやすい名前に変更
df_cm = df_cm.rename(
columns={
"mcname": "マンガ雑誌名",
"years": "年代",
"n_crt": "マンガ作者数",
"ratio": "マンガ作者数のシェア",
}
)
Show code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
| マンガ雑誌名 | 年代 | マンガ作者数 | years_total | マンガ作者数のシェア | text | |
|---|---|---|---|---|---|---|
| 0 | 週刊少年サンデー | 1970 | 197 | 866 | 0.227483 | 0.23 |
| 1 | 週刊少年サンデー | 1980 | 208 | 886 | 0.234763 | 0.23 |
| 2 | 週刊少年サンデー | 1990 | 182 | 768 | 0.236979 | 0.24 |
| 3 | 週刊少年サンデー | 2000 | 181 | 879 | 0.205916 | 0.21 |
| 4 | 週刊少年サンデー | 2010 | 191 | 946 | 0.201903 | 0.2 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../../data/cm/output/vol2/03/mosaic/cm.csv'.
Show code cell content
# 週刊少年チャンピオンのデータを抽出
df_cm[df_cm["マンガ雑誌名"] == "週刊少年チャンピオン"]
| マンガ雑誌名 | 年代 | マンガ作者数 | years_total | マンガ作者数のシェア | text | |
|---|---|---|---|---|---|---|
| 10 | 週刊少年チャンピオン | 1970 | 182 | 866 | 0.210162 | 0.21 |
| 11 | 週刊少年チャンピオン | 1980 | 239 | 886 | 0.269752 | 0.27 |
| 12 | 週刊少年チャンピオン | 1990 | 208 | 768 | 0.270833 | 0.27 |
| 13 | 週刊少年チャンピオン | 2000 | 265 | 879 | 0.301479 | 0.3 |
| 14 | 週刊少年チャンピオン | 2010 | 256 | 946 | 0.270613 | 0.27 |
アニメデータ#
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ac_act = pd.read_csv(DIR_AN / FN_AC_ACT)
df_ae = pd.read_csv(DIR_AN / FN_AE)
Show code cell content
# 各crtidに対して、関連するgenderのユニークな数を数える
# groupbyでcrtidごとにグループ化し、nuniqueメソッドで各グループのgenderのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各crtidが1つのgenderにのみ紐づいていること)を確認
assert all(df_ac_act.groupby("actid")["gender"].nunique() == 1)
Show code cell content
# df_aeに年代情報を追加するための関数add_years_to_dfを適用
# unit_years=5を指定することで、5年単位の年代情報を追加する
df_ae = add_years_to_df(df_ae, unit_years=5)
# acidとyearsのユニークな組み合わせを持つDataFrameを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てる
df_ac_years = df_ae.drop_duplicates(subset=["acid", "years"], ignore_index=True)[
["acid", "years"]
]
Show code cell content
# df_ac_actとdf_ac_yearsをacidを基準にして左結合(left join)し、df_mergeを作成
# これにより、df_ac_actのデータにdf_ac_yearsの年代情報が組み合わされる
df_merge = pd.merge(df_ac_act, df_ac_years, on="acid", how="left")
# df_mergeを性別と年代ごとにグループ化し、声優のユニーク数を集計
# nunique()を使用して各グループ内のユニークな声優数をカウント
df_an = (
df_merge.groupby(["gender", "years"])["acid"].nunique().reset_index(name="n_act")
)
# 列名をわかりやすい名前に変更
df_an = df_an.rename(columns={"gender": "性別", "years": "年代", "n_act": "声優数"})
Show code cell content
# 2000年代以降を可視化対象とする
df_an = df_an[df_an["年代"].astype(int) >= 2000].reset_index(drop=True)
Show code cell content
# 年代ごとの合計声優数を格納する辞書を作成
year2nact = df_an.groupby("年代")["声優数"].sum().to_dict()
# df_anに合計声優数という列を追加し、year2nactから値を取得して格納
df_an["合計声優数"] = df_an["年代"].map(year2nact)
# df_anに内訳列を追加。声優数 / 合計声優数で算出
df_an["声優数のシェア"] = df_an["声優数"] / df_an["合計声優数"]
# df_anにテキスト表示用の列を追加
df_an["text"] = df_an["声優数のシェア"].apply(lambda x: f"{x:.2f}")
Show code cell content
# 可視化対象のDataFrameを確認
df_an.head()
| 性別 | 年代 | 声優数 | 合計声優数 | 声優数のシェア | text | |
|---|---|---|---|---|---|---|
| 0 | female | 2000 | 561 | 1113 | 0.504043 | 0.50 |
| 1 | female | 2005 | 844 | 1695 | 0.497935 | 0.50 |
| 2 | female | 2010 | 856 | 1654 | 0.517533 | 0.52 |
| 3 | female | 2015 | 597 | 1149 | 0.519582 | 0.52 |
| 4 | male | 2000 | 552 | 1113 | 0.495957 | 0.50 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../../data/an/output/vol2/03/mosaic/an.csv'.
Show code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_anから、x軸として年代列を、y軸として声優数のシェア列を指定
# colorとして性別列を指定することで、性別に応じて色分け
# widthで合計声優数の数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(
df_an, x="年代", y="声優数のシェア", color="性別", width="合計声優数", text="text"
)
# モザイクプロットを表示
show_fig(fig)
Show code cell content
# df_aeに年代情報を追加するための関数add_years_to_dfを適用
# unit_years=1を指定することで、1年単位の年代情報を追加する
df_ae2 = add_years_to_df(df_ae, unit_years=1)
# acidとyearsのユニークな組み合わせを持つDataFrameを作成
# 重複するデータを削除し、必要なカラムのみを選択
# ignore_index=Trueで新しいインデックスを割り当てる
df_ac_years2 = df_ae2.drop_duplicates(subset=["acid", "years"], ignore_index=True)[
["acid", "years"]
]
Show code cell content
# df_ac_actとdf_ac_years2をacidを基準にして左結合(left join)し、df_mergeを作成
# これにより、df_ac_actのデータにdf_ac_yearsの年代情報が組み合わされる
df_merge2 = pd.merge(df_ac_act, df_ac_years2, on="acid", how="left")
# df_merge2を性別と年代ごとにグループ化し、声優のユニーク数を集計
# nunique()を使用して各グループ内のユニークな声優数をカウント
df_an2 = (
df_merge2.groupby(["gender", "years"])["acid"].nunique().reset_index(name="n_act")
)
# 列名をわかりやすい名前に変更
df_an2 = df_an2.rename(columns={"gender": "性別", "years": "年代", "n_act": "声優数"})
Show code cell content
# 2000年代以降を可視化対象とする
df_an2 = df_an2[df_an2["年代"].astype(int) >= 2000].reset_index(drop=True)
Show code cell content
# 年代ごとの合計声優数を格納する辞書を作成
year2nact2 = df_an2.groupby("年代")["声優数"].sum().to_dict()
# df_an2に合計声優数という列を追加し、year2nact2から値を取得して格納
df_an2["合計声優数"] = df_an2["年代"].map(year2nact2)
# df_an2に内訳列を追加。声優数 / 合計声優数で算出
df_an2["声優数のシェア"] = df_an2["声優数"] / df_an2["合計声優数"]
# df_an2にテキスト表示用の列を追加
df_an2["text"] = df_an2["声優数のシェア"].apply(lambda x: f"{x:.2f}")
Show code cell content
# 可視化対象のDataFrameを確認
df_an2.head()
| 性別 | 年代 | 声優数 | 合計声優数 | 声優数のシェア | text | |
|---|---|---|---|---|---|---|
| 0 | female | 2000 | 127 | 252 | 0.503968 | 0.50 |
| 1 | female | 2001 | 145 | 287 | 0.505226 | 0.51 |
| 2 | female | 2002 | 147 | 288 | 0.510417 | 0.51 |
| 3 | female | 2003 | 173 | 342 | 0.505848 | 0.51 |
| 4 | female | 2004 | 202 | 407 | 0.496314 | 0.50 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an2, DIR_OUT_AN, "an2")
DataFrame is saved as '../../../data/an/output/vol2/03/mosaic/an2.csv'.
Show code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_an2から、x軸として年代列を、y軸として声優数のシェア列を指定
# colorとして性別列を指定することで、性別に応じて色分け
# widthで合計声優数の数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(
df_an2, x="年代", y="声優数のシェア", color="性別", width="合計声優数", text="text"
)
# モザイクプロットを表示
show_fig(fig)
ゲームデータ#
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Show code cell content
# df_pkg_pfに年代(5年刻み)を追加
df_pkg_pf = add_years_to_df(df_pkg_pf, unit_years=5)
# date列をdatetimeオブジェクトに変換して、曜日情報を新たな列としてdf_pkg_pfに追加
df_pkg_pf["weekday"] = pd.to_datetime(df_pkg_pf["date"]).dt.weekday
Show code cell content
# 各pkgidに対して、関連するweekdayのユニークな数を数える
# groupbyでpkgidごとにグループ化し、nuniqueメソッドで各グループのweekdayのユニークな数を数える
# all関数を使って、nuniqueの結果が全て1であること(各pkgidが1つのweekdayにのみ紐づいていること)を確認
assert all(df_pkg_pf.groupby("pkgid")["weekday"].nunique() == 1)
Show code cell content
# years、weekdayごとにpkgidのユニーク数を集計し、n_pkg列として追加
df_gm = (
df_pkg_pf.groupby(["years", "weekday"])["pkgid"].nunique().reset_index(name="n_pkg")
)
# 数値で表されている曜日を文字列にマッピング
df_gm["yobi"] = df_gm["weekday"].map(WEEKDAY2YOBI)
# 可視化用に列名をリネーム
df_gm = df_gm.rename(
columns={"years": "発売年代", "yobi": "発売曜日", "n_pkg": "パッケージ数"}
)
Show code cell content
# 1990年以降のデータに絞る
df_gm = df_gm[df_gm["発売年代"].astype(int) >= 1990].reset_index(drop=True)
Show code cell content
# 年代別の合計ゲームパッケージ数を集計し、辞書として保存
year2npkg = df_gm.groupby("発売年代")["パッケージ数"].sum().to_dict()
# 年代をキーに、year2npkgで合計パッケージ数をマッピング
df_gm["合計パッケージ数"] = df_gm["発売年代"].map(year2npkg)
# 年ごとのシェアを計算
df_gm["パッケージ数のシェア"] = df_gm["パッケージ数"] / df_gm["合計パッケージ数"]
# 表示用に小数点以下二桁で丸めた列を追加
df_gm["text"] = df_gm["パッケージ数のシェア"].apply(lambda x: f"{x:.2}")
Show code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
| 発売年代 | weekday | パッケージ数 | 発売曜日 | 合計パッケージ数 | パッケージ数のシェア | text | |
|---|---|---|---|---|---|---|---|
| 0 | 1990 | 0 | 34 | 月 | 2074 | 0.016393 | 0.016 |
| 1 | 1990 | 1 | 81 | 火 | 2074 | 0.039055 | 0.039 |
| 2 | 1990 | 2 | 66 | 水 | 2074 | 0.031823 | 0.032 |
| 3 | 1990 | 3 | 122 | 木 | 2074 | 0.058824 | 0.059 |
| 4 | 1990 | 4 | 1559 | 金 | 2074 | 0.751688 | 0.75 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../../data/gm/output/vol2/03/mosaic/gm.csv'.
Show code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_gmから、x軸として年代列を、y軸としてパッケージ数のシェア列を指定
# colorとして曜日列を指定することで、曜日に応じて色分け
# widthで合計パッケージ数の数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(
df_gm,
x="発売年代",
y="パッケージ数のシェア",
color="発売曜日",
width="合計パッケージ数",
text="text",
)
# モザイクプロットを表示
show_fig(fig)