上巻 第4章 解答例#

ここでは、 本書の学習内容の定着 を目的とした練習問題とその解答・解説を掲載します。 なお、問題の性質上、本書で取り上げた処理と重複することがあります。 ご了承ください。

前提#

以下のように、ライブラリのインポートと変数の定義が完了していることを前提とします。

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")

# ファイル名の定義
FN_CE = "cm_ce.csv"
FN_CC_CRT = "cm_cc_crt.csv"

# plotlyの描画設定の定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# 質的変数の描画用のカラースケールの定義(Okabe and Ito 2008基準)
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 = 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
# マンガ各話に関するファイルの読み込み
df_ce = pd.read_csv(DIR_IN / "cm_ce.csv")
# マンガ作品と作者の対応関係に関するファイルの読み込み
df_cc_crt = pd.read_csv(DIR_IN / "cm_cc_crt.csv")
# date列をdatetime型に変換
df_ce["date"] = pd.to_datetime(df_ce["date"])

基礎 問題1:合計話数上位10作品#

関連セクション: マンガデータの量を見る

本書で紹介したマンガ作品の合計話数ランキングを復習しましょう。 合計話数が多い上位10作品を横棒グラフで可視化してください。

Hide code cell source
# マンガ作品ごとの掲載話数を集計
df_bar = df_ce.groupby("ccname")["ceid"].nunique().reset_index(name="n_ce")

# 掲載話数が多い順にソートし、上位10件を抽出
df_bar = df_bar.sort_values("n_ce", ascending=False).head(10)

# 列名を日本語に変更
df_bar = df_bar.rename(columns={"ccname": "マンガ作品名", "n_ce": "合計話数"})

# 横棒グラフを作成
fig = px.bar(df_bar, x="合計話数", y="マンガ作品名", orientation="h", height=400)

# グラフを表示
show_fig(fig)

解説

横棒グラフは、カテゴリごとの を比較するのに最適な手法です。

groupby()nunique() を組み合わせることで、各マンガ作品に含まれるユニークな各話数をカウントできます。 上位10作品に絞ることで、長期連載作品の傾向がより明確に見えます。

関連セクション: 詳しくはマンガデータの量を見るを参照してください。

基礎 問題2:ページ数の分布#

関連セクション: マンガデータの分布を見る

マンガ各話のページ数がどのように分布しているかを確認しましょう。 df_ceのページ数(pages)のヒストグラムを、ビン数30で作成してください。

Hide code cell source
# ページ数のヒストグラムを作成(ビン数30)
fig = px.histogram(df_ce, x="pages", nbins=30, labels={"pages": "ページ数"})

# y軸のラベルを設定
fig.update_yaxes(title="各話数")

# グラフを表示
show_fig(fig)

解説

ヒストグラムは、連続値の 分布 を把握するのに適した手法です。

ビン数(nbins)を調整することで、データの粒度を変えて分布の特徴を観察できます。 様々な値に変えて、形状の変化を確認してみましょう。

関連セクション: 詳しくはマンガデータの分布を見るを参照してください。

標準 問題3:週刊少年ジャンプの合計話数上位作品#

関連セクション: マンガデータの量を見る

特定の雑誌に絞って分析することで、雑誌ごとの特徴が見えてきます。 週刊少年ジャンプ(mcnameが「週刊少年ジャンプ」)のみに絞り、合計話数上位10作品を横棒グラフで可視化してください。

Hide code cell source
# 週刊少年ジャンプのみに絞り込む
df_jump = df_ce[df_ce["mcname"] == "週刊少年ジャンプ"]

# マンガ作品ごとの掲載話数を集計
df_bar_jump = df_jump.groupby("ccname")["ceid"].nunique().reset_index(name="n_ce")

# 掲載話数が多い順にソートし、上位10件を抽出
df_bar_jump = df_bar_jump.sort_values("n_ce", ascending=False).head(10)

# 列名を日本語に変更
df_bar_jump = df_bar_jump.rename(columns={"ccname": "マンガ作品名", "n_ce": "合計話数"})

# 横棒グラフを作成
fig = px.bar(df_bar_jump, x="合計話数", y="マンガ作品名", orientation="h", height=400)

# グラフを表示
show_fig(fig)

解説

ブールインデックスで特定の雑誌に絞り込むことで、雑誌ごとの傾向を分析できます。

週刊少年ジャンプの長期連載作品を見ると、全雑誌を対象とした場合とは異なるランキングが現れます。 このように、分析対象を絞ることで、より詳細な知見が得られることがあるのです。

関連セクション: 詳しくはマンガデータの量を見るを参照してください。

標準 問題4:4色カラー各話のページ数分布#

関連セクション: マンガデータの分布を見る

