📚 背景知識(読んでから問題へ)
numpyは「数値計算の道具箱」です。Pythonのリストと似ていますが、全要素が同じ型(数値)で格納されるため、数万件のデータでも一瞬で計算できるという特徴があります。
なぜnumpyが必要なのか?
Kaggleのコンペでは、数万〜数百万行のデータを扱います。Pythonのfor文でループを回すと遅すぎてタイムアウトします。numpyは内部でC言語を使って計算するため、Pythonの10〜100倍速いのです。
重要な概念
| 概念 | 直感的な説明 | 例 |
|---|---|---|
| ndarray | numpyの配列(数値のリスト) | 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 解説
np.isnan(ages)
# array([False, False, False, False, True, False, ..., True, ...])
np.isnan() は「各要素がnanかどうか」を True/False で返します。True = 1, False = 0 として合計すると欠損値の個数になります。
# 通常のmean()はnanがあるとnanを返してしまう
np.mean(ages) # → nan(使えない)
np.nanmean(ages) # → 25.92(nanを無視して計算)
nan が混じったデータには np.nan*** 系の関数(nanmean, nanstd, nanmax など)を使います。
ages_filled = ages.copy() # 元データを変更しないようにコピー
ages_filled[np.isnan(ages_filled)] = mean_age
np.isnan(ages_filled) が True の位置(欠損値の場所)に対して一括代入しています。これがnumpy流の「マスク補完」です。
正規化前: 7.25 〜 71.28(バラバラな単位)
正規化後: 0.0 〜 1.0(統一されたスケール)
異なるスケールの特徴量(年齢と運賃など)をモデルに入れると、スケールの大きい方が優先されてしまいます。正規化でこれを防ぎます。
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の上に構築されたテーブル操作ライブラリ