第6回の最後に、平均値・中央値補完には「分散が縮小し、分布が不自然に歪む」という重要な副作用があることをいお話ししました。
すべての欠損を1つの代表値で埋めるため、その値の周辺にデータが人工的に集中してしまうためです。
この問題を解決するのが、今回扱う ランダムサンプル補完(random sample imputation) です。
「その列の実際の値からランダムに選んで埋める」というシンプルな発想ながら、元のデータ分布をそのまま保てる という大きな利点があります。
Contents
ランダムサンプル補完とは?
ランダムサンプル補完 とは、欠損値を、同じ列にある観測値(欠損していない値)の中からランダムに選んで 埋める方法です。
たとえば age 列に欠損が3件あり、観測値が [22, 35, 28, 40, 19, ...] だとします。
このとき、欠損3件を埋めるために、観測値の中からランダムに3つ(たとえば 28, 19, 35)を選んで割り当てます。
どんなときに使うべきか
ランダムサンプル補完が向いているのは、主に次のような場面です。
- 欠損が MCAR(完全にランダム)と想定される:観測値の分布が、欠損値の本来の分布をよく代表していると考えられる場合
- 分布の形を保ったまま補完したい:相関分析や統計モデリングで、ばらつきを潰したくない場合
- 数値・カテゴリのどちらにも使いたい:平均・中央値が使えないカテゴリ変数にも適用できる
一方で、注意すべき点もあります。
- 再現性の確保が必須:ランダム性があるため、乱数の種(シード)を固定しないと実行のたびに結果が変わる
- 共分散への影響:1変数ずつ独立にランダム補完するため、変数間の相関関係(共分散)が弱まることがある
- メモリを使う:補完のために、観測値のプールを保持しておく必要がある
サンプルデータの準備
前回までと同じ、seaborn の Titanic データセットを使います。age 列(欠損177件)を題材にします。
以下、コードです。
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib
# Titanicデータセットの読み込み
df = sns.load_dataset('titanic')
print(f"age の欠損数: {df['age'].isnull().sum()}")
print(f"age の観測値数: {df['age'].notnull().sum()}")
以下、実行結果です。
age の欠損数: 177 age の観測値数: 714
実行すると、age 列に177件の欠損があり、714件の観測値があることが確認されました。
pandas でのランダムサンプル補完
基本的な流れは次の3ステップです。
- ステップ 1:欠損の 件数 を数える
- ステップ 2:観測値の中から、その件数分だけ ランダムに抽出
- ステップ 3:抽出した値を欠損箇所に 割り当てる
学習データでの実装
データ漏洩を防ぐため、第6回で学んだ通り 学習データの観測値プールから補完値を取る という原則を守ります。
以下、コードです。
from sklearn.model_selection import train_test_split
# 学習用とテスト用に分割
df_train, df_test = train_test_split(
df, # 全データフレーム
test_size=0.2, # テストデータの割合
random_state=42 # 乱数シード
)
# 学習データのコピーを作成
df_train_imp = df_train.copy()
# ステップ1:学習データの欠損数を数える
n_missing_train = (
df_train_imp['age'] # 年齢の変数の列
.isnull() # 欠損判定
.sum() # 件数合計
)
print(f"学習データの age 欠損数: {n_missing_train}")
# ステップ2:学習データの観測値からランダムに n_missing 個を抽出
random_sample_train = (
df_train_imp['age'] # 年齢の変数の列
.dropna() # 欠損を除去
.sample( # ランダムサンプリング
n=n_missing_train, # 欠損数と同数を抽出
random_state=42 # 乱数シード
)
)
# ステップ3:抽出した値を欠損箇所に割り当てる
random_sample_train.index = (
df_train_imp[ # 学習データフレーム
df_train_imp['age'] # 年齢の変数の列
.isnull() # 欠損判定
].index # 欠損箇所のインデックス
)
# 補完実行
df_train_imp.loc[
df_train_imp['age'].isnull(), # 欠損箇所のインデックス
'age' # 年齢の変数の列
] = random_sample_train
print(f"補完後の欠損数: {df_train_imp['age'].isnull().sum()}")
df_train_imp['age'].dropna():欠損を除いた観測値だけのプールを作る.sample(n=n_missing_train, random_state=42):そこから欠損数と同じ件数をランダム抽出random_sample_train.index = ...:抽出した値のインデックスを、欠損箇所のインデックスに合わせる(これをしないと正しく代入されない)df_train_imp.loc[欠損箇所, 'age'] = ...:欠損箇所に抽出値を代入
以下、実行結果です。
学習データの age 欠損数: 140 補完後の欠損数: 0
学習データの age 欠損がゼロになっています。
テストデータでの実装(学習データのプールを使う)
テストデータの欠損も、学習データの観測値プール から抽出して埋めます。これがデータ漏洩を防ぐ正しい流れです。
以下、コードです。
# テストデータのコピーを作成
df_test_imp = df_test.copy()
# テストデータの欠損数を数える
n_missing_test = (
df_test_imp['age'] # 年齢の変数の列
.isnull() # 欠損判定
.sum() # 件数合計
)
print(f"テストデータの age 欠損数: {n_missing_test}")
# 補完値は「学習データの観測値」から抽出する(テストデータ自身からではない)
random_sample_test = (
df_train['age'] # 学習データの年齢列
.dropna() # 欠損を除去
.sample( # ランダムサンプリング
n=n_missing_test, # 欠損数と同数を抽出
random_state=42 # 乱数シード
)
)
# 補完値のインデックスを欠損箇所のインデックスに合わせる
random_sample_test.index = (
df_test_imp[ # テストデータフレーム
df_test_imp['age'] # 年齢の変数の列
.isnull() # 欠損判定
].index # 欠損箇所のインデックス
)
# 補完実行
df_test_imp.loc[
df_test_imp['age'].isnull(), # 欠損箇所のインデックス
'age' # 年齢の変数の列
] = random_sample_test
print(f"補完後の欠損数: {df_test_imp['age'].isnull().sum()}")
df_train['age'].dropna().sample(...):補完値の抽出元は 学習データ。テストデータ自身は使わない- これにより、テストデータの情報が補完に紛れ込むのを防ぐ
以下、実行結果です。
テストデータの age 欠損数: 37 補完後の欠損数: 0
テストデータの age 欠損もゼロになりました。
「補完値のプールは常に学習データから」 という原則は、ランダムサンプル補完でも変わりません。
再現性の確保:random_state の重要性
ランダムサンプル補完には乱数が関わるため、実行するたびに結果が変わってしまう という問題があります。
これでは、分析の再現や結果の検証ができません。
random_state(乱数の種)を固定すると、毎回同じ結果が得られます。
逆に固定しないと、毎回違う値で補完されてしまうことを確認してみましょう。
以下、コードです。
# poolに年齢の変数の列を代入
pool = df_train['age'].dropna()
# random_state を固定しない場合:毎回違う結果
print("【random_state なし】")
print("1回目:", pool.sample(n=3).values)
print("2回目:", pool.sample(n=3).values)
# random_state を固定する場合:毎回同じ結果
print("\n【random_state=42 で固定】")
print("1回目:", pool.sample(n=3, random_state=42).values)
print("2回目:", pool.sample(n=3, random_state=42).values)
random_stateを指定しないsample()は、呼ぶたびに異なる値を返すrandom_state=42を指定すると、何度呼んでも同じ値を返す
以下、実行結果です。
【random_state なし】 1回目: [ 4. 16. 50.] 2回目: [27. 18. 35.] 【random_state=42 で固定】 1回目: [ 2. 65. 33.] 2回目: [ 2. 65. 33.]
何度か実行してみてくだい。
random_state なしでは1回目と2回目で違う値が、固定すると同じ値が返ることがわかります。
分布が保たれることを可視化する
ランダムサンプル補完の真価は、元の分布を保つ ことにあります。
第6回の中央値補完と並べて、分布がどう変わるかを比較してみましょう。
以下、コードです。
# 比較用に3パターンを用意
df_orig = df.copy() # 元データ(欠損行は除いて描画)
# パターンA:第6回の中央値補完
df_median = df.copy()
df_median['age'] = df_median['age'].fillna(df_median['age'].median())
# パターンB:今回のランダムサンプル補完
df_random = df.copy()
n_miss = df_random['age'].isnull().sum()
samples = df_random['age'].dropna().sample(n=n_miss, random_state=42)
samples.index = df_random[df_random['age'].isnull()].index
df_random.loc[df_random['age'].isnull(), 'age'] = samples
# 3つを並べて可視化
fig, axes = plt.subplots(3, 1, figsize=(8, 10))
# 元データ(欠損行は除く)のヒストグラム
axes[0].hist(
df_orig['age'].dropna(),
bins=30,
color='gray',
edgecolor='black'
)
axes[0].set_title('元データ(欠損行は除く)')
axes[0].set_xlabel('Age')
axes[0].set_ylabel('Frequency')
# パターンA:第6回の中央値補完のヒストグラム
axes[1].hist(
df_median['age'],
bins=30,
color='steelblue',
edgecolor='black'
)
axes[1].set_title('パターンA:第6回の中央値補完')
axes[1].set_xlabel('Age')
# パターンB:今回のランダムサンプル補完のヒストグラム
axes[2].hist(
df_random['age'],
bins=30,
color='seagreen',
edgecolor='black'
)
axes[2].set_title('パターンB:今回のランダムサンプル補完')
axes[2].set_xlabel('Age')
plt.tight_layout()
plt.show()
以下、実行結果です。

