マンガデータの分布を見る#

準備#

Import#

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

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# 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.figure_factoryのインポート
# 高度なプロットとデータ可視化のためのユーティリティ
# ffという名前で参照可能
import plotly.figure_factory as ff

# plotly.graph_objectsのインポート
# より詳細なグラフ作成機能を利用可能
# goという名前で参照可能
import plotly.graph_objects as go

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

# plotly.subplotsからmake_subplotsのインポート
# 複数のサブプロットを含む複合的な図を作成する際に使用
from plotly.subplots import make_subplots

変数#

Hide code cell content
# マンガデータが保存されているディレクトリのパス
DIR_IN = Path("../../../data/cm/input")

# 分析結果の出力先ディレクトリのパス
DIR_OUT = (
    DIR_IN.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "dists"
)
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# マンガ作品と原作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"
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 create_distplot(
    df: pd.DataFrame,
    x: str,
    color: str = None,
    show_hist: bool = False,
    show_rug: bool = False,
    **kwargs: Any
) -> Figure:
    """
    データフレームから密度プロットとヒストグラムを作成する

    Parameters
    ----------
    df : pd.DataFrame
        プロットするデータを含むデータフレーム
    x : str
        密度プロットの描画対象とするカラム名
    color : str, optional
        データを分割する基準とするカラム名、指定しない場合はx列の全データを用いる
    show_hist : bool, optional
        ヒストグラムを表示するか否か、デフォルトはFalse
    show_rug : bool, optional
        ラグプロットを表示するか否か、デフォルトはFalse
    **kwargs
        ff.create_distplotに渡すその他のキーワード引数

    Returns
    -------
    Figure
        作成されたプロットのFigureオブジェクト
    """

    if color:
        # colorカラムの値でデータをグループ分け
        grouped = df.groupby(color)

        # 各グループのxカラムのデータをリストに格納、可視化用に逆順に並び替え
        hist_data = [group[x].values for _, group in grouped][::-1]

        # 各グループの名前(colorカラムの値)をラベルとしてリストに格納、可視化用に逆順に並び替え
        labels = [str(name) for name, _ in grouped][::-1]

        # 密度プロットとヒストグラムを作成
        fig = ff.create_distplot(
            hist_data, labels, show_hist=show_hist, show_rug=show_rug, **kwargs
        )
    else:
        # colorが指定されていない場合はx列の全データを用いる
        hist_data = [df[x].values]

        # 密度プロットを作成(ラベルはxを指定)
        fig = ff.create_distplot(
            hist_data,
            group_labels=[x],
            show_hist=show_hist,
            show_rug=show_rug,
            **kwargs
        )

    # x軸のタイトルをxに変更
    fig.update_xaxes(title=x)

    # y軸のタイトルを"確率密度"に変更
    fig.update_yaxes(title="確率密度")

    # 作成されたプロットを返す
    return fig
Hide code cell content
def create_split_violin_plot(
    df: pd.DataFrame, x: str, y: str, split: str, **kwargs
) -> Figure:
    """
    DataFrameからsplit violin plotを作成する関数

    Parameters
    ----------
    df : pandas.DataFrame
        データを含むDataFrame
    x : str
        X軸に使用するカラム名
    y : str
        Y軸に使用するカラム名
    split : str
        バイオリンを分割する際に使用するカラム名、ブール値である必要がある
    **kwargs : dict
        go.Violinに渡す追加のキーワード引数


    Returns
    -------
    fig : plotly.graph_objects.Figure
        生成されたバイオリンプロットの図
    """

    # 新しい図オブジェクトを作成
    fig = go.Figure()

    # Trueのデータでバイオリンプロットを作成
    # ここでX軸とY軸にデータを割り当て、プロットの色やスタイルを設定
    fig.add_trace(
        go.Violin(
            x=df[x][df[split]],
            y=df[y][df[split]],
            legendgroup="True",  # 凡例グループを設定
            scalegroup="True",  # スケールグループを設定
            name="True",  # 凡例名を設定
            side="negative",  # バイオリンの配置を左側に設定
            line_color="blue",  # ラインの色を青に設定
            points=False,  # ポイントを表示しないように設定
            **kwargs
        )
    )

    # Falseのデータでバイオリンプロットを作成
    # Trueのときと同様にデータを割り当て、プロットの色やスタイルを設定
    fig.add_trace(
        go.Violin(
            x=df[x][~df[split]],
            y=df[y][~df[split]],
            legendgroup="False",  # 凡例グループを設定
            scalegroup="False",  # スケールグループを設定
            name="False",  # 凡例名を設定
            side="positive",  # バイオリンの配置を右側に設定
            line_color="orange",  # ラインの色をオレンジに設定
            points=False,  # ポイントを表示しないように設定
            **kwargs
        )
    )

    # プロットのスケールモードを"count"に設定し、中央値の線を表示
    fig.update_traces(scalemode="count", meanline_visible=True)

    # 図のレイアウトを更新
    # 軸のタイトルと凡例のタイトルを設定し、バイオリン間のギャップとモードを設定
    fig.update_layout(
        xaxis_title=x,  # X軸のタイトルを設定
        yaxis_title=y,  # Y軸のタイトルを設定
        legend_title=split,  # 凡例のタイトルを設定
        violingap=0,  # バイオリン間のギャップを0に設定
        violinmode="overlay",  # バイオリンモードを"overlay"に設定
    )

    return fig
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
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_ce = pd.read_csv(DIR_IN / FN_CE)
df_cc_crt = pd.read_csv(DIR_IN / FN_CC_CRT)
Hide code cell content
# 企画系のマンガを除外するため、5ページ以上のマンガ各話を分析とする
df_ce = df_ce[df_ce["pages"] >= 5].reset_index(drop=True)

