積上げ棒グラフ#

準備#

Import#

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

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# itertoolsモジュールのインポート
# 様々なパターンのループを効率的に実行可能
import itertools

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# typingモジュールからの型ヒント関連のインポート
# 関数やクラスの引数・返り値の型を注釈するためのツール
from typing import Any, Dict, List, Optional, Union

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# 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] / "sbar"
)
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = (
    DIR_AN.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "sbar"
)
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = (
    DIR_GM.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "sbar"
)
Hide code cell content
# 読み込み対象ファイル名の定義

# Comic CollectionとCReaTor関連のファイル名
FN_CC_CRT = "cm_cc_crt.csv"

# Comic Episode関連のファイル名
FN_CE = "cm_ce.csv"

# Anime Episode関連のファイル名
FN_AE = "an_ae.csv"

# PacKaGeとPlatForm関連のファイル名
FN_PKG_PF = "gm_pkg_pf.csv"
Hide code cell content
# 可視化に関する設定値の定義

# 可視化対象のマンガ作者数
N_CRT = 8

# 可視化対象のアニメ作品数
N_AC = 10

# 可視化対象のゲームプラットフォーム数
N_PF = 10

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

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"

関数#

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 resample_df_by_col_and_years(df: pd.DataFrame, col: str) -> pd.DataFrame:
    """
    指定されたカラムと年数に基づき、データフレームを再サンプル
    colとyearsの全ての組み合わせが存在するように0埋めを行う
    この処理は、作図時にX軸方向の順序が変わることを防ぐために必要

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    col : str
        サンプリング対象のカラム名

    Returns
    -------
    pd.DataFrame
        再サンプルされたデータフレーム
    """

    # 入力データフレームを新しい変数にコピー
    df_new = df.copy()

    # データフレームからユニークな年数一覧を取得
    unique_years = df["years"].unique()

    # データフレームからユニークなcolの値一覧を取得
    unique_vals = df[col].unique()

    # 一意なカラムの値と年数の全ての組み合わせに対して処理
    for val, years in itertools.product(unique_vals, unique_years):
        # 対象のカラムの値と年数が一致するデータを抽出
        df_tmp = df_new[(df_new[col] == val) & (df_new["years"] == years)]

        # 該当するデータが存在しない場合
        if df_tmp.shape[0] == 0:
            # 0埋めのデータを作成
            default_data = {c: 0 for c in df_tmp.columns}
            # col列についてはvalで埋める
            default_data[col] = val
            # years列についてはyearで埋める
            default_data["years"] = years
            # 新たなレコードとして追加
            df_add = pd.DataFrame(default_data, index=[0])

            # 0埋めのデータをデータフレームに追加
            df_new = pd.concat([df_new, df_add], ignore_index=True)

    return df_new
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
def format_cols(df: pd.DataFrame, cols_rename: Dict[str, str]) -> pd.DataFrame:
    """
    指定されたカラムのみをデータフレームから抽出し、カラム名をリネームする関数

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    cols_rename : Dict[str, str]
        リネームしたいカラム名のマッピング(元のカラム名: 新しいカラム名)

    Returns
    -------
    pd.DataFrame
        カラムが抽出・リネームされたデータフレーム
    """

    # 指定されたカラムのみを抽出し、リネーム
    df = df[cols_rename.keys()].rename(columns=cols_rename)

    return df

可視化例#

マンガデータ#

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_CM / FN_CE)
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)
Hide code cell content
# 可視化用のデータ集計

# df_ceに年代を示す'years'カラムを追加
df_ce = add_years_to_df(df_ce)

# 'ccid'と'years'に基づいて、df_ceを集計し、'n_ce'カラムにその頻度を格納
df_cc = df_ce.groupby("ccid")["years"].value_counts().reset_index(name="n_ce")

# df_cc_crtから必要なカラムのみを抽出
df_cc_crt = df_cc_crt[["ccid", "crtid", "crtname"]]

# df_ccとdf_cc_crtを'mangaid'を基準にして結合
# その後、'crtname'と'years'に基づいて、合計話数を集計
df_cm = pd.merge(df_cc, df_cc_crt, on="ccid", how="left")
df_cm = df_cm.groupby(["crtname", "years"])["n_ce"].sum().reset_index()

# 合計話数が多いN_CRT名のマンガ作者名を抽出
df_tmp = df_cm.groupby("crtname")["n_ce"].sum().reset_index()
crtnames = list(df_tmp.sort_values("n_ce", ascending=False)["crtname"].head(N_CRT))
# 上位のマンガ作者名に絞り込み
df_cm = df_cm[df_cm["crtname"].isin(crtnames)].reset_index(drop=True)

# 'crtname'と'years'に基づいて、df_cmをアップサンプリング
df_cm = resample_df_by_col_and_years(df_cm, "crtname")

# crtnameをcrtnames順に従うカテゴリカル変数として定義し、crtnameとyearsでソート
df_cm["crtname"] = pd.Categorical(df_cm["crtname"], categories=crtnames, ordered=True)
df_cm = df_cm.sort_values(["crtname", "years"], ignore_index=True)

