不均衡データ処理【概要】

:::note
Day3: [機械学習 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/machine-learning)

「不均衡データ処理」シリーズの1つ目の記事です。
1: [不均衡データ処理【概要】]()
2: 不均衡データ処理【オーバーサンプリング】 / coming soon
3: 不均衡データ処理【アンダーサンプリング】 / coming soon
4: 不均衡データ処理【ハイブリッド手法】 / coming soon
5: 未定
:::

:::note warn
サンプル紹介の場所では、続記事でのリサンプリングの都合上初期執筆時とパラメータなどを微妙に変更している可能性があります。そのため、サンプルデータの分布に関する説明文は参考程度にご覧ください
:::


# 不均衡データとは？課題と解決策の全体像
不均衡データとは、あるクラス（カテゴリ）のデータが圧倒的に少なく、もう一方のクラスのデータが非常に多い状況を指します。この問題は、分類モデルにおいて重要な課題となり得る。<br>特に、少数派クラス（ポジティブクラスや重要なイベントを示すクラス）が十分に学習されないため、モデルがバイアスを持ち、予測精度が低くなることがある。

## 不均衡データの事例

不均衡データの代表的な事例として、以下のようなものがあります：
- **クレジットカードの不正利用検出**：不正利用は全取引の中では非常に少ないため、不正利用を検出するモデルでは不均衡データが問題となる
- **医療診断（癌の予測など）**：健康な患者が圧倒的に多く、病気にかかっている患者は少数派となる
- **詐欺検出**：不正な取引や詐欺行為はまれなため、詐欺データが少数派を形成する

## 不均衡データが引き起こす問題

不均衡データが問題となるのは、モデルが多数派クラス（通常は「ノーマル」なデータ）に偏ってしまい、少数派クラスを無視した予測を行うためだ。<br>例えば、クレジットカードの不正利用を検出する場合、正常な取引が99%以上を占め、不正利用は1%未満であることが一般的である。
この場合、モデルが常に「正常取引」と予測しても精度（Accuracy）は高くなりがちだが、不正利用を検出する能力は全くないことになる。

## 不均衡データへのアプローチ

不均衡データに対するアプローチには、主に以下の手法がある。

- **リサンプリング**：
  - **オーバーサンプリング**：少数派クラスのデータを増加させる（後述）
  - **アンダーサンプリング**：多数派クラスのデータを減少させる（後述）

- **評価指標の変更**：
  - 精度（Accuracy）ではなく、**Precision**、**Recall**、**F1スコア**などの指標を使って評価

- **アルゴリズムの変更**：
  - クラス重みを調整することで、少数派クラスを重視するようなアルゴリズム（例：ランダムフォレストやSVMなど）を使用

## サンプルデータセット
次回以降の記事で使用するサンプルデータセットを紹介する
不均衡データのリサンプリング手法を実践するために、いくつかのサンプルデータセットを作成する。
## Sample 1
### データ分布について
2つのクラス（Class 0とClass 1）のデータが異なる平均値と共分散を持つ2次元ガウス分布に基づいて生成する。
クラス0は、平均値が$[2, 0]$、共分散行列が$[[1, 0], [0, 50]]$であり、**縦方向に広がり、横方向にはほとんど広がっていない**が、クラス1は、平均値が$[5, 0]$、共分散行列が$[[0.1, 0], [0, 100]]$で、**主に縦に広がり、横方向は非常に狭い**。
なお、`n(Class_0):n(Class_1) = 4:1`

### 例
Chat GPTより
> このような分布が実世界にあった場合、以下のようなシナリオが考えられます。
> サンプル1のような分布が適用されるシナリオ:
> ある製品の購入者と非購入者を分類する場合で、非購入者（Class 0）は広範囲の購買傾向を示しているが、購入者（Class 1）は限られた購入パターンを持っているケース。
> 特に、購買者が非常に特定の条件を満たす少数派で、広範囲に散らばった非購入者と明確に区別されるようなシナリオが考えられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748559/3023f230-38f0-dad3-9fe0-29f0f15dd2b6.png)
<details><summary>generate code</summary>

```python
import numpy as np
import matplotlib.pyplot as plt

mean_0 = [2, 0]
mean_1 = [5, 0]
cov_0 = [[1, 0], [0, 50]]
cov_1 = [[0.1, 0], [0, 100]]
X_class_0 = np.random.multivariate_normal(mean_0, cov_0, 800)
X_class_1 = np.random.multivariate_normal(mean_1, cov_1, 200)
y_class_0 = np.zeros(800)
y_class_1 = np.ones(200)
X = np.vstack([X_class_0, X_class_1])
y = np.hstack([y_class_0, y_class_1])
X += np.random.normal(0, 1.0, X.shape)
class_0_count = 960
class_1_count = 240
class_0_indices = np.where(y == 0)[0]
class_1_indices = np.where(y == 1)[0]
sampled_class_0_indices = np.random.choice(class_0_indices, size=class_0_count, replace=True)
sampled_class_1_indices = np.random.choice(class_1_indices, size=class_1_count, replace=True)
X_resampled = np.vstack([X[sampled_class_0_indices], X[sampled_class_1_indices]])
y_resampled = np.hstack([y[sampled_class_0_indices], y[sampled_class_1_indices]])
plt.figure(figsize=(6, 4), dpi=120)
plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1], color='indianred', label='Class 0', s=20)
plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1], color='forestgreen', label='Class 1', s=20)
plt.title("Sample 1 / 4:1")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.legend()
plt.show()
```