ヒストグラム#

Hide code cell content
# 連載マンガ作品として扱う最小のマンガ各話数を、マンガデータの基礎分析を踏まえ設定
min_nce = 8

# 'ccid'でグループ化し、各ccnameについてユニークなceidの数をカウント
df_cc_nce = df_ce.groupby(["ccid"])["ceid"].nunique().reset_index(name="n_ce")
# n_ceの値がmin_nce以上の行だけを保持
df_cc_nce = df_cc_nce[df_cc_nce["n_ce"] >= min_nce].reset_index(drop=True)
# n_ceの値でデータフレームを昇順にソート
df_cc_nce = df_cc_nce.sort_values("n_ce", ignore_index=True)
Hide code cell content
# df_ceを日付とceidでソートし、各ccidについて最初のmin_nce件のデータを取り出す
df_hist = (
    df_ce.sort_values(["date", "ceid"], ignore_index=True).groupby("ccid").head(min_nce)
)

# df_cc_nceのccid列に含まれるccidの行だけを保持
df_hist = df_hist[df_hist["ccid"].isin(df_cc_nce["ccid"].unique())]

# 可視化用に保持するカラム一覧
cols2rename = {
    "page_start_position": "掲載位置",
    "ceid": "ceid",
    "ccid": "ccid",
    "mcname": "mcname",
    "date": "date",
}
# 可視化用にカラム名を変更
df_hist = format_cols(df_hist, cols2rename)
Hide code cell content
# 可視化対象のDataFrameを確認
df_hist.head()
掲載位置 ceid ccid mcname date
0 0.051724 CE117459 C94272 週刊少年チャンピオン 1970-07-27
1 0.165517 CE117460 C94289 週刊少年チャンピオン 1970-07-27
2 0.231034 CE117461 C94447 週刊少年チャンピオン 1970-07-27
3 0.296552 CE117462 C94949 週刊少年チャンピオン 1970-07-27
4 0.451724 CE117466 C95858 週刊少年チャンピオン 1970-07-27
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_hist, DIR_OUT, "hist")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/hist.csv'.
Hide code cell source
# df_histを用いて「掲載位置」に関するヒストグラムを作成し、y軸のタイトルを「各話数」に設定
fig = px.histogram(df_hist, x="掲載位置").update_yaxes(title="各話数")

# ヒストグラムを表示
show_fig(fig)
Hide code cell source
# df_histのうち第一話のみを抽出して「掲載位置」に関するヒストグラムを作成
# y軸のタイトルを「各話数」に設定
fig = px.histogram(df_hist.groupby("ccid").head(1), x="掲載位置").update_yaxes(
    title="各話数"
)

# ヒストグラムを表示
show_fig(fig)
Hide code cell content
# min_nce、四分位数、最大値+1を使ってデータの範囲(threshold)を示すリストths_ceを作成
# 後に登場するfor文をきれいに書くために、min_nceと、n_ceの最大値+1を追加
ths_ce = (
    [min_nce]  # min_nceをリストの最初に追加
    + list(df_cc_nce["n_ce"].quantile([0.25, 0.5, 0.75]).astype(int))  # 四分位数を追加
    + [df_cc_nce["n_ce"].max() + 1]  # 最大値+1をリストの最後に追加
)

# ccidをグループ名にマップするための辞書を初期化
ccid2gname = {}

