アニメデータの前処理#

本書の再現に、前処理の再実行は不要

前処理後のデータは全てvizbook-jupyter/data/*以下に格納されています。 本書の再現のため、前処理を再実行頂く必要はありません。 (仮に再実行したとしても、同じファイルが出力されるだけですので問題はありません。)

準備#

Import#

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# jsonモジュールのインポート
# JSON形式のデータの読み書きをサポート
import json

# osモジュールのインポート
# オペレーティングシステムとのインターフェースを提供
import os

# reモジュールのインポート
# 正規表現操作をサポート
import re

# zipfileモジュールのインポート
# ZIPアーカイブファイルの読み書きをサポート
import zipfile

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# pprintモジュールのインポート
# データ構造を見やすく整形して表示するための関数
from pprint import pprint

# typingモジュールからの型ヒント関連のインポート
# 関数やクラスの引数・返り値の型を注釈するためのツール
from typing import Any, Dict, List, Optional, Union

# ijsonモジュールのインポート
# ストリームから大きなJSONオブジェクトを効率的に解析・抽出
import ijson

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# tqdm_notebookのインポート
# Jupyter Notebook内でのプログレスバー表示をサポート
from tqdm import tqdm_notebook as tqdm

変数#

Hide code cell content
# 入出力ディレクトリの定義

# 入力ファイル(MADB)を格納しているディレクトリのパス
DIR_INPUT = Path("../../../madb/data/json-ld")

# 外部データソース(Wikipedia)から取得したファイルを格納しているディレクトリのパス
DIR_EXTERNAL = Path("../../../data/an/external/20230224_petscan")

# 一時的にファイルを保存するディレクトリのパス
DIR_TMP = Path("../../../data/an/tmp")

# 中間ファイルを保存するディレクトリのパス
DIR_INTERIM = Path("../../../data/an/interim")

# 出力ファイルを保存するディレクトリのパス
DIR_OUTPUT = Path("../../../data/an/input")
Hide code cell content
# 声優ファイル名の定義
FN_ACTORS = "actors_*.csv"
Hide code cell content
# MADBの読み込み対象ファイル名のリストを定義
# - `an201`:テレビアニメレギュラー各話に関する情報を格納
# - `an207`:テレビアニメレギュラーシリーズに関する情報を格納
FNS_AN = [
    "an201",
    "an207",
]
Hide code cell content
# Anime Collectionとして利用するカラムとその新しいカラム名のマッピングを定義
COLS_AC = {
    "@id": "acid",
    "name": "acname",
    "originalWorkCreator": "crtname",
    "actor": "actname",
    "isPartOf": "asid",
}
Hide code cell content
# Anime Episodeとして利用するカラムとその新しいカラム名のマッピングを定義
COLS_AE = {
    "@id": "aeid",
    "alternativeHeadline": "aename",
    "datePublished": "date",
    "episodeNumber": "aeno",
    "isPartOf": "acid",
}
Hide code cell content
# 声優用のデータフレームで利用するカラムとその新しいカラム名のマッピングを定義
COLS_ACT = {
    "title": "actname",
    "length": "wiki_size",
    "gender": "gender",
}

関数#

Hide code cell content
def read_json(path: Union[str, Path]) -> Dict[str, Any]:
    """
    jsonファイルを辞書として読み込む

    Parameters
    ----------
    path : Union[str, Path]
        読み込みたいjsonファイルのパス

    Returns
    -------
    Dict[str, Any]
        jsonデータを格納した辞書
    """

    # 指定したパスのjsonファイルを読み込みモードで開く
    with open(path, "r", encoding="utf-8") as f:
        # json.loadを使用して、ファイル内容を辞書として読み込む
        dct = json.load(f)

    # 読み込んだ辞書を返す
    return dct
Hide code cell content
def save_json(path: Union[str, Path], dct: Dict) -> None:
    """
    辞書をjson形式で保存

    Parameters
    ----------
    path : Union[str, Path]
        保存先のファイルパス
    dct : Dict
        保存する辞書

    Returns
    -------
    None
    """

    # 指定したパスのjsonファイルを書き込みモードで開く
    with open(path, "w", encoding="utf-8") as f:
        # json.dumpを使用して、辞書の内容をjson形式でファイルに書き込む
        # ensure_ascii=Falseで非ASCII文字もそのまま保存し、indent=4で整形して保存
        json.dump(dct, f, ensure_ascii=False, indent=4)
Hide code cell content
def read_json_w_filters(
    path: Union[str, Path], items: List[str], filters: Dict[str, List[Any]]
) -> List[Dict[str, Any]]:
    """
    itemsのうち、filtersの条件を満たすもののみを抽出して返す

    Parameters
    ----------
    path : Union[str, Path]
        読み込み対象のjsonファイルのパス、文字列またはPathオブジェクト
    items : List[str]
        読み込む項目名のリスト
    filters : Dict[str, List[Any]]
        抽出条件を指定する辞書、キーはフィルタリング対象の項目名、値は条件となる値のリスト

    Returns
    -------
    List[Dict[str, Any]]
        フィルタリングされた項目の辞書を要素とするリスト
    """

    # 出力結果を格納するための空のリストを初期化
    out = []

    # 指定したパスからファイルを読み込みモードで開く
    with open(path, "r", encoding="utf-8") as f:
        # ijsonを使用して、特定の項目を逐次読み込む
        parse = ijson.items(f, items)
        # parseを順に処理し、各項目をitemとして取得
        for item in parse:
            # filtersの条件をすべて満足するもの以外はbreak
            for k, v in filters.items():
                # フィルタリング対象の項目名がitemのキーに含まれていない場合、break
                if k not in item.keys():
                    break
                # 項目の値がフィルタリング条件に含まれていない場合、break
                if item[k] not in v:
                    break
            else:
                # 上記のforループでbreakされなかった場合(全ての条件を満たす場合)、outに追加
                out.append(item)

    # 処理結果を返す
    return out
Hide 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
Hide code cell content
def format_name(name: Optional[str]) -> Optional[str]:
    """
    nameから名称情報を抽出する関数
    ccname, crtname, ocrtnameの値の生成に利用

    Parameters
    ----------
    name : Optional[str]
        名称情報を含むデータ

    Returns
    -------
    Optional[str]
        抽出された名称情報、nameがNoneまたは適切な形式でない場合はNoneを返す

    Raises
    ------
    Exception:
        nameから適切な名称情報を抽出できなかった場合
    """

    # nameがNoneまたは辞書の場合
    if name is np.nan or isinstance(name, dict):
        return None

    # nameが文字列の場合
    if isinstance(name, str):
        return name

    # nameがリストの場合
    if isinstance(name, list):
        for item in name:
            if isinstance(item, str):
                return item

    # 上記の条件に合致しない場合、例外を発生させる
    raise Exception(f"No name in {name}!")
Hide code cell content
def get_id_from_uri(uri: Optional[str]) -> Optional[str]:
    """
    URIから末尾のIDを取得する関数

    Parameters
    ----------
    uri : Optional[str]
        解析対象のURI、Noneの場合も考慮

    Returns
    -------
    Optional[str]
        URIから取得したID、URIがNoneまたはNaNの場合はNoneを返す
    """

    # uriがNaNの場合、Noneを返す
    if uri is np.nan:
        return None
    # uriからIDを抽出して返す
    else:
        return uri.split("/")[-1]
Hide code cell content
def preprocess_df_ac(df: pd.DataFrame) -> pd.DataFrame:
    """
    `df_ac` の前処理を行う関数

    Parameters
    ----------
    df : pd.DataFrame
        前処理前の`df_ac`

    Returns
    -------
    pd.DataFrame
        前処理が完了した`df_ac`
    """

    # 必要なカラムの抽出とカラム名のリネーム
    df = format_cols(df, COLS_AC)

    # 声優名の整形
    df["acname"] = df["acname"].apply(format_name)

    # 声優IDの抽出
    df["acid"] = df["acid"].apply(get_id_from_uri)

    # アニメシリーズIDの抽出
    df["asid"] = df["asid"].apply(get_id_from_uri)

    # 作品名のテキストから原作者名を抽出
    df["crtname"] = df["crtname"].apply(get_crtnames_from_text)

    return df
Hide code cell content
def preprocess_df_ae(df: pd.DataFrame) -> pd.DataFrame:
    """
    `df_ae` の前処理を行う関数

    Parameters
    ----------
    df : pd.DataFrame
        前処理前の`df_ae`

    Returns
    -------
    pd.DataFrame
        前処理が完了した`df_ae`

    """

    # 必要なカラムの抽出とカラム名のリネーム
    df = format_cols(df, COLS_AE)

    # 声優IDの抽出
    df["acid"] = df["acid"].apply(get_id_from_uri)

    # アニメ各話IDの抽出
    df["aeid"] = df["aeid"].apply(get_id_from_uri)

    # アニメ各話名の整形
    df["aename"] = df["aename"].apply(format_name)

    return df
Hide code cell content
def preprocess_df_act(df: pd.DataFrame) -> pd.DataFrame:
    """
    `df_act` の前処理を行う関数

    Parameters
    ----------
    df : pd.DataFrame
        前処理前の`df_act`

    Returns
    -------
    pd.DataFrame
        前処理が完了した`df_act`

    """

    # `namespace` がNaNのレコードのみを残す
    df = df[df["namespace"].isna()].reset_index(drop=True)

    # 必要なカラムの抽出とカラム名のリネーム
    df = format_cols(df, COLS_ACT)

    # キャラクター名から括弧とその中身を削除
    df["actname"] = df["actname"].replace("_\(.*\)", "", regex=True)

    # `wiki_size` に基づいてソート
    df = df.sort_values("wiki_size", ascending=False, ignore_index=True)

    return df
Hide code cell content
def get_actname_from_text(text: Optional[Any], names_all: List[str]) -> Optional[str]:
    """
    与えられたテキストから`names_all`に一致する名前を抽出する関数

    Parameters
    ----------
    text : Optional[Any]
        名前を抽出する対象のテキスト
    names_all : List[str]
        全名前のリスト

    Returns
    -------
    Optional[str]
        一致する名前。一致しない場合はNone
    """

    # '【役名】名前' -> '名前'
    text = re.sub("【.*】", "", text)
    # '[役名]名前' -> '名前'
    text = re.sub("\[.*\]", "", text)
    # 〈.*〉で区切られている場合に対応
    text = re.sub("〈.*〉", "", text)
    # <.*>で区切られている場合に対応
    text = re.sub("<.*>", "", text)
    # 空白文字を削除
    text = re.sub("\s*", "", text)

    name_matches = [x for x in names_all if x in text]

    if name_matches:
        # 一致した名前の中から最も長いものを返す
        return sorted(name_matches, key=len, reverse=True)[0]
    else:
        return None
Hide code cell content
def get_actnames_from_text(
    text: Optional[str], names_all: List[str]
) -> Optional[List[str]]:
    """
    与えられたテキストから`names_all`に一致する名前のリストを抽出する関数

    Parameters
    ----------
    text : Optional[str]
        名前を抽出する対象のテキスト
    names_all : List[str]
        全ての名前のリスト

    Returns
    -------
    Optional[List[str]]
        一致する名前のリスト、一致しない場合はNone
    """

    # textがNaNやNoneの場合はNoneを返す
    if pd.isna(text) or text is None:
        return None

    # 名前を抽出
    extracted_names = [
        get_actname_from_text(name, names_all) for name in text.split("/")
    ]
    # Noneを除外
    extracted_names = [name for name in extracted_names if name is not None]

    return list(set(extracted_names)) if extracted_names else None
Hide code cell content
def get_crtname_from_text(text: str) -> str:
    """
    与えられたテキストから原作者名を抽出する関数

    Parameters
    ----------
    text : str
        原作者名を抽出する対象のテキスト

    Returns
    -------
    str
        抽出された原作者名
    """

    # 全角の角括弧とその中身を削除
    text = re.sub("[.*]", "", text)
    # 空白文字を削除
    text = re.sub("\s*", "", text)

    return text
Hide code cell content
def get_crtnames_from_text(text: str) -> Optional[List[str]]:
    """
    文字列から原作者名のリストを抽出する。

    Parameters:
    - text (str): 原作者名が"/"で区切られた文字列。

    Returns:
    - Optional[List[str]]: 抽出された原作者名のリスト。入力がNaNの場合はNoneを返す。
    """

    # 入力がNaNであるかの確認
    if text is np.nan:
        return None

    # "/"で区切られた原作者名を分割する
    crtnames = text.split("/")

    # 各原作者名を整形する
    crtnames = [get_crtname_from_text(c) for c in crtnames]

    return crtnames
Hide code cell content
def read_csvs(pathes: List) -> pd.DataFrame:
    """
    複数のCSVファイルを順番に読み込み、それらを結合する関数

    Parameters
    ----------
    pathes : List
        読み込みたいCSVファイルのパスのリスト

    Returns
    -------
    pd.DataFrame
        結合されたデータフレーム
    """

    # 空のデータフレームを初期化
    df_all = pd.DataFrame()

    # 各CSVファイルのパスについて処理を行う
    for p in pathes:
        # CSVファイルを読み込む
        df = pd.read_csv(p)

        # 読み込んだデータフレームをdf_allに結合する
        df_all = pd.concat([df_all, df], ignore_index=True)

    return df_all
Hide code cell content
def cast_str_to_list(text: Optional[str]) -> List[str]:
    """
    文字列形式で表現されたリストを、実際のリストに変換する関数

    Parameters
    ----------
    text : Optional[str]
        リストとして文字列で表現されたデータ(例: "[1, 2, 3]")

    Returns
    -------
    List[str]
        文字列から変換されたリスト:元の文字列がNaNの場合は空のリストを返す

    Example
    -------
    >>> cast_str_to_list("[a, b, c]")
    ['a', 'b', 'c']
    """

    # NaNの場合は空のリストを返す
    if text is np.nan:
        return []

    # 不要な文字を取り除いて、カンマで分割
    # この操作で文字列形式のリストを実際のリストに変換する
    return (
        text.replace("[", "")
        .replace("]", "")
        .replace("'", "")
        .replace(" ", "")
        .split(",")
    )

出力先の生成#

Hide code cell content
# DIR_TMPという名前のディレクトリを作成する
# すでに存在する場合は何もしない
DIR_TMP.mkdir(exist_ok=True, parents=True)

# DIR_INTERIMという名前のディレクトリを作成する
# すでに存在する場合は何もしない
DIR_INTERIM.mkdir(exist_ok=True, parents=True)

# DIR_OUTPUTという名前のディレクトリを作成する
# すでに存在する場合は何もしない
DIR_OUTPUT.mkdir(exist_ok=True, parents=True)

DIR_TMPへの一時的な出力#

zipファイルの解凍#

Hide code cell content
# DIR_INPUTディレクトリ内で`_an`を含むファイルのパスをすべて検索し、リストとして取得
ps_an = sorted(list(DIR_INPUT.glob("*_an-*")))
Hide code cell content
# `ps_an`リストに含まれる各.zipファイルに対して処理を実行
# tqdmを使用することで、進行状況のバーが表示される
for p_from in tqdm(ps_an):
    # 出力先のパスを設定する
    # 元のファイルパスから、DIR_INPUTをDIR_TMPに変更し、ファイル拡張子の.zipを削除
    p_to = DIR_TMP / p_from.parts[-1].replace(".zip", "")

    # zipfileを使用して、zipファイルを開く
    with zipfile.ZipFile(p_from) as z:
        # zipファイル内のすべてのファイル・ディレクトリをp_toのパスに展開
        z.extractall(p_to)

入力ファイルのサイズ圧縮#

対象#

Hide code cell content
# MADBの各ファイル名をキーとして、該当するファイルのパスをリストとして取得する
# これを辞書型変数`ps_an`に格納する
# 例: {'an201': ['path1', 'path2', ...], 'an207': ['path3', 'path4', ...],}
ps_an = {an: sorted(list(DIR_TMP.glob(f"*{an}*/*"))) for an in FNS_AN}
Hide code cell content
# 内容を確認
pprint(ps_an)
{'an201': [PosixPath('../../../data/an/tmp/metadata_an-item_an201_json/metadata_an-item_an201_json\\metadata_an-item_an201_00001.json'),
           PosixPath('../../../data/an/tmp/metadata_an-item_an201_json/metadata_an-item_an201_json\\metadata_an-item_an201_00002.json')],
 'an207': [PosixPath('../../../data/an/tmp/metadata_an-col_an207_json/metadata_an-col_an207_json\\metadata_an-col_an207_00001.json')]}