# カラム名をわかりやすい名称に変更
df_cm = df_cm.rename(columns={"crtname": "マンガ作者名", "years": "年代", "n_ce": "合計話数"})
Hide code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
マンガ作者名 年代 合計話数
0 水島新司 1970 711
1 水島新司 1980 747
2 水島新司 1990 481
3 水島新司 2000 486
4 水島新司 2010 373
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../../data/cm/output/vol2/01/sbar/cm.csv'.
Hide code cell source
# データを積上げ棒グラフで可視化

# px.barを使用して、マンガ作者名ごと、年代ごとの合計話数を積上げ棒グラフで表示
# x軸はマンガ作者名、y軸は合計話数、色は年代に基づき積上げて表示
# カラースケールとしてpx.colors.diverging.Portlandを選択
fig = px.bar(
    df_cm,
    x="マンガ作者名",
    y="合計話数",
    color="年代",
    barmode="stack",
    height=500,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例の位置を図の右上に固定
# yanchorとxanchorは凡例の基準点(top:上部、right:右端)を指定
# yとxはその基準点の位置を指定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# show_fig関数を使用して、図を表示
show_fig(fig)

アニメデータ#

Hide code cell content
# pandasのread_csv関数でCSVファイルをデータフレームとして読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
Hide code cell content
# add_years_to_df関数を使用して、df_aeに年代の情報を追加
df_ae = add_years_to_df(df_ae)

# asidごとにaeidのユニーク数を集計しreset_indexでdataframe化
df_an = df_ae.groupby(["asid", "years"])["aeid"].nunique().reset_index()

# 合計話数が多い上位N_ACのasidを抽出
df_tmp = df_an.groupby("asid")["aeid"].sum().reset_index()
asids = list(df_tmp.sort_values("aeid", ascending=False)["asid"].head(N_AC))

# 上位N_ACのasidのみをdf_anから抽出
df_an = df_an[df_an["asid"].isin(asids)].reset_index(drop=True)

# アニメシリーズIDと年代でアップサンプリング
# これにより、すべての作品がすべての年代でデータを持つようになる
df_an = resample_df_by_col_and_years(df_an, "asid")
Hide code cell content
# dateが最も若いacnameを便宜上asnameとし、asidとasnameを紐づける辞書を作成
asid2asname = df_ae.sort_values("date").groupby("asid")["acname"].first().to_dict()
# asid列に基づきasid2asnameでマッピングした結果をasname列に格納
df_an["asname"] = df_an["asid"].map(asid2asname)

# asid列をasidsの順序に従ったカテゴリカル型にキャスト
df_an["asid"] = pd.Categorical(df_an["asid"], categories=asids, ordered=True)
# asidとyearsに基づいて昇順ソート
df_an = df_an.sort_values(["asid", "years"], ignore_index=True)

# 可視化用に列名を変更
df_an = df_an.rename(
    columns={"asname": "代表的なアニメ作品名", "years": "年代", "aeid": "シリーズ合計話数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_an.head()
asid 年代 シリーズ合計話数 代表的なアニメ作品名
0 C1462 1990 696 忍たま 乱太郎[第1期]
1 C1462 2000 765 忍たま 乱太郎[第1期]
2 C1462 2010 623 忍たま 乱太郎[第1期]
3 C1327 1990 1166 クレヨンしんちゃん
4 C1327 2000 382 クレヨンしんちゃん
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../../data/an/output/vol2/01/sbar/an.csv'.
Hide code cell source
# アニメシリーズごとの年代別合計話数を積上げ棒グラフで可視化

# plotly.expressのbar関数を使用して、棒グラフを作成
# x軸には'代表的なアニメ作品名'、y軸には'シリーズ合計話数'を設定
# 各棒の色は'年代'に基づいてdiverging.Portlandで配色
fig = px.bar(
    df_an,
    x="代表的なアニメ作品名",
    y="シリーズ合計話数",
    color="年代",
    barmode="stack",
    height=500,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例の位置を図の右上に固定
# yanchorとxanchorは凡例の基準点(top:上部、right:右端)を指定し、yとxはその基準点の位置を指定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# show_fig関数を使用して、図を表示
show_fig(fig)
Hide code cell source
# アニメ作品名ごとの年代別合計話数を積上げ横棒グラフで可視化

# plotly.expressのbar関数を使用して、積上げ横棒グラフを作成
# y軸には'代表的なアニメ作品名'、x軸には'シリーズ合計話数'、
# 各棒の色は'年代'に基づいてdiverging.Portlandで配色
# orientation="h"で棒を横方向にする
fig = px.bar(
    df_an,
    y="代表的なアニメ作品名",
    x="シリーズ合計話数",
    color="年代",
    orientation="h",
    barmode="stack",
    height=500,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# グラフのレイアウトを更新し、凡例の位置を図の右上に固定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# show_fig関数を使用して、図を表示
show_fig(fig)
Hide code cell content
# asidごとにdateのユニーク数を集計しreset_indexでdataframe化
df_an2 = df_ae.groupby(["asid", "years"])["date"].nunique().reset_index()

# 合計話数が多い上位N_ACのasidを抽出
df_tmp = df_an2.groupby("asid")["date"].sum().reset_index()
asids = list(df_tmp.sort_values("date", ascending=False)["asid"].head(N_AC))

# 上位N_ACのasidのみをdf_anから抽出
df_an2 = df_an2[df_an2["asid"].isin(asids)].reset_index(drop=True)

# アニメシリーズIDと年代でアップサンプリング
# これにより、すべての作品がすべての年代でデータを持つようになる
df_an2 = resample_df_by_col_and_years(df_an2, "asid")
Hide code cell content
# asid列に基づきasid2asnameでマッピングした結果をasname列に格納
df_an2["asname"] = df_an2["asid"].map(asid2asname)

# asid列をasidsの順序に従ったカテゴリカル型にキャスト
df_an2["asid"] = pd.Categorical(df_an2["asid"], categories=asids, ordered=True)
# asidとyearsに基づいて昇順ソート
df_an2 = df_an2.sort_values(["asid", "years"], ignore_index=True)

# 可視化用に列名を変更
df_an2 = df_an2.rename(
    columns={"asname": "代表的なアニメ作品名", "years": "年代", "date": "シリーズ合計放送日数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_an2.head()
asid 年代 シリーズ合計放送日数 代表的なアニメ作品名
0 C1462 1990 648 忍たま 乱太郎[第1期]
1 C1462 2000 759 忍たま 乱太郎[第1期]
2 C1462 2010 617 忍たま 乱太郎[第1期]
3 C2158 1990 180 おじゃる丸
4 C2158 2000 889 おじゃる丸
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an2, DIR_OUT_AN, "an2")
DataFrame is saved as '../../../data/an/output/vol2/01/sbar/an2.csv'.
Hide code cell source
# アニメ作品名ごとの合計放送日を積上げ横棒グラフで可視化する

# plotlyのbar関数を使用して積上げ横棒グラフを作成
# y軸に代表的なアニメ作品名, x軸にシリーズ合計放送日数を設定し、各棒の色は年代ごとに変わるようにする
# orientation="h"で横棒グラフに設定し、barmode="stack"でグループ化する
# height=500でグラフの高さを指定し、color_discrete_sequenceで色の配列を指定する
fig = px.bar(
    df_an2,
    y="代表的なアニメ作品名",
    x="シリーズ合計放送日数",
    color="年代",
    orientation="h",
    barmode="stack",
    height=500,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例の位置を図の右上に固定する
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# グラフを表示する関数を呼び出す
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 = add_years_to_df(df_pkg_pf)

# プラットフォーム名と年代ごとに、ユニークなパッケージIDの数(合計パッケージ数)を集計
df_gm = (
    df_pkg_pf.groupby(["pfname", "years"])["pkgid"].nunique().reset_index(name="n_pkg")
)

# 合計パッケージ数が多い上位N_PFのプラットフォーム名を抽出
df_tmp = df_gm.groupby("pfname")["n_pkg"].sum().reset_index()
pfnames = list(df_tmp.sort_values("n_pkg", ascending=False)["pfname"].head(N_PF))
# 上位N_PFのプラットフォームのみのデータに絞り込む
df_gm = df_gm[df_gm["pfname"].isin(pfnames)].reset_index(drop=True)

# プラットフォーム名と年代ごとにデータをアップサンプリング
df_gm = resample_df_by_col_and_years(df_gm, "pfname")

# pfnamesの順序に従うカテゴリカル変数としてpfname列を定義し、pfnameとyearsでソート
df_gm["pfname"] = pd.Categorical(df_gm["pfname"], categories=pfnames, ordered=True)
df_gm = df_gm.sort_values(["pfname", "years"], ignore_index=True)

# カラム名を変更して、わかりやすい名称にする
df_gm = df_gm.rename(
    columns={"pfname": "プラットフォーム名", "years": "年代", "n_pkg": "合計パッケージ数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
プラットフォーム名 年代 合計パッケージ数
0 プレイステーション2 1990 0
1 プレイステーション2 2000 4105
2 プレイステーション2 2010 111
3 プレイステーション 1990 2233
4 プレイステーション 2000 1516
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../../data/gm/output/vol2/01/sbar/gm.csv'.
Hide code cell source
# プラットフォーム名ごとの合計パッケージ数を積上げ横棒グラフで可視化するための処理

# px.bar関数を使用して積上げ横棒グラフを作成
# y軸には「プラットフォーム名」、x軸には「合計パッケージ数」を設定
# 各棒の色は「年代」に基づく
fig = px.bar(
    df_gm,
    y="プラットフォーム名",
    x="合計パッケージ数",
    color="年代",
    barmode="stack",
    orientation="h",
    height=500,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例の位置を図の右上に設定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# show_fig関数を使用してグラフを表示
show_fig(fig)