# ths_ceリスト内の閾値ペアをループして処理
for i in range(len(ths_ce) - 1):
    # 現在の閾値を下限、次の閾値を上限として設定
    lower = ths_ce[i]
    upper = ths_ce[i + 1]

    # n_ceが現在の閾値範囲内にある行だけを抽出してdf_qに保存
    df_q = df_cc_nce[(df_cc_nce["n_ce"] >= lower) & (df_cc_nce["n_ce"] < upper)]

    # ccidとグループ名をマッピングする辞書を作成
    gname = f"第{i+1}群(合計話数:{lower}-{upper-1}話)"
    ccid2gname.update({ccid: gname for ccid in df_q["ccid"]})
Hide code cell content
# 可視化用に新たなDataFrameを作成
df_hist2 = df_hist.copy()

# df_hist2のccnameに基づきにグループ名をマッピング
df_hist2["gname"] = df_hist2["ccid"].map(ccid2gname)

# gnameとmcnameでdf_hist2をソート
df_hist2 = df_hist2.sort_values(["gname", "mcname"], ignore_index=True)

# 可視化用にカラム名を変更
df_hist2 = df_hist2.rename(columns={"gname": "グループ名"})
Hide code cell content
# 可視化対象のDataFrameを確認
df_hist2.head()
掲載位置 ceid ccid mcname date グループ名
0 0.373239 CE155567 C93019 週刊少年サンデー 1970-08-02 第1群(合計話数:8-16話)
1 0.029316 CE155551 C93060 週刊少年サンデー 1970-08-09 第1群(合計話数:8-16話)
2 0.224756 CE155554 C92231 週刊少年サンデー 1970-08-09 第1群(合計話数:8-16話)
3 0.801303 CE155560 C93019 週刊少年サンデー 1970-08-09 第1群(合計話数:8-16話)
4 0.200637 CE155541 C93060 週刊少年サンデー 1970-08-16 第1群(合計話数:8-16話)
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_hist2, DIR_OUT, "hist2")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/hist2.csv'.
Hide code cell source
# df_hist2の「掲載位置」に関するヒストグラムを「グループ」ごとに作成
# y軸のタイトルを「各話数」に設定
fig = px.histogram(
    df_hist2, x="掲載位置", facet_col="グループ名", facet_col_wrap=1, height=600
).update_yaxes(title="各話数")

# ファセット(グループごとのヒストグラム)のタイトルを簡潔にする処理
# デフォルトではタイトルは「グループ=xxx」という形式になっている
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# ヒストグラムを表示
show_fig(fig)
Hide code cell content
# グループ名ごとの、ccnameとceidのユニーク数を集計
df_hist2.groupby(["グループ名"]).agg(
    マンガ作品数=("ccid", "nunique"), マンガ各話数=("ceid", "nunique")
).reset_index()
グループ名 マンガ作品数 マンガ各話数
0 第1群(合計話数:8-16話) 559 4472
1 第2群(合計話数:17-31話) 566 4528
2 第3群(合計話数:32-81話) 594 4752
3 第4群(合計話数:82-1956話) 584 4672
Hide code cell content
# データフレームを日付で降順にソート
df_ce_sorted = df_ce.sort_values("date", ascending=False)

# mcnameごとにグループ化し、最新のmiidを取得
# mcname2miid_latest = {mcname: 最新のmiid}
mcname2miid_latest = df_ce_sorted.groupby("mcname")["miid"].first().to_dict()
Hide code cell content
# 例として、最新の週刊少年ジャンプに掲載されている作品を表示
df_ce[df_ce["miid"] == mcname2miid_latest["週刊少年ジャンプ"]][
    ["date", "ccname", "cename", "page_start_position"]
]
date ccname cename page_start_position
170928 2017-07-31 ONE PIECE 第872話 とろふわ 0.006237
170929 2017-07-31 祝!ONE PIECE 20周年!!尾田さんとの思い出漫画! by しまぶー. NaN 0.126819
170930 2017-07-31 ONE PIECE PARTY CONGRATULATIONS ON 20 INCREDIBLE YEARS!! 0.160083
170931 2017-07-31 僕のヒーローアカデミア No.145 烈怒頼雄斗 2 0.180873
170932 2017-07-31 約束のネバーランド 第47話 昔話 0.222453
170933 2017-07-31 食戟のソーマ 223 フィールドを超えて 0.268191
170934 2017-07-31 Dr.STONE Z=19 200万年の在処 0.309771
170935 2017-07-31 銀魂 第643訓 血と涙 0.351351
170936 2017-07-31 ブラッククローバー ページ 117 二人の空間魔法使い 0.388773
170937 2017-07-31 ROBOT×LASERBEAM 17th round 強敵 0.426195
170938 2017-07-31 鬼滅の刃 第70話 人攫い 0.467775
170939 2017-07-31 クロスアカウント #5 噂×嘘 0.509356
170940 2017-07-31 斉木楠雄のΨ難 第252χ 自慢の粘土Ψ工を披露しよう 0.550936
170941 2017-07-31 ハイキュー!! 第262話 いつだって前のめり 0.584200
170942 2017-07-31 ゆらぎ荘の幽奈さん 71 ラブラブバイト大作戦 0.679834
170943 2017-07-31 シューダン! 6 奮起する浜西 0.721414
170944 2017-07-31 ぼくたちは勉強ができない 問23. 天才たちの花園に[x]は不可欠である 0.762994
170945 2017-07-31 火ノ丸相撲 第153番 未来 0.804574
170946 2017-07-31 青春兵器ナンバーワン mission 37: ROMANCE DAWN 0.846154
170947 2017-07-31 HUNTER×HUNTER No.364 思惑 0.879418
170948 2017-07-31 腹ペコのマリー ペコ 20 恋するファッションショー 0.920998
170949 2017-07-31 磯部磯兵衛物語~浮世はつらいよ~ 第244話 拙者には娘さんが…で候 0.979210

