下巻 第6章 解答例#
ここでは、 本書の学習内容の定着 を目的とした練習問題とその解答・解説を掲載します。 なお、問題の性質上、本書で取り上げた処理と重複することがあります。 ご了承ください。
前提#
以下のように、ライブラリのインポートと変数の定義が完了していることを前提とします。
Show code cell content
# difflibモジュールのインポート
# 文字列間の類似度や差異を計算するためのユーティリティを提供
import difflib
# 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
Show code cell content
# マンガデータの中間出力ファイルが格納されているディレクトリ
DIR_CM_INTERIM = Path("../../../data/cm/interim")
# アニメデータの中間出力ファイルが格納されているディレクトリ
DIR_AN_INTERIM = Path("../../../data/an/interim")
# アニメデータの入力ファイルが格納されているディレクトリ
DIR_AN_INPUT = Path("../../../data/an/input")
# 外部データソースから作成した対応表などを格納しているディレクトリ
DIR_MIX_EXTERNAL = Path("../../../data/mix/external")
# メディア展開データの入力ファイルが格納されているディレクトリ
DIR_MIX_INPUT = Path("../../../data/mix/input")
Show code cell content
# アニメ作品とマンガ作品の全対応関係が記されたファイル
FN_AC_CC = "ac_cc.csv"
# マンガ作品の結合用メタデータファイル
FN_CC_MERGE = "cc_merge.csv"
# アニメ作品の結合用メタデータファイル
FN_AC_MERGE = "ac_merge.csv"
# アニメ各話データのファイル
FN_AE = "an_ae.csv"
# アニメ各話と原作マンガの作者の対応関係に関するファイル
FN_AE_CRT = "mix_ae_crt.csv"
# マンガ各話とアニメ作品の対応関係に関するファイル
FN_CE_AC = "mix_ce_ac.csv"
Show code cell content
# plotlyの描画設定の定義
# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
# 質的変数の描画用のカラースケールの定義
# 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)
]
また、本書中で取り上げた以下の関数も、同様に利用可能とします。
Show 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)
Show 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
また、以下のようにデータを読み込み済みと仮定します。
Show code cell content
# 全てのメディア展開を網羅する対応表の読み込み
df_ac_cc = pd.read_csv(DIR_MIX_EXTERNAL / FN_AC_CC)
# マンガ作品メタデータの読み込み(日付列を日時型として解釈)
df_cc_merge = pd.read_csv(DIR_CM_INTERIM / FN_CC_MERGE, parse_dates=["first_date"])
# アニメ作品メタデータの読み込み(日付列を日時型として解釈)
df_ac_merge = pd.read_csv(DIR_AN_INTERIM / FN_AC_MERGE, parse_dates=["first_date"])
# アニメ各話データの読み込み
df_ae = pd.read_csv(DIR_AN_INPUT / FN_AE)
# アニメ各話と原作マンガの作者の対応関係データの読み込み
df_ae_crt = pd.read_csv(DIR_MIX_INPUT / FN_AE_CRT)
# マンガ各話とアニメ作品の対応関係データの読み込み
df_ce_ac = pd.read_csv(DIR_MIX_INPUT / FN_CE_AC)
基礎 問題1:マンガ作者別のアニメ放送話数#
関連セクション: 棒グラフ
本文では、マンガ作者別のアニメ合計放送話数を 縦棒グラフ で可視化しました。 ここでは、同様のデータを 横棒グラフ で可視化してみましょう。
df_ae_crtを用いて、マンガ作者名(crtname)ごとにアニメ各話ID(aeid)のユニーク数を集計してください集計結果を降順にソートし、上位 10件 に絞り込んでください(本文では20件)
px.bar()を用いて 横棒グラフ を作成してください
ヒント
横棒グラフにするには
orientation="h"を指定します横棒グラフでは x と y の指定が縦棒グラフと逆になります
例:
px.bar(..., orientation="h")でアニメ各話数が20未満の作品
Show code cell source
# 作者ごとの合計話数を集計
# 'crtname'(マンガ作者名)でグループ化し、'aeid'のユニーク数をカウント
df_bar = df_ae_crt.groupby("crtname")["aeid"].nunique().reset_index(name="counts")
# 降順ソートして上位10件に絞り込み
df_bar = df_bar.sort_values("counts", ascending=False, ignore_index=True).head(10)
# 列名を分かりやすく変更
df_bar = df_bar.rename(
columns={"crtname": "原作マンガ作者名", "counts": "アニメの合計放送話数"}
)
# 3. 横棒グラフを作成
# orientation="h" で横棒グラフを指定
# 横棒グラフでは x に数値、y にカテゴリを指定
fig = px.bar(
df_bar,
x="アニメの合計放送話数",
y="原作マンガ作者名",
orientation="h",
)
# show_fig関数を使用して図を表示
show_fig(fig)
解説
orientation="h" を指定することで、縦棒グラフを横棒グラフに変更できます。
横棒グラフでは、x軸とy軸の指定が縦棒グラフとは逆になる点に注意してください。
縦棒グラフ:x=カテゴリ、y=数値
横棒グラフ:x=数値、y=カテゴリ
横棒グラフは、カテゴリ名が長い場合や、カテゴリ数が多い場合に特に有効です。 今回のようにマンガ作者名を表示する場合、横棒グラフの方がラベルが読みやすくなります。
関連セクション: 詳しくは棒グラフを参照してください。
基礎 問題2:掲載特性の散布図行列#
関連セクション: 散布図行列
本文では、マンガ作品の掲載特性を「平均掲載位置」「平均ページ数」「カラー獲得率」「連載開始日」の 4変数 で散布図行列にしました。 ここでは、「連載開始日」を除いた 3変数 で散布図行列を作成してみましょう。
df_ce_acを用いて、本文と同様にマンガ作品ごとの最初の8話分の掲載特性を集計してくださいpx.scatter_matrix()を用いて、「平均掲載位置」「平均ページ数」「カラー獲得率」の 3変数 で散布図行列を作成してくださいアニメ化有無(
is_animated)で色分けしてください
ヒント
dimensions引数に表示したい変数名のリストを指定します例:
px.scatter_matrix(..., dimensions=["変数A", "変数B"])で2変数のみ表示
Show code cell source
# データの前処理(本文と同様)
min_nce = 8
# df_ce_acを日付で昇順に並び替え
df_ce_ac_sorted = df_ce_ac.sort_values("date", ignore_index=True)
# first_date_cc列を日付型に変換
df_ce_ac_sorted["first_date_cc"] = pd.to_datetime(df_ce_ac_sorted["first_date_cc"])
# n_ceがmin_nce以上かつ1990年以降のレコードをフィルタリング
df_tmp = df_ce_ac_sorted[
(df_ce_ac_sorted["n_ce"] >= min_nce)
& (df_ce_ac_sorted["first_date_cc"].dt.year >= 1990)
].reset_index(drop=True)
# 各ccidについて、最初のmin_nce個のレコードのみを保持
df_tmp = df_tmp.groupby("ccid").head(min_nce).reset_index(drop=True)
# アニメ化有無のフラグを付与
df_tmp["is_animated"] = ~df_tmp["acid"].isna()
# マンガ作品ごとに掲載特性の平均値を集計
df_scat = (
df_tmp.groupby(["mcname", "ccid", "ccname"])[
["page_start_position", "pages", "four_colored", "is_animated"]
]
.mean()
.reset_index()
)
# is_animated列を論理値に変換
df_scat["is_animated"] = df_scat["is_animated"] == 1
# 列名を分かりやすく変更
df_scat = df_scat.rename(
columns={
"page_start_position": "平均掲載位置",
"pages": "平均ページ数",
"four_colored": "カラー獲得率",
"is_animated": "アニメ化",
}
)
# 散布図行列を作成(3変数のみ)
fig = px.scatter_matrix(
df_scat,
dimensions=["平均掲載位置", "平均ページ数", "カラー獲得率"],
color="アニメ化",
opacity=0.6,
height=500,
color_discrete_sequence=OKABE_ITO,
)
# マーカーのスタイルをカスタマイズ
fig.update_traces(
marker={
"line_width": 1,
"line_color": "white",
}
)
# 可視化結果を表示
show_fig(fig)
解説
px.scatter_matrix() の dimensions 引数を使うことで、表示する変数を絞り込むことができます。
本文の4変数(4×4=16パネル)から3変数(3×3=9パネル)に減らすことで、特定の変数間の関係に注目しやすくなります。 「連載開始日」は時間軸の変数であり、他の掲載特性(位置、ページ数、カラー率)とは性質が異なるため、今回はこれを除外しました。
散布図行列は変数の数が増えるほどパネル数が急増するため( \(n\) 変数で \(n^2\) パネル)、目的に応じて変数を絞り込むことが重要です。
関連セクション: 詳しくは散布図行列を参照してください。
標準 問題3:週刊少年サンデーのアニメ化推移#
関連セクション: 積上げ密度プロット
本文では、4つの雑誌すべてを対象に、連載開始年ごとのアニメ化作品数の推移を積上げ密度プロットで可視化しました。 ここでは、週刊少年サンデー に絞り込んで同様の可視化を行ってみましょう。
df_ce_acから 週刊少年サンデー のデータのみをフィルタリングしてください連載開始年(
first_date_ccの年)とアニメ化有無でグループ化し、マンガ作品数を集計してくださいpx.area()を用いて積上げ密度プロットを作成してください
ヒント
雑誌名でフィルタリングするには
df[df["mcname"] == "週刊少年サンデー"]のようにします例:
df[df["platform"] == "Switch"]でSwitchプラットフォームのみに絞り込み
Show code cell source
# 0. 前処理(本文と同様)
min_nce = 8
# df_ce_acを日付で昇順に並び替え、ccidで重複を削除
df_ce_ac_sorted = df_ce_ac.sort_values(["date", "ceid"], ignore_index=True)
df_cc_ac = df_ce_ac_sorted.drop_duplicates("ccid", ignore_index=True)
# n_ceがmin_nce以上、かつfirst_date_ccが1990年以降の行のみ選択
df_cc_ac = df_cc_ac[
(df_cc_ac["n_ce"] >= min_nce)
& (pd.to_datetime(df_cc_ac["first_date_cc"]).dt.year >= 1990)
].reset_index(drop=True)
# アニメ化有無のフラグを付与
df_cc_ac["is_animated"] = ~df_cc_ac["acid"].isna()
# 週刊少年サンデーのみにフィルタリング
df_sunday = df_cc_ac[df_cc_ac["mcname"] == "週刊少年サンデー"].reset_index(drop=True)
# 連載開始年とアニメ化有無でグループ化し、マンガ作品数を集計
df_sunday["first_year_cc"] = pd.to_datetime(df_sunday["first_date_cc"]).dt.year
df_area = (
df_sunday.groupby(["first_year_cc", "is_animated"])["ccid"]
.nunique()
.reset_index(name="マンガ作品数")
)
# アニメ化されたものを優先して表示するためソート
df_area = df_area.sort_values(
by=["first_year_cc", "is_animated"], ascending=[True, False], ignore_index=True
)
# is_animated列を文字列に変換
df_area["is_animated"] = df_area["is_animated"].astype(str)
# 列名を分かりやすく変更
df_area = df_area.rename(
columns={"first_year_cc": "連載開始年", "is_animated": "アニメ化"}
)
# 積上げ密度プロットを作成
fig = px.area(
df_area,
x="連載開始年",
y="マンガ作品数",
color="アニメ化",
color_discrete_sequence=OKABE_ITO[:2][::-1],
)
# ホバー時の挙動を設定
fig.update_layout(hovermode="x unified")
# 可視化結果を表示
show_fig(fig)
解説
ブールインデックスを使って特定の雑誌に絞り込むことで、その雑誌に特化した傾向を詳しく分析できます。
週刊少年サンデーのデータだけを見ると、アニメ化作品数の年次変動や、全体の作品数に対するアニメ化率の推移がより明確に見えます。 本文のように4雑誌を並べて比較する方法と、このように1つの雑誌に絞り込む方法は、それぞれ異なる洞察を得られます。
探索的データ分析では、全体を俯瞰する可視化と、特定の部分に注目した可視化を組み合わせることが重要です。
関連セクション: 詳しくは積上げ密度プロットを参照してください。
標準 問題4:変数配置を変えたバブルチャート#
関連セクション: バブルチャート
本文では、マンガ作品の掲載特性をバブルチャートで可視化しました。その際、以下のように変数を配置しました:
x軸:平均掲載位置
y軸:平均ページ数
size:カラー獲得率
ここでは、変数の配置を変えて、カラー獲得率をx軸に、平均掲載位置をsizeに 設定したバブルチャートを作成してみましょう。
問題2と同様に、
df_ce_acから掲載特性のデータを準備してくださいpx.scatter()を用いて、以下の配置でバブルチャートを作成してください:x軸:カラー獲得率
y軸:平均ページ数
size:平均掲載位置
ヒント
バブルチャートは
px.scatter()にsize引数を追加することで作成できます例:
px.scatter(..., size="売上")で売上を円のサイズに反映
Show code cell source
# データの前処理(問題2と同様)
min_nce = 8
df_ce_ac_sorted = df_ce_ac.sort_values("date", ignore_index=True)
df_ce_ac_sorted["first_date_cc"] = pd.to_datetime(df_ce_ac_sorted["first_date_cc"])
df_tmp = df_ce_ac_sorted[
(df_ce_ac_sorted["n_ce"] >= min_nce)
& (df_ce_ac_sorted["first_date_cc"].dt.year >= 1990)
].reset_index(drop=True)
df_tmp = df_tmp.groupby("ccid").head(min_nce).reset_index(drop=True)
df_tmp["is_animated"] = ~df_tmp["acid"].isna()
df_bub = (
df_tmp.groupby(["mcname", "ccid", "ccname"])[
["page_start_position", "pages", "four_colored", "is_animated"]
]
.mean()
.reset_index()
)
df_bub["is_animated"] = df_bub["is_animated"] == 1
df_bub = df_bub.rename(
columns={
"page_start_position": "平均掲載位置",
"pages": "平均ページ数",
"four_colored": "カラー獲得率",
"is_animated": "アニメ化",
"ccname": "マンガ作品名",
}
)
# 変数配置を変えたバブルチャートを作成
# x=カラー獲得率, y=平均ページ数, size=平均掲載位置
fig = px.scatter(
df_bub,
x="カラー獲得率",
y="平均ページ数",
size="平均掲載位置",
color="アニメ化",
opacity=0.7,
hover_name="マンガ作品名",
color_discrete_sequence=OKABE_ITO,
)
# マーカーのスタイルをカスタマイズ
fig.update_traces(
marker={
"line_width": 1,
"line_color": "white",
}
)
# 可視化結果を表示
show_fig(fig)
解説
変数の配置を変えることで、同じデータでも異なる印象を受けることがわかります。
今回の問題で重要な学びは、「size(円の面積)で表現された変数は定量的な比較が難しい」という点[1]です。
人間の知覚は面積の違いを正確に判断することが苦手であり、円の大きさの違いから「どれくらい差があるか」を読み取ることは困難です。
本文の設定(x=平均掲載位置, size=カラー獲得率)と、今回の設定(x=カラー獲得率, size=平均掲載位置)を比較すると、x軸に配置した変数の方が正確に読み取れることが体感できます。
バブルチャートを設計する際は、以下の原則を意識しましょう:
正確に読み取りたい変数 → x軸またはy軸に配置
大まかな傾向を見たい変数 → sizeに配置
関連セクション: 詳しくはバブルチャートを参照してください。
応用 問題5:アニメ化までのリードタイム#
関連セクション: ヒストグラム
マンガ作品が連載を開始してから、アニメが放送されるまでにどれくらいの期間(インターバル)があるかを可視化しましょう。
第6章で学んだ「分布を見るための手法」と「ファセット」を活用してください。
読み込んだ
df_ac_ccを主軸とし、マンガの連載開始日(first_date)とアニメの放送開始日(first_date)をそれぞれ結合して一つのデータフレームを作成してください「アニメ放送開始日 - マンガ連載開始日」を計算し、経過年数を算出してください
マンガ雑誌(
mcname)ごとに、この経過日数の分布をヒストグラムで可視化してください可視化に際しては、雑誌ごとの傾向の違いが比較しやすいよう、ファセット機能を利用してください
ヒント
データの結合には
pd.merge()を使用します日付の差分は
.dt.daysで日数に変換できますヒストグラムには
px.histogram()を使用しますファセットは
facet_col引数で指定できます
Show code cell source
# データの結合
# 後の結合で列名が重複するため、あらかじめ意味のわかる名前に変更しておく
df_cc_tmp = df_cc_merge[["ccid", "mcname", "first_date"]].rename(
columns={"first_date": "first_date_cc"}
)
df_ac_tmp = df_ac_merge[["acid", "first_date"]].rename(
columns={"first_date": "first_date_ac"}
)
# 対応表(df_ac_cc)に対して、マンガ情報を結合
df_interval = pd.merge(df_ac_cc, df_cc_tmp, on="ccid", how="inner")
# さらにアニメ情報を結合
df_interval = pd.merge(df_interval, df_ac_tmp, on="acid", how="inner")
# インターバルの計算
# 日付の差分を取って年数に変換
df_interval["interval_years"] = (
df_interval["first_date_ac"] - df_interval["first_date_cc"]
).dt.days / 365.25
# 異常値の処理:マンガ開始前のアニメ化(特殊なケースやデータミス)を除外する
# 今回の分析目的である「連載開始からのリードタイム」に集中するため
df_interval = df_interval[df_interval["interval_years"] >= 0].reset_index(drop=True)
# Plotly Expressを用いた可視化
# 雑誌ごとにファセットを分け、分布を比較する
fig = px.histogram(
df_interval,
x="interval_years", # x軸:アニメ化までの経過年数
facet_col="mcname", # ファセット:雑誌名で分割
facet_col_wrap=1, # 折り返し設定:2列表示
nbins=40, # ビン数:分布を細かく見るため40に設定
height=500,
labels={"interval_years": "アニメ化までの経過年数", "mcname": "雑誌名"},
)
# Y軸のラベルを「該当作品数」に設定
fig.update_yaxes(title_text="該当作品数")
# ファセットタイトルを省略
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
# 定義した show_fig 関数で図を表示
show_fig(fig)
解説
この問題では、複数のデータソースを統合して新しい変数(アニメ化までのリードタイム)を作成し、その「分布」を多角的に観察するプロセスを学びました。
可視化の結果を見ると、多くの作品が連載開始から 2〜6年程度 でアニメ化されていることが分かります。これは「単行本が数巻発売され、人気が定着したタイミング」でのメディア展開が多いと解釈できるかもしれません[2]。
また、ファセットを利用することで、雑誌ごとに特徴が見えてきます。例えば、週刊少年チャンピオンのアニメ化までのリードタイムのピークは、他の雑誌と異なるように見えます。
興味深いのはマンガ初掲載から20年以上経ってからアニメ化される作品がいくつか見られる点です。この中にはリメイク作品も含まれていると想像できます。
関連セクション: 詳しくはヒストグラムを参照してください。
応用 問題6:アニメ各話数における原作区分の推移#
関連セクション: 積上げ密度プロット
1990年以降の全アニメ作品の各話数に対して、「四大少年誌を原作とするアニメ」が占める割合がどのように推移してきたかを可視化しましょう。
アニメ産業の規模の変化と、その中での四大少年誌の影響力を同時に確認するために、以下の2つの図を作成してください。
絶対数の推移: 年度ごとのアニメ総話数を、原作の区分(「四大少年誌」または「その他」)で積み上げた積上げ密度プロット(エリアチャート)
割合の推移: 年度ごとの総話数を1(100%)とした時の、各区分の構成比を示した積上げ密度プロット(エリアチャート)
ヒント
アニメ各話データ(
df_ae)とメディア展開対応表(df_ac_cc)を統合して利用しますdf_ac_ccに含まれるacidはすべて四大少年誌原作の作品です可視化には
px.area()を使用します引数として
groupnorm='fraction'を用いると、別途DataFrameを集計することなく構成比の可視化が可能になります
Show code cell source
# 前処理
# 放送日から放送年を抽出
df_ae_tmp = df_ae.copy()
df_ae_tmp["years"] = pd.to_datetime(df_ae_tmp["date"]).dt.year
# 1990年以降のデータに絞り込む(データの欠損を考慮)
df_ae_tmp = df_ae_tmp[df_ae_tmp["years"] >= 1990].reset_index(drop=True)
# years列を文字列型に変換(resample_df_by_col_and_years関数の仕様に合わせる)
df_ae_tmp["years"] = df_ae_tmp["years"].astype(str)
# 四大少年誌原作かどうかのフラグを付与
# df_ac_ccに含まれるacidをセットとして取得
big_four_acids = set(df_ac_cc["acid"])
# acidが四大少年誌原作のセットに含まれるかどうかで区分を付与
df_ae_tmp["原作区分"] = df_ae_tmp["acid"].apply(
lambda x: "四大少年誌" if x in big_four_acids else "その他"
)
# 集計:年・区分ごとの各話数をカウント
df_agg = (
df_ae_tmp.groupby(["years", "原作区分"]).size().reset_index(name="アニメ各話数")
)
# resample_df_by_col_and_years関数を使用して欠損年を補完(0埋め)
df_agg = resample_df_by_col_and_years(df_agg, "原作区分")
# years列でソートして時系列順に並べる
df_agg = df_agg.sort_values("years", ignore_index=True)
# 可視化
# グラフ1:絶対数の積上げ密度プロット
fig = px.area(
df_agg,
x="years",
y="アニメ各話数",
color="原作区分",
labels={"years": "放送年", "アニメ各話数": "総話数"},
category_orders={"原作区分": ["その他", "四大少年誌"]},
color_discrete_sequence=OKABE_ITO,
)
show_fig(fig)
# グラフ2:割合の積上げ密度プロット
fig = px.area(
df_agg,
x="years",
y="アニメ各話数",
color="原作区分",
groupnorm="fraction", # 合計を1にスケーリング
labels={"years": "放送年", "アニメ各話数": "構成比"},
category_orders={"原作区分": ["その他", "四大少年誌"]},
color_discrete_sequence=OKABE_ITO,
)
show_fig(fig)
解説
積上げ密度プロットを用いて、時系列における総量と内訳の変化を同時に捉える手法を学びました。
絶対数のグラフからは、1990年代後半から2010年代にかけてアニメの総放送話数が劇的に増加していることがわかります。 一方、割合のグラフに切り替えると、総数が増える中で四大少年誌のシェアは必ずしも一定ではなく、他の原作ソース(ライトノベル、ゲーム、オリジナル等)の台頭による「多様化」が示唆されます。
groupnorm='fraction' を使うだけで100%積上げグラフを作成できる点は、探索的データ分析において強力[3]です。
関連セクション: 詳しくは積上げ密度プロットを参照してください。
応用 問題7:マンガ作品名とアニメ作品名の類似度#
関連セクション: 箱ひげ図
マンガ作品がアニメ化される際、作品名がそのまま使われることもあれば、サブタイトルが追加されたり、全く異なる名称に変更されることもあります。 この「作品名の類似度」は、メディア展開の戦略を反映している可能性があります。
本書で学んだ difflib.SequenceMatcher を用いて、マンガ作品名(ccname)とアニメ作品名(acname)の類似度を計算し、その分布を雑誌ごとに比較してみましょう。
df_ac_ccを主軸として、マンガ作品名(ccname)とアニメ作品名(acname)を結合してください。雑誌名(mcname)も含めてくださいdifflib.SequenceMatcherのratio()メソッドを用いて、各行のccnameとacnameの類似度(0〜1)を計算してください雑誌ごとの類似度の分布を 箱ひげ図 で可視化してください
ヒント
difflib.SequenceMatcher(None, str1, str2).ratio()で2つの文字列の類似度が得られます箱ひげ図には
px.box()を使用しますapply()メソッドで各行に関数を適用できます
Show code cell source
# データの結合
# マンガ作品名と雑誌名を取得
df_cc_tmp = df_cc_merge[["ccid", "ccname", "mcname"]]
# アニメ作品名を取得
df_ac_tmp = df_ac_merge[["acid", "acname"]]
# 対応表(df_ac_cc)に対して、マンガ情報を結合
df_similarity = pd.merge(df_ac_cc, df_cc_tmp, on="ccid", how="inner")
# さらにアニメ情報を結合
df_similarity = pd.merge(df_similarity, df_ac_tmp, on="acid", how="inner")
# 類似度の計算
# difflib.SequenceMatcherを用いて、マンガ作品名とアニメ作品名の類似度を計算
# ratio()は0〜1の値を返し、1に近いほど類似度が高い
df_similarity["類似度"] = df_similarity.apply(
lambda row: difflib.SequenceMatcher(None, row["ccname"], row["acname"]).ratio(),
axis=1,
)
# 3. 箱ひげ図による可視化
# 雑誌ごとの類似度の分布を比較
fig = px.box(
df_similarity,
x="mcname",
y="類似度",
labels={"mcname": "雑誌名", "類似度": "マンガ作品名とアニメ作品名の類似度"},
)
# 可視化結果を表示
show_fig(fig)
解説
文字列の類似度という新しい指標を導入し、その分布を箱ひげ図で可視化する手法を学びました。
difflib.SequenceMatcher の ratio() は最長共通部分列に基づく類似度(0〜1)を返します。
結果を見ると、多くの作品で類似度が0.6〜1.0と高く、マンガ作品名がそのままアニメ作品名として使われるケースが多いことがわかります。
一方、類似度が低い外れ値は、サブタイトルの追加や大幅な名称変更を示唆しています。
関連セクション: 詳しくは箱ひげ図を参照してください。
応用 問題8:マンガ話数とアニメ話数の関係#
関連セクション: 散布図
マンガ作品の連載話数とアニメ作品の放送話数には、どのような関係があるでしょうか? 直感的には「マンガの話数が多いほど、アニメの話数も多くなる」と予想されますが、実際のデータはこの仮説を支持するでしょうか。
df_ac_cc で紐づけられたマンガ作品とアニメ作品について、それぞれの話数の関係を散布図で可視化してみましょう。
df_ac_ccを主軸として、マンガ作品の話数(n_ce)とアニメ作品の話数(n_ae)を結合してください。作品名と放送開始日も含めてくださいアニメの放送開始日から「放送年」を抽出してください
マンガ話数をX軸、アニメ話数をY軸とした散布図を作成し、放送年で色分けしてください
ホバー時にマンガ作品名とアニメ作品名が確認できるようにしてください
ヒント
散布図には
px.scatter()を使用しますホバー情報は
hover_data引数で指定できます重複が多い場合は
update_traces()でマーカーの透明度やサイズを調整すると見やすくなります
Show code cell source
# データの結合
# マンガ作品の話数と作品名を取得
df_cc_tmp = df_cc_merge[["ccid", "ccname", "n_ce"]]
# アニメ作品の話数、作品名、放送開始日を取得
df_ac_tmp = df_ac_merge[["acid", "acname", "n_ae", "first_date"]]
# 対応表(df_ac_cc)に対して、マンガ情報を結合
df_episodes = pd.merge(df_ac_cc, df_cc_tmp, on="ccid", how="inner")
# さらにアニメ情報を結合
df_episodes = pd.merge(df_episodes, df_ac_tmp, on="acid", how="inner")
# 放送年の抽出
# 放送開始日から年を抽出
df_episodes["放送年"] = df_episodes["first_date"].dt.year
# 散布図による可視化
# マンガ話数とアニメ話数の関係を放送年で色分け
fig = px.scatter(
df_episodes,
x="n_ce",
y="n_ae",
color="放送年",
hover_data=["ccname", "acname"],
labels={
"n_ce": "マンガ話数",
"n_ae": "アニメ話数",
"ccname": "マンガ作品名",
"acname": "アニメ作品名",
},
)
# 散布図のマーカーのスタイルを更新
# サイズを10に設定し、線幅を1、透明度を0.5に設定
fig.update_traces(
marker={
"size": 10,
"line_width": 1,
"opacity": 0.7,
}
)
# 可視化結果を表示
show_fig(fig)
解説
散布図を用いて2つの量的変数(マンガ話数とアニメ話数)の関係を可視化しました。
結果を見ると、必ずしも強い正の相関があるとは言えません。長期連載マンガでもアニメの話数が比較的少ない作品が多く存在し、これは人気エピソードを選んでアニメ化したり、クール単位で制作される現代のアニメ産業の特性を反映していると考えられます。
放送年による色分けで時代による傾向の違いも観察でき、近年の作品は1〜2クール(12〜26話程度)で完結するパターンが多いことが読み取れます。
関連セクション: 詳しくは散布図を参照してください。