持っているヨーヨーの直径・幅を Matplotlib の散布図で可視化する

## やりたいこと

僕の趣味のひとつがヨーヨーです。たくさんのヨーヨーを収集し、振って遊んでいます。ヨーヨーにはいろいろな大きさ (直径・幅) があり、それによって振り心地が変わります。

![ヨーヨー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6697/58888511-906e-013c-614e-88245881c7f0.jpeg)

ふと「僕が持っているヨーヨーの直径・幅ってどんな傾向があるんだろう？」と思いました。そこで [Matplotlib](https://matplotlib.org/) を使って散布図を描画することで可視化してみることにしました。

ヨーヨーについて気になる方はこちら。

- [小学生から大人までおすすめ！初めてのヨーヨーの選び方](https://yoyostorerewind.com/collections/for-beginners)

## バージョン情報

```shell-session
$ sw_vers
ProductName:		macOS
ProductVersion:		15.0.1
BuildVersion:		24A348

$ python --version
Python 3.13.0

$ pip list | grep -e matplotlib -e numpy -e scipy
matplotlib      3.9.2
numpy           2.1.3
scipy           1.14.1
```

## 方法

### 準備

持っているヨーヨーのスペック (名前と直径 (mm)、幅 (mm) など) を一覧化した CSV を用意します。僕は [Notion](https://www.notion.so/) で管理しているため、そのエクスポート機能を使用して CSV を生成しました。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6697/b170ce9e-3150-bbd1-ab3d-5f7de1518816.png)

用意した CSV を `~/Downloads/yoyos.csv` に配置します。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6697/37b29a14-6c9b-7f7b-83d7-72dc88eec6f2.png)

### 必要なライブラリのインストール

```shell-session
$ pip install matplotlib numpy scipy
```

### 散布図の生成

以下の Python スクリプトを用意して、実行します。

```py
import csv
import os
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np
from scipy.stats import gaussian_kde

matplotlib.use('Agg')

XMIN = 40.0
XMAX = 80.0
YMIIN = 30.0
YMAX = 60.0

diameters = []
widths = []

csv_filepath = os.path.join(os.environ['HOME'], 'Downloads/yoyos.csv')

with open(csv_filepath, 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    for i, row in enumerate(reader):
        if i == 0:
            continue
        diameters.append(round(float(row[3]), 1))
        widths.append(round(float(row[4]), 1))

x = np.array(diameters)
y = np.array(widths)

fig, ax = plt.subplots()

ax.set_xlabel('Diameter (mm)', fontsize=10)
ax.set_xlim(XMIN, XMAX)
ax.xaxis.set_major_locator(ticker.MultipleLocator(5.0))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(1.0))

ax.set_ylabel('Width (mm)', fontsize=10)
ax.set_ylim(YMIIN, YMAX)
ax.yaxis.set_major_locator(ticker.MultipleLocator(5.0))
ax.yaxis.set_minor_locator(ticker.MultipleLocator(1.0))

ax.set_axisbelow(True)
ax.grid(which='both')

xy = np.vstack([x, y])
z = gaussian_kde(xy)(xy)

ax.scatter(x, y, alpha=0.8, c=z, cmap='jet')
fig.savefig(os.path.join(os.environ['HOME'], 'Downloads/fig.png'))
```

そうすると `~/Downloads/fig.png` に以下の散布図が PNG ファイルとして出力されます。

![fig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6697/49b4fd24-121e-bc1e-0afe-c547533a4720.png)

僕はどうやら直径 56mm、幅 44mm くらいのヨーヨーをたくさん持っているようです。

## 解説

### Matplotlib のバックエンドを指定する

```py
matplotlib.use('Agg')
```

Matplotlib は図を描画する際にデフォルトで [Tkinter](https://docs.python.org/ja/3/library/tkinter.html) を使用します。しかし、僕の環境では

> ModuleNotFoundError: No module named '_tkinter'

というエラーが発生してしまいます。Tkinter のインストールや Python の再インストールをしても解決しなかったため、とりあえず [AGG (Anti-Grain Geometry)](https://en.wikipedia.org/wiki/Anti-Grain_Geometry) を使うことにしました。

僕の環境には気付いたらインストールされていましたが、無さそうな場合は Homebrew でインストールしてみてください。

```shell-session
$ brew install agg
```

### CSV から値を読み込む

```py
diameters = []
widths = []

csv_filepath = os.path.join(os.environ['HOME'], 'Downloads/yoyos.csv')

with open(csv_filepath, 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    for i, row in enumerate(reader):
        if i == 0:
            continue
        diameters.append(round(float(row[3]), 1))
        widths.append(round(float(row[4]), 1))

x = np.array(diameters)
y = np.array(widths)
```

用意した `Downloads/yoyos.csv` には 4 列目に直径の値、5 列目に幅の値があります。そこで CSV を読み込んで、それらの値を配列に格納します。そして直径を x 軸、幅を y 軸として使用します。なお、CSV の 1 行目はヘッダ行なので無視します。

### 散布図の設定を行う

```py
XMIN = 40.0
XMAX = 80.0
YMIIN = 30.0
YMAX = 60.0

# 省略

fig, ax = plt.subplots()

ax.set_xlabel('Diameter (mm)', fontsize=10)
ax.set_xlim(XMIN, XMAX)
ax.xaxis.set_major_locator(ticker.MultipleLocator(5.0))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(1.0))

ax.set_ylabel('Width (mm)', fontsize=10)
ax.set_ylim(YMIIN, YMAX)
ax.yaxis.set_major_locator(ticker.MultipleLocator(5.0))
ax.yaxis.set_minor_locator(ticker.MultipleLocator(1.0))

ax.set_axisbelow(True)
ax.grid(which='both')
```

ここでは散布図の軸のラベルと表示範囲、そして目盛と罫線を設定しています。大きな目盛り (major) は 5mm 単位、小さな目盛り (minor) は 1mm 単位で設定しています。大きな目盛りと小さな目盛り両方で罫線を表示するには [matplotlib.pyplot.grid](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.grid.html) 関数で `which='both'` を指定する必要があります。

### 散布図をプロットする

```py
xy = np.vstack([x, y])
z = gaussian_kde(xy)(xy)

plt.scatter(x, y, alpha=0.8, c=z, cmap='jet')
```

- [散布図においてプロットの色で密度を表す方法（Python / matplotlib / カーネル密度推定） #R - Qiita](https://qiita.com/kujira_0120/items/c00a7bb22144ad6c565c)

という記事を参考に [scipy.stats.gaussian_kde](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.gaussian_kde.html) クラスを使って散布図の密度を表現できるようにします。

### 散布図を出力する

```py
plt.savefig(os.path.join(os.environ['HOME'], 'Downloads/fig.png'))
```

`Downloads/fig.png` に散布図を PNG ファイルとして出力することで完了です。

## 参考

- [（matplotlib）グラフの体裁調整のためのTips【軸・図枠・目盛・目盛文字編】 #Python - Qiita](https://qiita.com/code0327/items/d2f199c5e1267d4822fb)
- [散布図においてプロットの色で密度を表す方法（Python / matplotlib / カーネル密度推定） #R - Qiita](https://qiita.com/kujira_0120/items/c00a7bb22144ad6c565c)
- [神戸学院大学 経営学部 林坂ゼミ > カーネル密度推定をしてみよう](https://rinsaka.com/python/kde.html)