4色カラーで掲載された各話は、通常のモノクロ掲載とはページ数が異なる可能性があります。 four_coloredがTrueの各話のみに絞り、ページ数のヒストグラムを作成してください。

Hide code cell source
# 4色カラー各話のみに絞り込む
df_color = df_ce[df_ce["four_colored"]]

# ページ数のヒストグラムを作成
fig = px.histogram(df_color, x="pages", nbins=30, labels={"pages": "ページ数"})

# y軸のラベルを設定
fig.update_yaxes(title="各話数")

# グラフを表示
show_fig(fig)

解説

条件による絞り込みを行うことで、特定の属性を持つデータの分布を確認できます。

4色カラー各話のページ数分布を見ると、全体のページ数分布(問題2)とは異なる傾向があるようです。 ビン数(nbins)などの条件を揃えて、再度比較してみましょう。 分布の違いはどこから来ているか、考えてみると面白いかもしれません。

関連セクション: 詳しくはマンガデータの分布を見るを参照してください。

発展 問題5:マンガ作者別の合計ページ数#

関連セクション: マンガデータの量を見る

マンガ作者[1]がその生涯で「何ページ描いたか」は、その作業量を測る一つの指標となります。 ページ数という観点から、マンガ作者の を可視化してみましょう。

  • df_cedf_cc_crtccid をキーにマージしてください

  • 各マンガ作者(crtname)ごとの合計ページ数(pages)を集計してください

  • 上位15名を横棒グラフで可視化してください

Hide code cell source
# 作品情報と作者情報をマージ
df_merged = pd.merge(df_ce, df_cc_crt[["ccid", "crtname"]], on="ccid", how="inner")
# マンガ作者ごとに合計ページ数を集計
df_crt_pages = df_merged.groupby("crtname")["pages"].sum().reset_index()
# 合計ページ数で降順にソートし、上位15名を抽出
df_crt_pages_top15 = df_crt_pages.sort_values("pages", ascending=False).head(15)

# 横棒グラフを作成
fig = px.bar(
    df_crt_pages_top15,  # 対象のデータフレーム
    x="pages",  # x軸に合計ページ数
    y="crtname",  # y軸にマンガ作者名
    orientation="h",  # 横棒グラフを指定
    labels={"pages": "合計ページ数", "crtname": "マンガ作者名"},  # 軸ラベルの変更
    height=500,  # グラフの高さを設定
)
# 作成したグラフを表示
show_fig(fig)

解説

棒グラフは、質的変数(ここではマンガ作者)に関する を比較するのに最適な手法です。

今回、話数ではなく ページ数 で集計することで、週刊連載を何十年も維持し続けるトップクリエイターの仕事量を可視化しました。 集計単位を工夫することで、ドメイン知識として持っている「巨匠」の凄さを定量的に再確認できます。

関連セクション: 詳しくはマンガデータの量を見るを参照してください。

発展 問題6:合計話数の累積分布#

関連セクション: マンガデータの分布を見る

マンガ業界、とくに週刊少年誌は非常に厳しい世界です。 多くの作品が短期間で連載を終える一方で、ごく一部の作品だけが長期連載を勝ち取ります。 掲載された作品が、どれくらいの話数まで到達できるのか、その生存競争の過酷さを可視化してみましょう。

  • df_ceccid(マンガ作品ID)でグループ化し、各作品の合計話数を集計してください

  • 集計した合計話数の累積ヒストグラムを作成してください

  • X軸の範囲を 0 から 200 までに制限してください

Hide code cell source
# マンガ作品ごとの合計話数を集計
df_cc = df_ce.groupby("ccid")["ceid"].nunique().reset_index(name="n_ce")

# 累積ヒストグラムを作成
fig = px.histogram(
    df_cc,  # 対象のデータフレーム
    x="n_ce",  # 分布を見たい列(合計話数)
    cumulative=True,  # 累積表示を有効化
    nbins=200,  # ビン数を設定
    labels={"n_ce": "合計話数"},  # 軸ラベルの変更
    height=500,  # グラフの高さを設定
)
# x軸の表示範囲を0から200までに制限
fig.update_xaxes(range=[0, 200])
# y軸のタイトルを「累積作品数」に変更
fig.update_yaxes(title="累積作品数")
# 作成したグラフを表示
show_fig(fig)

解説

累積ヒストグラムを用いることで、特定の「壁」を越えられた作品がどれくらい存在するかを直感的に理解しやすくなります。

グラフの立ち上がりが急であるほど、初期の話数で連載を終える作品が多いことを示しています。 200話に到達する頃には傾きがほぼ平坦になっていることから、連載を継続することがいかに困難か見て取れます。

関連セクション: 詳しくはマンガデータの分布を見るを参照してください。

発展 問題7:カラー掲載割合の年代別推移#

関連セクション: マンガデータの内訳を見る

