マンガデータの量を見る#

準備#

Import#

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

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

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

# 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_IN = Path("../../../data/cm/input")

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

# Comic Episode関連のファイル名
FN_CE = "cm_ce.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 add_years_to_df(
    df: pd.DataFrame, unit_years: int = 10, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトは10
    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
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_IN / FN_CE)
Hide code cell content
# date列をdatetime型に変換
df_ce["date"] = pd.to_datetime(df_ce["date"])

棒グラフ#

Hide code cell content
# マンガ作品ごとの掲載話数を集計するためのDataFrameを作成
# 'groupby' と 'nunique' を使用して、各マンガ作品ごとにユニークな 'ceid'(掲載話数)を数える
df_bar = df_ce.groupby(["ccname"])["ceid"].nunique().reset_index(name="n_ce")

# 掲載話数が多い順にデータを並び替えて、上位N_CC件のデータを選択
# 'sort_values' でソートし、'head(20)' で上位20件を取得
df_bar = df_bar.sort_values(by="n_ce", ascending=False, ignore_index=True).head(20)

# 可視化のために'rename'メソッドを用いて列名をわかりやすい名前に変更
df_bar = df_bar.rename(columns={"ccname": "マンガ作品名", "n_ce": "合計話数"})
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_bar.head()
マンガ作品名 合計話数
0 こちら葛飾区亀有公園前派出所 1968
1 はじめの一歩 1186
2 名探偵コナン 1009
3 ONE PIECE 893
4 MAJOR 748
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_bar, DIR_OUT, "bar")
DataFrame is saved as '../../../data/cm/output/vol1/04/amounts/bar.csv'.
Hide code cell source
# 'px.bar' を使用して棒グラフを作成
# x軸には '合計話数'、y軸には 'マンガ作品名' を設定
# 'orientation' を 'h' に設定することで、棒グラフを水平(横向き)に表示
# 'height' でグラフの高さを指定
fig = px.bar(df_bar, x="合計話数", y="マンガ作品名", orientation="h", height=500)

# 作成した棒グラフを表示
show_fig(fig)

集合棒グラフ#

Hide code cell content
# 10年区切りの年代情報を追加
df_ce = add_years_to_df(df_ce)
Hide code cell content
# 「ccname」と「years」列で集計し、ユニークな「ceid」の数を「n_ce」列に格納
df_gbar = df_ce.groupby(["ccname", "years"])["ceid"].nunique().reset_index(name="n_ce")

# df_barに含まれるから「マンガ作品名」の上位10作品をリストに格納
ccnames = list(df_bar["マンガ作品名"])[:10]
# df_gbarをフィルタリングして、ccnamesに含まれるccnameのみを選択
df_gbar = df_gbar[df_gbar["ccname"].isin(ccnames)].reset_index(drop=True)
# 「ccname」列と「years」列を使ってdf_gbarをリサンプリング
df_gbar = resample_df_by_col_and_years(df_gbar, "ccname")

# 「ccname」列をカテゴリカルデータとして扱い、ccnamesの順序で並べ替え
df_gbar["ccname"] = pd.Categorical(df_gbar["ccname"], categories=ccnames, ordered=True)
# 「ccname」と「years」でソートし、インデックスをリセット
df_gbar = df_gbar.sort_values(["ccname", "years"], ignore_index=True)

