モザイクプロット#

準備#

Import#

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide 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

変数#

Hide 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"
)
Hide 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"
Hide code cell content
# 可視化に関する設定値の定義

# 「年代」の集計単位
UNIT_YEARS = 10
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}
Hide 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)
]

関数#

Hide 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)
Hide 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
Hide 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
Hide 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}'.")

可視化例#

マンガデータ#

Hide 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)
Hide 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"]
]
Hide 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": "マンガ作者数のシェア",
    }
)
Hide 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
Hide 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'.
Hide code cell source
# create_mosaicplot()を使ってモザイクプロットを作成
# df_cmから、x軸として年代列を、y軸としてマンガ作者のシェア列を指定
# colorとしてマンガ雑誌名列を指定することで、マンガ雑誌に応じて色分け
# widthでyears_totalの数量に応じて年代の幅を調整
# textでtext列(シェアを小数点以下2桁で丸めた値)を長方形の中に表示
fig = create_mosaicplot(
    df_cm,
    x="年代",
    y="マンガ作者数のシェア",
    color="マンガ雑誌名",
    width="years_total",
    text="text",
)

# モザイクプロットを表示
show_fig(fig)
Hide 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

アニメデータ#

Hide 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)
Hide 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)
Hide 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"]
]
Hide 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": "声優数"})
Hide code cell content
# 2000年代以降を可視化対象とする
df_an = df_an[df_an["年代"].astype(int) >= 2000].reset_index(drop=True)
Hide 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}")
Hide 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
Hide 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'.
Hide 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)
Hide 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"]
]
Hide 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": "声優数"})
Hide code cell content
# 2000年代以降を可視化対象とする
df_an2 = df_an2[df_an2["年代"].astype(int) >= 2000].reset_index(drop=True)
Hide 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}")
Hide 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
Hide 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'.
Hide 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)

ゲームデータ#

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Hide 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
Hide 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)
Hide 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": "パッケージ数"}
)
Hide code cell content
# 1990年以降のデータに絞る
df_gm = df_gm[df_gm["発売年代"].astype(int) >= 1990].reset_index(drop=True)
Hide 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}")
Hide 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
Hide 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'.
Hide 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)