下巻 第3章 解答例#

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

前提#

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

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

# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
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 content
# 国内主要ゲームメーカーのプラットフォームとメーカー名の対応辞書
# キー: プラットフォーム名、値: メーカー名の略称
PF2MK = {
    "プレイステーション": "ソニー",
    "プレイステーション2": "ソニー",
    "プレイステーション・ポータブル": "ソニー",
    "プレイステーション3": "ソニー",
    "プレイステーションVita": "ソニー",
    "プレイステーション4": "ソニー",
    "ゲームアーカイブス": "ソニー",
    "SG-1000": "セガ",
    "SC-3000": "セガ",
    "SEGAマーク3": "セガ",
    "セガ・マスターシステム": "セガ",
    "メガドライブ": "セガ",
    "ゲームギア": "セガ",
    "セガサターン": "セガ",
    "ドリームキャスト": "セガ",
    "ファミリーコンピュータ": "任天堂",
    "ゲームボーイ": "任天堂",
    "スーパーファミコン": "任天堂",
    "NINTENDO64": "任天堂",
    "ゲームボーイアドバンス": "任天堂",
    "ニンテンドーゲームキューブ": "任天堂",
    "ニンテンドーDS": "任天堂",
    "ニンテンドー3DS": "任天堂",
    "Wii": "任天堂",
    "WiiU": "任天堂",
    "NintendoSwitch": "任天堂",
}
Hide code cell content
# pandasのweekday関数で取得できる曜日の数値と実際の曜日名を対応させる辞書を定義
# 0:月曜日, 1:火曜日, ... , 6:日曜日
WEEKDAY2YOBI = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}

また、本書中で取り上げた以下の関数も、同様に利用可能とします。

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
# 各種データの読み込み
df_ce = pd.read_csv(DIR_CM / FN_CE)  # マンガ各話に関するデータ
df_cc_crt = pd.read_csv(DIR_CM / FN_CC_CRT)  # マンガ作品と作者の対応データ
df_ae = pd.read_csv(DIR_AN / FN_AE)  # アニメ各話に関するデータ
df_ac_act = pd.read_csv(DIR_AN / FN_AC_ACT)  # アニメ作品と声優の対応データ
df_pkg_pf = pd.read_csv(
    DIR_GM / FN_PKG_PF
)  # ゲームパッケージとプラットフォームの対応データ

基礎 問題1:年代別・曜日別のゲームパッケージ数#

関連セクション: 円グラフ

本書では、年代別に発売曜日ごとのゲームパッケージ数を円グラフで可視化し、category_ordersを指定して曜日順(月〜日)に並べました。 ここでは、category_orders指定しない場合にどのようなグラフになるか確認してみましょう。

  • df_pkg_pfを用いて、1990年代以降の年代別(5年刻み)・曜日別のゲームパッケージ数を、ファセット分割した円グラフで可視化してください。

  • ただし、category_orders指定しない でください。

Hide code cell source
# df_pkg_pfに年代(5年刻み)を追加
df_gm = add_years_to_df(df_pkg_pf, unit_years=5)

# date列をdatetimeオブジェクトに変換して、曜日情報を追加
df_gm["weekday"] = pd.to_datetime(df_gm["date"]).dt.weekday

# years、weekdayごとにpkgidのユニーク数を集計
df_gm = df_gm.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": "パッケージ数"})

# 1990年代以降を抽出
df_gm = df_gm[df_gm["年代"].astype(int) >= 1990].reset_index(drop=True)

# 円グラフを作成(category_ordersを指定しない)
fig = px.pie(
    df_gm,
    values="パッケージ数",
    names="発売曜日",
    facet_col="年代",
    color_discrete_sequence=OKABE_ITO,
)

# textinfo: パーセントのみ表示
# textposition: グラフの内側に配置
fig.update_traces(textposition="inside", textinfo="percent")

# 凡例名を追加し、フォントサイズが12px未満になる場合はラベルを隠す設定
fig.update_layout(
    legend_title_text="発売曜日", uniformtext_minsize=12, uniformtext_mode="hide"
)

# ファセットのタイトルを簡潔にする
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 円グラフを表示
show_fig(fig)

解説