密度プロット#

Hide code cell content
# ヒストグラムと同様のデータを利用
df_dist = df_hist2.copy()
Hide code cell content
# 可視化対象のDataFrameを確認
df_dist.head()
掲載位置 ceid ccid mcname date グループ名
0 0.373239 CE155567 C93019 週刊少年サンデー 1970-08-02 第1群(合計話数:8-16話)
1 0.029316 CE155551 C93060 週刊少年サンデー 1970-08-09 第1群(合計話数:8-16話)
2 0.224756 CE155554 C92231 週刊少年サンデー 1970-08-09 第1群(合計話数:8-16話)
3 0.801303 CE155560 C93019 週刊少年サンデー 1970-08-09 第1群(合計話数:8-16話)
4 0.200637 CE155541 C93060 週刊少年サンデー 1970-08-16 第1群(合計話数:8-16話)
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_dist, DIR_OUT, "dist")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/dist.csv'.
Hide code cell source
# df_distデータフレームを使用して密度プロットを作成
# "掲載位置"をx軸に、"グループ名"を色分けの基準にしてプロット
# 色はPortlandスタイルで指定
fig = create_distplot(
    df_dist, x="掲載位置", color="グループ名", colors=px.colors.diverging.Portland
)

# グラフのレイアウトを更新
# ホバーモードを"x unified"に設定して、x軸に沿った統一されたホバー情報を表示
# 凡例をグラフの右上に配置(yanchorとxanchorで位置調整)
fig.update_layout(
    hovermode="x unified", legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99)
)

# 作成したグラフを表示
show_fig(fig)
Hide code cell content
# データフレームからユニークなマンガ雑誌名を取得
mcnames = df_dist["mcname"].unique()

# サブプロットを配置するための行数を計算
rows = len(mcnames)

# y軸の最大値を格納するためのリストを初期化
y_max_values = []
Hide code cell source
# 複数のサブプロットを持つ図を作成。各マンガ雑誌名をサブプロットのタイトルとして設定
# vertical_spacingで縦方向のファセット間の余白を調整
fig = make_subplots(
    rows=rows,
    cols=1,
    vertical_spacing=0.1,
    subplot_titles=mcnames,
)

# マンガ雑誌名の数だけ繰り返し処理
for i, mcname in enumerate(mcnames):
    # 現在のマンガ雑誌名に対応するデータをフィルタリング
    df_mc = df_dist[df_dist["mcname"] == mcname].sort_values(
        "グループ名", ignore_index=True
    )
    # 掲載位置の分布プロットを作成
    distplot = create_distplot(
        df_mc, x="掲載位置", color="グループ名", colors=px.colors.diverging.Portland
    )

    # 各サブプロットのy軸の最大値をリストに追加
    y_max_values.append(np.max([trace.y for trace in distplot.data]))

    # 作成した分布プロットを図に追加、可視化のために逆順でtraceを追加
    for trace in distplot.data[::-1]:
        # 凡例が重複しないよう、i==0のときのみ一つだけ表示
        if i > 0:
            trace.showlegend = False
        fig.add_trace(trace, row=i + 1, col=1)

# 全サブプロットの中で最大のy軸値を計算
y_max = np.max(y_max_values)

