上巻 第4章 解答例#
ここでは、 本書の学習内容の定着 を目的とした練習問題とその解答・解説を掲載します。 なお、問題の性質上、本書で取り上げた処理と重複することがあります。 ご了承ください。
前提#
以下のように、ライブラリのインポートと変数の定義が完了していることを前提とします。
Show 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
Show 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"
Show 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)
]
また、本書中で取り上げた以下の関数も、同様に利用可能とします。
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 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
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_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作品を横棒グラフで可視化してください。
ヒント
マンガ作品名(
ccname)ごとにユニークな各話ID(ceid)の数をカウントしますsort_values()で降順にソートし、head()で上位を抽出します横棒グラフは
px.bar()でorientation="h"を指定します
Show 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で作成してください。
ヒント
ヒストグラムは
px.histogram()で作成しますビン数は
nbins引数で指定できます(例:nbins=30)
Show 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作品を横棒グラフで可視化してください。
ヒント
まず
df_ce[df_ce["mcname"] == "週刊少年ジャンプ"]でデータを絞り込みます絞り込んだデータに対して、問題1と同様の集計・可視化を行います
Show 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の各話のみに絞り、ページ数のヒストグラムを作成してください。
ヒント
df_ce[df_ce["four_colored"]]でカラー各話のみを抽出できます抽出したデータに対して
px.histogram()でヒストグラムを作成します
Show 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_ceとdf_cc_crtをccidをキーにマージしてください各マンガ作者(
crtname)ごとの合計ページ数(pages)を集計してください上位15名を横棒グラフで可視化してください
ヒント
2つのDataFrameの結合には
pd.merge()を使用します横棒グラフは
px.bar()でorientation="h"を指定しますソート後に
.head(15)で上位15件を取得できます
Show 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_ceをccid(マンガ作品ID)でグループ化し、各作品の合計話数を集計してください集計した合計話数の累積ヒストグラムを作成してください
X軸の範囲を
0から200までに制限してください
ヒント
マンガ作品ごとの話数カウントには
.groupby("ccid")["ceid"].nunique()を使用します累積ヒストグラムは
px.histogram()でcumulative=Trueを指定しますX軸の範囲は
.update_xaxes(range=[min, max])で設定できます
Show 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_ceにadd_years_to_df()関数で年代情報を追加してください年代ごとにカラー有無の割合(合計を1.0としたスケーリング)を算出してください
積上げ棒グラフを作成し、配色には
OKABE_ITOカラーパレットを使用してください
ヒント
年代とカラー有無で
.groupby()してカウント後、割合を計算します各年代の合計は
.transform("sum")で各行に付与できます積上げ棒グラフは
px.bar()でbarmode="stack"を指定します
Show 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)の散布図を作成してください雑誌別にファセットを分割し、重なりを防いで各雑誌の傾向を詳しく見られるようにしてください
ヒント
作品ごとの話数は
.groupby("ccid")["ceid"].nunique()でカウントできます第1話の抽出は
.sort_values(["ccid", "date"]).groupby("ccid").head(1)で実現できますファセット分割は
px.scatter()のfacet_col引数で指定します
Show 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軸の高い位置(増ページ)に点が密集しており、新連載を強力にプッシュする業界共通の構造が確認できます。 一方で、巻頭以外の位置からスタートする例外的な作品の数や、ページ数のばらつき具合には雑誌ごとの違いも見られます。
関連セクション: 詳しくはマンガデータの関係を見るを参照してください。