# 可視化用に列名をわかりやすい日本語の名前に変更
df_gbar = df_gbar.rename(
    columns={"ccname": "マンガ作品名", "years": "年代", "n_ce": "合計話数"}
)
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_gbar.head()
マンガ作品名 年代 合計話数
0 こちら葛飾区亀有公園前派出所 1970 160
1 こちら葛飾区亀有公園前派出所 1980 501
2 こちら葛飾区亀有公園前派出所 1990 482
3 こちら葛飾区亀有公園前派出所 2000 492
4 こちら葛飾区亀有公園前派出所 2010 333
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gbar, DIR_OUT, "gbar")
DataFrame is saved as '../../../data/cm/output/vol1/04/amounts/gbar.csv'.
Hide code cell source
# df_gbarデータを使用して棒グラフを作成
# Y軸にマンガ作品名をとり、X軸に合計話数をとり、色は年代別に表示
# orientationで水平方向の横棒グラフを指定し、barmode=groupで集合棒グラフ化
# グラフの高さは600に調整し、カラーパレットはPortlandを指定
fig = px.bar(
    df_gbar,
    x="合計話数",
    y="マンガ作品名",
    color="年代",
    orientation="h",
    barmode="group",
    height=600,
    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(fig)

積上げ棒グラフ#

Hide code cell content
# 比較のため、集合棒グラフと同じDataFrameを利用
df_sbar = df_gbar.copy()
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_sbar.head()
マンガ作品名 年代 合計話数
0 こちら葛飾区亀有公園前派出所 1970 160
1 こちら葛飾区亀有公園前派出所 1980 501
2 こちら葛飾区亀有公園前派出所 1990 482
3 こちら葛飾区亀有公園前派出所 2000 492
4 こちら葛飾区亀有公園前派出所 2010 333
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_sbar, DIR_OUT, "sbar")
DataFrame is saved as '../../../data/cm/output/vol1/04/amounts/sbar.csv'.
Hide code cell source
# df_gbarデータを使用して棒グラフを作成
# Y軸にマンガ作品名をとり、X軸に合計話数をとり、色は年代別に表示
# orientationで水平方向の横棒グラフを指定し、barmode=stackで積上げ棒グラフ化
# グラフの高さは400に調整し、カラーパレットはPortlandを指定
fig = px.bar(
    df_gbar,
    x="合計話数",
    y="マンガ作品名",
    color="年代",
    orientation="h",
    barmode="stack",
    height=400,
    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(fig)

ヒートマップ#

Hide code cell content
# 1年区切りの年代情報を追加
df_ce = add_years_to_df(df_ce, unit_years=1)

# df_ceをdate順でソートし、全ccnameを事前に抽出しておく、時間順に並んだヒートマップを作成するための工夫
ccnames = (
    df_ce.sort_values(["date", "ccid"], ignore_index=True)["ccname"].unique().tolist()
)
Hide code cell content
# 「ccname」と「years」と「mcname」列で集計し、ユニークな「ceid」の数を「n_ce」列に格納
df_hm = (
    df_ce.groupby(["mcname", "ccname", "years"])["ceid"]
    .nunique()
    .reset_index(name="n_ce")
)

# ccnamesのうち、df_barの「マンガ作品名」の上位10作品を抽出
# 直接list(df_bar["マンガ作品名"])[:20]を用いないのはccnamesの順序を維持するため
ccnames = [ccname for ccname in ccnames if ccname in list(df_bar["マンガ作品名"])[:20]]
# df_hmをフィルタリングして、ccnamesに含まれるccnameのみを選択
df_hm = df_hm[df_hm["ccname"].isin(ccnames)].reset_index(drop=True)
# 「ccname」列と「years」列を使ってdf_gbarをリサンプリング
df_hm = resample_df_by_col_and_years(df_hm, "ccname")

# 「ccname」列をカテゴリカルデータとして扱い、ccnamesの順序で並べ替え
df_hm["ccname"] = pd.Categorical(df_hm["ccname"], categories=ccnames, ordered=True)
# 「ccname」と「years」でソートし、インデックスをリセット
df_hm = df_hm.sort_values(["ccname", "years"], ignore_index=True)

# 可視化用に列名をわかりやすい日本語の名前に変更
df_hm = df_hm.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "ccname": "マンガ作品名",
        "years": "掲載年",
        "n_ce": "合計話数",
    }
)
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_hm.head()
マンガ雑誌名 マンガ作品名 掲載年 合計話数
0 週刊少年サンデー ダメおやじ 1970 9
1 週刊少年サンデー ダメおやじ 1971 51
2 週刊少年サンデー ダメおやじ 1972 51
3 週刊少年サンデー ダメおやじ 1973 50
4 週刊少年サンデー ダメおやじ 1974 50
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_hm, DIR_OUT, "hm")
DataFrame is saved as '../../../data/cm/output/vol1/04/amounts/hm.csv'.
Hide code cell source
# df_hmデータを使ってヒートマップを作成
# x軸に掲載年、y軸にマンガ作品名、z軸に合計話数を設定
fig = px.density_heatmap(df_hm, x="掲載年", y="マンガ作品名", z="合計話数", height=600)

# ヒートマップを表示
show_fig(fig)
Hide code cell content
# 週刊少年ジャンプの2009年のデータを選択し、雑誌巻号ごとにCEIDのユニーク数をカウント
# グループ化して、新しい列n_ceとして格納
df_mi = (
    df_ce[(df_ce["mcname"] == "週刊少年ジャンプ") & (df_ce["years"] == "2009")]
    .groupby(["miid", "miname"])["ceid"]
    .nunique()
    .reset_index(name="n_ce")
)