# Y軸のラベルを表示し、表示範囲を最大値の1.1倍に調整
fig.update_yaxes(title_text="確率密度", range=[0, y_max * 1.1])
# X軸のラベルを下側のサブプロットのみに表示
fig.update_xaxes(title_text="掲載位置", row=rows, col=1)

# ホバーモードを"x unified"に設定して、x軸に沿った統一されたホバー情報を表示
# 各密度プロットが潰れてしまわないように、heightで高さを調整
fig.update_layout(hovermode="x unified", height=800)

# 作成した図を表示する
show_fig(fig)

箱ひげ図#

Hide code cell content
# df_ceのデータをccname(マンガ作品名)ごとにグループ化し、各作品の掲載位置の開始点に関する統計情報を集計
# 'count' はその作品が何回掲載されたか、'mean' は掲載位置の平均値
df_tmp = (
    df_ce.groupby("ccname")["page_start_position"].agg(["count", "mean"]).reset_index()
)

# 集計したデータを 'count'(掲載回数)に基づいて降順にソートし、
# トップ10の作品を抽出(最も多く掲載された作品10個を選ぶ)
df_tmp = df_tmp.sort_values("count", ascending=False).head(10)

# 抽出したトップ10の作品を 'mean'(平均掲載位置)に基づいて昇順にソート
# 平均掲載位置が低い(前の方に掲載される)作品が上位に来る
ccnames = df_tmp.sort_values("mean")["ccname"].to_list()

# 元のdf_ceデータフレームから、トップ10の作品名(ccname)に該当するデータのみを抽出
# reset_index(drop=True)は、新しいデータフレームのインデックスをリセットして整理するためのもの
df_box = df_ce[df_ce["ccname"].isin(ccnames)].reset_index(drop=True)

# 抽出したデータフレーム(df_box)のccname列をカテゴリー型に変換し、
# カテゴリーの順序を先ほどソートしたccnamesの順に設定
# これにより、データフレームの並び替えがこの順序に基づくようになる
df_box["ccname"] = pd.Categorical(df_box["ccname"], categories=ccnames, ordered=True)

# df_boxをccnameに基づいてソート(カテゴリー型なので、設定した順序に従ってソートされる)
# ignore_index=Trueは、ソート後のインデックスもリセットして整理するためのもの
df_box = df_box.sort_values("ccname", ignore_index=True)
Hide code cell content
# 可視化用に保持するカラム
cols2rename = {
    "ccname": "マンガ作品名",
    "page_start_position": "掲載位置",
    "ceid": "ceid",
    "date": "date",
}
# 可視化用に列名を変更
df_box = format_cols(df_box, cols2rename)
Hide code cell content
# 可視化対象のDataFrameを確認
df_box.head()
マンガ作品名 掲載位置 ceid date
0 ドカベン 0.213768 CE113217 1976-07-19
1 ドカベン 0.141304 CE112518 1977-07-11
2 ドカベン 0.097473 CE112532 1977-07-04
3 ドカベン 0.141304 CE112548 1977-06-27
4 ドカベン 0.010830 CE112561 1977-06-20
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_box, DIR_OUT, "box")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/box.csv'.
Hide code cell source
# 'df_box' データフレームを使い、x軸にはマンガ作品名、y軸には各作品の掲載位置を設定
fig = px.box(df_box, x="マンガ作品名", y="掲載位置")

