ゲームデータの前処理#

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

前処理後のデータは全て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
# 入出力ディレクトリの定義

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

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

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

# 出力ファイルを保存するディレクトリのパス
DIR_OUTPUT = Path("../../../data/gm/input")
Hide code cell content
# MADBの読み込み対象ファイル名のリストを定義
# - `gm301`:ゲームパッケージに関する情報を格納
FNS_GM = [
    "gm301",  # ゲームパッケージ
]
Hide code cell content
# PacKaGeとして利用するカラムとその新しいカラム名のマッピングを定義
COLS_PKG = {
    "@id": "pkgid",
    "label": "pkgname",
    "publisher": "publisher",
    "gamePlatform": "pfname",
    "datePublished": "date",
    "price": "price",
}
Hide code cell content
# price列の外れ値を定義
PRICES_SKIP = [
    "80クラブニンテンドーポイント",
    "[2016年11月10日]",
]

関数#

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 try_mkdirs(path: str) -> None:
    """
    指定されたパスにディレクトリを作成

    `os.makedirs`を使用してディレクトリを作成しようとする。
    既に存在する場合は、例外を無視する。

    Parameters
    ----------
    path : str
        作成したいディレクトリのパス

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

    # 指定されたパスでディレクトリを作成しようと試みる
    try:
        os.makedirs(path)
    # ディレクトリが既に存在する場合の例外をキャッチ
    except FileExistsError:
        # 例外が発生した場合は何もせずに終了
        pass
Hide code cell content
def preprocess_json(p_from: Union[str, Path], p_to: Union[str, Path]) -> None:
    """
    JSONファイル中の特定の特殊文字(\x0b)を置換して新しいファイルに保存する。

    Parameters
    ----------
    p_from : Union[str, Path]
        読み込むJSONファイルのパス。
    p_to : Union[str, Path]
        置換後の内容を保存するファイルのパス。

    Returns
    -------
    None

    """

    # 置換後の行を保持するためのリストを初期化する
    new_json = []

    # 入力ファイルを読み込むために開く
    with open(p_from, "r") as f:
        # ファイルの各行を読み込む
        for l in f.readlines():
            # 特殊文字(\x0b)を空白に置換する
            l = l.replace("\x0b", " ")
            # 置換後の行をリストに追加する
            new_json.append(l)

    # 置換後の内容を新しいファイルに書き込むために開く
    with open(p_to, "w") as f:
        # リストの内容を新しいファイルに書き込む
        f.writelines(new_json)
Hide code cell content
def preprocess_df_pkg(df: pd.DataFrame) -> pd.DataFrame:
    """
    データフレーム`df_pkg`に対する共通の前処理を行う。

    Parameters
    ----------
    df : pd.DataFrame
        前処理前のデータフレーム。

    Returns
    -------
    pd.DataFrame
        前処理後のデータフレーム。

    """

    # df_pkgの列の形式を整える
    df = format_cols(df, COLS_PKG)

    # COLS_PKGのいずれかの列に欠損値がある行を削除する
    df = df[~df[COLS_PKG.values()].isna().any(axis=1)].reset_index(drop=True)

    # 'pkgid'の値をURIからIDの形式に変換する
    df["pkgid"] = df["pkgid"].apply(get_id_from_uri)

    return df
Hide code cell content
def format_price(text: Optional[str]) -> Optional[int]:
    """
    与えられたテキストから価格情報を抽出

    Parameters
    ----------
    text : Optional[str]
        価格情報を含む可能性のあるテキスト

    Returns
    -------
    Optional[int]
        抽出された価格、価格が存在しない場合はNone
    """

    # 価格情報をスキップするテキストの場合はNoneを返す
    if text in PRICES_SKIP:
        return None

    # カンマやピリオドをテキストから削除
    text = text.replace(",", "").replace(".", "")

    # テキストから価格情報を抽出
    prices = re.findall("[0-9]{2,7}", text)
    prices = [int(p) for p in prices]

    # 価格情報が存在する場合は、最小の価格を返す
    if prices:
        return min(prices)

    # 価格情報が存在しない場合はNoneを返す
    return None
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 get_pfnames_from_text(text: Optional[str]) -> Optional[List[str]]:
    """
    与えられたテキストからpfnameのリストを抽出

    Parameters
    ----------
    text : Optional[str]
        pfname情報を含む可能性のあるテキスト

    Returns
    -------
    Optional[List[str]]
        抽出されたpfnameのリスト 存在しない場合はNone
    """

    # テキストがNoneかNaNの場合はNoneを返す
    if text is np.nan or text is None:
        return None

    # テキストをカンマで分割してリストとして返す
    pfnames = text.split(",")
    return pfnames
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
def check_date_format(text: Optional[str]) -> bool:
    """
    与えられたテキストが「YYYY-MM-DD」の日付フォーマットかどうかを判定

    Parameters
    ----------
    text : Optional[str]
        判定対象のテキスト

    Returns
    -------
    bool
        フォーマットが正しい場合はTrue、そうでない場合はFalse
    """

    # YYYY-MM-DDフォーマットの正規表現パターンを定義
    pattern = re.compile(r"^\d{4}-\d{2}-\d{2}$")

    # テキストが指定されたフォーマットに一致するかどうかを返す
    return bool(pattern.match(text))

出力先の生成#

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ディレクトリ内で`_gm`を含むファイルのパスをすべて検索し、リストとして取得
ps_gm = sorted(list(DIR_INPUT.glob("*_gm-*")))
Hide code cell content
# `ps_gm`リストに含まれる各.zipファイルに対して処理を実行
# tqdmを使用することで、進行状況のバーが表示される
for p_from in tqdm(ps_gm):
    # 出力先のパスを設定する
    # 元のファイルパスから、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_gm`に格納する
# 例: {'gm301': ['path1', 'path2', ...]}
ps_gm = {gm: sorted(list(DIR_TMP.glob(f"*{gm}*/*"))) for gm in FNS_GM}
Hide code cell content
# 内容を確認
pprint(ps_gm)
{'gm301': [PosixPath('../../../data/gm/tmp/metadata_gm-item_gm301_json/metadata_gm-item_gm301_json\\metadata_gm-item_gm301_00001.json')]}

gm301#

Hide code cell content
# gm301のパス一覧を取得
ps_gm301 = ps_gm["gm301"]
Hide code cell content
# `ps_gm301`に格納されているjsonファイルに対して前処理を行う
# 新しいファイルパスを`ps_gm301_new`に保存する

# 新しいファイルのパスを格納するためのリストを初期化
ps_gm301_new = []

# 各jsonファイルのパスに対して処理を行う
for i, p_gm301 in enumerate(ps_gm301):
    # 元のファイルのパスを指定
    p_from = p_gm301
    # 新しいファイルのパスを作成
    #p_to = os.path.join(DIR_TMP, p_from.split("/")[-1])
    p_to = DIR_TMP / p_from.parts[-1]

    # jsonファイルの前処理を行い、特殊文字を削除
    preprocess_json(p_from, p_to)

    # 新しいファイルのパスをリストに追加
    ps_gm301_new.append(p_to)
Hide code cell content
# 各jsonファイルのパスに対して処理を行う
for i, p_gm301 in enumerate(ps_gm301_new):
    # 抽出条件を定義:ジャンルが`パッケージ`であるもの
    filters = {
        "genre": ["パッケージ"],
    }

    # jsonファイルから指定の条件を満たすデータを抽出
    pkg = read_json_w_filters(p_gm301, "@graph.item", filters)

    # 抽出したデータをデータフレームに変換
    df_pkg = pd.DataFrame(pkg)

    # データフレームの前処理を行う
    df_pkg = preprocess_df_pkg(df_pkg)

    # `pfname`列のデータを整形
    df_pkg["pfname"] = df_pkg["pfname"].apply(get_pfnames_from_text)

    # 前処理が完了したデータをCSVとして保存
    # 保存するファイル名を生成
    df_pkg.to_csv(DIR_TMP / f"gm301_{i+1:05}.csv", index=False)

DIR_INTERIMへの中間出力#

pkg.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`gm301_`で始まるCSVファイルのパスをすべて取得する
ps_pkg = sorted(list(DIR_TMP.glob("gm301_*.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_tmp = read_csvs(ps_pkg)
Hide code cell content
# 各レコードに対して、日付の形式を確認する
# YYYY-MM-DD形式であるレコードだけをリストに格納する
data = [r for r in df_tmp.to_dict("records") if check_date_format(r["date"])]

# 抽出したデータを新しいデータフレームに変換する
df_pkg = pd.DataFrame(data)

# `date`列を基に昇順にソートする
df_pkg = df_pkg.sort_values("date", ignore_index=True)
Hide code cell content
# `price`列の各エントリに対して、`format_price`関数を適用して価格情報を整形する
df_pkg["price"] = df_pkg["price"].apply(format_price)
Hide code cell content
# データフレーム`df_pkg`の列順を調整するためのリストを定義
cols_pkg = ["pkgid", "pkgname", "publisher", "date", "price"]
# `cols_pkg`に従って列順を変更
df_pkg = df_pkg[cols_pkg]
Hide code cell content
# head()メソッドを利用し、df_pkgの先頭5行を表示する
df_pkg.head()
pkgid pkgname publisher date price
0 M735723 精彩グラフィック・マージャン カセット・サービス 株式会社コムパック 1982-04-25 3500.0
1 M735295 MP-82用ハード・コピールーチン 株式会社コムパック 1982-05-25 3500.0
2 M735396 グラフィック・カーソル 株式会社コムパック 1982-06-25 3500.0
3 M735265 ALL CAST STAR TREK 株式会社コムパック 1982-07-25 3500.0
4 M735791 夜空のシンフォニー「星系編」(夏の星座) ㈱マイクロ・テクノロジー研究所 1982-08-01 6500.0
Hide code cell content
# 所定のディレクトリにdf_pkgをCSVファイルとして保存
df_pkg.to_csv(DIR_INTERIM / "pkg.csv", index=False)

pf.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`gm301_`で始まるCSVファイルのパスをすべて取得する
ps_pkg = sorted(list(DIR_TMP.glob("gm301_*.csv")))
# 取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_pkg = read_csvs(ps_pkg)
Hide code cell content
# データフレーム`df_pkg`からプラットフォーム名の集合`pfnames`を作成する
pfnames = set()

# データフレームの各行を調査する
for r in df_pkg.to_dict("records"):
    # 日付の形式が`YYYY-MM-DD`でない場合は、その行をスキップする
    if not check_date_format(r["date"]):
        continue

    # プラットフォーム名を取得して集合に追加する
    pf = set(cast_str_to_list(r["pfname"]))
    pfnames.update(pf)

# `pfnames`をソートしてリストに変換する
pfnames = sorted(list(pfnames))
Hide code cell content
# プラットフォーム名のリスト`pfnames`を基に、データフレーム`df_pf`を作成する

# 各プラットフォームに一意のIDを生成する
pfids = [f"PF{i:05}" for i in range(len(pfnames))]

# pfidsとpfnamesを使ってデータフレームを作成する
df_pf = pd.DataFrame(
    {
        "pfid": pfids,
        "pfname": pfnames,
    }
)
Hide code cell content
# `head()`メソッドを用いて、先頭5行を確認
df_pf.head()
pfid pfname
0 PF00000 3DO
1 PF00001 64DD
2 PF00002 ClassicMacOS
3 PF00003 MSX
4 PF00004 MSX2
Hide code cell content
# データフレーム`df_pf`をCSVファイルとして保存
# 保存先のパスは、`DIR_INTERIM`ディレクトリ内の`pf.csv`
df_pf.to_csv(DIR_INTERIM / "pf.csv", index=False)

pkg_pf.csv#

Hide code cell content
# `DIR_TMP`ディレクトリ内の`gm301_`で始まるCSVファイルのパスをすべて取得する
ps_pkg = DIR_TMP.glob("gm301_*.csv")
# `DIR_TMP`ディレクトリ内の`pf.csv`に該当するCSVファイルのパスを全て取得する
ps_pf = DIR_INTERIM.glob("pf.csv")

# それぞれ、取得したCSVファイルを読み込み、一つのデータフレームに結合する
df_pkg = read_csvs(ps_pkg)
df_pf = read_csvs(ps_pf)
Hide code cell content
# `pfname`をキー、`pfid`を値とする辞書を作成
pfname2pfid = df_pf.groupby("pfname")["pfid"].first().to_dict()
Hide code cell content
# `pkgid`と`pfid`の組み合わせを格納するリストを初期化
pkg_pf = []

# `df_pkg`の各行に対して処理を実行
for r in df_pkg.to_dict("records"):
    # `date`がYYYY-MM-DD形式でない場合、この行の処理をスキップ
    if not check_date_format(r["date"]):
        continue

    # パッケージIDを取得
    pkgid = r["pkgid"]
    # 対応するプラットフォーム名のリストを取得
    pfnames = cast_str_to_list(r["pfname"])

    # 各プラットフォーム名に対しての処理
    for pfname in pfnames:
        # プラットフォーム名に対応するIDを取得
        pfid = pfname2pfid[pfname]
        # 結果リストにパッケージIDとプラットフォームIDの組み合わせを追加
        pkg_pf.append([pkgid, pfid])

# 結果リストからデータフレームを作成
df_pkg_pf = pd.DataFrame(columns=["pkgid", "pfid"], data=pkg_pf)
Hide code cell content
# head()メソッドで先頭5行を確認
df_pkg_pf.head()
pkgid pfid
0 M718871 PF00026
1 M718876 PF00028
2 M718877 PF00028
3 M718878 PF00028
4 M718879 PF00000
Hide code cell content
# データフレーム`df_pkg_pf`をCSVファイルとして保存
# 保存先のパスは、`DIR_INTERIM`ディレクトリ内の`pkg_pf.csv`
df_pkg_pf.to_csv(DIR_INTERIM / "pkg_pf.csv", index=False)

DIR_OUTPUTへの最終出力#

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

# ゲームパッケージに関する情報を読み込む
df_pkg = pd.read_csv(DIR_INTERIM / "pkg.csv")

# ゲームプラットフォームに関する情報を読み込む
df_pf = pd.read_csv(DIR_INTERIM / "pf.csv")

# ゲームパッケージとゲームプラットフォームの対応関係に関する情報を読み込む
df_pkg_pf = pd.read_csv(DIR_INTERIM / "pkg_pf.csv")

gm_pkg_pf.csv#

Hide code cell content
# `df_pkg_pf`と`df_pkg`を`pkgid`をキーにして結合
df_gm = pd.merge(df_pkg_pf, df_pkg, on="pkgid", how="left").reset_index(drop=True)

# 上記の結果をさらに`df_pf`と`pfid`をキーにして結合
df_gm = pd.merge(df_gm, df_pf, on="pfid", how="left").reset_index(drop=True)
Hide code cell content
# head()メソッドで先頭5行を確認
df_gm.head()
pkgid pfid pkgname publisher date price pfname
0 M718871 PF00026 くにおくん 熱血コレクション 1 アトラス 2005-08-25 5040.0 ゲームボーイアドバンス
1 M718876 PF00028 野々村病院の人々 エ・ル・フ 1996-04-26 6800.0 セガサターン
2 M718877 PF00028 アイドル雀士スーチーパイ Remix ジャレコ 1995-09-29 6900.0 セガサターン
3 M718878 PF00028 天地無用! 魎皇鬼 ごくらくCD-ROM for SEGA SATURN アローマ 1995-09-29 7800.0 セガサターン
4 M718879 PF00000 Superリアル麻雀 P4 + 相性診断 セタ 1995-03-10 9500.0 3DO
Hide code cell content
# `df_gm`内で、`pkgid`と`pfid`の組み合わせが重複していないことを確認する
assert df_gm.duplicated(subset=["pkgid", "pfid"]).sum() == 0
Hide code cell content
# データフレーム`df_gm_pkg_pf`をCSVファイルとして保存
# 保存先のパスは、`DIR_OUTPUT`ディレクトリ内の`gm_pkg_pf.csv`
df_gm.to_csv(DIR_OUTPUT / "gm_pkg_pf.csv", index=False)