</details>

## Sample 2
### データ分布について
make_moons関数を使って、2つのクラスが月のような形状で分布させた。
データにノイズ（`noise=0.31`）を加えたため、クラス0とクラス1はおおよそ2つの月型の領域に分かれていながらも少し重なっている部分がある。
また、データセットのバランス比率が`0.2`に設定されているため、クラス1（少数派）はクラス0（多数派）に比べて少ない数である。
追記: ランダムノイズを加えたため、さらに分散的でまとまりがなくなった。

### 例
Chat GPTより
> 月のような形の分布は、以下のようなケースに適用される可能性があります。
サンプル2のような分布が適用されるシナリオ:
例えば、病気の診断において、2つのグループ（陽性と陰性）を分ける場合、病気の進行具合が異なる2つのステージに分かれており、それぞれのグループに対して異なる症状のパターンが見られるときに、このような分布が観察されるかもしれません。例えば、進行度合いが少し重なった病気のサンプル。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748559/eac2e43e-712c-d945-4228-77463d03fe51.png)


<details><summary>generate code</summary>

```python
from sklearn.datasets import make_moons
import numpy as np
import matplotlib.pyplot as plt


X, y = make_moons(n_samples=2000, noise=0.31, random_state=42)
class_0_count = 1600
class_1_count = 400
class_0_indices = np.where(y == 0)[0]
class_1_indices = np.where(y == 1)[0]
sampled_class_0_indices = np.random.choice(class_0_indices, class_0_count, replace=True)
sampled_class_1_indices = np.random.choice(class_1_indices, class_1_count, replace=True)

X_resampled = np.vstack([X[sampled_class_0_indices], X[sampled_class_1_indices]])
y_resampled = np.hstack([y[sampled_class_0_indices], y[sampled_class_1_indices]])

plt.figure(figsize=(6, 4), dpi=120)
plt.scatter(X_resampled[y_resampled == 0][:, 0], X_resampled[y_resampled == 0][:, 1], color='indianred', label='Class 0', s=20)
plt.scatter(X_resampled[y_resampled == 1][:, 0], X_resampled[y_resampled == 1][:, 1], color='forestgreen', label='Class 1', s=20)
plt.title("Sample 2 / 4:1")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.legend()
plt.show()

```

</details>

## Sample 3
### データ分布について
Sample 3では、データが2つの円環状の分布を持っている。クラス0は内側の円環にあり、クラス1は外側の円環にある。各データ点にはランダムなノイズが加えられており、これによって円形の分布がわずかにずれている。

クラス0とクラス1は、共に円環上に均等に分布しており、内部と外部で分けられた状態。ノイズのため、円環の境界は完全にクリーンではなく、少し重なりがある。

### 例
Chat GPTより
> このような分布が実世界で見られる場合、以下のシナリオが考えられます。
サンプル3のような分布が適用されるシナリオ:
例えば、天文学の分野で、星が銀河系内の異なる軌道に沿って分布しているケース。内側の円環に位置する星（クラス0）は、比較的若い星々で、外側の円環に位置する星（クラス1）は年齢が高いものといった具合です。銀河の円環構造や物質の分布が円形に見える場合、ここからデータを抽出することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748559/a43f6446-d8af-5a98-508f-e8afbcc9fb70.png)

<details><summary>generate code</summary>

```python
import numpy as np
import matplotlib.pyplot as plt
r_0 = np.linspace(0.5, 1.5, 600)
theta_0 = np.linspace(0, 2 * np.pi, 600)
x_0 = r_0 * np.cos(theta_0)
y_0 = r_0 * np.sin(theta_0)
r_1 = np.linspace(1.5, 2.5, 600)
theta_1 = np.linspace(0, 2 * np.pi, 600)
x_1 = r_1 * np.cos(theta_1)
y_1 = r_1 * np.sin(theta_1)
x_0 += np.random.normal(0, 0.5, 600)
y_0 += np.random.normal(0, 0.5, 600)
x_1 += np.random.normal(0, 0.5, 600)
y_1 += np.random.normal(0, 0.5, 600)
X = np.vstack([np.column_stack([x_0, y_0]), np.column_stack([x_1, y_1])])
y = np.hstack([np.zeros(600), np.ones(600)])
class_0_count = 960
class_1_count = 240
class_0_indices = np.where(y == 0)[0]
class_1_indices = np.where(y == 1)[0]
sampled_class_0_indices = np.random.choice(class_0_indices, size=class_0_count, replace=True)
sampled_class_1_indices = np.random.choice(class_1_indices, size=class_1_count, replace=True)
X_resampled = np.vstack([X[sampled_class_0_indices], X[sampled_class_1_indices]])
y_resampled = np.hstack([y[sampled_class_0_indices], y[sampled_class_1_indices]])

plt.figure(figsize=(6, 4), dpi=120)
plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1], color='indianred', label='Class 0', s=20)
plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1], color='forestgreen', label='Class 1', s=20)
plt.title("Sample 3 / 4:1")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.legend()
plt.show()


```


</details>

---
次回に続く...

次記事:「[2クラス分類におけるリサンプリング手法について ②]("xxxxxx")」
オーバーサンプリングを扱う予定

---

追記: 各データに対してランダムノイズを加えたため、さらに分散的でまとまりがなくなった。