# 描画した箱ひげ図を表示する
show_fig(fig)
Hide code cell content
# '名探偵コナン'というタイトルのマンガに関連するデータをdf_ceから抽出
# そのデータを 'page_start_position'(掲載位置の開始点)に基づいて降順にソート
# ソートされたデータから、特定のカラムのみを選択し、上位10行のみを表示
df_ce[df_ce["ccname"] == "名探偵コナン"].sort_values(
    "page_start_position", ascending=False
)[["miname", "cename", "page_start_position", "pages"]].head(10)
miname cename page_start_position pages
152328 週刊少年サンデー 2011年 表示号数32 FILE 783 菱形と菱形 0.801402 16.0
163865 週刊少年サンデー 2015年 表示号数29 REVIVAL FILE 06 黎明(SSC55巻より) 0.761411 16.0
152261 週刊少年サンデー 2011年 表示号数29 FILE 780 魔法の料理 0.750000 16.0
152928 週刊少年サンデー 2012年 表示号数9 FILE 805 ワタル・ブラザーズ 0.748826 16.0
163695 週刊少年サンデー 2015年 表示号数22 REVIVAL FILE 04 終極(SSC16巻より) 0.721591 18.0
155282 週刊少年サンデー 2014年 表示号数8 FILE 885 凧揚げ大会 0.720085 16.0
154799 週刊少年サンデー 2013年 表示号数39 FILE 870 願いが叶った時に… 0.720085 16.0
163667 週刊少年サンデー 2015年 表示号数21 REVIVAL FILE 03 気配(SSC16巻より) 0.717308 16.0
154928 週刊少年サンデー 2013年 表示号数44 FILE 874 赤き昔日 0.714592 16.0
152677 週刊少年サンデー 2011年 表示号数49 FILE 795 炎へと回帰する運 0.713992 16.0
Hide code cell content
# df_ceから 'cename'(各話名)に 'REVIVAL FILE' が含まれる行を抽出
# 抽出されたデータから特定のカラムのみを選択
df_ce[df_ce["cename"].str.contains("REVIVAL FILE") > 0][
    ["miname", "ccname", "cename", "page_start_position"]
]
miname ccname cename page_start_position
163600 週刊少年サンデー 2015年 表示号数19 名探偵コナン REVIVAL FILE 01 邂逅(SSC16巻より) 0.449187
163627 週刊少年サンデー 2015年 表示号数20 名探偵コナン REVIVAL FILE 02 消滅(SSC16巻より) 0.439271
163667 週刊少年サンデー 2015年 表示号数21 名探偵コナン REVIVAL FILE 03 気配(SSC16巻より) 0.717308
163695 週刊少年サンデー 2015年 表示号数22 名探偵コナン REVIVAL FILE 04 終極(SSC16巻より) 0.721591
163828 週刊少年サンデー 2015年 表示号数28 名探偵コナン REVIVAL FILE 05 月下(SSC55巻より) 0.469262
163865 週刊少年サンデー 2015年 表示号数29 名探偵コナン REVIVAL FILE 06 黎明(SSC55巻より) 0.761411
163890 週刊少年サンデー 2015年 表示号数30 名探偵コナン REVIVAL FILE 07 白昼(SSC55巻より) 0.697154
163919 週刊少年サンデー 2015年 表示号数31 名探偵コナン REVIVAL FILE 08 落日(SSC55巻より) 0.656379

バイオリンプロット#

Hide code cell content
# 箱ひげ図と同じデータを利用
df_violin = df_box.copy()
Hide code cell content
# 可視化対象のDataFrameを確認
df_violin.head()
マンガ作品名 掲載位置 ceid date
0 ドカベン 0.213768 CE113217 1976-07-19
1 ドカベン 0.141304 CE112518 1977-07-11
2 ドカベン 0.097473 CE112532 1977-07-04
3 ドカベン 0.141304 CE112548 1977-06-27
4 ドカベン 0.010830 CE112561 1977-06-20
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_violin, DIR_OUT, "violin")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/violin.csv'.
Hide code cell source
# df_violinデータフレームを使用してバイオリンプロットを描画
# "マンガ作品名"をx軸に、"掲載位置"をy軸に設定し、図の高さを500に設定
fig = px.violin(df_violin, x="マンガ作品名", y="掲載位置", height=500)

# バイオリンプロットの幅を1に設定し、平均線を表示
fig.update_traces(width=1, meanline_visible=True)

# バイオリンプロットを重ねて表示し、バイオリン間の間隔を0に設定
fig.update_layout(violinmode="overlay", violingap=0)

# 作成したバイオリンプロットを表示
show_fig(fig)
Hide code cell content
# 新たな可視化のためにdf_violinのコピーを作成
df_violin2 = df_violin.copy()

# ユニークな 'ceid' の数を計算し、新しいカラム 'half_count' に保存
df_violin2["half_count"] = df_violin2.groupby("マンガ作品名")["ceid"].transform(
    lambda x: x.nunique() / 2
)

# データをマンガ作品名とdateでソートしておく
df_violin2 = df_violin2.sort_values(["マンガ作品名", "date"], ignore_index=True)

# cumcountメソッドを用いて各マンガ作品名ごとに話数インデックス(ceno)を振る
df_violin2["ceno"] = df_violin2.groupby("マンガ作品名").cumcount()

