データサイエンス初心者がポケモンカードで投資分析したみた話

## はじめに

以前友人から，趣味と実益を兼ねてポケモンカード（以下ポケカ）を大量に購入しているという話を聞いた。
そこで，これまで学習してきたPythonやそのライブラリを活用して，ポケカは投資になりうるのかを分析してみようと考えた。

## 実行環境

OS：Windows 11
開発言語：Python 3.13.3
ライブラリ：pandas，matplotlib，BeautifulSoup，requests

## 1.課題の設定
ポケモンカードの価格推移を分析し、投資対象として有望かどうかを過去データから検証する。最終的には、どのような種類のカードが価値上昇しやすいか、また投資戦略として成立する条件を明らかにすることで、実用的な判断材料を提供することを目指す。

## 2. データの収集

データの収集にあたっては、対象サイトの `robots.txt` や利用規約を事前に確認し、規定に反しない範囲でクローリングおよびスクレイピングを実施した。

### 2.1 ポケモンカードの買取価格データ

ポケモンカードに関しては、Webスクレイピングを用いて、パック名およびBOX名ごとの買取価格データを収集した。  
市場価値を把握することを目的に、以下のコードにより買取価格を自動取得している。

```python
# ライブラリのインポート
import pandas as pd

#　データ取得用の関数定義
def fetch_data(url: str):
    try:
        dfs = pd.read_html(url)
        return dfs[0]
    except Exception as e:
        print(f"データ取得中にエラーが発生: {e}")
        return None

# フィルターと保存用の関数定義
def filter_and_save(df: pd.DataFrame, column_name: str, keyword: str, filename: str):
 
    filtered_df = df[df[column_name].str.contains(keyword, na=False)]
    filtered_df.to_csv(filename, index=False, encoding='utf-8-sig')
    print(filtered_df.head())
    print(f"該当数: {len(filtered_df)} 件")
# データ取得
url = 'https://cardrush.media/pokemon/buying_prices?displayMode=%E3%83%AA%E3%82%B9%E3%83%88&limit=500&name=&rarity=&model_number=&amount=&page=1&sort%5Bkey%5D=amount&sort%5Border%5D=desc&associations%5B%5D=ocha_product&to_json_option%5Bexcept%5D%5B%5D=original_image_source&to_json_option%5Bexcept%5D%5B%5D=created_at&to_json_option%5Binclude%5D%5Bocha_product%5D%5Bonly%5D%5B%5D=id&to_json_option%5Binclude%5D%5Bocha_product%5D%5Bmethods%5D%5B%5D=image_source&pack_code=&element=&pack_name=&display_category=%E3%83%91%E3%83%83%E3%82%AF&is_hot%5B%5D=true&is_hot%5B%5D=false'
df = fetch_data(url) 

# 取得したdfから条件ごとにデータを保存
if df is not None:
    filter_and_save(df, 'カード名/商品名', '未開封BOX|スペシャルBOX', 'BOX_only.csv')
    filter_and_save(df, 'カード名/商品名','未開封パック', 'pack_only.csv')
```
### 2.2 ポケモンカードの希望小売価格データ

希望小売価格については、magi.campに掲載されている複数のブログページから、  
パック名・発売日・希望小売価格・カード枚数の情報を抽出。