an207#

Hide code cell content
# `an207`に関連するファイルパスを抽出する
ps_an207 = ps_an["an207"]
Hide code cell content
# ps_an207に含まれる全てのファイルパスに対して処理を繰り返す
for i, p_an207 in enumerate(ps_an207):
    # フィルタリング対象となるジャンルを指定
    # ここでは"テレビレギュラーアニメシリーズ"のみを対象とする
    filters = {
        "genre": ["テレビレギュラーアニメシリーズ"],
    }

    # 指定したフィルターを利用して、JSONファイルからデータを読み込む
    ac = read_json_w_filters(p_an207, "@graph.item", filters)

    # 読み込んだデータをデータフレームに変換
    df_ac = pd.DataFrame(ac)

    # データフレームの前処理を実行
    df_ac = preprocess_df_ac(df_ac)

    # 前処理後のデータをCSVファイルとして一時保存
    # ファイル名は連番を含めた形式にしている
    df_ac.to_csv(DIR_TMP / f"an207_{i+1:05}.csv", index=False)

an201#

Hide code cell content
# `an201`に関連するファイルパスを抽出する
ps_an201 = ps_an["an201"]
Hide code cell content
# ps_an201に含まれる全てのファイルパスに対して処理を繰り返す
for i, p_an201 in enumerate(ps_an201):
    # フィルタリング対象となるジャンルを指定
    # ここでは"テレビレギュラー"のみを対象とする
    filters = {"genre": ["テレビレギュラー"]}

    # 指定したフィルターを利用して、JSONファイルからデータを読み込む
    ae = read_json_w_filters(p_an201, "@graph.item", filters)

    # 読み込んだデータをデータフレームに変換
    df_ae = pd.DataFrame(ae)

    # データフレームの前処理を実行
    df_ae = preprocess_df_ae(df_ae)

    # 前処理後のデータをCSVファイルとして一時保存
    # ファイル名は連番を含めた形式にしている
    df_ae.to_csv(DIR_TMP / f"an201_{i+1:05}.csv", index=False)