# 掲載時期の前半・後半を割り当てる
# まずはデフォルト値として「前半」を割り当てておく
df_violin2["連載前半"] = True
# 話数インデックスが合計話数の半分以上の場合は、掲載時期を「後半」に更新
df_violin2.loc[df_violin2["ceno"] >= df_violin2["half_count"], "連載前半"] = False
Hide code cell content
# 可視化対象のDataFrameを確認
df_violin2.head()
マンガ作品名 掲載位置 ceid date half_count ceno 連載前半
0 ドカベン 0.008451 CE116042 1972-04-24 317.5 0 True
1 ドカベン 0.188406 CE116029 1972-05-01 317.5 1 True
2 ドカベン 0.119565 CE116013 1972-05-08 317.5 2 True
3 ドカベン 0.113333 CE115997 1972-05-15 317.5 3 True
4 ドカベン 0.094156 CE115981 1972-05-22 317.5 4 True
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_violin2, DIR_OUT, "violin2")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/violin2.csv'.
Hide code cell source
# df_violinデータフレームを使用してスプリットバイオリンプロットを作成
# "マンガ作品名"をx軸に、"掲載位置"をy軸に設定し、"連載前半"をスプリット基準にしてプロット
# バイオリンの幅を1に設定
fig = create_split_violin_plot(
    df_violin2, x="マンガ作品名", y="掲載位置", split="連載前半", width=1
)

# グラフのレイアウトを更新
# 凡例をグラフの左上に配置(yanchorとxanchorで位置調整)し、グラフの高さを500に設定
fig.update_layout(
    legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01), height=500
)

# 作成したバイオリンプロットを表示
show_fig(fig)

リッジラインプロット#

Hide code cell content
# ヒストグラムで用いたものと同じデータを使用
df_ridge = df_hist2.copy()
Hide code cell content
# データをccnameとdateでソートしておく
df_ridge = df_ridge.sort_values(["ccid", "date"], ignore_index=True)

# cumcountメソッドを用いて各マンガ作品名ごとに話数インデックス(ceno)を振る
df_ridge["話数"] = df_ridge.groupby("ccid").cumcount() + 1
Hide code cell content
# 可視化対象のDataFrameを確認
df_ridge.head()
掲載位置 ceid ccid mcname date グループ名 話数
0 0.015291 CE71082 C109295 週刊少年ジャンプ 1980-08-18 第1群(合計話数:8-16話) 1
1 0.198777 CE71068 C109295 週刊少年ジャンプ 1980-08-25 第1群(合計話数:8-16話) 2
2 0.266055 CE71051 C109295 週刊少年ジャンプ 1980-09-01 第1群(合計話数:8-16話) 3
3 0.394495 CE71038 C109295 週刊少年ジャンプ 1980-09-08 第1群(合計話数:8-16話) 4
4 0.266055 CE71019 C109295 週刊少年ジャンプ 1980-09-15 第1群(合計話数:8-16話) 5
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_ridge, DIR_OUT, "ridge")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/ridge.csv'.
Hide code cell source
# df_ridgeデータフレームを使ってリッジラインプロットを作成
# "話数"をy軸に、"掲載位置"をx軸に設定し、図の高さを500に設定
# orientation="h"で水平方向のバイオリンプロットを作成
fig = px.violin(
    df_ridge, y="話数", x="掲載位置", orientation="h", points=False, height=500
)

# side="positive"でバイオリンの片側だけを表示し、width=3でバイオリンの幅を設定
fig.update_traces(side="positive", width=3)

# y軸の表示範囲を設定
# 0からdf_ridge内の"話数"の最大値+1までの範囲に設定
fig.update_yaxes(range=[0, df_ridge["話数"].max() + 1])

# 作成したリッジラインプロットを表示
show_fig(fig)
Hide code cell source
# ファセットがグループ名順に並ぶように事前にソートしておく
df_ridge = df_ridge.sort_values(["グループ名"], ignore_index=True)

# df_ridgeデータフレームを使ってリッジラインプロットを作成
# "話数"をy軸に、"掲載位置"をx軸に設定し、図の高さを400に設定
# facet_colでグループ名を指定することでグループごとの可視化を実現
# orientation="h"で水平方向のバイオリンプロットを作成
fig = px.violin(
    df_ridge,
    y="話数",
    x="掲載位置",
    orientation="h",
    facet_col="グループ名",
    points=False,
    height=400,
)

# side="positive"でバイオリンの片側だけを表示し、width=3でバイオリンの幅を設定
fig.update_traces(side="positive", width=3)

# y軸の表示範囲を設定
# 0からdf_ridge内の"話数"の最大値+1までの範囲に設定
fig.update_yaxes(range=[0, df_ridge["話数"].max() + 1])

# ファセット(グループごとのリッジラインプロット)のタイトルを簡潔にする処理
# デフォルトではタイトルは「グループ=xxx」という形式になっている
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成したリッジラインプロットを表示
show_fig(fig)
Hide code cell content
# データフレームからユニークな話数を取得
cenos = sorted(df_ridge["話数"].unique())

# サブプロットを配置するための行数を計算
rows = len(cenos)