```python
# ライブラリのインポート
import requests
from bs4 import BeautifulSoup
import re

# HTMLから欲しいテキストを抽出
urls = [
    "https://magi.camp/blogs/167305889",
    "https://magi.camp/blogs/47058427", 
    "https://magi.camp/blogs/774748184"   
]

all_lines = []
all_titles = []

for url in urls:
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser")

    
    lines = soup.get_text(separator="\n").split("\n")
    all_lines.append(lines)

    
    titles = []
    keywords = ["拡張パック", "ジム拡張", "コンセプトパック", "ハイクラスパック", "スペシャルパック"]

    for p in soup.find_all("p"):
        text = p.get_text(strip=True)
        if any(kw in text for kw in keywords):
            titles.append(text.replace("■", "").strip())

 
    all_titles.append(titles)


# 抽出したテキストから、「パック名、発売日、希望小売価格、カード枚数」を切り分ける
data = []

for page_index in range(len(all_lines)):
    lines = all_lines[page_index]
    titles = all_titles[page_index]
    title_index = 0
    i = 0

    while i < len(lines):
        line = lines[i].strip()

        if "発売日" in line:
            release = re.search(r"発売日[:：]?\s*(\d{4}年\d{1,2}月\d{1,2}日)", line)
            price   = re.search(r"希望小売価格[:：]?\s*([0-9,]+円)", lines[i+1].strip()) if i+1 < len(lines) else None
            count   = re.search(r"カード[／/]\s*([0-9]+)枚", lines[i+2].strip()) if i+2 < len(lines) else None

            if release and price and count:
                title = titles[title_index] if title_index < len(titles) else "不明"
                data.append({
                    "パック名": title,
                    "発売日": release.group(1),
                    "希望小売価格": price.group(1),
                    "カード枚数": count.group(1)
                })
                title_index += 1
                i += 3
            else:
                i += 1
        else:
            i += 1

# 切り分けたデータをデータフレームにまとめて、CSVで出力する

df = pd.DataFrame(data)
df.to_csv("pokemon_cards_with_titles.csv", index=False, encoding="utf-8-sig")
```

### 2.3 貴金属データ（金・銀・プラチナ）

金・銀・プラチナの価格データについては、Investing.com より各種CSVデータをダウンロードし、  
それぞれ `gold_data.csv`、`silver_data.csv`、`platinum_data.csv` として保存した。


## 3.データの前処理
### 3.1 小売価格データの前処理の概要
・ファイル：`pokemon_cards_with_titles.csv`
・問題点：行31・48に2商品が1行で記載
・対応策：各行を2行に分割して整理
・出力：整形済みデータを`pokemon_cards_split.csv`に保存
```python
# ファイルの読み込み
file_path = "pokemon_cards_with_titles.csv"
df = pd.read_csv(file_path)

# 分割対象の行インデックス指定と構造確認
rows_to_split = [29, 46]
df.loc[rows_to_split, 'パック名']

# パック名の分割処理
new_rows = []

# 全行を処理
for i, row in df.iterrows():
    if i in rows_to_split and row['パック名'].count('『') == 2 and row['パック名'].count('』') == 2:
        # 接頭辞を抽出
        prefix = row['パック名'].split('『')[0]

        # 2つの商品名に分割
        parts = row['パック名'].split('』『')
        part1 = prefix + parts[0].replace(prefix, '') + '』'
        part2 = prefix + '『' + parts[1]

        # 分割結果を2行として追加
        new_rows.append([part1, row['発売日'], row['希望小売価格'], row['カード枚数']])
        new_rows.append([part2, row['発売日'], row['希望小売価格'], row['カード枚数']])
    else:
        # その他の行はそのまま追加
        new_rows.append(row.tolist())
        
# 新しいDataFrameの作成と保存
new_df = pd.DataFrame(new_rows, columns=df.columns)
new_df.to_csv("pokemon_cards_split.csv", index=False)
```

### 3.2 買取価格データの前処理の概要
ファイル：`pack_only.csv`
ステップ1：不要なパック名を除外 → `filtered_pack_only.csv`に保存
ステップ2：小売価格データと同じ表記に統一 → `filtered_pack_only_cleansing.csv`に保存

```python
# CSVファイルの読み込み
file_path = "pack_only.csv"
df = pd.read_csv(file_path)

#　除外するキーワードリストの定義
exclude_keywords = [
    'ギフトパック', 'スターターパック', 'VSパック', '構築',
    'コレクションシート', 'デッキ', 'スペシャル', 'プレミアム',
    'サザンアイランド', '映画', 'タイプユニット'
]

pattern = '|'.join(exclude_keywords)

# フィルタ処理の実行とCSVファイルへの保存
filtered_df = df[~df['カード名/商品名'].str.contains(pattern, na=False)]
output_path = "filtered_pack_only.csv"
filtered_df.to_csv(output_path, index=False, encoding='utf-8-sig')
```