3つのヒストグラムが並んでいます。
- 元データ(欠損行は除く)のヒストグラム:元の分布
- パターンA:第6回の中央値補完のヒストグラム:中央値付近(約28)に 不自然に高い棒 が出現
- パターンB:今回のランダムサンプル補完のヒストグラム:元データとほぼ 同じ形 を保っている
ランダムサンプル補完だけが、元の分布の形を崩していないことが視覚的にはっきりわかります。
分散が保たれることを数値で確認する
第6回では、平均値・中央値補完で標準偏差が縮小することを確認しました。
ランダムサンプル補完では、これがどうなるか数値で見てみましょう。
以下、コードです。
# 標準偏差(ばらつきの大きさ)を各データで計算
# - 元データは欠損を除外して計算
# - 中央値補完とランダム補完は、すでに欠損が埋まっているためそのまま計算
std_original = df['age'].dropna().std()
std_median = df_median['age'].std()
std_random = df_random['age'].std()
# 各パターンの標準偏差を小数点4桁で表示
print(f"元データの標準偏差 : {std_original:.4f}")
print(f"中央値補完後の標準偏差 : {std_median:.4f}")
print(f"ランダム補完後の標準偏差 : {std_random:.4f}")
# 元データに対して標準偏差がどれだけ減少したか(%)を表示
# 減少率 = (1 - 補完後の標準偏差 / 元データの標準偏差) * 100
print(f"\n中央値補完での減少率 : {(1 - std_median / std_original) * 100:.2f}%")
print(f"ランダム補完での減少率 : {(1 - std_random / std_original) * 100:.2f}%")
以下、実行結果です。
元データの標準偏差 : 14.5265 中央値補完後の標準偏差 : 13.0197 ランダム補完後の標準偏差 : 14.2953 中央値補完での減少率 : 10.37% ランダム補完での減少率 : 1.59%
実行結果を見ると、中央値補完では標準偏差が大きく減少するのに対し、ランダムサンプル補完では元データとほぼ同じ標準偏差 が保たれていることがわかります。
これは、補完値が「実在する値のばらつき」をそのまま使っているためです。
カテゴリ変数への応用
ランダムサンプル補完は、平均・中央値が使えない カテゴリ変数にもそのまま適用できる のが利点です。
embarked 列(カテゴリ)で試してみましょう。
以下、コードです。
# df_cat に元のデータフレーム df をコピー
df_cat = df.copy()
# 補完前の embarked 列の分布(相対度数)を表示
print("補完前の embarked の分布:")
print(df_cat['embarked'].value_counts(normalize=True).round(4))
print(f"補完前の欠損数: {df_cat['embarked'].isnull().sum()}")
# カテゴリ変数(embarked)のランダムサンプル補完を行う
# 手順の意図:元データの分布をできるだけ維持しつつ欠損を埋める
# 1) embarked の欠損数をカウント
n_miss_cat = df_cat['embarked'].isnull().sum()
# 2) 観測されている値(欠損でないもの)から
# 欠損数と同数だけランダムサンプリング
samples_cat = df_cat['embarked'].dropna().sample(
n=n_miss_cat, random_state=42
)
# 3) サンプリングした補完値のインデックスを、
# 欠損している行のインデックスに合わせる
samples_cat.index = df_cat[df_cat['embarked'].isnull()].index
# 4) 欠損箇所にサンプリングした値を代入して補完
df_cat.loc[df_cat['embarked'].isnull(), 'embarked'] = samples_cat
# 補完後の分布と欠損数を確認
print(f"\n補完後の embarked の分布:")
print(df_cat['embarked'].value_counts(normalize=True).round(4))
print(f"補完後の欠損数: {df_cat['embarked'].isnull().sum()}")
- カテゴリ変数でも、数値変数とまったく同じコードで補完できる
value_counts(normalize=True):各カテゴリの割合を確認
以下、実行結果です。
補完前の embarked の分布: embarked S 0.7244 C 0.1890 Q 0.0866 Name: proportion, dtype: float64 補完前の欠損数: 2 補完後の embarked の分布: embarked S 0.7250 C 0.1886 Q 0.0864 Name: proportion, dtype: float64 補完後の欠損数: 0
embarked の欠損2件がランダムに選ばれたカテゴリで埋めました。
欠損数がごく少ないため分布はほぼ変わりませんが、最頻値補完と違って「最も多いカテゴリだけを増やさない」 ため、カテゴリの構成比を保ちやすい利点があります。
単変量補完(第5~第7回)の整理
第5回〜第7回で扱った単変量補完を、ここで整理します。
| 手法 | 分布の保存 | 分散の保存 | 再現性 | 主な用途 |
|---|---|---|---|---|
| 定数補完(第5回) | × 強く歪む | × | ◎ 常に同じ | カテゴリの「未回答」明示 |
| 平均値補完(第6回) | × 1点集中 | × 縮小 | ◎ 常に同じ | 正規分布の数値変数 |
| 中央値補完(第6回) | × 1点集中 | × 縮小 | ◎ 常に同じ | 外れ値のある数値変数 |
| 最頻値補完(第6回) | △ 最頻値増加 | – | ◎ 常に同じ | カテゴリ変数 |
| ランダムサンプル補完(第7回) | ◎ ほぼ保つ | ◎ ほぼ保つ | △ 要シード固定 |
単変量補完は「その列の情報だけ」を使うため、実装が簡単で高速です。
しかし、どの手法も 変数間の関係(相関) を補完値に反映できません。
「年齢が高い人は運賃も高い」といった関係を活かして補完したい場合は、次回からの 多変量補完 が必要になります。
まとめ
今回のポイントを振り返ります。
- ランダムサンプル補完 は、観測値の中からランダムに選んで欠損を埋める手法
- 平均値・中央値補完と違い、元の分布の形と分散をほぼ保てる のが最大の利点
- 実装は「欠損数を数える → 観測値から抽出 → インデックスを揃えて代入」の3ステップ
- インデックスの付け替え を忘れると正しく代入されないので注意
- 補完値のプールは学習データから取る(データ漏洩を防ぐ原則は変わらない)
- ランダム性があるため、
random_stateの固定による再現性の確保が必須 - カテゴリ変数にもそのまま適用できる
- ただし、変数間の関係は補完値に反映されない(→ 多変量補完へ)