category_ordersを指定しない場合、円グラフ内のカテゴリ(曜日)の配置順序はデータの出現順や内部的な処理順に依存します。 その結果、年代ごとに曜日の並び順がばらばらになり、年代別の比較が困難になってしまいます。

本書のようにcategory_orders={"発売曜日": ["月", "火", "水", "木", "金", "土", "日"]}を指定すると、全てのファセットで曜日が統一された順序で表示され、年代間の比較が容易になります。 可視化の目的に依りますが、category_ordersで順序を明示的に指定するテクニックがあることは覚えておきましょう。

関連セクション: 詳しくは円グラフを参照してください。

基礎 問題2:性別ごとの声優数の推移#

関連セクション: 積上げ密度プロット

アニメ作品に出演する声優の性別[1]比率は、時代によって変化があるのでしょうか。 df_aedf_ac_actを用いて、2005年以降の放送年ごとの性別別声優数を集計し、積上げ密度プロット(px.area)で可視化してください。

Hide code cell source
# df_aeに年情報を追加(1年単位)
df_ae_tmp = add_years_to_df(df_ae, unit_years=1)

# acidとyearsのユニークな組み合わせを持つDataFrameを作成
df_ac_years = df_ae_tmp.drop_duplicates(subset=["acid", "years"], ignore_index=True)[
    ["acid", "years"]
]

# df_ac_actとdf_ac_yearsをacidを基準にして左結合
df_merge = pd.merge(df_ac_act, df_ac_years, on="acid", how="left")

# 性別と年ごとにグループ化し、声優のユニーク数を集計
df_an = (
    df_merge.groupby(["gender", "years"])["actid"].nunique().reset_index(name="n_act")
)

# 列名をわかりやすい名前に変更
df_an = df_an.rename(columns={"gender": "性別", "years": "年", "n_act": "声優数"})

# 2005年以降のデータのみを抽出
df_an = df_an[df_an["年"].astype(int) >= 2005].reset_index(drop=True)

# 積上げ密度プロットを作成
fig = px.area(
    df_an, x="年", y="声優数", color="性別", color_discrete_sequence=OKABE_ITO
)

# ホバーモードを設定
fig.update_layout(hovermode="x unified")

# 積上げ密度プロットを表示
show_fig(fig)

解説

px.areaを用いることで、時系列に沿った内訳の変化を視覚的に表現できます。 本文では2000年以降を対象としていましたが、今回は2005年以降に絞ることで、より近年の傾向に焦点を当てました。

関連セクション: 詳しくは積上げ密度プロットを参照してください。

標準 問題3:週刊少年マガジンの年代別作者数#

関連セクション: 円グラフ

本文では四大少年誌全体のマンガ作者数を年代別に可視化しました。 ここでは、週刊少年マガジンのみに絞り込んで、年代別のマンガ作者数を円グラフで可視化してください。

df_cedf_cc_crtを用いて、週刊少年マガジンに掲載されたマンガ作者の年代別内訳を示す円グラフを作成しましょう。 その際、以下の点に注意してください:

  • category_ordersを指定して、年代順(1970, 1980, …)に並べる

  • 年代は 順序のある質的変数 なので、px.colors.diverging.Portlandパレットを使用する

Hide code cell source
# 週刊少年マガジンのみに絞り込み
df_cc_magazine = df_cc_crt[df_cc_crt["mcname"] == "週刊少年マガジン"].copy()

# df_ceに年代情報を追加
df_ce_years = add_years_to_df(df_ce, unit_years=UNIT_YEARS)

# 作品(ccid)ごとに年代情報を取得(最初の各話の年代を使用)
df_cc_years = df_ce_years.drop_duplicates(subset=["ccid"], ignore_index=True)[
    ["ccid", "years"]
]

# 週刊少年マガジンの作品データと年代情報をマージ
df_magazine_merged = pd.merge(df_cc_magazine, df_cc_years, on="ccid", how="left")

# 年代ごとにユニークな作者数を集計
df_magazine_crt = (
    df_magazine_merged.groupby("years")["crtid"].nunique().reset_index(name="n_crt")
)

# 列名をわかりやすい名前に変更
df_magazine_crt = df_magazine_crt.rename(columns={"years": "年代", "n_crt": "作者数"})