### 3.3 小売価格・買取価格データの結合
・2つの前処理済みCSVをパック名をキーに結合
・結果を `merged_data.csv` として保存

```python
# CSVファイルの読み込み
df1 = pd.read_csv("pokemon_cards_split.csv")
df2 = pd.read_csv("filtered_pack_only_cleansing.csv")

# パック名で結合
merged_df = pd.merge(df1, df2, on="パック名", how="left")

#　列の順番を並び変える
desired_order = ["パック名", "発売日", "希望小売価格", "買取価格", "カード枚数"]
merged_df = merged_df[desired_order]

# CSVファイルとして保存
output_filename = "merged_data_ver2.csv"
merged_df.to_csv(output_filename, index=False, encoding="utf-8-sig")
```

### 3.4 買取価格の欠損補完（merged_data_ver2）

- 欠損の種類：

  - **BOXのみに価格あり**  
    → BOX価格 ÷ パック数で補完（※小数点切り捨て）

  - **カードラッシュに価格なし**  
    → 他サイトの買取価格で補完

- 補完後のデータを `merged_data_all.csv` として保存

## 4.分析準備
ROI・CAGRの算出
・ポケカ：発売日から2025/06/01まで保有したと仮定
・貴金属：ポケカの発売日を購入日とし、休場日は直近営業日の価格を使用
・各資産について、ROIおよびCAGRを計算
```python
from datetime import datetime

# ファイル読み込み
poke_df = pd.read_csv("merged_data_all.csv")
gold_df = pd.read_csv("gold_data.csv")
silver_df = pd.read_csv("silver_data.csv", encoding="utf-8-sig")
platinum_df = pd.read_csv("plutinum_data.csv", encoding="utf-8-sig")

# ポケカ整形
poke_df["発売日"] = pd.to_datetime(poke_df["発売日"], format="%Y年%m月%d日", errors="coerce")
poke_df["希望小売価格"] = poke_df["希望小売価格"].astype(str).str.replace("円", "").str.replace(",", "").astype(float)
poke_df["買取価格"] = poke_df["買取価格"].astype(str).str.replace("¥", "").str.replace(",", "").astype(float)

# 金・銀・プラチナ整形
for df in [gold_df, silver_df, platinum_df]:
    df["日付け"] = pd.to_datetime(df["日付け"])
    df["終値"] = df["終値"].astype(str).str.replace(",", "").astype(float)

# 比較基準日（2025年6月1日）
latest_date = pd.to_datetime("2025-06-01")
poke_df["経過年数"] = (latest_date - poke_df["発売日"]).dt.days / 365.25

# 発売日当日以降の最も近い価格を取得する関数
def get_price_on_release(df, release_date):
    eligible = df[df["日付け"] >= release_date]
    return eligible.sort_values("日付け", ascending=True).iloc[0]["終値"] if not eligible.empty else None

poke_df["金価格_発売日"] = poke_df["発売日"].apply(lambda x: get_price_on_release(gold_df, x))
poke_df["銀価格_発売日"] = poke_df["発売日"].apply(lambda x: get_price_on_release(silver_df, x))

latinum_start = pd.to_datetime("2009-10-14")

def get_platinum_price_or_nan(release_date):
    if release_date < platinum_start:
        return None
    return get_price_on_release(platinum_df, release_date)

poke_df["プラチナ価格_発売日"] = poke_df["発売日"].apply(get_platinum_price_or_nan)

gold_latest = gold_df[gold_df["日付け"] <= latest_date].sort_values("日付け", ascending=False).iloc[0]["終値"]
silver_latest = silver_df[silver_df["日付け"] <= latest_date].sort_values("日付け", ascending=False).iloc[0]["終値"]
platinum_latest = platinum_df[platinum_df["日付け"] <= latest_date].sort_values("日付け", ascending=False).iloc[0]["終値"]

for metal, latest in [("金", gold_latest), ("銀", silver_latest)]:
    col_price = f"{metal}価格_発売日"
    poke_df[f"{metal}_ROI"] = (latest - poke_df[col_price]) / poke_df[col_price]
    poke_df[f"{metal}_CAGR"] = (latest / poke_df[col_price]) ** (1 / poke_df["経過年数"]) - 1

poke_df["プラチナ_ROI"] = poke_df.apply(
    lambda row: (platinum_latest - row["プラチナ価格_発売日"]) / row["プラチナ価格_発売日"]
    if pd.notnull(row["プラチナ価格_発売日"]) else None, axis=1)

poke_df["プラチナ_CAGR"] = poke_df.apply(
    lambda row: (platinum_latest / row["プラチナ価格_発売日"]) ** (1 / row["経過年数"]) - 1
    if pd.notnull(row["プラチナ価格_発売日"]) else None, axis=1)

poke_df["ポケカ_ROI"] = (poke_df["買取価格"] - poke_df["希望小売価格"]) / poke_df["希望小売価格"]
poke_df["ポケカ_CAGR"] = (poke_df["買取価格"] / poke_df["希望小売価格"]) ** (1 / poke_df["経過年数"]) - 1

columns_to_export = [
    "パック名", "発売日", "希望小売価格", "買取価格", "カード枚数", "経過年数",
    "金価格_発売日", "金_ROI", "金_CAGR",
    "銀価格_発売日", "銀_ROI", "銀_CAGR",
    "プラチナ価格_発売日", "プラチナ_ROI", "プラチナ_CAGR",
    "ポケカ_ROI", "ポケカ_CAGR"
]

poke_df[columns_to_export].to_csv("pokeka_vs_all_assets_utf8bom.csv", index=False, encoding="utf-8-sig")
```

