リッジラインプロット#
準備#
Import#
Show code cell content
# warningsモジュールのインポート
import warnings
# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Show code cell content
# 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
変数#
Show code cell content
# マンガデータ保存ディレクトリのパス
DIR_CM = Path("../../../data/cm/input")
# アニメデータ保存ディレクトリのパス
DIR_AN = Path("../../../data/an/input")
# ゲームデータ保存ディレクトリのパス
DIR_GM = Path("../../../data/gm/input")
# マンガデータの分析結果の出力先ディレクトリのパス
DIR_OUT_CM = (
DIR_CM.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "ridge"
)
# アニメデータの分析結果の出力先ディレクトリのパス
DIR_OUT_AN = (
DIR_AN.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "ridge"
)
# ゲームデータの分析結果の出力先ディレクトリのパス
DIR_OUT_GM = (
DIR_GM.parent / "output" / Path.cwd().parts[-2] / Path.cwd().parts[-1] / "ridge"
)
Show code cell content
# 読み込み対象ファイル名の定義
# Comic Episode関連のファイル名
FN_CE = "cm_ce.csv"
# Anime Episode関連のファイル名
FN_AE = "an_ae.csv"
# PacKaGeとPlatForm関連のファイル名
FN_PKG_PF = "gm_pkg_pf.csv"
Show code cell content
# 可視化に関する設定値の定義
# 「年代」の集計単位
UNIT_YEARS = 10
# 可視化対象とするマンガ作品の条件として、最小の各話数を定義
MIN_N_CE = 5
Show code cell content
# サンプリング時のシード値
RAND = 0
Show code cell content
# plotlyの描画設定の定義
# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
関数#
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 = 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
Show code cell content
def format_cols(df: pd.DataFrame, cols_rename: Dict[str, str]) -> pd.DataFrame:
"""
指定されたカラムのみをデータフレームから抽出し、カラム名をリネームする関数
Parameters
----------
df : pd.DataFrame
入力データフレーム
cols_rename : Dict[str, str]
リネームしたいカラム名のマッピング(元のカラム名: 新しいカラム名)
Returns
-------
pd.DataFrame
カラムが抽出・リネームされたデータフレーム
"""
# 指定されたカラムのみを抽出し、リネーム
df = df[cols_rename.keys()].rename(columns=cols_rename)
return df
Show 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}'.")
可視化例#
マンガデータ#
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_CM / FN_CE)
Show code cell content
# マンガ雑誌の掲載データから、特定の条件を満たす作品のみを選択して集計を行う
# 各マンガ作品(ccid)に対して、掲載された回数(ceidのユニーク数)をカウント
df_tmp = df_ce.groupby("ccid")["ceid"].nunique().reset_index(name="n_ce")
# 掲載された回数がMIN_N_CE以上のマンガ作品のIDをリストとして取得
ccids = df_tmp[df_tmp["n_ce"] >= MIN_N_CE]["ccid"].unique().tolist()
# 上で取得したマンガ作品IDのみを含むデータをdf_cmに格納
df_cm = df_ce[df_ce["ccid"].isin(ccids)].reset_index(drop=True)
# df_ceにyears列を追加
df_cm = add_years_to_df(df_ce)
# 列名をわかりやすいものに変更
cols_cm = {
"mcname": "マンガ雑誌名",
"years": "年代",
"pages": "一話あたりのページ数",
"ceid": "ceid",
}
df_cm = format_cols(df_cm, cols_cm)
Show code cell content
# 事前にグループ化変数間でサンプルサイズに違いがないか確認
df_cm.groupby("年代")["ceid"].nunique().reset_index()
| 年代 | ceid | |
|---|---|---|
| 0 | 1970 | 25833 |
| 1 | 1980 | 33765 |
| 2 | 1990 | 40432 |
| 3 | 2000 | 44792 |
| 4 | 2010 | 35254 |
Show code cell content
# 可視化対象のDataFrameを確認
df_cm.head()
| マンガ雑誌名 | 年代 | 一話あたりのページ数 | ceid | |
|---|---|---|---|---|
| 0 | 週刊少年マガジン | 2010 | 22.0 | CE00000 |
| 1 | 週刊少年マガジン | 2010 | 18.0 | CE00001 |
| 2 | 週刊少年マガジン | 2010 | 18.0 | CE00002 |
| 3 | 週刊少年マガジン | 2010 | 20.0 | CE00003 |
| 4 | 週刊少年マガジン | 2010 | 20.0 | CE00004 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cm, DIR_OUT_CM, "cm")
DataFrame is saved as '../../../data/cm/output/vol2/02/ridge/cm.csv'.
Show code cell source
# リッジラインプロットを作成し、年代ごとの各話ページ数を可視化
# この際、データセットの半分をランダムにサンプリングし、データサイズを圧縮
# orientation="h"と指定することで、縦横転置
# points=Falseによって、サンプルを表示しないよう設定
fig = px.violin(
df_cm.sample(frac=0.5, random_state=RAND).sort_values("年代"),
y="年代",
x="一話あたりのページ数",
orientation="h",
points=False,
)
# scalemode="count"とし、グループ間のサンプルサイズをスケールに反映
# side="positive"とし、上部にのみ密度分布が表示されるよう設定
fig.update_traces(scalemode="count", side="positive")
# 作成した図を表示
show_fig(fig)
Show code cell source
# widthを手動で再設定
fig.update_traces(width=2)
# リッジラインプロットを再表示
show_fig(fig)
Show code cell source
# bandwidthを手動で再設定
fig.update_traces(bandwidth=0.5)
# 各話ページ数の表示範囲を再設定
fig.update_xaxes(range=[0, 50])
# 再表示
show_fig(fig)
Show code cell source
# リッジラインプロットを作成し、年代とマンガ雑誌名ごとにページ数を可視化
# データセットの半分をランダムにサンプリングし、データサイズを圧縮
# facet_colを使用してマンガ雑誌名ごとに分割し、facet_col_wrap=2で2列で表示
# height=600で図の高さを設定し、orientation="h"で縦横転置
# points=Falseでサンプルを表示しない設定
fig = px.violin(
df_cm.sample(frac=0.5, random_state=RAND).sort_values(["年代", "マンガ雑誌名"]),
y="年代",
x="一話あたりのページ数",
facet_col="マンガ雑誌名",
facet_col_wrap=2,
height=600,
orientation="h",
points=False,
)
# 各トレースの幅を2に設定、side="positive"で上部にのみ密度分布を表示
# bandwidth=0.5でバイオリンの幅を調整
fig.update_traces(width=2, side="positive", bandwidth=0.5)
# ファセット(マンガ雑誌名ごとのリッジラインプロット)のタイトルを簡潔にする処理
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
# X軸とY軸の範囲を設定
fig.update_xaxes(range=[0, 50])
fig.update_yaxes(range=[0, 5])
# 作成した図を表示
show_fig(fig)
アニメデータ#
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ae = pd.read_csv(DIR_AN / FN_AE)
Show code cell content
# 可視化用の集計
# df_aeにyears列を追加。unit_yearsは5年を指定
df_ae = add_years_to_df(df_ae, unit_years=5)
# 年代、アニメ作品ごとの合計各話数を集計し、n_ae列と命名
df_an = df_ae.groupby(["years", "acid"])["aeid"].nunique().reset_index(name="n_ae")
Show code cell content
# 年代別のアニメ作品数を集計
df_an.groupby("years")["acid"].nunique().reset_index()
| years | acid | |
|---|---|---|
| 0 | 1960 | 2 |
| 1 | 1965 | 2 |
| 2 | 1970 | 3 |
| 3 | 1975 | 2 |
| 4 | 1980 | 1 |
| 5 | 1990 | 148 |
| 6 | 1995 | 336 |
| 7 | 2000 | 665 |
| 8 | 2005 | 1038 |
| 9 | 2010 | 1040 |
| 10 | 2015 | 755 |
Show code cell content
# ある程度のサンプル数を確保できる、1990年代以降に限定
df_an = df_an[df_an["years"].astype(int) >= 1990].reset_index(drop=True)
# 列名をわかりやすいものに変更
cols_an = {"years": "年代", "acid": "アニメ作品ID", "n_ae": "アニメ作品の合計話数"}
df_an = format_cols(df_an, cols_an)
Show code cell content
# 可視化対象のDataFrameを確認
df_an.head()
| 年代 | アニメ作品ID | アニメ作品の合計話数 | |
|---|---|---|---|
| 0 | 1990 | C12701 | 99 |
| 1 | 1990 | C12715 | 26 |
| 2 | 1990 | C12729 | 14 |
| 3 | 1990 | C15787 | 13 |
| 4 | 1990 | C8319 | 49 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_an, DIR_OUT_AN, "an")
DataFrame is saved as '../../../data/an/output/vol2/02/ridge/an.csv'.
Show code cell source
# リッジラインプロットを作成し、年代ごとの合計話数を可視化
# orientation="h"で縦横転置し、points=Falseでサンプルを表示しない設定
fig = px.violin(
df_an, y="年代", x="アニメ作品の合計話数", orientation="h", points=False
)
# 各トレースの幅を5に設定し、side="positive"で上部にのみ密度分布を表示
fig.update_traces(width=5, side="positive")
# X軸の範囲を0から500に設定
fig.update_xaxes(range=[0, 500])
# 作成した図を表示
show_fig(fig)
Show code cell source
# bandwidthを細かく調整することで、詳細な分布を表示
fig.update_traces(bandwidth=1)
# X軸の表示領域を100話までに変更
fig.update_xaxes(range=[0, 100])
# 再表示
show_fig(fig)
ゲームデータ#
Show code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_pkg_pf = pd.read_csv(DIR_GM / FN_PKG_PF)
Show code cell content
# add_years_to_df関数で5年区切りのyears列を追加
df_pkg_pf = add_years_to_df(df_pkg_pf, unit_years=5)
Show code cell content
# yearsごとのゲームパッケージ数を集計
df_pkg_pf.groupby("years")["pkgid"].nunique().reset_index()
| years | pkgid | |
|---|---|---|
| 0 | 1980 | 53 |
| 1 | 1985 | 453 |
| 2 | 1990 | 2074 |
| 3 | 1995 | 4590 |
| 4 | 2000 | 5587 |
| 5 | 2005 | 7637 |
| 6 | 2010 | 9344 |
| 7 | 2015 | 5900 |
Show code cell content
# ある程度のサンプルサイズを確保できる、1985年代以降を分析対象とする
df_gm = df_pkg_pf[df_pkg_pf["years"].astype(int) >= 1985].reset_index(drop=True)
# すべてを描画すると重くなるので、0.5だけサンプリング
df_gm = df_gm.sample(frac=0.5, random_state=RAND).sort_values(
"years", ignore_index=True
)
# 列名をわかりやすいものに変更
cols_gm = {
"pfname": "プラットフォーム名",
"pkgname": "パッケージ名",
"price": "ゲームパッケージの価格",
"years": "年代",
}
df_gm = format_cols(df_gm, cols_gm)
Show code cell content
# 可視化対象のDataFrameを確認
df_gm.head()
| プラットフォーム名 | パッケージ名 | ゲームパッケージの価格 | 年代 | |
|---|---|---|---|---|
| 0 | ファミリーコンピュータ | SDガンダムワールド ガチャポン戦士2 カプセル戦記 | 6800.0 | 1985 |
| 1 | ファミリーコンピュータ | ラサール石井のチャイルズクエスト | 5500.0 | 1985 |
| 2 | SC-3000 | 倉庫番 | 4300.0 | 1985 |
| 3 | ファミリーコンピュータ | 未来戦史ライオス | 6300.0 | 1985 |
| 4 | ファミリーコンピュータ | じゃじゃ丸 忍法帳 | 5800.0 | 1985 |
Show code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gm, DIR_OUT_GM, "gm")
DataFrame is saved as '../../../data/gm/output/vol2/02/ridge/gm.csv'.
Show code cell source
# リッジラインプロットを作成し、年代ごとの価格を可視化
# orientation="h"で縦横転置し、points=Falseでサンプルを表示しない設定
fig = px.violin(
df_gm, y="年代", x="ゲームパッケージの価格", orientation="h", points=False
)
# 各トレースの幅を3に設定し、side="positive"で上部にのみ密度分布を表示
fig.update_traces(width=3, side="positive")
# 作成した図を表示
show_fig(fig)
Show code cell source
# bandwidth=100でバイオリンの粒度を調整
fig.update_traces(bandwidth=100)
# X軸の表示範囲を0から10000に設定
fig.update_xaxes(range=[0, 10000])
# 更新した図を表示
show_fig(fig)
Show code cell content
# 1995年のデータを抽出し、プラットフォーム名ごとに価格の統計情報を集計
# describe()を使って、各プラットフォームの価格の要約統計量を計算
df_tmp = (
df_gm[df_gm["年代"] == "1995"]
.groupby("プラットフォーム名")["ゲームパッケージの価格"]
.describe()
.reset_index()
)
# 集計結果を価格のデータ数(count)で降順にソートし、上位5つを表示
# これにより、最も多くの販売データがあるプラットフォームを確認できる
df_tmp.sort_values("count", ascending=False).head()
| プラットフォーム名 | count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|---|
| 17 | プレイステーション | 1063.0 | 5453.513641 | 1838.891268 | 980.0 | 4800.00 | 5800.0 | 5800.0 | 29800.0 |
| 10 | セガサターン | 429.0 | 6296.783217 | 3119.043093 | 1280.0 | 5800.00 | 5800.0 | 6800.0 | 36800.0 |
| 9 | スーパーファミコン | 232.0 | 8908.435345 | 2702.894889 | 2000.0 | 7794.25 | 9800.0 | 10575.0 | 19800.0 |
| 8 | ゲームボーイ | 173.0 | 3715.445087 | 1457.178148 | 800.0 | 3000.00 | 3980.0 | 3980.0 | 14800.0 |
| 3 | NINTENDO64 | 73.0 | 7064.383562 | 936.804537 | 4800.0 | 6800.00 | 6800.0 | 7500.0 | 9800.0 |