# y軸の最大値を格納するためのリストを初期化
y_max_values = []
Hide code cell source
# 複数のサブプロットを持つ図を作成。各話数をサブプロットのタイトルとして設定
# vertical_spacingで縦方向のファセット間の余白を調整
fig = make_subplots(
    rows=rows,
    cols=1,
    vertical_spacing=0.01,
)

# 話数の数だけ繰り返し処理
for i, ceno in enumerate(cenos):
    # 現在の話数に対応するデータをフィルタリング
    df_ceno = df_ridge[df_ridge["話数"] == ceno].sort_values(
        "グループ名", ignore_index=True
    )
    # 掲載位置の分布プロットを作成
    distplot = create_distplot(
        df_ceno, x="掲載位置", color="グループ名", colors=px.colors.diverging.Portland
    )

    # 作成した分布プロットを図に追加、可視化のために逆順でtraceを追加
    for trace in distplot.data[::-1]:
        # 凡例が重複しないよう、i==0のときのみ一つだけ表示
        if i > 0:
            trace.showlegend = False
        fig.add_trace(trace, row=i + 1, col=1)

    # Y軸のラベルとして話数を表示
    fig.update_yaxes(title_text=f"{ceno}話目", row=i + 1)
    # X軸のメモリを表示しないように設定
    fig.update_xaxes(showticklabels=False, row=i + 1)

# X軸のラベルを下側のサブプロットのみに表示
fig.update_xaxes(title_text="掲載位置", showticklabels=True, row=rows)

# ホバーモードを"x unified"に設定して、x軸に沿った統一されたホバー情報を表示
# 各密度プロットが潰れてしまわないように、heightで高さを調整
fig.update_layout(hovermode="x unified", height=800)

# 作成した図を表示する
show_fig(fig)
Hide code cell content
# 週刊少年ジャンプのマンガ作品のみを抽出
df_ridge_jump = df_ridge[df_ridge["mcname"] == "週刊少年ジャンプ"].reset_index(
    drop=True
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_ridge_jump.head()
掲載位置 ceid ccid mcname date グループ名 話数
0 0.015291 CE71082 C109295 週刊少年ジャンプ 1980-08-18 第1群(合計話数:8-16話) 1
1 0.629393 CE74085 C89161 週刊少年ジャンプ 1976-08-16 第1群(合計話数:8-16話) 8
2 0.808307 CE74103 C89161 週刊少年ジャンプ 1976-08-09 第1群(合計話数:8-16話) 7
3 0.565495 CE74115 C89161 週刊少年ジャンプ 1976-08-02 第1群(合計話数:8-16話) 6
4 0.719870 CE74134 C89161 週刊少年ジャンプ 1976-07-26 第1群(合計話数:8-16話) 5
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_ridge_jump, DIR_OUT, "ridge_jump")
DataFrame is saved as '../../../data/cm/output/vol1/04/dists/ridge_jump.csv'.
Hide code cell source
# 複数のサブプロットを持つ図を作成。各話数をサブプロットのタイトルとして設定
# vertical_spacingで縦方向のファセット間の余白を調整
fig = make_subplots(
    rows=rows,
    cols=1,
    vertical_spacing=0.01,
)

# 話数の数だけ繰り返し処理
for i, ceno in enumerate(cenos):
    # 現在の話数に対応するデータをフィルタリング
    df_ceno = df_ridge_jump[df_ridge_jump["話数"] == ceno].sort_values(
        "グループ名", ignore_index=True
    )
    # 掲載位置の分布プロットを作成
    distplot = create_distplot(
        df_ceno, x="掲載位置", color="グループ名", colors=px.colors.diverging.Portland
    )

    # 作成した分布プロットを図に追加、可視化のために逆順でtraceを追加
    for trace in distplot.data[::-1]:
        # 凡例が重複しないよう、i==0のときのみ一つだけ表示
        if i > 0:
            trace.showlegend = False
        fig.add_trace(trace, row=i + 1, col=1)

    # Y軸のラベルとして話数を表示
    fig.update_yaxes(title_text=f"{ceno}話目", row=i + 1)
    # X軸のメモリを表示しないように設定
    fig.update_xaxes(showticklabels=False, row=i + 1)

# X軸のラベルを下側のサブプロットのみに表示
fig.update_xaxes(title_text="掲載位置", showticklabels=True, row=rows)

# ホバーモードを"x unified"に設定して、x軸に沿った統一されたホバー情報を表示
# 各密度プロットが潰れてしまわないように、heightで高さを調整
fig.update_layout(hovermode="x unified", height=800)

# 作成した図を表示する
show_fig(fig)