## 5.分析
### 5.1 ROI・CAGRの可視化

- 各パックに対して計算したROIとCAGRの**平均値**を棒グラフで可視化
- 結果：ポケカは他の資産に比べて非常に高いROI・CAGRを示した

```python
import matplotlib.pyplot as plt
import matplotlib as mpl

# 日本語フォントの設定
mpl.rcParams['font.family'] = 'MS Gothic'  
mpl.rcParams['axes.unicode_minus'] = False

# ROIの平均値
roi_means = {
    "ポケカ": poke_df["ポケカ_ROI"].mean(),
    "金": poke_df["金_ROI"].mean(),
    "銀": poke_df["銀_ROI"].mean(),
    "プラチナ": poke_df["プラチナ_ROI"].mean()
}

# CAGRの平均値
cagr_means = {
    "ポケカ": poke_df["ポケカ_CAGR"].mean(),
    "金": poke_df["金_CAGR"].mean(),
    "銀": poke_df["銀_CAGR"].mean(),
    "プラチナ": poke_df["プラチナ_CAGR"].mean()
}

# 平均ROIの棒グラフ
plt.figure(figsize=(8, 5))
plt.bar(roi_means.keys(), roi_means.values())
plt.title("平均ROIの比較")
plt.ylabel("ROI")
plt.show()

# 平均CAGRの棒グラフ
plt.figure(figsize=(8, 5))
plt.bar(cagr_means.keys(), cagr_means.values())
plt.title("平均CAGRの比較")
plt.ylabel("CAGR")
plt.show()
```
![roi_average.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/d86e50f3-61be-4446-9de7-207fada4ce62.png)
![cagr_average.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/a39c122c-bbef-4efc-9419-a557378979aa.png)

### 5.2 分析の見直し：リスクの視点から再評価

- いかにリターンが高くても、リスク（価格の変動幅）が大きければ「投資」ではなく「投機（ギャンブル）」となる可能性がある
- そこで、各資産の **リスク（標準偏差）** と **リターン（平均CAGR）** を散布図で比較する
- 補助線の傾きは、簡易的な **シャープレシオ（投資効率）** を示している

> 高リスク・高リターンを示すポケカは右上に位置し、
> 一見魅力的だが投資効率で見ると必ずしも最良ではない。
> 結果として、**投資効率は プラチナ ＜ 銀 ＜ ポケカ ＜ 金** となった。