# 年代順に表示されるように順序を指定
orders_years = {"年代": ["1970", "1980", "1990", "2000", "2010"]}

# 円グラフを作成
# 年代は順序のある質的変数なので、Portlandパレットを使用
fig = px.pie(
    df_magazine_crt,
    values="作者数",
    names="年代",
    category_orders=orders_years,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例名を追加
fig.update_layout(legend_title_text="年代")

# 円グラフを表示
show_fig(fig)

解説

本文では四大少年誌全体を対象としていましたが、ここでは週刊少年マガジンのみに絞り込みました。 ブールインデックス df_cc_crt["mcname"] == "週刊少年マガジン" を用いることで、特定の雑誌に限定したデータを抽出できます。

category_ordersを指定することで、年代が1970→1980→…→2010の順に配置され、時系列の推移が直感的に把握できます。 また、年代は「順序のある質的変数」であるため、順序を表現できるPortlandパレットを使用しています。

結果を見ると、2010年代の作者数が最も多く、1980年代がそれに続いています。

関連セクション: 詳しくは円グラフを参照してください。

標準 問題4:プレイステーションシリーズの発売数推移#

関連セクション: 積上げ棒グラフ

ソニー[2]のプレイステーションシリーズは、世代を重ねるごとに新しいプラットフォームが登場してきました。 df_pkg_pfを用いて、プレイステーションシリーズ(プラットフォーム名が「プレイステーション」で始まるもの)に絞り込み、発売年ごとのプラットフォーム別パッケージ数を積上げ棒グラフで可視化してください。

色でプラットフォームを区別し、各プラットフォームの発売数推移を確認しましょう。

Hide code cell source
# ゲームパッケージデータをコピー
df_gm_ps = df_pkg_pf.copy()

# プレイステーションシリーズのみに絞り込み
# プラットフォーム名が「プレイステーション」で始まるものを抽出
df_gm_ps = df_gm_ps[df_gm_ps["pfname"].str.startswith("プレイステーション")].copy()

# 日付をdatetimeに変換し、年を追加
df_gm_ps["date"] = pd.to_datetime(df_gm_ps["date"])
df_gm_ps["year"] = df_gm_ps["date"].dt.year

# 年、プラットフォームごとにパッケージ数を集計
df_gm_ps_agg = (
    df_gm_ps.groupby(["year", "pfname"])["pkgid"]
    .nunique()
    .reset_index(name="発売数")
)

# 列名をわかりやすく変更
df_gm_ps_agg = df_gm_ps_agg.rename(columns={"pfname": "プラットフォーム"})

# 積上げ棒グラフを作成
fig = px.bar(
    df_gm_ps_agg,
    x="year",
    y="発売数",
    color="プラットフォーム",
    color_discrete_sequence=OKABE_ITO,
    barmode="stack",
    labels={"year": "発売年"},
)

# 積上げ棒グラフを表示
show_fig(fig)

解説

str.startswith()を用いることで、プラットフォーム名が特定の文字列で始まるデータを抽出できます。 これにより、プレイステーション、プレイステーション2、プレイステーション3、プレイステーション4、プレイステーション・ポータブル、プレイステーションVitaなどのソニー製据置・携帯機を一括で抽出しています。

結果を見ると、各世代のプラットフォームが順番に登場し、世代交代の様子が視覚的に確認できます。 プレイステーション2が最も長期間にわたって多くのタイトルをリリースしており、ソニーの主力プラットフォームであったことがわかります。

関連セクション: 詳しくは積上げ棒グラフを参照してください。

発展 問題5:第1話・最終話のカラー獲得率#

関連セクション: 積上げ棒グラフ

新連載の第1話は、読者の目を引くために巻頭カラーやセンターカラーで華々しく飾られることが一般的です。 一方で、連載の最後を飾る最終話の扱いは、雑誌の編集方針や作品の評価によって分かれるかもしれません。 合計話数が8以上の連載作品を対象に、各作品の「最初の一話」と「最後の一話」を抽出し、それぞれのカラー掲載率(four_colored[3])を比較しましょう。

横軸に率(合計を1として標準化)、縦軸にマンガ雑誌名を並べた積み上げ横棒グラフを作成してください。 その際、最初の一話か最後の一話かをファセット(facet_col)で分けて表示しましょう。

Hide code cell source
# df_ceをベースに作品ごとの合計話数を集計し、8話以上の作品(ccid)を特定
df_episode_counts = df_ce.groupby("ccid").size().reset_index(name="total_eps")
serial_ccids = df_episode_counts[df_episode_counts["total_eps"] >= 8]["ccid"]

# 連載作品に該当する各話データのみを抽出
df_serial_ce = df_ce[df_ce["ccid"].isin(serial_ccids)].copy()

# 各作品(ccid)ごとの最初と最後の各話データを特定
# idxmin/idxmaxを用いて、作品内でもっとも古い日付ともっとも新しい日付の行を取得
idx_first = df_serial_ce.groupby("ccid")["date"].idxmin()
idx_last = df_serial_ce.groupby("ccid")["date"].idxmax()

# それぞれのデータを抽出し、比較用のラベルを付与
df_first = df_serial_ce.loc[idx_first].copy()
df_first["連載順序"] = "最初の一話"

df_last = df_serial_ce.loc[idx_last].copy()
df_last["連載順序"] = "最後の一話"

# 分析用にデータを結合
df_compare = pd.concat([df_first, df_last], ignore_index=True)

# 凡例用にカラー掲載の有無を文字列に変換
df_compare["カラー掲載"] = df_compare["four_colored"].map({True: "カラー", False: "モノクロ"})

# 集計と正規化(手動計算)
# 雑誌、連載順序、カラー掲載ごとに作品数をカウント
df_agg = df_compare.groupby(["mcname", "連載順序", "カラー掲載"]).size().reset_index(name="作品数")

# 雑誌・連載順序ごとの合計数で割って比率を算出
# transform('sum')を使用することで、所属グループ(雑誌×順序)の総和で各行を割る
df_total = df_agg.groupby(["mcname", "連載順序"])["作品数"].transform("sum")
df_agg["比率"] = df_agg["作品数"] / df_total

# 5. px.barを用いて可視化
# 手動で算出した「比率」をx軸に指定する
fig = px.bar(
    df_agg,
    x="比率",
    y="mcname",
    color="カラー掲載",
    facet_col="連載順序",
    orientation="h",
    barmode="stack", # 積み上げ形式
    category_orders={"連載順序": ["最初の一話", "最後の一話"]},
    color_discrete_sequence=OKABE_ITO[:2][::-1],
    labels={"mcname": "マンガ雑誌名", "比率": "構成比"},
)

# ファセットのタイトルを整理し、X軸をパーセント表記に変更
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.update_xaxes(tickformat=".0%")

# 図を表示
show_fig(fig)

解説

本問題は、Plotlyの「積み上げ棒グラフ」と「ファセット」を使い、ドメイン知識を検証する実践的な演習です。

可視化の結果、一般的に半分以上の新連載はカラーで掲載されるようですが、雑誌によってばらつきがあります。 特に 週刊少年ジャンプのカラー掲載率が比較的高く、週刊少年サンデーが比較的低いように見えます。

一方で最終話に関しては、どの雑誌でも10%未満のカラー掲載率を記録しています。

関連セクション: 詳しくは積上げ棒グラフを参照してください。

応用 問題6:曜日別・メーカー別の発売数推移#

関連セクション: 積上げ棒グラフ

ゲームパッケージの発売曜日は、時代とともに変遷してきました。 先に定義したPF2MK辞書を用いてメーカー情報を付与し、メーカーごとにファセット(サブプロット)を分割して、発売年ごとの曜日内訳を積上げ棒グラフで可視化してください。

Hide code cell source
# ゲームパッケージデータに発売年と曜日情報を追加
df_gm = df_pkg_pf.copy()
df_gm["date"] = pd.to_datetime(df_gm["date"])
df_gm["year"] = df_gm["date"].dt.year
# 曜日のインデックスを名前に変換
df_gm["曜日"] = df_gm["date"].dt.weekday.map(WEEKDAY2YOBI)

# 辞書を用いてメーカー名を追加。該当しないものは'その他'で埋める
df_gm["メーカー"] = df_gm["pfname"].map(PF2MK).fillna("その他")

# 1990年から2017年の範囲に限定
df_gm = df_gm[(df_gm["year"] >= 1990) & (df_gm["year"] <= 2017)]

# 年、曜日、メーカーごとにパッケージ数を集計
df_gm_agg = (
    df_gm.groupby(["year", "曜日", "メーカー"])["pkgid"]
    .nunique()
    .reset_index(name="発売数")
)

# px.barを用いて、曜日ごとのメーカーシェア変遷を可視化
fig = px.bar(
    df_gm_agg,
    x="year",  # 横軸に発売年
    y="発売数",  # 縦軸に発売数
    color="曜日",  # メーカーごとに色分け
    facet_col="メーカー",  # 曜日ごとにファセットを分割
    facet_col_wrap=2,  # 2列で折り返し
    category_orders={
        "曜日": ["月", "火", "水", "木", "金", "土", "日"]
    },  # 曜日の表示順を指定
    color_discrete_sequence=OKABE_ITO,  # カラーパレットを適用
    barmode="stack",  # 積上げ棒グラフを指定
    height=600,  # 高さを指定
    labels={"year": "発売年"},  # 表示ラベルを変更
)

# ファセットのタイトルを簡潔に変更
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 結果を表示
show_fig(fig)

解説

ファセット分割を用いることで、メーカー別の発売曜日の推移を多角的に表現できます。 例えば、「いつ頃から金曜日から木曜日にシフトしたか」といった、メーカーごとの戦略の変遷を知るきっかけになるかもしれません。 複数の軸(年、メーカー、曜日)を同時に扱うことは難しいですが、積極的にトライする価値はあります。

関連セクション: 詳しくは積上げ棒グラフを参照してください。

応用 問題7:アニメ話数の推移#

関連セクション: 積上げ密度プロット

本書で何度か触れたように、1990年代後半からアニメ作品の短尺化が進んだ可能性があります。 そこで、作品あたりの合計話数が 13 話以下のものを「1クール」、それ以外を「その他」と分類し、 放送年ごとの作品数の内訳を、積上げ密度プロット(px.area、エリアチャート)で表現してください。

分類にはdf_aeを用い、1995年前後で構成がどのように変化したか観察しましょう。

Hide code cell source
# アニメ作品(acid)ごとに合計話数を集計
df_work_len = df_ae.groupby("acid").size().reset_index(name="total_eps")

# 13話以下を「1クール」、それ以外を「その他」として分類
df_work_len["クール分類"] = df_work_len["total_eps"].apply(
    lambda x: "1クール" if x <= 13 else "その他"
)

# 各話データ(df_ae)と作品分類情報を結合
df_ae_merged = pd.merge(df_ae, df_work_len[["acid", "クール分類"]], on="acid")

# 放送日から年を抽出し、年・分類ごとに「ユニークな作品数」を集計
df_ae_merged["year"] = pd.to_datetime(df_ae_merged["date"]).dt.year
df_cour_trend = (
    df_ae_merged.groupby(["year", "クール分類"])["acid"]
    .nunique()
    .reset_index(name="作品数")
)

# 1990年以降のデータに限定して抽出
df_cour_trend = df_cour_trend[df_cour_trend["year"] >= 1990]

# px.areaを用いて、内訳の変遷を積上げ密度プロットとして可視化
fig = px.area(
    df_cour_trend,
    x="year",
    y="作品数",
    color="クール分類",
    color_discrete_sequence=OKABE_ITO,
    labels={"year": "放送年", "作品数": "アニメ作品数"},
)

# x unifiedを指定することで、同じxに対する複数のyの値を同時に比較できる
fig.update_layout(hovermode="x unified")

# 結果を表示
show_fig(fig)

解説

積上げ密度プロット(エリアチャート)を活用して、時系列に伴う内訳の変遷を捉える問題です。 1995年頃を境に「1クール作品」の領域が急激に拡大している様子が視覚的に確認でき、業界全体の構造変化が起きたという仮説と整合します。 一方で、1クール以外の作品数もある程度維持されているように見えます。

「内訳」に関する議論からは外れますが、2009年から2011年にかけて、全体的にアニメ話数が減少しています。 執筆時間の都合のためこれ以上の調査は断念しましたが、気になる方は調べてみましょう[4]

関連セクション: 詳しくは積上げ密度プロットを参照してください。