時代とともに、マンガ雑誌の「カラー掲載」作品の扱いは変化してきたのでしょうか。 各年代(years)の中で、4色カラー各話が占める割合を比較してみましょう。

  • df_ceadd_years_to_df() 関数で年代情報を追加してください

  • 年代ごとにカラー有無の割合(合計を1.0としたスケーリング)を算出してください

  • 積上げ棒グラフを作成し、配色には OKABE_ITO カラーパレットを使用してください

Hide code cell source
# 年代情報を追加
df_ce_years = add_years_to_df(df_ce)
# 年代とカラー有無でグループ化し件数を集計
df_prop = (
    df_ce_years.groupby(["years", "four_colored"]).size().reset_index(name="count")
)
# 各年代ごとの合計件数を計算
df_prop["total"] = df_prop.groupby("years")["count"].transform("sum")
# 各要素の割合(1.0基準)を計算
df_prop["proportion"] = df_prop["count"] / df_prop["total"]
# 割合に基づいた積上げ棒グラフを作成

fig = px.bar(
    df_prop,  # 対象のデータフレーム
    x="years",  # x軸に年代
    y="proportion",  # y軸に算出された割合
    color="four_colored",  # カラー有無で色分け
    barmode="stack",  # 積上げ表示を指定
    color_discrete_sequence=OKABE_ITO,  # OKABE_ITOカラーパレットを適用
    labels={
        "years": "年代",
        "proportion": "掲載割合",
        "four_colored": "4色カラー有無",
    },  # ラベル
    height=500,  # グラフの高さを設定
)

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

解説

絶対数ではなく「割合」にスケーリングすることで、掲載総数の変化に左右されず、カラーページの 相対的な重要度 の変遷を公平に比較できます。 ただし、分母を揃えると分母の情報(各年代の合計話数)が失われてしまう点には注意が必要です。

結果を見てみると、2000年以降、平均的にカラー各話の割合が増加しているように見えます。 印刷コストの低下や電子版普及によるカラー需要の増大といった仮説につなげ、さらなる分析を行ってもみても良いでしょう。

関連セクション: 詳しくはマンガデータの内訳を見るを参照してください。

応用 問題8:初回ページ数と掲載位置の関係#

関連セクション: マンガデータの関係を見る

新連載がどのような条件でスタートするかは、雑誌ごとの編集戦略の違いを映し出します。 8話以上継続した作品の「第1話」について、掲載位置とページ数の関係を散布図で可視化しましょう。

  • 8話以上継続した作品のIDを抽出し、各作品の第1話のみを取得してください

  • 掲載位置(page_start_position)とページ数(pages)の散布図を作成してください

  • 雑誌別にファセットを分割し、重なりを防いで各雑誌の傾向を詳しく見られるようにしてください

Hide code cell source
# 8話以上継続した作品のIDを抽出
ccids_8plus = df_ce.groupby("ccid")["ceid"].nunique()
# 条件に合致するIDをフィルタリング
ccids_8plus = ccids_8plus[ccids_8plus >= 8].index
# 対象作品の全データを抽出
df_8plus = df_ce[df_ce["ccid"].isin(ccids_8plus)]
# 作品ごとに日付でソートし、最初の各話(第1話)のみを抽出
df_first = df_8plus.sort_values(["ccid", "date"]).groupby("ccid").head(1)

# 散布図を作成(雑誌別にファセットを分割)
fig = px.scatter(
    df_first,           # 対象のデータフレーム
    x="page_start_position", # x軸に掲載位置
    y="pages",          # y軸にページ数
    facet_col="mcname", # 雑誌別にファセットを分割
    facet_col_wrap=2,   # 2列で折り返し表示
    opacity=0.6,        # 透明度を設定
    hover_name="ccname", # ホバー時に作品名を表示
    labels={"page_start_position": "第1話の掲載位置", "pages": "第1話のページ数", "mcname": "マンガ雑誌名"}, # ラベル
    height=600          # ファセット表示のため少し高めに設定
)

# マーカーのサイズ、境界線の太さと色を設定
fig.update_traces(
    marker={
        "size": 10,
        "line_width": 1,
        "line_color": "white",
    }
)

# 各ファセットのタイトルをマンガ雑誌名のみに短縮
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

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

解説

散布図をファセット(雑誌別)に分割することで、データ点の重なりが解消され、各雑誌固有のパターンがより鮮明になります。

どの雑誌もX軸の左端(巻頭)かつY軸の高い位置(増ページ)に点が密集しており、新連載を強力にプッシュする業界共通の構造が確認できます。 一方で、巻頭以外の位置からスタートする例外的な作品の数や、ページ数のばらつき具合には雑誌ごとの違いも見られます。

関連セクション: 詳しくはマンガデータの関係を見るを参照してください。