```python
assets = {
    "ポケカ": {"color": "tab:blue"},
    "金": {"color": "tab:orange"},
    "銀": {"color": "tab:green"},
    "プラチナ": {"color": "tab:red"}
}

for name in assets:
    cagr_col = f"{name}_CAGR"
    data = poke_df[cagr_col].dropna()
    assets[name]["return"] = data.mean()
    assets[name]["risk"] = data.std()

plt.figure(figsize=(8, 6))
for name, info in assets.items():
    r = info["risk"]
    m = info["return"]
    c = info["color"]

    plt.scatter(r, m, color=c, label=name)
    plt.text(r, m, name, fontsize=12, ha='left', va='bottom')
    plt.plot([0, r], [0, m], linestyle='--', color=c, alpha=0.5)

plt.title("リスク（標準偏差） vs リターン（平均CAGR）と傾き補助線")
plt.xlabel("リスク（標準偏差）")
plt.ylabel("リターン（平均CAGR）")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
```
![risk_return.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/d34c2e9b-d4d7-4f99-9344-2ee4a2d8853c.png)

### 5.3 外れ値の考慮：ポケカのデータ数が少ないことに着目し、影響を検討する

- ポケカのデータ数は約114件と比較的少なく、 **外れ値の影響が分析結果を歪める可能性** がある
- 各資産のCAGR（年平均成長率）の分布を箱ひげ図で可視化し、外れ値の有無を確認
- 結果：ポケカのデータに**7件の外れ値**が存在することが判明

> 分布の広がりと外れ値の存在を無視した分析は、実際の投資判断に誤解を与える可能性がある。

```python
# CAGRデータの箱ひげ図（外れ値確認）
cagr_data = [
    poke_df["ポケカ_CAGR"].dropna(),
    poke_df["金_CAGR"].dropna(),
    poke_df["銀_CAGR"].dropna(),
    poke_df["プラチナ_CAGR"].dropna()
]

labels = ["ポケカ", "金", "銀", "プラチナ"]

plt.figure(figsize=(10, 6))
plt.boxplot(cagr_data, patch_artist=True)
plt.title("資産別CAGRの分布（箱ひげ図）")
plt.ylabel("CAGR")
plt.xticks([1, 2, 3, 4], labels)
plt.grid(True)
plt.show()
```
![cagr_box.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/7786b2d4-6901-4578-92b7-6435cdabf0b6.png)

### 5.4 外れ値除去後の再評価

- ポケカのCAGRデータから外れ値を除去し、再度リスクリターンの散布図を作成
- 外れ値の除去には、四分位範囲（IQR）を用いて、下限・上限を超えるデータを除外
- 結果：
  - リターンはやや減少
  - しかしリスク（標準偏差）の減少幅の方が大きく、
  - 結果として **投資効率（リターン/リスク）は上昇** 

> 外れ値を除いたことで、ポケカは「より安定した高リターン資産」に見えるようになった。

```python
# 外れ値除去（ポケカのみ）
poke_cagr = poke_df["ポケカ_CAGR"].dropna()
q1 = poke_cagr.quantile(0.25)
q3 = poke_cagr.quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr

poke_cagr_no_outliers = poke_cagr[(poke_cagr >= lower) & (poke_cagr <= upper)]
mean_no_outliers = poke_cagr_no_outliers.mean()
std_no_outliers = poke_cagr_no_outliers.std()

# 再描画（外れ値除去版）
assets = {
    "ポケカ（外れ値除去）": {
        "mean": mean_no_outliers,
        "std": std_no_outliers,
        "color": "tab:blue"
    },
    "金": {
        "mean": poke_df["金_CAGR"].mean(),
        "std": poke_df["金_CAGR"].std(),
        "color": "tab:orange"
    },
    "銀": {
        "mean": poke_df["銀_CAGR"].mean(),
        "std": poke_df["銀_CAGR"].std(),
        "color": "tab:green"
    },
    "プラチナ": {
        "mean": poke_df["プラチナ_CAGR"].mean(),
        "std": poke_df["プラチナ_CAGR"].std(),
        "color": "tab:red"
    }
}

plt.figure(figsize=(9, 6))
for name, info in assets.items():
    x = info["std"]
    y = info["mean"]
    c = info["color"]

    plt.scatter(x, y, color=c, label=name, s=80)
    plt.text(x, y, name, fontsize=11, ha='right', va='bottom')
    plt.plot([0, x], [0, y], linestyle='--', color=c, alpha=0.5)

plt.xlabel("リスク（標準偏差）")
plt.ylabel("リターン（平均CAGR）")
plt.title("リスク（標準偏差） vs リターン（平均CAGR）と傾き補助線")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()

```
![risk_return_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/1be1aeb8-5752-43cd-993b-3d7ec735769b.png)

