メディア展開データの量を見る#

準備#

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/mix/input/")

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

# アニメ各話と原作マンガの作者の対応関係に関するファイル
FN_AE_CRT = "mix_ae_crt.csv"

# マンガ各話とアニメ作品の対応関係に関するファイル
FN_CE_AC = "mix_ce_ac.csv"
Hide code cell content
# 可視化に関する設定値の定義

# 「年代」の集計単位
UNIT_YEARS = 10
Hide code cell content
# 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 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
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_ae_crt = pd.read_csv(DIR_IN / FN_AE_CRT)

棒グラフ#

Hide code cell content
# 作図用の集計

# データフレームdf_ae_crtを'crtname'(マンガ作者名)ごとにグループ化し、
# 'aeid'(アニメ各話ID)のユニークな値の数を集計
# 集計結果を新しいカラム'name'としてデータフレームに追加
df_bar = df_ae_crt.groupby("crtname")["aeid"].nunique().reset_index(name="counts")

# 得られた集計結果を'counts'カラムの値が多い順に並び替え
# ascending=Falseで降順ソートを指定
# ignore_index=Trueで新しいインデックスを割り当て直す
# head(20)で上位20件のデータのみを抽出
df_bar = df_bar.sort_values("counts", ascending=False, ignore_index=True).head(20)

