Day 037 — numpy入門(配列操作・数値計算)

2026-05-16 白 / Phase 1 コーディング numpy入門(配列操作・数値計算)

📚 背景知識(読んでから問題へ)

numpyは「数値計算の道具箱」です。Pythonのリストと似ていますが、全要素が同じ型(数値)で格納されるため、数万件のデータでも一瞬で計算できるという特徴があります。

なぜnumpyが必要なのか?

Kaggleのコンペでは、数万〜数百万行のデータを扱います。Pythonのfor文でループを回すと遅すぎてタイムアウトします。numpyは内部でC言語を使って計算するため、Pythonの10〜100倍速いのです。

重要な概念

概念直感的な説明
ndarraynumpyの配列(数値のリスト)np.array([1, 2, 3])
shape配列の形(行数×列数)(3,)(2, 3)
dtype配列内の数値の型float64, int32
ブロードキャスト形が違う配列でも自動で計算合わせる配列 + スカラー
スライス配列の一部を取り出すarr[1:3]

Titanic問題との接続

Titanicコンペでは乗客の年齢・運賃などの数値データを扱います。欠損値の補完や特徴量の正規化はすべてnumpyの演算で行われます。


📝 問題

以下のタスクをすべて実装してください。「Titanic風」の乗客データをnumpyで処理します。

import numpy as np

# Titanic風の乗客データ(年齢)
ages = np.array([22.0, 38.0, 26.0, 35.0, np.nan, 54.0, 2.0, 27.0, 14.0, np.nan, 4.0, 58.0, 20.0, 39.0, 14.0])

# 運賃データ
fares = np.array([7.25, 71.28, 7.92, 53.10, 8.05, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 13.00, 8.05, 31.68, 7.85])

# 生存フラグ (1=生存, 0=死亡)
survived = np.array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0])

タスク1: ages の欠損値(np.nan)の数を数えてください。

タスク2: 欠損値を除いた ages の平均値を計算し、欠損値をその平均値で埋めてください。

タスク3: fares を「最小値0・最大値1」の範囲に正規化してください(Min-Max正規化)。

公式: (値 - 最小値) / (最大値 - 最小値)

タスク4: survived == 1(生存者)の fares の平均と、survived == 0(死亡者)の fares の平均を比較してください。

タスク5: 年齢が30歳以上の乗客の数を数えてください(欠損値補完後の ages を使用)。


🔍 ヒント(段階的開示)

ヒント1(方向性)

numpyには欠損値を扱う専用関数があります。np.nan を含む配列は通常の np.mean() では計算できません。

ヒント2(アプローチ)
  • 欠損値カウント: np.isnan()np.sum() を組み合わせる
  • 欠損値補完: np.isnan() でboolマスクを作り、インデックスとして使う
  • Min-Max正規化: 最小値は arr.min()、最大値は arr.max()
  • 条件フィルタ: survived == 1 はbool配列になり、インデックスとして使える
  • 欠損値を無視した計算: np.nanmean() を使う
ヒント3(コード骨格)
# タスク1
nan_count = np.sum(np.isnan(ages))

# タスク2
mean_age = np.nanmean(ages)
ages_filled = ages.copy()
ages_filled[np.isnan(ages_filled)] = mean_age

# タスク3
fares_normalized = (fares - fares.min()) / (fares.max() - fares.min())

# タスク4
survived_fare_mean = fares[survived == 1].mean()
died_fare_mean = fares[survived == 0].mean()

# タスク5
count_30plus = np.sum(ages_filled >= 30)

模範解答

import numpy as np

# Titanic風の乗客データ(年齢)
ages = np.array([22.0, 38.0, 26.0, 35.0, np.nan, 54.0, 2.0, 27.0, 14.0, np.nan, 4.0, 58.0, 20.0, 39.0, 14.0])

# 運賃データ
fares = np.array([7.25, 71.28, 7.92, 53.10, 8.05, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 13.00, 8.05, 31.68, 7.85])

# 生存フラグ (1=生存, 0=死亡)
survived = np.array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0])

# タスク1: 欠損値の数
nan_count = np.sum(np.isnan(ages))
print(f"タスク1: 欠損値の数 = {nan_count}")  # 2

# タスク2: 欠損値を平均で補完
mean_age = np.nanmean(ages)
ages_filled = ages.copy()
ages_filled[np.isnan(ages_filled)] = mean_age
print(f"タスク2: 平均年齢(欠損除外) = {mean_age:.2f}")
print(f"タスク2: 補完後の配列 = {ages_filled}")

# タスク3: Min-Max正規化
fares_normalized = (fares - fares.min()) / (fares.max() - fares.min())
print(f"タスク3: 正規化後の運賃 = {fares_normalized.round(4)}")
print(f"タスク3: 確認(最小={fares_normalized.min():.1f}, 最大={fares_normalized.max():.1f})")

# タスク4: 生存者 vs 死亡者の運賃比較
survived_fare_mean = fares[survived == 1].mean()
died_fare_mean = fares[survived == 0].mean()
print(f"タスク4: 生存者の平均運賃 = {survived_fare_mean:.2f}")
print(f"タスク4: 死亡者の平均運賃 = {died_fare_mean:.2f}")
print(f"タスク4: 生存者の方が {survived_fare_mean - died_fare_mean:.2f} 円高い")