### 5.5  パックカテゴリ別 平均CAGRの比較  
- ポケモンカードのパックは、以下の3つのカテゴリに分類される：
  - 拡張パック
  - コンセプトパック
  - ハイクラスパック

- 各カテゴリに属するパックについて、CAGR（年平均成長率）の平均を算出し、カテゴリごとに比較した

> ハイクラスパック（3件）とコンセプトパック（7件）は、拡張パック（その他カテゴリ、104件）よりも平均CAGRが高い傾向が見られた。
ただし、ハイクラスパックやコンセプトパックはサンプル数が非常に少ないため、外れ値やデータの偏りによる影響に注意が必要である。

```python
# パック名を文字列に変換して検索用に整形
df["パック名"] = df["パック名"].astype(str)

# カテゴリ列の作成
def classify_pack(name):
    if "コンセプト" in name:
        return "コンセプト"
    elif "ハイクラス" in name:
        return "ハイクラス"
    else:
        return "拡張"

df["カテゴリ"] = df["パック名"].apply(classify_pack)

# 各カテゴリの平均CAGRを計算
grouped = df.groupby("カテゴリ")["ポケカ_CAGR"].mean().reindex(["コンセプト", "ハイクラス", "拡張"])

# 件数も取得（棒グラフ注記用）
counts = df["カテゴリ"].value_counts().reindex(["コンセプト", "ハイクラス", "拡張"])

# 棒グラフで可視化
plt.figure(figsize=(8, 5))
bars = plt.bar(grouped.index, grouped.values, color=["tab:blue", "tab:orange", "tab:gray"])
plt.title("パックカテゴリ別 平均CAGRの比較")
plt.ylabel("平均CAGR")
plt.xlabel("カテゴリ")
plt.grid(axis="y")

# 件数ラベルを上に表示
for bar, count in zip(bars, counts):
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width() / 2, height, f"{count}件", ha='center', va='bottom')

plt.tight_layout()
plt.show()
```
![pack_category_cagr2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/bded0386-a2cf-4141-bebe-6ed9ff7eedd6.png)

### 5.6 シリーズ別 平均CAGRの比較

- 各シリーズに属するパックをグループ化し、シリーズごとの平均CAGR（年平均成長率）を算出した

- 横軸に示されている「ポケカ1」「ポケカ2」はそれぞれ、正式には
  - 「ポケットモンスターカードゲーム」
  - 「ポケモンカードゲーム」  
  を意味する。可視化の都合上、略称を用いて表示している。

- シリーズの並び順は、**発売時期の時系列順**に配置している

>**パック数が20件を超えるXYシリーズ（21件）とSMシリーズ（29件）は、CAGRが特に高く、他シリーズと比べて突出している**ことがわかった。
 一方、データ数が少ないシリーズ（20件未満）については、CAGRに明確な傾向は見られなかった。データのばらつきや外れ値の影響を受けやすく、慎重な解釈が求められる。

 ```python
# シリーズ名リスト
series_order = (
    ["ポケカ1"] * 6 + ["neo"] * 4 + ["e"] * 5 + ["ADV"] * 5 + ["ポケカ2"] * 9 +
    ["DP"] * 9 + ["DPt"] * 4 + ["LEGEND"] * 5 + ["BW"] * 17 +
    ["XY"] * 21 + ["SM"] * 29
)
df["シリーズ"] = series_order

# 順序付きカテゴリとして設定
series_categories = pd.Categorical(df["シリーズ"], categories=list(dict.fromkeys(series_order)), ordered=True)
df["シリーズ"] = series_categories

# 平均CAGRと件数を集計（並び順保持）
series_cagr = df.groupby("シリーズ", observed = True)["ポケカ_CAGR"].mean()
series_counts = df["シリーズ"].value_counts().reindex(series_cagr.index)

# 棒グラフ描画
plt.figure(figsize=(12, 6))
bars = plt.bar(series_cagr.index, series_cagr.values)
plt.title("シリーズ別 平均CAGRの比較")
plt.ylabel("平均CAGR")
plt.xlabel("シリーズ")
plt.xticks(rotation=45)
plt.grid(axis="y")

# 件数をバーの上に表示
for bar, count in zip(bars, series_counts):
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2, height, f"{count}件", ha='center', va='bottom', fontsize=9)

plt.tight_layout()
plt.show()
 ```