# データフレームdf_barのカラム名を変更
df_bar = df_bar.rename(
    columns={"crtname": "原作マンガ作者名", "counts": "アニメの合計放送話数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_bar.head()
原作マンガ作者名 アニメの合計放送話数
0 青山剛昌 806
1 尾田栄一郎 783
2 岸本斉史 738
3 久保帯人 369
4 冨樫義博 336
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_bar, DIR_OUT, "bar")
DataFrame is saved as '../../../data/mix/output/vol2/06/amounts/bar.csv'.
Hide code cell source
# plotly.expressを使用して棒グラフを作成
# 'df_bar'データフレームの'原作マンガ作者名'列をx軸、'アニメの合計放送話数'列をy軸に設定してプロット
fig = px.bar(df_bar, x="原作マンガ作者名", y="アニメの合計放送話数")

# show_fig関数を使用して図を表示
show_fig(fig)
Hide code cell content
# 青山剛昌さんによる作品のデータを抽出し、アニメ作品名でグループ化
# 各アニメ作品について合計話数、最初の放送日、最後の放送日を集計
df_tmp = (
    df_ae_crt[df_ae_crt["crtname"] == "青山剛昌"]
    .groupby("acname")[["n_ae", "first_date", "last_date"]]
    .first()
    .reset_index()
)

# 最初の放送日でソートし、インデックスをリセット
df_tmp.sort_values("first_date", ignore_index=True)
acname n_ae first_date last_date
0 剣勇伝説YAIBA 52 1993-04-09 1994-04-01
1 名探偵コナン 729 1999-12-06 2016-12-24
2 まじっく快斗 KID THE PHANTOM THIEF 第1話 「蘇る怪盗」 1 2010-04-17 2010-04-17
3 まじっく快斗 1412 24 2014-10-04 2015-03-28
Hide code cell content
# 後段の処理で利用するため、アニメ各話数が多いマンガ作者一覧を保存
crtnames = df_bar["原作マンガ作者名"].values.tolist()
Hide code cell content
# 特番をアニメ作品から除外するために合計話数の下限値を設定
min_nae = 10

# df_ae_crtデータフレームをアニメ作品ID(acid)でグループ化し、
# 各アニメに含まれるユニークなアニメ各話ID(aeid)の数をカウント
df_ac_nae = df_ae_crt.groupby("acid")["aeid"].nunique().reset_index(name="n_ae")

# 合計話数がmin_nae以上のアニメ作品ID(acid)を抽出し、リストを作成
acids = df_ac_nae[df_ac_nae["n_ae"] >= min_nae]["acid"].unique().tolist()
Hide code cell content
# acidがacidsに含まれる(つまりmin_nae話以上放送された)アニメ各話に対して
# 'crtname'(マンガ作者名)ごとにグループ化し、'acid'(アニメ作品ID)のユニークな値の数を集計
# 集計結果を新しいカラム'name'としてデータフレームに追加
df_bar2 = (
    df_ae_crt[df_ae_crt["acid"].isin(acids)]
    .groupby("crtname")["acid"]
    .nunique()
    .reset_index(name="counts")
)

# 得られた集計結果を'counts'カラムの値が多い順に並び替え
# ignore_index=Trueで新しいインデックスを割り当て直す
# head(20)で上位20件のデータのみを抽出
df_bar2 = df_bar2.sort_values("counts", ascending=False, ignore_index=True).head(20)

# データフレームdf_bar2のカラム名を変更
df_bar2 = df_bar2.rename(
    columns={"crtname": "原作マンガ作者名", "counts": "アニメ作品数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_bar2.head()
原作マンガ作者名 アニメ作品数
0 小畑健 6
1 満田拓也 6
2 空知英秋 5
3 高橋留美子 5
4 大場つぐみ 4
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_bar2, DIR_OUT, "bar2")
DataFrame is saved as '../../../data/mix/output/vol2/06/amounts/bar2.csv'.
Hide code cell source
# plotly.expressを使用して棒グラフを作成
# 'df_bar2'データフレームの'マンガ作者名'列をx軸、'アニメ作品数'列をy軸に設定してプロット
fig = px.bar(df_bar2, x="原作マンガ作者名", y="アニメ作品数")

# show_fig関数を使用して図を表示
show_fig(fig)
Hide code cell content
# 満田拓也さんによる作品のデータを抽出し、アニメ作品名でグループ化
# 各アニメ作品について合計話数、最初の放送日、最後の放送日を集計
df_tmp = (
    df_ae_crt[df_ae_crt["crtname"] == "満田拓也"]
    .groupby("acname")[["n_ae", "first_date", "last_date"]]
    .first()
    .reset_index()
)

# 最初の放送日でソートし、インデックスをリセット
df_tmp.sort_values("first_date", ignore_index=True)
acname n_ae first_date last_date
0 メジャー 26 2004-11-13 2005-05-07
1 メジャー[第2期] 30 2005-12-10 2007-01-03
2 メジャー[第3期] 27 2007-01-06 2007-06-30
3 メジャー[第4期] 26 2008-01-05 2008-06-28
4 メジャー[第5期] 25 2009-01-10 2009-06-27
5 メジャー[第6期] 26 2010-04-03 2010-09-25
Hide code cell content
# 特番をアニメ作品から除外するために合計話数の最小数を設定
min_nae = 10

# df_ae_crtデータフレームをアニメシリーズID(asid)でグループ化し、
# 各アニメに含まれるユニークなアニメ各話ID(aeid)の数をカウント
df_as_nae = df_ae_crt.groupby("asid")["aeid"].nunique().reset_index(name="n_ae")

# 合計話数がmin_nae以上のアニメのアニメシリーズID(asid)を抽出し、リストを作成
asids = df_as_nae[df_as_nae["n_ae"] >= min_nae]["asid"].unique().tolist()
Hide code cell content
# asidがasidsに含まれる(つまりmin_nae話以上放送された)アニメ各話に対して
# 'crtname'(マンガ作者名)ごとにグループ化し、'asid'(アニメシリーズID)のユニークな値の数を集計
# 集計結果を新しいカラム'name'としてデータフレームに追加
df_bar3 = (
    df_ae_crt[df_ae_crt["asid"].isin(asids)]
    .groupby("crtname")["asid"]
    .nunique()
    .reset_index(name="counts")
)

# 得られた集計結果を'counts'カラムの値が多い順に並び替え
# ignore_index=Trueで新しいインデックスを割り当て直す
# head(20)で上位20件のデータのみを抽出
df_bar3 = df_bar3.sort_values("counts", ascending=False, ignore_index=True).head(20)

# データフレームdf_bar3のカラム名を変更
df_bar3 = df_bar3.rename(
    columns={"crtname": "原作マンガ作者名", "counts": "アニメシリーズ数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_bar3.head()
原作マンガ作者名 アニメシリーズ数
0 小畑健 4
1 瀬尾公治 3
2 冨樫義博 3
3 椎名高志 2
4 赤松健 2
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_bar3, DIR_OUT, "bar3")
DataFrame is saved as '../../../data/mix/output/vol2/06/amounts/bar3.csv'.
Hide code cell source
# plotly.expressを使用して棒グラフを作成
# 'df_bar3'データフレームの'マンガ作者名'列をx軸、'アニメシリーズ数'列をy軸に設定してプロット
fig = px.bar(df_bar3, x="原作マンガ作者名", y="アニメシリーズ数")

# show_fig関数を使用して図を表示
show_fig(fig)

集合棒グラフ#

Hide code cell content
# 可視化のための集計

# アニメ各話放送数が多い上位10名のマンガ家を抽出
df_gbar = df_ae_crt[df_ae_crt["crtname"].isin(crtnames[:10])].reset_index(drop=True)
# date列が欠損していないデータのみを集計対象とする
df_gbar = df_gbar[~df_gbar["date"].isna()].reset_index(drop=True)

# 'years'カラムをデータフレームに追加し、年代情報を含める
# つぎに、年代(years)をカテゴリカルなデータとして扱うために文字列型に変換
# さらに、'years'カラムの欠損値を持つ行を削除し、インデックスをリセット
df_gbar = add_years_to_df(df_gbar)
df_gbar["years"] = df_gbar["years"].astype(str)
df_gbar = df_gbar[~df_gbar["years"].isna()].reset_index(drop=True)

# マンガ作者名と年度でグループ化し、各グループ内の合計話数をカウント
df_gbar = df_gbar.groupby(["crtname", "years"])["aeid"].nunique().reset_index()

# # 'crtname'と'years'カラムを基準にして、データをアップサンプリング
df_gbar = resample_df_by_col_and_years(df_gbar, "crtname")

# マンガ作者の名前のリストに基づいて、ソート用の順序カラムを作成
# 'order'と'years'カラムに基づいてデータフレームをソートし、作図用に整える
df_gbar["order"] = df_gbar["crtname"].apply(lambda x: crtnames.index(x))
df_gbar = df_gbar.sort_values(["order", "years"], ignore_index=True)

# 作図しやすいよう列名を変更
df_gbar = df_gbar.rename(
    columns={
        "crtname": "原作マンガ作者名",
        "aeid": "アニメの合計放送話数",
        "years": "年代",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_gbar.head()
原作マンガ作者名 年代 アニメの合計放送話数 order
0 青山剛昌 1990 55 0
1 青山剛昌 2000 401 0
2 青山剛昌 2010 350 0
3 尾田栄一郎 1990 9 1
4 尾田栄一郎 2000 432 1
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gbar, DIR_OUT, "gbar")
DataFrame is saved as '../../../data/mix/output/vol2/06/amounts/gbar.csv'.
Hide code cell source
# px.barを使用して、マンガ作者ごとの年代別アニメ放送話数の集合棒グラフを作成
# 'df_gbar'データフレームの'原作マンガ作者名'列をx軸に、'アニメの合計放送話数'列をy軸に設定
# '年代'列を色分けの基準にし、Portlandの発散色スキームを使用して色分け
# barmodeを"group"に設定して、年代ごとにグループ化された集合棒グラフを表示
fig = px.bar(
    df_gbar,
    x="原作マンガ作者名",
    y="アニメの合計放送話数",
    color="年代",
    color_discrete_sequence=px.colors.diverging.Portland,
    barmode="group",
)

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

積上げ棒グラフ#

Hide code cell source
# px.barを使用して、マンガ作者ごとの年代別アニメ放送話数の積上げ棒グラフを作成
# 'df_gbar'データフレームの'原作マンガ作者名'列をx軸に、'アニメの合計放送話数'列をy軸に設定
# '年代'列で色分けし、Portlandの発散色スキームを使用して色分け
# barmodeを"stack"に設定して、年代ごとに積み上げられた棒グラフを表示
fig = px.bar(
    df_gbar,
    x="原作マンガ作者名",
    y="アニメの合計放送話数",
    color="年代",
    color_discrete_sequence=px.colors.diverging.Portland,
    barmode="stack",
)

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

ヒートマップ#

Hide code cell content
# 日付が欠損していないデータのみを選択
df_hm = df_ae_crt[~df_ae_crt["date"].isna()].reset_index(drop=True)

# df_hmに年代(years)を追加
df_hm = add_years_to_df(df_hm, 1)
# df_hmをdate順でソートし、全crtnameを事前に抽出しておく、時間順に並んだヒートマップを作成するための工夫
crtnames = df_hm.sort_values(["date", "crtname"], ignore_index=True)["crtname"].unique()
Hide code cell content
# マンガ作者名と年代ごとに異なるaeidの数を集計
df_hm = df_hm.groupby(["crtname", "years"])["aeid"].nunique().reset_index(name="n_ae")

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

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

# 可視化用に列名をわかりやすい日本語の名前に変更
df_hm = df_hm.rename(
    columns={"crtname": "原作マンガ作者名", "years": "放送年", "n_ae": "アニメ各話数"}
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_hm.head()
原作マンガ作者名 放送年 アニメ各話数
0 冨樫義博 1992 12
1 冨樫義博 1993 49
2 冨樫義博 1994 50
3 冨樫義博 1995 1
4 冨樫義博 1997 0
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_hm, DIR_OUT, "hm")
DataFrame is saved as '../../../data/mix/output/vol2/06/amounts/hm.csv'.
Hide code cell source
# df_hmデータを用いて年代とマンガ作者名に基づく密度ヒートマップを作成
# アニメ各話数を色の濃淡で表現し、ヒートマップの高さを500に設定
fig = px.density_heatmap(
    df_hm, x="放送年", y="原作マンガ作者名", z="アニメ各話数", height=500
)
# ヒートマップのカラーバーにタイトル「アニメ各話数」を設定
fig.update_layout(coloraxis_colorbar={"title": "アニメ各話数"})
# 作成したヒートマップを表示する関数を実行
show_fig(fig)
Hide code cell content
# 青山剛昌さんによる作品のデータを抽出し、アニメ作品名でグループ化
# 各アニメ作品について合計話数、最初の放送日、最後の放送日を集計
df_tmp = (
    df_ae_crt[df_ae_crt["crtname"] == "青山剛昌"]
    .groupby("acname")[["n_ae", "first_date", "last_date"]]
    .first()
    .reset_index()
)

# 最初の放送日でソートし、インデックスをリセットして表示
df_tmp.sort_values("first_date", ignore_index=True)
acname n_ae first_date last_date
0 剣勇伝説YAIBA 52 1993-04-09 1994-04-01
1 名探偵コナン 729 1999-12-06 2016-12-24
2 まじっく快斗 KID THE PHANTOM THIEF 第1話 「蘇る怪盗」 1 2010-04-17 2010-04-17
3 まじっく快斗 1412 24 2014-10-04 2015-03-28