# n_ceで降順ソートして上位5件を表示
df_mi.sort_values("n_ce", ascending=False).head()
miid miname n_ce
47 M542881 週刊少年ジャンプ 2009年 表示号数6 44
0 M542834 週刊少年ジャンプ 2010年 表示号数03 43
18 M542852 週刊少年ジャンプ 2009年 表示号数37 37
43 M542877 週刊少年ジャンプ 2009年 表示号数11 26
17 M542851 週刊少年ジャンプ 2009年 表示号数39 24
Hide code cell content
# miidが'M542881'であるレコードを選択し、必要な列のみを抽出
# ccname, cename, page_start, pages, dateを選択
# page_startでソートして、ページの開始順に並べ替え
df_ce[df_ce["miid"] == "M542881"][
    ["ccname", "cename", "page_start", "pages", "date"]
].sort_values("page_start")
ccname cename page_start pages date
39979 NARUTO-ナルト- 430:ナルト帰還!! 9.0 33.0 2009-01-28
39980 ONE PIECE 第527話 紅蓮地獄 43.0 19.0 2009-01-28
39981 トリコ グルメ 32 ロックドラム!! 63.0 19.0 2009-01-28
39982 BLEACH 340. The Antagonizer 83.0 19.0 2009-01-28
39983 家庭教師ヒットマンREBORN! 標的 224 XANXUS VS. Rasiel 103.0 17.0 2009-01-28
39984 ONE PIECE 消されたライセンス 130.0 1.0 2009-01-28
39985 こちら葛飾区亀有公園前派出所 コタツとみかん 130.0 1.0 2009-01-28
39986 ぬらりひょんの孫 1ゆら 2カナ 3つらら 131.0 1.0 2009-01-28
39988 BLEACH 白哉玉 132.0 1.0 2009-01-28
39987 魔人探偵脳噛ネウロ 弥子の正月 132.0 1.0 2009-01-28
39989 アスクレピオス はねつきっス!! 133.0 1.0 2009-01-28
39990 To LOVEる -とらぶる- ダーク・オ・セチー 134.0 1.0 2009-01-28
39991 アイシールド21 一富士二鷹三茄子は最高に縁起のいい初夢です 134.0 1.0 2009-01-28
39992 マイスター 蹴り初め 135.0 1.0 2009-01-28
39993 銀魂 汁粉と善哉 136.0 1.0 2009-01-28
39994 SKET DANCE 今年の目標 136.0 1.0 2009-01-28
39995 黒子のバスケ 本の虫 137.0 1.0 2009-01-28
39997 PSYREN-サイレン- おねがいごと 138.0 1.0 2009-01-28
39996 家庭教師ヒットマンREBORN! ランボはどこ? 138.0 1.0 2009-01-28
39998 ぼっけさん ヒノとサユの初詣 139.0 1.0 2009-01-28
40000 トリコ トリコの正月 140.0 1.0 2009-01-28
39999 バクマン。 俺達に正月休みはない! 140.0 1.0 2009-01-28
40001 いぬまるだしっ お正月の思い出作文 141.0 1.0 2009-01-28
40002 ピューと吹く!ジャガー ぼやき初め 142.0 1.0 2009-01-28
40003 NARUTO-ナルト- 10年目の真実 142.0 1.0 2009-01-28
40004 SKET DANCE 第72話 ファッショナブル侍 145.0 17.0 2009-01-28
40005 ダブルマメダイチ NaN 163.0 49.0 2009-01-28
40006 バクマン。 20ページ 未来と階段 213.0 21.0 2009-01-28
40007 銀魂 第243訓 何回見てもラピュタはいい 237.0 19.0 2009-01-28
40008 アイシールド21 312th down 新世代へ 257.0 19.0 2009-01-28
40009 いぬまるだしっ 第20回 1 「たまこ先生の年賀状」 299.0 3.0 2009-01-28
40010 いぬまるだしっ 第20回 2 「アレに似てるおじさん」 302.0 4.0 2009-01-28
40011 いぬまるだしっ 第20回 3 「善と悪」 306.0 2.0 2009-01-28
40012 黒子のバスケ 第4Q まともじゃないかもしんないスね 311.0 19.0 2009-01-28
40013 ぼっけさん 第3怪 存在の証明 331.0 27.0 2009-01-28
40014 マイスター Play.5 スタープレーヤー 359.0 19.0 2009-01-28
40015 こちら葛飾区亀有公園前派出所 初夢の正月クルーズの巻 379.0 19.0 2009-01-28
40016 PSYREN-サイレン- CALL.53 “痛み” 399.0 19.0 2009-01-28
40017 ぬらりひょんの孫 第41幕 百鬼夜行対八十八鬼夜行 419.0 19.0 2009-01-28
40018 魔人探偵脳噛ネウロ 第188話 距【きょり】 439.0 19.0 2009-01-28
40019 To LOVEる -とらぶる- トラブル 131 クイーンの反抗 459.0 19.0 2009-01-28
40020 アスクレピオス 第15話 結紮!! 481.0 19.0 2009-01-28
40021 ピューと吹く!ジャガー 映画公開記念特別編 ・~いま、吹きにゆきます~~の撮影にいま、ゆきます~ 511.0 1.0 2009-01-28
40022 ピューと吹く!ジャガー 週刊!?ハミ通SP 特集:文字で見る映画の裏側 512.0 2.0 2009-01-28