![serires_cagr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067830/16b5aebc-3f61-4deb-beda-c719272b7076.png)


## 結果
・ポケモンカードは、金・銀・プラチナと比較して非常に高いROIおよびCAGRを示した。

・ただし、価格変動の大きさ（リスク）も非常に高いことが確認された。

・リターンとリスクの比率（シャープレシオに類似する指標）を用いた場合、最も効率的な資産は金であった。

・ポケモンカードのデータ件数は少なく、外れ値の影響が大きい。

・外れ値を除去した再分析では、リスクが減少し、投資効率が上昇した。

・パックの種類別に比較すると、ハイクラスパックやコンセプトパックといった特別仕様のパックは、拡張パックよりも高い平均CAGRを示した。

・シリーズ別の比較では、XY・SMシリーズのCAGRが他シリーズと比較して突出しており、近年のシリーズほど高成長傾向にあることが読み取れた。


## 考察
・高リターン≠良い投資対象であるということが、リスクとのバランスを可視化することで明確になった。

・シャープレシオ的な考え方は、異なる種類の資産を比較する際に有効な視点である。

・ポケモンカードのようなオルタナティブ資産は、外れ値やデータの少なさが分析に大きな影響を与えるため、慎重な取り扱いが必要である。

・外れ値除去によって投資効率が改善されたことから、価格形成が投機的である可能性も示唆される。

・カテゴリ別・シリーズ別の分析では、特定のパック（ハイクラス・コンセプト）やシリーズ（XY・SM）において、投資効率の高い傾向が見られた。これは、商品設計やコレクター人気といった非金融的要素がリターンに影響していることを示唆している。

## まとめ
・投資判断を行う際には、リターンだけでなくリスクや効率性を含めた総合的な評価が不可欠である。

・データに基づく冷静な分析は、主観や一時的なブームに流されない意思決定を支えるものである。

・今回の分析を通じて、ポケモンカードは高リスク・高リターン型のオルタナティブ投資でありつつも，特定カテゴリ・シリーズでは一定の投資効果を期待できることが明らかになった。

・今後，さらに長期的な価格動向や再販・限定流通といった要因を含めた検討を行うことで，ポケモンカードの投資価値に関する理解が一層深まるだろう。

以上の結果は、どのようなパック・シリーズが価値上昇しやすいか、また投資戦略として成立しうる条件を明らかにするという当初の課題設定に対する一つの実証的な答えを示している。

## 参考文献・データソース

- [requestsとBeautifulSoupによるWebスクレイピングの基本](https://note.com/gourmet_thinker/n/n3325009ed793#fc40c9c5-2a4b-4b28-889b-ea5645aebaf0)
- [10分で理解する Beautiful Soup](https://qiita.com/Chanmoro/items/db51658b073acddea4ac)
- [Investing.com - 金・銀・プラチナ価格データ](https://jp.investing.com/)
- [magi - ポケモンカード商品名・小売価格・発売日情報](https://magi.camp/)
- [cardrush - ポケモンカード買取価格](https://cardrush.media/pokemon/buying_prices)
- [ポケモンカードゲーム トレーナーズウェブサイト - ポケカシリーズ別パック紹介](https://www.pokemon-card.com/products/archives/)