# タスク5: 30歳以上の乗客数
count_30plus = np.sum(ages_filled >= 30)
print(f"タスク5: 30歳以上の乗客数 = {count_30plus}")

出力例:

▶ 出力を見る
タスク1: 欠損値の数 = 2
タスク2: 平均年齢(欠損除外) = 25.92
タスク2: 補完後の配列 = [22.   38.   26.   35.   25.92 54.    2.   27.   14.   25.92  4.   58.   20.   39.   14.  ]
タスク3: 正規化後の運賃 = [0.     1.     0.0103 0.7174 0.0123 0.6944 0.2142 0.0606 0.3578 0.1458 0.2989 0.0924 0.0123 0.3785 0.0094]
タスク3: 確認(最小=0.0, 最大=1.0)
タスク4: 生存者の平均運賃 = 32.82
タスク4: 死亡者の平均運賃 = 15.27
タスク4: 生存者の方が 17.55 円高い
タスク5: 30歳以上の乗客数 = 6

🪜 Step-by-Step 解説

1
欠損値の検出
np.isnan(ages)
# array([False, False, False, False,  True, False, ..., True, ...])

np.isnan() は「各要素がnanかどうか」を True/False で返します。True = 1, False = 0 として合計すると欠損値の個数になります。

2
欠損値を無視した平均計算
# 通常のmean()はnanがあるとnanを返してしまう
np.mean(ages)      # → nan(使えない)
np.nanmean(ages)   # → 25.92(nanを無視して計算)

nan が混じったデータには np.nan*** 系の関数(nanmean, nanstd, nanmax など)を使います。

3
ブールインデックスで補完
ages_filled = ages.copy()  # 元データを変更しないようにコピー
ages_filled[np.isnan(ages_filled)] = mean_age

np.isnan(ages_filled)True の位置(欠損値の場所)に対して一括代入しています。これがnumpy流の「マスク補完」です。

4
Min-Max正規化の意味
正規化前: 7.25 〜 71.28(バラバラな単位)
正規化後: 0.0 〜 1.0(統一されたスケール)

異なるスケールの特徴量(年齢と運賃など)をモデルに入れると、スケールの大きい方が優先されてしまいます。正規化でこれを防ぎます。

5
条件フィルタリング
survived == 1
# array([False,  True,  True,  True, False, ...])

fares[survived == 1]
# survived==1の位置の運賃だけを取り出す
# array([71.28,  7.92, 53.10, 11.13, 16.70, 26.55, 13.00])

これが「グループ別統計」の基本です。pandasの groupby も内部ではこれと同じことをしています。


📐 数学・統計の補足(文系向け)

Min-Max正規化を直感で理解する

「テストの点数を0〜100点から0〜1に変換する」イメージです。

  • クラス最低点(0点相当) → 0.0
  • クラス最高点(100点相当) → 1.0
  • 中間の点 → 0.0〜1.0の間の値

公式: (自分の点 - クラス最低点) / (クラス最高点 - クラス最低点)

これで全員が0〜1の間に収まります。

なぜコピーが必要か(重要!)

ages_filled = ages         # ❌ 同じ配列を参照(agesも変わる)
ages_filled = ages.copy()  # ✅ 別のコピーを作成(agesは変わらない)

numpyの配列代入は「住所のコピー」です。元データを守るために .copy() を使います。


🏆 Kaggleでの実践的な使い方

1. 欠損値補完はコンペの基本

TitanicコンペのAgeカラムには欠損値が177件(20%)あります。

# 実際のTitanicデータでの補完
df['Age'] = df['Age'].fillna(df['Age'].median())
# pandasでも同じことができるが、numpyの原理を理解しておくと応用できる

2. Min-Max正規化は必須前処理

ニューラルネットワーク・SVMなど多くのモデルは、特徴量が同じスケールでないとうまく学習できません。

from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
# 内部でnumpy演算を使っている

3. ブールマスクで高速フィルタリング

# 条件を複数組み合わせる
high_fare_survivors = fares[(survived == 1) & (fares > 30)]

&(AND)、|(OR)でフィルタを組み合わせられます。EDA(探索的データ分析)で多用します。


⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
np.mean(ages) が nan を返すnanが1つでも結果がnanになる仕様np.nanmean(ages) を使う
ages_filled = ages でコピーしたつもりPythonの変数は「参照」ages.copy() でディープコピー
正規化後に最大値が1.0より大きくなるテストデータに訓練データより大きい値があるfit は訓練データのみで行う
survived == 1 に括弧をつけ忘れる& の優先順位の問題(survived == 1) & (fares > 30) と括弧をつける

🚀 次のステップ

  • 発展: 標準化(Standardization / Z-score正規化)— (x - 平均) / 標準偏差
  • 次回予告: pandas入門(DataFrame・Series)— numpyの上に構築されたテーブル操作ライブラリ

🎯 自己評価

自分の回答

気づき・メモ