Wikipedia声優データの前処理#

Hide code cell content
# 声優データが格納されている、下記のパターンに合致するファイルのパス一覧を取得する
ps_actors = sorted(list(DIR_EXTERNAL.glob(FN_ACTORS)))
Hide code cell content
# 空のデータフレームを初期化
df_act = pd.DataFrame()

# 各声優ファイルを順番に処理
for p_act in ps_actors:
    # ファイル名から性別情報を抽出
    
    gnd = p_act.parts[-1].split("_")[-1].replace(".csv", "")

    # CSVファイルを読み込み
    df = pd.read_csv(p_act)

    # 新たに性別カラムを作成し、性別情報を追加
    df["gender"] = gnd

    # 既存のデータフレームに新たに読み込んだデータを結合
    df_act = pd.concat([df_act, df], ignore_index=True)

# データフレームの列を整理
df_act = format_cols(df_act, COLS_ACT)
Hide code cell content
# 作成したパスにデータフレームをCSVとして保存
df_act.to_csv(DIR_TMP / "act.csv", index=False)

DIR_INTERIMへの中間出力#

ac.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`an207_`で始まるCSVファイルのパスをすべて取得する
ps_ac = sorted(list(DIR_TMP.glob("an207_*.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_ac = read_csvs(ps_ac)
Hide code cell content
# データフレームからacidとacnameの列のみを取得する
df_ac = df_ac[["acid", "acname", "asid"]]
Hide code cell content
# 所定のディレクトリにdf_acをCSVファイルとして保存
df_ac.to_csv(DIR_INTERIM / "ac.csv", index=False)

ae.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`an201_`で始まるCSVファイルのパスをすべて取得する
ps_ae = sorted(list(DIR_TMP.glob("an201_*.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_ae = read_csvs(ps_ae)
# dateとacidを基準にデータフレームを昇順にソートする
df_ae = df_ae.sort_values(["date", "acid"], ignore_index=True)
Hide code cell content
# head()メソッドを利用し、先頭5行を表示する
df_ae.head()
aeid aename date aeno acid
0 M19760 アトム誕生の巻* 1963-01-01 第1話 C7163
1 M19761 フランケンの巻* 1963-01-08 第2話 C7163
2 M19762 火星探険の巻* 1963-01-15 第3話 C7163
3 M19763 ゲルニカの巻* 1963-01-22 第4話 C7163
4 M19764 スフィンクスの巻* 1963-01-29 第5話 C7163
Hide code cell content
# 所定のディレクトリにdf_aeをCSVファイルとして保存
df_ae.to_csv(DIR_INTERIM / "ae.csv", index=False)

act.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`act.csv`という名称のCSVファイルのパスをすべて取得する
ps_act = sorted(list(DIR_TMP.glob("act.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_act = read_csvs(ps_act)
Hide code cell content
# 現在のデータフレームの列のリストを取得
cols = df_act.columns.tolist()

# genderとactnameで並び替え
df_act = df_act.sort_values(["gender", "actname"], ignore_index=True)

# 新しい列`actid`を追加し、インデックス番号をそのまま値として格納
df_act["actid"] = df_act.index

# `actid`の値を「ACT」に続く5桁の数字の形式に変換
df_act["actid"] = df_act["actid"].apply(lambda x: f"ACT{x:05}")

# 列の順番を`actid`を先頭にして変更
df_act = df_act[["actid"] + cols]
Hide code cell content
# head()メソッドを利用し、先頭5行を表示する
df_act.head()
actid actname wiki_size gender
0 ACT00000 AIRI_(声優) 2597 female
1 ACT00001 AKIKO_(声優) 4359 female
2 ACT00002 AYA_(声優) 4471 female
3 ACT00003 Ashir 2057 female
4 ACT00004 Ayami_(アニソン歌手) 6910 female
Hide code cell content
# 所定のディレクトリにdf_actをCSVファイルとして保存
df_act.to_csv(DIR_INTERIM / "act.csv", index=False)

crt.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`an207_`で始まるCSVファイルのパスをすべて取得する
ps_ac = sorted(list(DIR_TMP.glob("an207_*.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_ac = read_csvs(ps_ac)
Hide code cell content
# `crtnames`という名前の空の集合を初期化
crtnames = set()

# `df_ac`の各行に対して処理を実行
for r in df_ac.to_dict("records"):
    # `crtname`がNaNの場合は次の行へ
    if r["crtname"] is np.nan:
        continue

    # `crtname`をリストに変換し、一時的な集合を作成
    crtname = set(cast_str_to_list(r["crtname"]))

    # `crtnames`集合に`crtname`集合の要素を追加
    crtnames.update(crtname)

# `crtnames`集合をリストに変換し、ソート
crtnames = sorted(list(crtnames))
Hide code cell content
# マンガ作者(原作者)名の数だけ固有のIDを生成
crtids = [f"ACRT{i:05}" for i in range(len(crtnames))]

# 生成したIDとマンガ作者(原作者)名を使用してDataFrameを作成
df_crt = pd.DataFrame({"crtid": crtids, "crtname": crtnames})
Hide code cell content
# head()メソッドを利用して、先頭5行の内容を確認
df_crt.head()
crtid crtname
0 ACRT00000 29
1 ACRT00001 5pb./Nitroplus
2 ACRT00002 6pack
3 ACRT00003 ACQUIRE
4 ACRT00004 AIC
Hide code cell content
# 所定のディレクトリにdf_crtをCSVファイルとして保存
df_crt.to_csv(DIR_INTERIM / "crt.csv", index=False)

ac_act.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`an207_`で始まるCSVファイルのパスをすべて取得する
ps_ac = sorted(list(DIR_TMP.glob("an207_*.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_ac = read_csvs(ps_ac)

# `DIR_INTERIM`ディレクトリ内の`act.csv`ファイルのパスを取得する
ps_act = sorted(list(DIR_INTERIM.glob("act.csv")))
# 取得したCSVファイルを読み込む
df_act = read_csvs(ps_act)
Hide code cell content
# `df_act`から一意の声優名を取得する
actnames_all = sorted(df_act["actname"].unique())

# `df_act`を用いて、声優名をキーとし、対応する`actid`を値とする辞書を作成する
actname2atcid = df_act.groupby("actname")["actid"].first().to_dict()
Hide code cell content
# `df_ac`から`acid`と`actname`のみを抽出して新しいデータフレームを作成
df_tmp = df_ac[["acid", "actname"]]

# `get_actnames_from_text`関数を用いて、`actname`列内の声優名を整形
df_tmp["actname"] = df_tmp["actname"].apply(
    lambda x: get_actnames_from_text(x, actnames_all)
)

# NaNを含む行を除去し、インデックスをリセット
df_tmp = df_tmp[~df_tmp["actname"].isna()].reset_index(drop=True)
Hide code cell content
# 各アニメ作品(ac)に対応する声優(act)の情報を集約するためのリストを初期化
ac_act = []

# `df_tmp`の各行に対して処理を実行
for r in df_tmp.to_dict("records"):
    # アニメ作品のIDを取得
    acid = r["acid"]
    # 当該アニメ作品に対応する声優名のリストを取得
    actnames = sorted(r["actname"])

    # 各声優名に対して、声優IDを取得し、結果をリストに追加
    for actname in actnames:
        actid = actname2atcid[actname]
        ac_act.append([acid, actid])

# 得られた結果をデータフレームに変換
df_ac_act = pd.DataFrame(columns=["acid", "actid"], data=ac_act)
Hide code cell content
# head()メソッドを利用し、先頭5行を表示
df_ac_act.head()
acid actid
0 C7158 ACT06218
1 C7158 ACT01691
2 C7158 ACT02696
3 C7162 ACT00975
4 C7162 ACT06522
Hide code cell content
# 所定のディレクトリにdf_ac_actをCSVファイルとして保存
df_ac_act.to_csv(DIR_INTERIM / "ac_act.csv", index=False)

ac_crt.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`an207_`で始まるCSVファイルのパスをすべて取得
ps_ac = sorted(list(DIR_TMP.glob("an207_*.csv")))
# `DIR_INTERIM`ディレクトリ内の`crt.csv`という名前のCSVファイルのパスを取得
ps_crt = sorted(list(DIR_INTERIM.glob("crt.csv")))

# 取得したアニメ作品に関するCSVファイルを読み込み、一つのデータフレームに結合
df_ac = read_csvs(ps_ac)
# 取得した原作者に関するCSVファイルを読み込み、一つのデータフレームに結合
df_crt = read_csvs(ps_crt)
Hide code cell content
# `df_crt`を用いて、原作者名をキーとし、対応する`crtid`を値とする辞書を作成する
crtname2crtid = df_crt.groupby("crtname")["crtid"].first().to_dict()
Hide code cell content
# アニメ作品と原作者の関連をリストに格納するための空リストを作成
ac_crt = []

# アニメ作品のデータフレームをレコード毎に処理
for r in df_ac.to_dict("records"):
    # アニメ作品のIDを取得
    acid = r["acid"]
    # アニメ作品に関連する原作者名をセットに変換
    crtnames = sorted(set(cast_str_to_list(r["crtname"])))

    # 各原作者名に対して処理
    for crtname in crtnames:
        # 原作者名から原作者IDを取得
        crtid = crtname2crtid[crtname]
        # アニメ作品IDと原作者IDのペアをリストに追加
        ac_crt.append([acid, crtid])

# アニメ作品と原作者の関連を示すデータフレームを作成
df_ac_crt = pd.DataFrame(columns=["acid", "crtid"], data=ac_crt)
Hide code cell content
# head()メソッドを利用し、先頭5行を表示
df_ac_crt.head()
acid crtid
0 C7158 ACRT00664
1 C7158 ACRT00837
2 C7158 ACRT00937
3 C7160 ACRT00799
4 C7162 ACRT00778
Hide code cell content
# 所定のディレクトリにdf_ac_crtをCSVファイルとして保存
df_ac_crt.to_csv(DIR_INTERIM / "ac_crt.csv", index=False)

DIR_OUTPUTへの最終出力#

Hide code cell content
# ファイルから各データフレームを読み込む

# アニメ各話に関する情報を読み込む
df_ae = pd.read_csv(DIR_INTERIM / "ae.csv")

# アニメ原作者に関する情報を読み込む
df_crt = pd.read_csv(DIR_INTERIM / "crt.csv")

# アニメ作品とアニメ原作者の対応関係に関する情報を読み込む
df_ac_crt = pd.read_csv(DIR_INTERIM / "ac_crt.csv")

# アニメ作品に関する情報を読み込む
df_ac = pd.read_csv(DIR_INTERIM / "ac.csv")

# 声優に関する情報を読み込む
df_act = pd.read_csv(DIR_INTERIM / "act.csv")

# アニメ作品と声優の対応関係に関する情報を読み込む
df_ac_act = pd.read_csv(DIR_INTERIM / "ac_act.csv")

an_ae.csv#

Hide code cell content
# 各データフレームを統合する

# `df_ae`と`df_ac`を`acid`をキーにして統合
df_an_ae = pd.merge(df_ae, df_ac, on="acid", how="left").reset_index(drop=True)

# 必須列である`date`あるいは`acid`が欠損している行をdf_an_ae_droppedとして保持
# (後の処理でデバッグ用途で利用)
df_an_ae_dropped = df_an_ae.loc[
    df_an_ae["date"].isna() | df_an_ae["acid"].isna()
].reset_index(drop=True)
# 必須列である`date`あるいは`acid`列が欠損しているレコードを削除
df_an_ae = df_an_ae.dropna(subset=["date", "acid"]).reset_index(drop=True)
Hide code cell content
# head()メソッドで先頭5行を確認
df_an_ae.head()
aeid aename date aeno acid acname asid
0 M19760 アトム誕生の巻* 1963-01-01 第1話 C7163 鉄腕アトム C979
1 M19761 フランケンの巻* 1963-01-08 第2話 C7163 鉄腕アトム C979
2 M19762 火星探険の巻* 1963-01-15 第3話 C7163 鉄腕アトム C979
3 M19763 ゲルニカの巻* 1963-01-22 第4話 C7163 鉄腕アトム C979
4 M19764 スフィンクスの巻* 1963-01-29 第5話 C7163 鉄腕アトム C979
Hide code cell content
# `aeid`列の値に重複がないことをアサーションで確認
assert df_an_ae.duplicated(subset=["aeid"]).sum() == 0
Hide code cell content
# データフレーム`df_an_ae`をCSVファイルとして保存
# 保存先のパスは、`DIR_OUTPUT`ディレクトリ内の`an_ae.csv`
df_an_ae.to_csv(DIR_OUTPUT / "an_ae.csv", index=False)
Hide code cell content
# データフレーム`df_an_ae`をCSVファイルとして保存
# 保存先のパスは、`DIR_INTERIM`ディレクトリ内の`ae_dropped.csv`
df_an_ae_dropped.to_csv(DIR_INTERIM / "ae_dropped.csv", index=False)
Hide code cell content
# `acid`ごとに`aeid`のユニークな数(話数)を集計
df_ac_nae = df_an_ae.groupby("acid")["aeid"].nunique().reset_index(name="n_ae")

# `acid`ごとに最初の放送日を取得
df_ac_fdate = df_an_ae.groupby("acid")["date"].min().reset_index(name="first_date")

# `acid`ごとに最後の放送日を取得
df_ac_ldate = df_an_ae.groupby("acid")["date"].max().reset_index(name="last_date")

# 上記で作成したデータフレームを`df_ac`にマージして、新しいデータフレームを作成
df_ac_merge = pd.merge(df_ac, df_ac_nae, on="acid", how="right").reset_index(drop=True)
df_ac_merge = pd.merge(df_ac_merge, df_ac_fdate, on="acid", how="left").reset_index(
    drop=True
)
df_ac_merge = pd.merge(df_ac_merge, df_ac_ldate, on="acid", how="left").reset_index(
    drop=True
)
Hide code cell content
# データフレーム`df_ac_merge`をCSVファイルとして保存
# 保存先のパスは、`DIR_INTERIM`ディレクトリ内の`ac_merge.csv`
df_ac_merge.to_csv(DIR_INTERIM / "ac_merge.csv", index=False)

an_ac_crt.csv#

Hide code cell content
# `df_ac_merge`と`df_ac_crt`を`acid`をキーにして統合
df_an_ac_crt = pd.merge(df_ac_merge, df_ac_crt, on="acid", how="left").reset_index(
    drop=True
)

# 結果を`df_crt`と`crtid`をキーにしてさらに統合
df_an_ac_crt = pd.merge(df_an_ac_crt, df_crt, on="crtid", how="left").reset_index(
    drop=True
)

# `crtid`が欠損しているレコードを削除
df_an_ac_crt = df_an_ac_crt.dropna(subset=["crtid"]).reset_index(drop=True)
Hide code cell content
# head()メソッドで先頭5行を表示
df_an_ac_crt.head()
acid acname asid n_ae first_date last_date crtid crtname
0 C10010 グラビテーション C2336 13 2000-10-04 2001-01-10 ACRT00944 村上真紀
1 C12657 ヒピラくん 原作/大友克洋 C3943 10 2009-12-21 2009-12-24 ACRT00733 大友克洋
2 C12663 カウボーイ ビバップ[WOWOW放送版] C2111 26 1998-10-24 1999-04-24 ACRT01173 矢立肇
3 C12681 ドラえもん[新] NaN 224 1999-12-03 2005-03-18 ACRT01283 藤子・F・不二雄
4 C13191 HUNTER × HUNTER[新] C2136 149 2011-10-02 2014-09-24 ACRT00647 冨樫義博
Hide code cell content
# `df_an_ac_crt`内の`acid`と`crtid`の組み合わせが重複していないことを確認
assert df_an_ac_crt.duplicated(subset=["acid", "crtid"]).sum() == 0
Hide code cell content
# データフレーム`df_an_ac_crt`をCSVファイルとして保存
# 保存先のパスは、`DIR_OUTPUT`ディレクトリ内の`an_acc_crt.csv`
df_an_ac_crt.to_csv(DIR_OUTPUT / "an_ac_crt.csv", index=False)

an_ac_act.csv#

Hide code cell content
# `df_ac_merge`と`df_ac_act`を`acid`をキーにして統合し、アニメ作品と声優の関係を表すデータフレームを作成
df_an_ac_act = pd.merge(df_ac_merge, df_ac_act, on="acid", how="left").reset_index(
    drop=True
)

# 結果を`df_act`と`actid`をキーにしてさらに統合し、声優の詳細情報を追加
df_an_ac_act = pd.merge(df_an_ac_act, df_act, on="actid", how="left").reset_index(
    drop=True
)

# `actid`が欠損しているレコードを削除
df_an_ac_act = df_an_ac_act.dropna(subset=["actid"]).reset_index(drop=True)
Hide code cell content
# head()メソッドで先頭5行を確認
df_an_ac_act.head()
acid acname asid n_ae first_date last_date actid actname wiki_size gender
0 C10001 ギャラクシー エンジェル C2483 24 2001-04-08 2001-09-30 ACT00102 かないみか 116003.0 female
1 C10001 ギャラクシー エンジェル C2483 24 2001-04-08 2001-09-30 ACT05700 保村真 45464.0 male
2 C10001 ギャラクシー エンジェル C2483 24 2001-04-08 2001-09-30 ACT06001 吉野裕行 149454.0 male
3 C10001 ギャラクシー エンジェル C2483 24 2001-04-08 2001-09-30 ACT01887 山口眞弓 19635.0 female
4 C10001 ギャラクシー エンジェル C2483 24 2001-04-08 2001-09-30 ACT02359 新谷良子 73259.0 female
Hide code cell content
# `df_an_ac_act`内の`acid`と`actid`の組み合わせが重複していないことを確認
assert df_an_ac_act.duplicated(subset=["acid", "actid"]).sum() == 0
Hide code cell content
# データフレーム`df_an_ac_act`をCSVファイルとして保存
# 保存先のパスは、`DIR_OUTPUT`ディレクトリ内の`an_ac_act.csv`
df_an_ac_act.to_csv(DIR_OUTPUT / "an_ac_act.csv", index=False)