本ページは 欠損メカニズム(Missing Data Mechanism)を多角的に解説します。 上のチップは、 検索・関連語の手がかりです。
本ページは 欠損メカニズム(Missing Data Mechanism)を 12+ セクションで多角的に解説します。
📂 分野:データ処理 / 関連語:MCAR / MAR / MNAR / 多重代入法 / Heckman
🍰 まずはやさしく
データの穴あき理由を分ける考え方です。
正しい埋め方や計算方法を決めるために使います。
アンケートの未回答がなぜ起きたかを探るようなものです。
ここでは欠損の3つの種類について読みます。
🍰 まずはやさしく
データ分析の基礎となる理論です。
結果がゆがむのを防ぐために使います。
スマホの調査などで空欄が出たときに対応します。
どのような場面でこの考え方が必要か読みます。
欠損メカニズム(Missing Data Mechanism)は、 統計学・データサイエンスの 基礎理論です。 アンケート、 医療データ、 センサー測定値など 欠損のないデータはまずないのが現実。 ところが「とりあえず平均で埋める」「行ごと削除」といった素朴な対応は、 メカニズムによっては 結果を大きく歪めることが知られています。
Rubin (1976) で定式化された統計理論。 「なぜ欠損したのか」によって、 削除・補完・モデル化のいずれが正当化されるかが決まります。
SSDSE-B-2026(47 都道府県統計)は欠損が 1 件も無い完備データなので、 本ページのコードでは練習用の欠損を人工的に作って(架空)MCAR / MAR / MNAR の違いを確かめます。 実務のデータでは MAR/MNAR の見極めが結論を左右します。 scikit-learn の Imputer 群、 R の mice、 SAS の PROC MI — どれもこの 3 分類が前提。
典型場面:(a) Methods 節で「12% 欠損を多重代入で対処」、 (b) 因果推論でサンプル選択バイアスを議論、 (c) 機械学習データパイプライン設計、 (d) アンケート設計時の必須設問判断。
🍰 まずはやさしく
データが消えた原因を分けるパズルです。
直感的にどのパターンかを見極めるために使います。
体重を書きたくない人が空欄にする例などが近いです。
具体的に3つのパターンを例と一緒に読みます。
3 つのメカニズムを具体例で:
| 分類 | 意味 | 例 | 対処 |
|---|---|---|---|
| MCAR(完全ランダム) | 欠損が完全に偶然 | 機器故障で一部測定不能 | 削除 or 平均補完 OK |
| MAR(観測値依存) | 他の観測値で欠損確率が決まる | 男性が体重欄を空欄にしがち | 多重代入法 推奨 |
| MNAR(欠損値自身依存) | 欠損値自身の値で欠損が決まる | 高所得者ほど所得欄を空欄に | モデル化が必要・困難 |
例:健康診断データで 「肥満気味の人ほど体重欄を空欄」にすると、 これは MNAR。 単純に「他の人の体重平均」で埋めると、 全体の平均体重を 過小評価してしまいます。
「データが穴あきになる原因」を 3 つに分類します。
| メカニズム | 定義 | 具体例 | 削除はOK? |
|---|---|---|---|
| MCAR | 欠損確率が全変数と独立 | 印刷ミスでランダム欠落 | ○ バイアスなし |
| MAR | 欠損確率が観測値に依存 | 農村県で IT 指標欠損 | △ MICE |
| MNAR | 欠損確率が欠損値自身に依存 | 高所得世帯が回答拒否 | × 補正困難 |
1000 通配って 200 通無回答のとき:
仮定の強さ:MCAR ⊂ MAR ⊂ MNAR。 強い仮定ほど分析は楽だが、 現実は MNAR 寄り。
🍰 まずはやさしく
欠損の仕組みを数式で表したルールです。
厳密に分類して正しく処理するために使います。
部活の出席簿で誰が欠席したかを数える感覚です。
数学的な定義とそれぞれの条件について読みます。
3 メカニズムの数学的定義($Y$ = 観測したい値、 $M$ = 欠損指示子、 $X$ = 他の観測変数):
$Y$ = 観測したい変数、 $M$ = 欠損指示子、 $X$ = 補助変数。 各メカニズムは $P(M\mid Y,X)$ の依存性で定義:
MICE が正当化されるのは MAR まで。 MNAR では Heckman 選択モデルが必要。
完全データを $(Y, M)$ とすると、 $p(Y_{obs}, M \mid \theta, \psi) = \int p(Y \mid \theta) p(M \mid Y, \psi) dY_{mis}$。 MAR では $\psi$ を無視できる(ignorable)。
完全データを $Y=(Y_{\text{obs}}, Y_{\text{mis}})$、 欠損インジケータを $R$($R_i=1$ なら観測、 $0$ なら欠損)、 補助変数を $X$ とおく。 Rubin (1976) は欠損メカニズムを次の 3 段階で分類した。
欠損確率がデータの値にも観測値にも一切依存しない。 例: アンケート用紙を風で飛ばされた、 サーバが落ちて記録できなかった。 観測されたデータは全データの単純無作為標本になる。
欠損確率は 観測されている変数 には依存するが、 欠損している値自体 には依存しない。 例: 「男性の方が収入を答えないことが多い」が、 答えなかった男性の収入分布は答えた男性と同じ。
欠損確率が 欠損している値そのもの に依存する。 例: 「収入が高い人ほど収入を答えない」。 観測データだけからは厳密に検証できず、 感度分析が必須。
$P(R\mid Y_{\text{obs}}, Y_{\text{mis}}, X) = P(R\mid Y_{\text{obs}}, X)$ という条件は、 補助変数 $X$ と観測値 $Y_{\text{obs}}$ で条件づけたときに欠損が「もはやランダム」になることを意味する。 だから、 「条件付け先の変数」さえ十分にモデルへ入れれば、 多重代入や最尤法(FIML)で 不偏推定が可能 になる。 これが MICE/Amelia/mice パッケージが「MAR を仮定する」と書く理由。
Rubin (1976, Biometrika) が定義した欠損メカニズムの枠組みは、 単なる分類ではなく「いつ欠損プロセスを無視できるか」を厳密に与える 無視可能性 (ignorability) の理論である。 ここでは数式と言葉の対応を最小限に絞って読み解く。
完備データを $Y = (Y_{obs}, Y_{mis})$、 欠損指示子を $R$($R_{ij}=1$ なら $Y_{ij}$ が観測)とする。 欠損メカニズムは条件付き分布 $f(R \mid Y, \phi)$ で定義され、 パラメータ $\phi$ を持つ。 ここで重要なのは、 $f$ が $Y_{mis}$ にどう依存するか である。
| メカニズム | 条件付き独立 | 補完手法の必要性 |
|---|---|---|
| MCAR | $f(R \mid Y, \phi) = f(R \mid \phi)$ | complete case でも不偏(効率は落ちる) |
| MAR | $f(R \mid Y, \phi) = f(R \mid Y_{obs}, \phi)$ | MI / FIML で不偏(complete case はバイアス) |
| MNAR | $f(R \mid Y, \phi)$ が $Y_{mis}$ に依存 | selection model / pattern-mixture + 感度分析必須 |
尤度ベース推論で欠損プロセスを「無視できる」とは、 (A) MAR が成り立ち、 (B) $\theta$(興味のあるパラメータ)と $\phi$(欠損機構パラメータ)が distinct(分離可能)であること。 この 2 条件下で観測尤度は $L(\theta \mid Y_{obs}) = \int f(Y_{obs}, Y_{mis} \mid \theta) dY_{mis}$ で書け、 $\phi$ を明示的に推定しなくてよい。
Bayesian の場合さらに事前 $p(\theta, \phi) = p(\theta) p(\phi)$ の分離(priori independence)が無視可能性の前提に追加される。 実務的にはこれが厳しく、 弱情報事前でも distinct でない場合があるため、 感度分析を併用する。
Rubin の「適切な多重代入 (proper MI)」は次の 3 条件を要求する: (1) 各代入が posterior 予測分布からの draw、 (2) m → ∞ で pooled 推定が完備データ ML と一致、 (3) Rubin の variance 結合式が完備データ漸近分散の 不偏推定になる。 sklearn の IterativeImputer(sample_posterior=True) は (1) を満たし、 (2)(3) は大標本で近似的に満たす。
$T = U + (1 + 1/m) B$、 ただし $U$ は within-imputation 分散の平均(各補完での通常標準誤差の二乗の平均)、 $B$ は between-imputation 分散(補完間の点推定のばらつき)、 $m$ は代入回数。 自由度は $\nu = (m-1)(1 + U / ((1+1/m) B))^2$ で、 m が大きく B が小さいと自由度は無限大に近づき正規近似で良い。 m が小さいと t 近似が必要。
💡 覚えるべき相対関係: $B/U$ 比は「欠損による情報損失の指標」、 $\lambda = (1+1/m)B / T$ は「全分散に占める欠損由来の割合」。 $\lambda > 0.5$ なら m を増やす、 ドメイン知識を追加する、 または対象パラメータの推定をあきらめる、 のいずれかが必要。
学術論文・社内分析報告書・行政白書のいずれでも、 欠損処理は「再現可能なレベル」で記述することが求められる。 以下は STROBE / CONSORT / Sterne et al. (2009, BMJ) のガイドラインに準拠したテンプレート。 そのまま雛形として使えるよう、 SSDSE 文脈での記述例を併記する。
| 項目 | 値 | 解釈 |
|---|---|---|
| 欠損率 | 21.3% (10/47) | 中程度 |
| Little's MCAR test | p < 0.001 | MCAR 棄却 |
| 代入回数 m | 20 | Bodner 推奨値以上 |
| B/U 比 | 0.155 | 軽度〜中等度の欠損由来不確実性 |
| λ (情報損失率) | 0.140 | 許容範囲(< 0.5) |
| δ 感度分析範囲 | [-1σ, +2σ] | 結論頑健 |
| 補完アルゴリズム | IterativeImputer (sklearn 1.4) | PMM ベース、 連続値向け |
| 再現用 seed | range(20) | 公開で再現可能 |
💡 査読対策: 査読者からの最頻指摘は「MNAR を検討したか」「m が小さすぎる」「Little's test だけで判定していないか」「complete case との比較がない」の 4 点。 上記テンプレートを使うとこの 4 点すべてに先回り回答できる。
繰返し測定では 3 種類の欠損形態を区別する: (a) intermittent(一時的に欠損後復帰)、 (b) dropout / attrition(脱落以降全欠損)、 (c) death(観測機会自体が消失)。 dropout の場合、 「dropout 確率が過去の観測値で説明できる」なら MAR、 「dropout 時点の未観測値で説明される」なら MNAR。 mixed-effects model + FIML は MAR を仮定するため、 MNAR が疑われるときは pattern-mixture / shared-parameter model に切り替える。
家計パネル調査などで「離脱確率を観測共変量で予測」し、 残った個体に 1/離脱確率 の重みをかけて推定する IPW (Inverse Probability Weighting) は、 MAR 仮定下で不偏推定を得る古典的手段。 doubly robust estimator(IPW + アウトカム回帰モデルの両方を併用)は片方のモデルが正しければ不偏で、 実務では IPW + MI のハイブリッドが推奨される。
ランダム化比較試験 (RCT) では「処置群の潜在アウトカム」と「対照群の潜在アウトカム」のうち、 各個体で一方が常に欠損する。 ATE(平均処置効果)の推定はこの欠損を「ランダム化により MCAR」と見做して可能になる。 観察研究での confounder 調整は MAR 化のプロセスと等価で、 propensity score matching は欠損アウトカムの補完を間接的に行っているとも解釈できる。 この視点は Rubin の「potential outcome framework」と直結する。
XGBoost / LightGBM などツリーモデルは欠損を「default direction」として学習でき、 単純な mean imputation より高精度になる場合が多い。 しかしこれは 予測タスク での話で、 因果推定や統計推論では Rubin の枠組みに従った MI が依然として必要。 「予測精度が良いから補完は不要」という主張は 推論目的では誤り。
GAIN (Yoon et al., 2018) は GAN を使った補完で MAR 下で MICE を上回ることが報告されている。 MIWAE (Mattei & Frellsen, 2019) は変分オートエンコーダによる補完で MNAR にも一定の頑健性を示す。 ただし両者とも「不確実性の定量化」は工夫が必要で、 Rubin の規則に従った標準誤差結合は別途実装する必要がある。
💡 発展まとめ: 欠損は「データの問題」ではなく 研究設計の一部。 設計段階で欠損メカニズムを予測し、 補助変数を用意し、 縦断追跡で attrition 対策を埋め込むのが「欠損に強い研究」の本質。 補完手法の選択はその後の話に過ぎない。
MCAR 下でも listwise deletion は 標本サイズが大幅に減ることで検出力が落ち、 信頼区間が無駄に広がる。 MAR 下では係数自体に偏りが入る。 例外的に「アウトカム変数のみの欠損で予測変数は完備」かつ「欠損メカニズムが共変量で説明される(MAR)」場合、 listwise はバイアス無し(ただし非効率)。 つまり「常に必ずダメ」ではないが、 デフォルトでは避けるべき。
いいえ。 Little's test は「MCAR からの逸脱」を検出する 必要条件 のテストで、 検出力が低い(小標本では非有意になりやすい)。 「棄却されなかった = MCAR」と結論するのは type II 誤りを犯すリスクが大きい。 必ずドメイン知識・可視化と組み合わせて判断する。
(1) センシティブ項目を中盤に置く(最初/最後だと回答疲労で空欄になる)、 (2) 「答えたくない」を明示的選択肢として用意し MAR 化する、 (3) 範囲選択肢(年収「300-500 万」など)で precise な数値を避ける、 (4) skip pattern を最小化、 (5) Web 調査では強制回答(validation)を慎重に使う(過剰だと脱落が増える)。
予測モデル(推論ではなく予測精度が目的)で、 欠損の発生自体が「ターゲットと関連する情報」を持つ場合に有効。 例: 「収入が空欄」自体が「答えたくない人=高所得や低所得の極端」を示すなら、 欠損ダミー missing_indicator を追加すると AUC が改善することがある。 ただし因果推論や統計推論ではこの手法は使えない。
mice パッケージと sklearn の IterativeImputer はどちらが優れている?「優劣」ではなく用途が異なる。 R の mice はカテゴリ変数や 2 値変数を自動で扱い、 predictor matrix を細かく制御でき、 pooling 関数も豊富で 統計推論向け。 sklearn IterativeImputer は連続値中心で 機械学習パイプラインに組み込みやすい。 推論目的なら R の mice、 予測パイプラインなら sklearn が標準的選択。
新しいデータセットを受け取り、 「欠損があります」と気づいた瞬間から「補完済みデータで本解析を始める」までの 12 ステップを並べる。 各ステップで「何を見て」「何を記録するか」を具体化したので、 そのままチェックリストとして使える。
df.isna().mean() で列ごと、 df.isna().sum(axis=1) で行ごとの欠損数を確認。 欠損率 0% の列と 30% 超の列を分類。missingno.matrix(df) で「どの行のどの列が欠損しているか」を画素的に確認。 縦縞 → 全変数同時欠損、 散在 → 個別欠損。statsmodels 公式実装は無いが、 R の misty::na.test() や Python の impyute で代替。 棄却 → MCAR ではない。 棄却なし ≠ MCAR。IterativeImputer(sample_posterior=True) を回す。 各補完で本解析を実行。| ステップ | アウトプット記録項目 | 使用ツール |
|---|---|---|
| 1 | 列別欠損率の表 | pandas |
| 2 | matrix/heatmap 図 | missingno |
| 3 | ロジスティック係数表 | sklearn/statsmodels |
| 4 | 2 群比較の p 値表 | scipy.stats |
| 5 | Little test の p 値 | impyute / misty (R) |
| 6 | インタビューメモ | Markdown ノート |
| 7 | 補助変数候補リスト | ヒアリング |
| 8 | 仮定宣言文(Methods) | 論文・報告書 |
| 9 | 補完版データ m 個 | sklearn / R mice |
| 10 | pooled β, SE, B/U, λ | Rubin's rule |
| 11 | complete case との対照表 | 同上 |
| 12 | δ vs β のプロット | matplotlib |
💡 運用 Tips: 上記 12 ステップを Jupyter Notebook の章立てとして並べておくと、 新規データセットに対して「セル順に走らせるだけで再現可能な欠損診断レポートが出る」状態を作れる。 SSDSE 系の公的統計、 医療コホート、 マーケティング調査いずれにも転用可能。
⚠️ 業務で見かけるパターン: 「欠損は KNNImputer で埋めたので大丈夫」だけの記述。 これは 補完手法の選択理由、 メカニズムの根拠、 感度分析 がすべて欠けている。 査読・監査で必ず指摘される。
欠損データ問題は古くから認識されていたが、 1970 年代の Rubin 論文以前は「経験則ベースの補完」が主流だった。 ここでは主要な転換点を年表で示す。
| 年 | 人物・出来事 | 貢献 |
|---|---|---|
| 1932 | Wilks | 多変量正規分布での欠損データ尤度の初期的扱い |
| 1965 | Hartley & Hocking | 最尤推定による欠損データ解析の枠組み提示 |
| 1976 | Rubin (Biometrika) | MCAR / MAR / MNAR の厳密定義と無視可能性の理論 |
| 1977 | Dempster, Laird & Rubin | EM アルゴリズム — 欠損のある最尤推定の実装基盤 |
| 1987 | Rubin "Multiple Imputation for Nonresponse" | 多重代入の体系化、 Rubin's rule の提示 |
| 1997 | Schafer "Analysis of Incomplete Multivariate Data" | MI の実装ソフトウェア (S-Plus norm) 公開 |
| 2001 | van Buuren mice パッケージ | Chained equations の標準実装 (R) |
| 2009 | Sterne et al. (BMJ) | 医学誌での欠損処理報告ガイドライン |
| 2018 | Yoon et al. | GAIN — GAN による補完 |
| 2019 | scikit-learn 0.21 | IterativeImputer の experimental 公開 |
| 2020- | Generative AI 時代 | Diffusion / Transformer ベース補完の研究進行 |
Rubin の 1976 論文以前は、 「欠損は厄介だが何とか埋めれば良い」程度の認識が一般的で、 mean imputation や last-observation-carried-forward (LOCF) が標準だった。 Rubin の貢献は (1) 欠損プロセス自体を確率モデルとして扱う視点を導入したこと、 (2) 「いつ無視できるか」の数学的条件を与えたこと、 (3) 多重代入で不確実性を定量化する道筋を開いたこと、 にある。 これは現代統計学・データサイエンス全体に影響を与え、 観察研究の因果推論や Bayesian inference の発展とも交差している。
💡 歴史から学ぶ教訓: 「最先端の補完手法(GAIN, MIWAE)を使えば良い」のではなく、 Rubin の理論枠組みの上で 不確実性を正しく定量化 することこそが本質。 1976 年の枠組みは 50 年経った今も全く色褪せず、 最新の深層生成モデルも結局この理論で評価される。
補完手法は 10 種類以上あり、 メカニズム別に妥当性が大きく異なる。 「とりあえず KNN」ではなく、 「データのメカニズム」と「解析目的(推論 vs 予測)」の組合せで選ぶ。
| 手法 | MCAR | MAR | MNAR | 特徴・注意点 |
|---|---|---|---|---|
| listwise deletion | ○ | × | × | 最も単純。 MCAR でも検出力が落ちる |
| mean / median imputation | △ | × | × | 分散を過小評価、 標準誤差が信用できない |
| regression imputation (単一) | △ | △ | × | 変数間関係を保てるが分散過小 |
| stochastic regression | ○ | ○ | × | 残差を加えて分散を回復、 MI の前身 |
| LOCF / NOCB | △ | × | × | 時系列でのみ使用、 強い時間相関を仮定 |
| KNN imputation | ○ | △ | × | 低次元で局所構造が強いときに有効 |
| MICE (IterativeImputer) | ○ | ○ | × | 統計推論のデフォルト、 m ≥ 20 |
| FIML (Full Information ML) | ○ | ○ | × | SEM (構造方程式) で標準的 |
| EM algorithm | ○ | ○ | × | 最尤推定、 多変量正規が前提 |
| pattern-mixture model | ○ | ○ | △ | MNAR の感度分析に有効 |
| selection model | ○ | ○ | △ | 欠損機構の明示モデル化 |
| GAIN (深層学習) | ○ | ○ | △ | 大規模・高次元向け、 不確実性定量化に工夫 |
| MIWAE (VAE) | ○ | ○ | △ | 変分推論で事後分布を扱える |
○ = 不偏で適切な不確実性定量化が可能、 △ = 条件付きで可、 × = 偏り or 不確実性過小評価。 MNAR の列は「感度分析と組み合わせるか、 欠損機構を明示モデル化するか」が必要なため、 完全な ○ は無い。
💡 選択フローチャート: (1) 推論目的か予測目的か → 推論なら MI / FIML、 予測なら ML 系も可。 (2) 欠損機構は MAR で十分か → MNAR 疑いなら pattern-mixture を追加。 (3) データ規模 → 小規模 (~1000 行) なら MI、 大規模なら MICE + 並列化または深層補完。 (4) カテゴリ変数の有無 → 多いなら R mice、 連続中心なら sklearn。
論文や教科書で頻出する欠損関連の専門用語をまとめる。 各用語の英語表記・略語・日本語訳を併記し、 さらに「いつ使う言葉か」を補足した。
| 英語 / 略語 | 日本語 | 説明と使い分け |
|---|---|---|
| MCAR | 完全ランダム欠損 | Missing Completely At Random、 欠損確率が完全独立 |
| MAR | 条件付きランダム欠損 | Missing At Random、 観測変数で条件付ければ独立 |
| MNAR / NMAR | 非ランダム欠損 | Missing Not At Random、 欠損値そのものに依存 |
| ignorable | 無視可能 | 尤度推論で欠損機構を無視できる条件 (MAR + distinct parameters) |
| MI | 多重代入 | Multiple Imputation、 m 個の補完を生成して統合 |
| FIML | 完全情報最尤推定 | Full Information Maximum Likelihood、 SEM で標準 |
| EM | EM アルゴリズム | Expectation-Maximization、 潜在変数モデルの推定 |
| MICE | 連鎖方程式 | Multivariate Imputation by Chained Equations、 R/Python の主要 MI 実装 |
| IPW | 逆確率重み付け | Inverse Probability Weighting、 attrition 対策の古典 |
| PMM | 予測平均マッチング | Predictive Mean Matching、 MICE で連続変数の標準法 |
| LOCF | 直前値繰越 | Last Observation Carried Forward、 時系列で歴史的に使用 |
| attrition | 脱落 | 縦断研究での参加者離脱 |
| dropout | 途中脱落 | attrition の同義語、 臨床試験で頻用 |
| δ-adjustment | δ 調整 | MNAR 感度分析のシフトパラメータ法 |
| tipping point | 転換点 | 感度分析で結論の符号が反転する閾値 |
| Rubin's rule | Rubin の規則 | MI の点推定と分散の統合公式 |
| FCS | 完全条件付き仕様 | Fully Conditional Specification、 MICE と同義 |
💡 用語の使い分けポイント: 統計系論文では Rubin の MCAR/MAR/MNAR、 計量経済では「missing-not-at-random」「selection on observables」、 機械学習では「missingness as a feature」「imputation」などの用語がよく見られる。 同じ概念でも分野ごとに表現が異なるため、 査読時は用語統一に注意。
truncation(切断)とcensoring(打ち切り)は欠損とは区別される。 truncation は「特定の条件を満たす個体しか観測されない」(例: 所得 0 円以下は標本に入らない)状況で、 標本選択バイアスを引き起こす。 censoring は「観測はあるが値が一部しか分からない」(例: 生存時間で追跡終了時点まで生存していた個体)。 これらは別個の統計理論(survival analysis, Tobit model)で扱われ、 単純に MI を当てるのではなく専用手法を選ぶこと。
measurement error(測定誤差)と欠損も近縁概念で、 「観測されているが真値とずれている」のが測定誤差、 「観測されていない」のが欠損。 ただし測定誤差の極端版(誤差が無限大 = 観測不能)として欠損を扱う統一的枠組みもある。
MAR の妥当性が確認できたら多重代入 (Multiple Imputation, MI) を実行する。 MI は欠損値を 1 つの値ではなく m 個の確率的代入で補い、 各補完データセットで解析し、 Rubin の規則で統合する。 これにより「欠損による不確実性」が標準誤差に正しく反映される。
このコードでやること: sklearn.experimental.IterativeImputer を sample_posterior=True で起動し、 20 個の補完版を生成。 各版で「人口 → 貯蓄」の回帰を当て、 Rubin の規則で統合する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import numpy as np import pandas as pd # df_mnar はこのあとのブロックで作っているので、ここでも用意しておく # MNAR: 値そのものの大きさが欠測しやすさを決める(消費支出が低い県ほど欠けやすい) _rng = np.random.default_rng(0) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) df_mnar = df.copy() _p = 1 - (df_mnar['L3221'].rank(pct=True)) # 低いほど欠測確率が高い df_mnar.loc[_rng.random(len(df_mnar)) < _p * 0.4, 'L3221'] = np.nan from sklearn.experimental import enable_iterative_imputer # noqa from sklearn.impute import IterativeImputer import statsmodels.api as sm import numpy as np import pandas as pd cols = ['A1101','A1301','A4103','L3221'] data = df_mnar[cols].values m = 20 betas, ses = [], [] for seed in range(m): imp = IterativeImputer(sample_posterior=True, random_state=seed, max_iter=20) completed = imp.fit_transform(data) cdf = pd.DataFrame(completed, columns=cols) X = sm.add_constant(cdf[['A1101']]) y = cdf['L3221'] res = sm.OLS(y, X).fit() betas.append(res.params['A1101']) ses.append(res.bse['A1101']) beta_bar = np.mean(betas) U = np.mean(np.array(ses)**2) # within-imputation 分散 B = np.var(betas, ddof=1) # between-imputation 分散 T = U + (1 + 1/m) * B # 総分散 se_total = np.sqrt(T) print(f'pooled beta = {beta_bar:.4f}') print(f'pooled SE = {se_total:.4f}') print(f'between/within 比 = {B/U:.3f}') print(f'欠損による情報損失率 λ = {(1 + 1/m) * B / T:.3f}') |
📤 実行例:
💬 pooled β = 0.0026 は「総人口 A1101 が 1 人増えると消費支出 L3221 が 0.0026 円/月 増える」= 人口 100 万人あたり月 2,600 円という関係を示す(どちらも生の単位のままなので係数が小さく出る)。 pooled SE = 0.0013 なので t = 0.0026/0.0013 ≒ 2.0 で、 かろうじて有意水準 5% に届く程度。 between/within 比 0.155 は欠損由来の分散が within 分散の 15.5% を上乗せしていることを意味し、 これを無視(= 単一代入)すると標準誤差を 7% 程度過小評価する。 λ=0.140 は「総分散の 14% が欠損由来」という Rubin の指標で、 0.5 を大きく下回るので m=20 の代入回数で十分。
このコードでやること: 各補完値に「欠損値は MAR 補完よりも δ だけ大きい」というシフトを加え、 δ を [-1, +2] の範囲で動かして結論(β の符号と有意性)の頑健性を確認する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import numpy as np import pandas as pd # 各 δ に対して MI 後 OLS を再計算 deltas = np.arange(-1.0, 2.01, 0.5) result = [] for delta in deltas: bs = [] for seed in range(m): imp = IterativeImputer(sample_posterior=True, random_state=seed, max_iter=20) completed = imp.fit_transform(data) cdf = pd.DataFrame(completed, columns=cols) # MNAR シフト: 元欠損行の貯蓄に δ × σ を加える sigma = cdf['L3221'].std() mask = df_mnar['L3221'].isna().values cdf.loc[mask, 'L3221'] += delta * sigma X = sm.add_constant(cdf[['A1101']]) res = sm.OLS(cdf['L3221'], X).fit() bs.append(res.params['A1101']) result.append((delta, np.mean(bs))) for d, b in result: print(f'δ = {d:+.1f} → pooled beta = {b:+.4f}') |
📤 実行例:
💬 δ を -1σ から +2σ まで動かしても β はすべて正の符号を保ち、 値も 0.0024-0.0031 の範囲(δ=0 の 0.0026 に対して ±20% 程度の振れ幅)。 結論「人口が大きい県ほど消費支出が多い」は MNAR 仮定の幅広いシナリオで頑健。 報告には「δ ∈ [-1, +2] σ で β > 0 が保たれた」と書けば査読でも説明可能。
| シナリオ | δ の値 | 解釈 |
|---|---|---|
| 楽観 (MAR + 小 MNAR) | δ = 0 | MAR 補完がそのまま正しい |
| 中等度の MNAR | δ = ±0.5σ | 欠損値は MAR 推定より 0.5σ 大/小 |
| 強い MNAR | δ = ±1σ-2σ | 秘匿傾向が明確 |
| 極端な MNAR | |δ| > 2σ | 事実上「別分布から欠損値が来ている」 |
💡 感度分析の報告ルール: 「δ ∈ [a, b] で結論が変わらない」と明示し、 結論が反転する tipping point(符号が逆転する δ)を併記すること。 査読者・規制当局が安心する。
欠損メカニズムは「データの性格」と密接に関連する。 同じ「MAR」でも、 医療と金融、 教育と Web 行動ログでは 典型的な発生機構 が異なる。 分野別パターンを掴むことで、 新しいデータセットに出会ったとき「最初にどこを疑うか」が即座にわかる。
| 分野 | 欠損の典型源 | 最頻メカニズム | SSDSE 流の例示 |
|---|---|---|---|
| 公的統計 | 回答拒否・調査未到達 | MAR / MNAR | SSDSE-B-2026 で人口の少ない県の世帯標本数が不足し、 一部の指標が秘匿される |
| 医療・疫学 | 脱落 (dropout) | MAR / MNAR | 追跡調査で「症状が重い人ほど脱落」する場合 MNAR、 「居住地が遠い人ほど脱落」なら MAR |
| 金融 | 機密保護・営業秘密 | MNAR | 高所得層の貯蓄額が非開示の比率が高い(欠損値自体が大きいから秘匿される) |
| 教育 | テスト未受験 | MAR | 「家庭の社会経済指標」で説明できる欠損は MAR、 「テストが苦手だから欠席」は MNAR |
| Web 行動ログ | セッション切断・拒否 | MCAR / MAR | ネットワーク障害は MCAR、 デバイス種別による違いは MAR |
| IoT・センサー | 機器故障・通信断 | MAR | 温度上昇でセンサー誤動作 → 観測温度で説明できる MAR |
| アンケート | 「答えたくない」 | MNAR | 年収・支持政党などセンシティブ項目の無回答は MNAR が定番 |
| パネル調査 | attrition | MAR + MNAR 混在 | 転居・死亡など観測可能な事象は MAR、 「不満で離脱」は MNAR |
SSDSE-B-2026 自体には欠損はほぼ無いが、 同種の「県別経済指標」では 小規模県のサンプル数不足が原因で一部の細目(例: 個別業種の出荷額)が「-」記号で欠損化されることがある。 これは「人口」という観測変数で説明できるため MAR と扱える。 公的統計では「秘匿」と「未集計」の区別が重要で、 メタデータ(注記)を必ず読むこと。
5 年追跡の高血圧コホートで「血圧の値」が脱落者で欠損したとする。 「血圧が高い被験者ほど脱落しやすい」と判断したい場合、 直接は確認できないが、 (a) 脱落直前の血圧と非脱落者の血圧分布の比較、 (b) 「脱落 vs 観測変数」のロジスティック回帰、 (c) 主治医インタビューでの「来院理由」確認、 を組み合わせて MAR/MNAR を判定する。 MNAR が疑われる場合は δ-adjustment や pattern-mixture を必ず提示する。
家計調査で「年収」項目の無回答率が約 15-25% に達することは珍しくない。 高所得層と無所得層の 両側 で欠損が増える「U 字パターン」が観測される。 この場合、 線形ロジスティックでは説明できないため、 二次項を入れたモデルや有限混合モデルで欠損機構を推定し、 selection model 系の MNAR フレームで再解析する。
温度センサーが 40℃ を超えると誤動作で NaN を返す仕様があったとする。 これは「観測温度(直前の値)」で説明できる MAR。 隣接時刻の値で Kalman smoother 補完するのが標準。 一方、 機械振動でセンサー自体が故障し全期間欠損になる場合は MNAR に近いため、 別個のセンサー(冗長系)からの補完か当該機械の除外を検討する。
JHPS(日本家計パネル調査)のような複数年追跡では、 ベースライン特性(年齢・初期所得・地域)が attrition の予測因子となることが多く、 MAR 仮定が概ね妥当。 ただし「政治意識調査」など態度変化と離脱が連動するケースでは MNAR を疑い、 inverse-probability weighting (IPW) や joint model を組み合わせる。
💡 分野横断の鉄則: 欠損メカニズムの仮定は「データだけ」では決められない。 必ず ドメイン知識(業務担当者・調査企画者・センサー仕様)と組み合わせて判定する。 純粋に統計的検定だけで MAR を主張するのは危険。
所得アンケート(真平均 650万、 高所得 MNAR):
| 方法 | 推定平均 | ズレ | 備考 |
|---|---|---|---|
| 真の平均 | 650 | ±0 | 参照 |
| Listwise | 520 | −130 | 高所得が抜けたまま |
| 平均補完 | 520 | −130 | 分散も過小 |
| KNN (k=5) | 535 | −115 | 近傍依存 |
| MICE (m=20) | 545 | −105 | MAR 仮定 |
| Heckman | 620 | −30 ✓ | 仮定要 |
$\hat\theta_1=540, \hat\theta_2=555, \hat\theta_3=548$、 各分散 $U_k=900,950,920$。
$\bar\theta = 547.67$、 $\bar U = 923.3$、 $B = 56.3$、 $T = \bar U + (1+1/m)B = 998.4$、 $SE \approx 31.6$、 95% CI = $(485.7, 609.6)$
🎯 このコードでやること: SSDSE-B-2026 の「総人口」を完全データとみなし、 MCAR / MAR / MNAR の 3 種類の欠損メカニズムを意図的に注入し、 mean imputation の平均がどれだけバイアスを受けるか比較する。
📥 入力: SSDSE-B-2026、 総人口(A1101)・15歳未満人口(A1301) の 2 列(総人口を欠損させ、 15歳未満人口を欠損確率のドライバに使う)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import pandas as pd import numpy as np from scipy.stats import bernoulli df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]); df = df[df['SSDSE-B-2026'] == 2023].dropna() pop = df['A1101'].values gdp = df['A1301'].values true_mean = pop.mean() rng = np.random.default_rng(0) # MCAR: 50% を完全無作為に欠損 mcar = pop.copy().astype(float) mcar[rng.random(len(pop)) < 0.5] = np.nan # MAR: 観測変数 A1301(15歳未満人口) が中央値以上の県ほど欠損しやすい mar = pop.copy().astype(float) prob = 0.2 + 0.6 * (gdp > np.median(gdp)) mar[rng.random(len(pop)) < prob] = np.nan # MNAR: 人口が大きいほど欠損しやすい(値自体に依存) mnar = pop.copy().astype(float) prob = 0.2 + 0.6 * (pop > np.median(pop)) mnar[rng.random(len(pop)) < prob] = np.nan for name, x in [('MCAR', mcar), ('MAR', mar), ('MNAR', mnar)]: obs = x[~np.isnan(x)] bias = obs.mean() - true_mean print(f'{name:5s}: 残存={len(obs):2d} 観測平均={obs.mean():,.0f} 真値={true_mean:,.0f} bias={bias:+,.0f}') |
📤 実行例:
💬 結果の読み方: MCAR は 50% を無作為に欠損させても観測平均が真値の周りに散らばるだけで、 バイアスは相対的に小さい(ここでは +255,451。 n=47 の標本変動によるもので、 n が増えれば 0 に近づく)。 一方 MAR・MNAR では「値の大きい県」が体系的に消えるため観測平均が大きくバイアスする。 今回は観測変数(15歳未満人口 A1301)が総人口と強く相関するため MAR のバイアスが最大になったが、 本質は MAR は補助変数(15歳未満人口)で補正できるのに対し、 MNAR は欠損値自身に依存するため補助変数では打ち消せない点にある。
ここでは SSDSE-B-2026(47 都道府県 × 統計指標)を素材に、 「欠損列が来たら次に何をするか」を 6 ステップで実行する。 全ステップ実値ベースで、 教科書的に「MAR を仮定して MI」と決めつけない手順を示す。
このコードでやること: SSDSE-B-2026 を読み込み、 まず欠損率と欠損パターンを表化する。 仮想的に「消費支出(L3221)の上位 5 県だけ欠損」を作って MNAR っぽい状況を再現する(実データ自体は完備しているため、 教材として人工的に欠損を入れる)。
📥 入力データ(SSDSE-B-2026 抜粋、 単位は元データに準拠):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd import numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) # 2023 年の 47 都道府県に絞る # 教材用: 消費支出(L3221)の上位 5 県を欠損化(MNAR シナリオ) top5 = df.nlargest(5, 'L3221').index df_mnar = df.copy() df_mnar.loc[top5, 'L3221'] = np.nan # 欠損率の確認 missing_rate = df_mnar.isna().mean().sort_values(ascending=False) print('--- 欠損率 ---') print(missing_rate[missing_rate > 0]) print() print('--- 欠損行の他変数の要約 ---') print(df_mnar[df_mnar['L3221'].isna()][['A1101','A1301','A4103']].describe()) |
📤 実行例:
💬 欠損率は約 10.6%(5/47)。 欠損 5 県の総人口(A1101)・15歳未満人口(A1301) は 全 47 県の平均より明らかに大きい(例: 欠損群の総人口平均 ≈ 520 万人)。 つまり「欠損が観測変数(人口規模)と相関している」サインがあり、 単純な MCAR ではない。 まず MAR を疑い、 さらに「消費支出そのものが大きい県で欠損」という前提を観測データからは確認できないため MNAR の可能性も保持する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from sklearn.linear_model import LogisticRegression import numpy as np # 欠損ダミー y = df_mnar['L3221'].isna().astype(int) # 観測変数のみで予測(標準化) X = df_mnar[['A1101','A1301','A4103']].copy() X = (X - X.mean()) / X.std(ddof=0) clf = LogisticRegression(max_iter=1000, penalty='l2', C=1.0) clf.fit(X, y) print('係数 (z-score 単位):') for name, coef in zip(X.columns, clf.coef_[0]): print(f' {name:12s}: {coef:+.3f}') print(f'切片 : {clf.intercept_[0]:+.3f}') print(f'pseudo R^2 : {clf.score(X, y):.3f}') |
📤 実行例:
💬 総人口(A1101) の係数が正(+0.284)で、合計特殊出生率(A4103) が負(−0.448)→ 観測変数で欠損傾向がある程度説明でき、 MAR と矛盾しない。 15歳未満人口(A1301) は +0.160 と小さく、 総人口とほぼ同じ情報を持つため単独では効きにくい。 ここでの pseudo R² は clf.score(= 正解率)で 0.915 だが、 47 県中 42 県が「欠損なし」なので全部「欠損なし」と答えるだけで 0.894 になる。 つまり 0.915 という値はほとんど下駄で、 説明力が高く見えること自体は MAR の証明にはならない点に注意。 さらに「消費支出そのもの」を説明変数に入れていない(欠損しているため観測できない)ので、 MNAR を完全には排除できない。 次のステップで多重代入を実行し、 後に MNAR 感度分析を回す。
合成データで欠損パターン別に平均推定値を計算する。
| シナリオ | 欠損理由 | 真の平均 | 観測平均 |
|---|---|---|---|
| MCAR | ランダム | 50 | 49.8 |
| MAR | 性別依存 | 50 | 49.2 |
| MNAR | 値自体依存 | 50 | 40.0 |
1 2 3 4 | truth = 50 obs = {'MCAR':49.8, 'MAR':49.2, 'MNAR':40.0} for k, v in obs.items(): print(f"{k}: バイアス={v-truth}") |
💬 手計算 (Step 2) と Python 出力が完全一致。 MNAR が最も深刻。
合成データではなく、 SSDSE-B-2026 の東北 6 県 食料費 (千円/月) で MCAR/MAR/MNAR の観測平均バイアスを 手で求めて Python と一致確認する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import numpy as np # SSDSE-B-2026 東北 6 県 食料費 (千円/月) prefs = ['青森', '秋田', '福島', '岩手', '宮城', '山形'] x = np.array([77.90, 78.12, 78.98, 82.00, 83.83, 84.11]) mu_true = x.mean() # Step 1 mcar_mask = np.array([False, True, False, True, False, True]) # Step 2 (秋田/岩手/山形 欠損) mar_mask = np.array([True, True, False, False, False, False]) # Step 3 (北 2 県 欠損) mnar_mask = x < 80 # Step 4 (x<80 欠損) for name, mask in [('MCAR', mcar_mask), ('MAR ', mar_mask), ('MNAR', mnar_mask)]: obs_mean = x[~mask].mean() print(f"{name} 観測平均={obs_mean:7.4f} バイアス={obs_mean-mu_true:+7.4f}") print(f"真の平均 = {mu_true:.4f}") |
💬 Step 2-4 の手計算 (80.2367 / 82.2300 / 83.3133) と Python 出力が小数 4 桁まで一致。 SSDSE-B の実値でも MNAR が最大の +2.49 バイアスを発生させ、 「値そのもので脱落する」メカニズムが最も補正困難であることが 実データの数値として確認できる。
📌 このページのコードで使う範囲について: 補完の比較は 最新年度の 47 行 × 7 列に絞っています。 SSDSE-B-2026 は 564 行 × 110 列あり、そのまま多重代入(MICE)にかけると 1 回の補完に数分かかってブラウザでは実行できません。 列を絞っても、MCAR / MAR / MNAR で補完のふるまいが変わるという 結論そのものは変わりません。 自分のデータで試すときは、まず小さく絞って挙動を確かめ、 そのうえで列数を増やしていくのが安全です。
最小コードで動かしてみる例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import pandas as pd from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 最新年度の 47 行だけを使う # ── 練習用の欠損をここで作ります(架空)───────────────────── # SSDSE-B-2026 には欠損値が 1 件もありません。そのままでは補完しても # 何も変わらないので、このページの主題である 3 つの仕組みを人工的に作ります。 import numpy as np _rng = np.random.default_rng(0) _n = len(df) # MCAR: 消費支出を、他の変数と無関係に 15% 落とす df.loc[_rng.random(_n) < 0.15, 'L3221'] = np.nan # MAR: 教育費を、15 歳未満人口(A1301)が中央値未満の県ほど落ちやすくする _p = np.where(df['A1301'] < df['A1301'].median(), 0.30, 0.05) df.loc[_rng.random(_n) < _p, 'L322108'] = np.nan # MNAR: 住宅地の標準価格を、値そのものが高い県ほど落ちやすくする _p = np.where(df['C5401'] > df['C5401'].quantile(0.75), 0.40, 0.05) df.loc[_rng.random(_n) < _p, 'C5401'] = np.nan # ──────────────────────────────────────────────────── _rate = df.isnull().mean() print('欠損率(0 より大きい列だけ):') print(_rate[_rate > 0].round(3)) # 多重代入法(MICE) — MAR を仮定 # 110 列すべてを説明変数にすると 1 回の補完に数分かかる。 # 欠損のある 3 列と、それを説明できる少数の列だけに絞る。 _use = ['L3221', 'L322108', 'C5401', 'A1101', 'A1301', 'A1303', 'A4101'] imputer = IterativeImputer(max_iter=10, random_state=0) df_filled = imputer.fit_transform(df[_use]) print('補完後の形:', df_filled.shape) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import pandas as pd import numpy as np import os import missingno as msno import matplotlib.pyplot as plt os.makedirs('figures', exist_ok=True) # 保存先が無いと savefig は失敗する df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) # ── 練習用の欠損をここで作ります(架空)───────────────────── # SSDSE-B-2026 には欠損値が 1 件もないので、可視化する対象を人工的に作ります。 import numpy as np _rng = np.random.default_rng(0) _n = len(df) df.loc[_rng.random(_n) < 0.15, '消費支出(二人以上の世帯)'] = np.nan # MCAR _p = np.where(df['15歳未満人口'] < df['15歳未満人口'].median(), 0.30, 0.05) df.loc[_rng.random(_n) < _p, '教育費(二人以上の世帯)'] = np.nan # MAR _p = np.where(df['標準価格(平均価格)(住宅地)'] > df['標準価格(平均価格)(住宅地)'].quantile(0.75), 0.40, 0.05) df.loc[_rng.random(_n) < _p, '標準価格(平均価格)(住宅地)'] = np.nan # MNAR # ──────────────────────────────────────────────────── print('欠損率:', df.isnull().mean().sort_values(ascending=False).head(10)) msno.matrix(df); plt.savefig('figures/missing_matrix.png', dpi=120) msno.heatmap(df); plt.savefig('figures/missing_heatmap.png', dpi=120) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # ── この抜粋で使うデータを用意します(練習用の欠損は架空)── # 110 列すべてを説明変数にすると 1 列あたり数十秒かかる。 # 最新年度の 47 行 × 欠損に関係しそうな 7 列だけで見る。 import numpy as np import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler _USE = ['L3221', 'L322108', 'C5401', 'A1101', 'A1301', 'A1303', 'A4101'] df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023][_USE].copy() _rng = np.random.default_rng(0) _n = len(df) df.loc[_rng.random(_n) < 0.15, 'L3221'] = np.nan # MCAR _p = np.where(df['A1301'] < df['A1301'].median(), 0.30, 0.05) df.loc[_rng.random(_n) < _p, 'L322108'] = np.nan # MAR _p = np.where(df['C5401'] > df['C5401'].quantile(0.75), 0.40, 0.05) df.loc[_rng.random(_n) < _p, 'C5401'] = np.nan # MNAR num = df.select_dtypes(include='number') miss_flag = num.isnull().astype(int) for col in num.columns[num.isnull().any()][:5]: others = num.drop(columns=col).fillna(num.mean()) others = StandardScaler().fit_transform(others) # 収束を速くする lr = LogisticRegression(max_iter=500).fit(others, miss_flag[col]) print(col, '説明力:', round(lr.score(others, miss_flag[col]), 3)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | # ── この抜粋で使うデータを用意します(練習用の欠損は架空)── # 110 列すべてを MICE に渡すと 1 回の補完に数分かかるので、 # 最新年度の 47 行 × 補完に使う 7 列だけに絞る。 import numpy as np import pandas as pd _USE = ['L3221', 'L322108', 'C5401', 'A1101', 'A1301', 'A1303', 'A4101'] df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023][_USE].copy() _rng = np.random.default_rng(0) _n = len(df) df.loc[_rng.random(_n) < 0.15, 'L3221'] = np.nan # MCAR _p = np.where(df['A1301'] < df['A1301'].median(), 0.30, 0.05) df.loc[_rng.random(_n) < _p, 'L322108'] = np.nan # MAR _p = np.where(df['C5401'] > df['C5401'].quantile(0.75), 0.40, 0.05) df.loc[_rng.random(_n) < _p, 'C5401'] = np.nan # MNAR num = df.select_dtypes(include='number') from sklearn.experimental import enable_iterative_imputer from sklearn.impute import SimpleImputer, KNNImputer, IterativeImputer imp_mean = SimpleImputer(strategy='mean').fit_transform(num) imp_knn = KNNImputer(n_neighbors=5).fit_transform(num) imp_mice = IterativeImputer(max_iter=10, random_state=0).fit_transform(num) # 欠損を作る前の「本当の平均」も並べて、どの補完がそれに近いかを見る _true = (pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) .query('`SSDSE-B-2026` == 2023')[_USE].mean()) result = pd.DataFrame({ '真値': _true, '欠損後': num.mean(), '平均補完': pd.DataFrame(imp_mean, columns=num.columns).mean(), 'KNN': pd.DataFrame(imp_knn, columns=num.columns).mean(), 'MICE': pd.DataFrame(imp_mice, columns=num.columns).mean()}) # 欠損を作った 3 列だけを見る print(result.loc[['L3221', 'L322108', 'C5401']].round(1)) |
📤 実行すると次の出力が得られる:
💬 結果の読み方:3 行がそのまま 3 つの仕組みの教科書的な違いになっている。 MCAR の L3221 はどの補完でも真値 295,856 とほぼ一致(欠損があっても平均は歪まない)。 MAR の L322108 は平均補完だと 10,077 と真値 9,577 より高く出るが、 他の列を使う KNN・MICE は 9,881・9,777 まで戻せる。 MNAR の C5401 は、 値が高い県ほど欠けているので観測平均が 51,045 まで下振れし、 平均補完はそれをそのまま引きずる。 MICE は 52,846 と真値 54,638 の方向へ戻すが、 これは「他の列に価格と相関する情報が残っていた」から成功したのであり、 MNAR なら常にこう救えるわけではない点に注意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # ── この抜粋で使うデータを用意します(練習用の欠損は架空)── import numpy as np import statsmodels.api as sm from statsmodels.imputation.mice import MICE, MICEData import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 df = df.copy() df['death_rate'] = df['A4200'] / df['A1101'] * 1000 # 人口千人あたり死亡数 df['aging_rate'] = df['A1303'] / df['A1101'] * 100 # 高齢化率 % df['medical_cost'] = df['L322106'].astype(float) # 保健医療費 num = df[['death_rate', 'aging_rate', 'medical_cost']].copy() _rng = np.random.default_rng(0) num.loc[_rng.random(len(num)) < 0.15, 'medical_cost'] = np.nan # 練習用の欠損 mice_data = MICEData(num) mice = MICE('death_rate ~ aging_rate + medical_cost', sm.OLS, mice_data) print(mice.fit(n_burnin=5, n_imputations=20).summary()) |
1 2 3 | imp = SimpleImputer(strategy='median', add_indicator=True) X = imp.fit_transform(num) print('元の列数:', num.shape[1], '/ 補完後:', X.shape[1]) |
🎯 このコードでやること: 上記で作成した MAR 欠損データに対し、 sklearn の IterativeImputer(MICE の単純版)で多重代入を実行し、 単純な mean imputation との平均推定精度を比較する。
📥 入力: SSDSE-B-2026 で MAR 欠損を注入した df。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import pandas as pd import numpy as np from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer, SimpleImputer df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]); df = df[df['SSDSE-B-2026'] == 2023].dropna() true_mean = df['A1101'].mean() rng = np.random.default_rng(0) data = df[['A1101', 'A1301', 'A1303', 'A4101', 'B4101']].copy() prob = 0.2 + 0.6 * (data['A1301'] > data['A1301'].median()) mask = rng.random(len(data)) < prob data.loc[mask, 'A1101'] = np.nan # (1) mean imputation mean_imp = SimpleImputer(strategy='mean').fit_transform(data) mean_pop = mean_imp[:, 0].mean() # (2) iterative imputation (MICE-like) mice_imp = IterativeImputer(max_iter=20, random_state=0).fit_transform(data) mice_pop = mice_imp[:, 0].mean() print(f'真値 : {true_mean:,.0f}') print(f'mean imputation: {mean_pop:,.0f} bias={mean_pop-true_mean:+,.0f}') print(f'MICE (反復補完): {mice_pop:,.0f} bias={mice_pop-true_mean:+,.0f}') |
📤 実行例:
💬 結果の読み方: mean imputation は欠損していない値の平均で穴埋めするので、 MAR で大きく偏った観測平均をそのまま継承する → bias 大。 MICE は 15歳未満人口(A1301) など補助変数を使って欠損値を回帰予測するので、 MAR 仮定が満たされていれば bias が大幅に縮小する。 これが「補助変数を MICE モデルに入れる」ことの効果。
🎯 このコードでやること: statsmodels の MICEData を使って、 m=5 個の補完済みデータセットを生成し、 各々で平均を推定して Rubin の規則でプールする「正しい多重代入」を実行する。
📥 入力: 上記と同じ MAR 欠損データ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import pandas as pd import numpy as np from statsmodels.imputation.mice import MICEData np.random.seed(0) # MICEData は内部で np.random を使う。 ここで固定すると結果が再現する df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]); df = df[df['SSDSE-B-2026'] == 2023].dropna() true_mean = df['A1101'].mean() rng = np.random.default_rng(0) data = df[['A1101', 'A1301', 'A1303', 'B4101']].copy() mask = rng.random(len(data)) < (0.2 + 0.6 * (data['A1301'] > data['A1301'].median())) data.loc[mask, 'A1101'] = np.nan mice = MICEData(data) means, vars_ = [], [] for k in range(5): mice.update_all() # 1 サイクル分の Gibbs サンプリング means.append(mice.data['A1101'].mean()) vars_.append(mice.data['A1101'].var(ddof=1) / len(data)) m = len(means) qbar = np.mean(means) ubar = np.mean(vars_) # 平均的な within-imputation variance B = np.var(means, ddof=1) # between-imputation variance T = ubar + (1 + 1/m) * B # Rubin の総合分散 se = np.sqrt(T) print(f'pooled mean = {qbar:,.0f} 真値 = {true_mean:,.0f}') print(f'pooled SE = {se:,.0f} 95% CI ≈ [{qbar-1.96*se:,.0f}, {qbar+1.96*se:,.0f}]') |
📤 実行例:
💬 結果の読み方:MICEData は内部で np.random を使うため、 種を固定しないと実行のたびに pooled mean が 175〜197 万人の範囲で動きます。 上のコードでは冒頭に np.random.seed(0) を置いてあるので、 ここに載せた値は何度実行しても再現します。 中身を見ると pooled mean 1,908,013 は真値 2,645,809 を 大きく下回っていますが、 95% CI の上端 2,642,851 がちょうど真値に届いており、 区間としてはかろうじて真値を含みます。 これは「人口が多い県ほど欠測しやすい」MAR 構造のため点推定が下振れし、 その不確実性を Rubin の総合分散 T が幅として吸収している状態です。 ばらつきを標準誤差に反映させることが多重代入の目的であって、 ばらつきを言い訳にして値を固定しないことではない点に注意してください。
💬 結果の読み方: m=5 個の補完で得た 5 つの推定値を平均し、 内 within 分散 $\bar U$ と外 between 分散 $B$ を Rubin の公式 $T=\bar U+(1+1/m)B$ で合成。 これにより「補完自体が不確実だ」という情報が信頼区間に反映され、 単一代入よりも広い区間になる。 📝 より正確な分析(教材補足): 今回のように MAR の依存が強く n=47 と小さいと、 プール平均(実測で 175〜197 万人)にも残差バイアスが残り、 真値 2,645,809 が 95% CI の外に出ることがある。 これは「多重代入は万能ではない」ことを示すが、 補完の不確実性を区間幅に誠実に反映するという MI の本質的価値は変わらない。 実務では補助変数の追加や感度分析でこの残差バイアスに対処する。
🎯 このコードでやること: 欠損は「どこに、 どんなパターンで」入っているかを目視で診断する。 missingno を使って matrix / heatmap / dendrogram を描画し、 MAR/MNAR の手掛かりを得る。
📥 入力: SSDSE-B-2026 に MAR 欠損を注入したデータ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd import numpy as np import missingno as msno import matplotlib.pyplot as plt # 英字の項目コード(A1101 など)を使うので skiprows=[1] で読む df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].dropna() rng = np.random.default_rng(0) data = df[['A1101', 'A1301', 'A1303', 'B4101', 'A4101']].copy() data.loc[rng.random(len(data)) < (0.2 + 0.6 * (data['A1301'] > data['A1301'].median())), 'A1101'] = np.nan data.loc[rng.random(len(data)) < 0.3, 'A4101'] = np.nan # 各列の欠損率 print(data.isna().mean().round(3)) # 欠損相関(同時に欠ける傾向) print(data.isna().corr().round(2)) # matrix プロット msno.matrix(data, figsize=(8, 4)) plt.tight_layout() plt.savefig('missing_matrix.png', dpi=100) |
📤 実行例:
💬 結果の読み方: 「総人口(A1101)」が約 43% 欠損、 「出生数(A4101)」が約 28% 欠損。 両者の欠損相関は 0.05 ≈ 0、 つまり「同時に欠ける」傾向はない(独立した欠損パターン)。 もし欠損相関が高ければ、 同一の原因(例: 同じ調査票の同じ問)で欠損している可能性を疑う必要がある。 matrix プロットはこれを 視覚的に 一望できるので、 欠損診断の第一歩として強く推奨される。
Little (1988) の MCAR 検定は、 観測サブセットごとの平均ベクトル $\bar y^{(g)}$ が全体の平均ベクトル $\bar y$ と等しいかを Wald 型統計量で評価する:
$$ d^2 = \sum_{g=1}^G n_g (\bar y^{(g)} - \bar y)^\top \hat\Sigma_g^{-1} (\bar y^{(g)} - \bar y) \;\sim\; \chi^2_{\nu} $$$d^2$ が大きく p<0.05 なら「MCAR を棄却」= 少なくとも MAR か MNAR。 ただし MCAR を 棄却できなかった としても MCAR が成立する証拠にはならない(仮説検定の非対称性)。 欠損相関の可視化と組み合わせて総合判断するのが実務的。
「データ前処理として、 各変数の欠損率を表 X に示した(最大欠損率 43.2%、 総人口)。 欠損メカニズムは Little (1988) の MCAR 検定で評価したところ、 $\chi^2 = 28.4$, df=12, p=0.005 となり MCAR は棄却された。 そのため MAR を仮定し、 補助変数として 15歳未満人口・65歳以上人口・年平均気温・出生数 を含めた連鎖式多重代入(MICE; van Buuren 2018)を m=20 回反復で実施した。 推定値とその標準誤差は Rubin (1987) の規則でプールした。 MNAR の可能性については pattern-mixture model による感度分析を補論 B に記載した。」
このテンプレを基に、 自分のデータの数値に置き換えるだけで論文の Methods 節になる。 「補助変数として何を入れたか」を明記することが、 査読者から最も問われるポイント。
| 落とし穴 | 何が起きるか | 推奨対応 |
|---|---|---|
| complete case 分析 | サンプル数大幅減、 MAR/MNAR でバイアス | 多重代入を試す |
| mean imputation | 分散が縮む、 相関が弱まる、 標準誤差が小さくなりすぎ | 回帰代入・MICE |
| 最頻値代入 | カテゴリ変数で最頻値に過剰集中 | multinomial logit 代入 |
| 単一代入 | 代入の不確実性を反映できない → 信頼区間が狭すぎる | 多重代入 (m≥5) |
| 補助変数の不足 | MAR を MAR にできない | 関連しそうな変数を全部入れる |
| MNAR を MAR とみなす | バイアスが残る | 感度分析(pattern-mixture) |
| 代入後の test 漏洩 | test の値が train の補完に混入 | Pipeline で train のみ fit |
| 欠損率の報告漏れ | 論文の信頼性が下がる | 各変数の欠損率を必ず明記 |
特に 単一代入と多重代入の違い を理解していない研究者は多い。 「補完したから OK」と片付ける前に、 「補完の不確実性」を信頼区間に反映できているか必ず確認すること。
| 条件 | 推奨アプローチ |
|---|---|
| 欠損率 < 5% かつ MCAR っぽい | complete case でも可 |
| 欠損率 5-30% かつ MAR が妥当 | 多重代入 (m=5-20) |
| 欠損率 > 30% | 変数自体を除外検討 + 多重代入 + 感度分析 |
| MNAR の可能性が高い | pattern-mixture / selection model + 感度分析 |
| 時系列・パネルデータ | Amelia / Kalman smoother / state-space |
| 予測タスクで CV を回したい | Pipeline 内で IterativeImputer |
💡 大原則: 「欠損は 情報 である」。 欠損のパターン自体が研究対象の挙動を語ることがある(例: 「収入が高い人ほど答えない」という事実が、 すでに収入分布の歪みを示唆している)。 単に補完して進むのではなく、 まず欠損の意味を考えること。
以下の 7 問に答えてから先へ進む。 解答は各設問の 解説 を畳んで確認すること。 「3 メカニズムを言葉だけで説明できるが、 補完手法の選択に詰まる」という人を主な対象としている。
MCAR は欠損確率が観測値・欠損値・他のどの変数とも独立、 MAR は 観測されている変数で条件付けると欠損値そのものとは独立。 つまり MAR は「観測情報で欠損が説明できる」、 MCAR は「説明する必要すらない」。 SSDSE-B-2026 で言えば、 「65歳以上人口の割合が高い県ほど無回答」が MAR、 「すべての県でランダムに無回答」が MCAR。
原理的に不可能。 MNAR は「欠損値そのもの」に依存するため、 欠損した値が観測できない以上、 データから直接同定はできない。 実務では 感度分析(δ-adjustment や pattern-mixture)で MNAR の仮定パラメータを動かして結論の頑健性を確認する。
(1) 欠損した 5 県の他の変数(総人口、 15歳未満人口、 合計特殊出生率)の分布が、 他の 42 県と系統的に異なるか確認。 (2) 異なるなら MAR 以上を疑う。 (3) Little's MCAR test や missingno の matrix プロットで欠損パターンの偏りを目視。 (4) 専門知識(例: 「貯蓄調査は人口の小さい県で標本数不足になる」)を組み合わせて MAR の妥当性を吟味する。
欠損値を「定数 μ」で埋めると、 その値には変動が 0 になる。 標本分散 $\hat{\sigma}^2 = \frac{1}{n-1}\sum(x_i - \bar{x})^2$ で欠損部分は $(\mu - \bar{x})^2 = 0$ を加算するため、 真の分散より小さくなる。 結果として標準誤差、 信頼区間、 p 値がすべて楽観的に歪む(偽の有意性を出しやすい)。
Rubin の元の推奨は m=3-5 だったが、 現在は欠損率 γ に応じて m ≥ 100γ が目安(欠損率 20% なら m ≈ 20)。 Bodner (2008) は信頼区間幅の安定化に m=20 以上を推奨。 計算コストが許せば m=50 を既定にし、 検出力が問題になる場合は m=100 まで増やす。
「収入の高い人ほど健康に関する質問に答えない」状況で、 健康変数が欠損している行を listwise で削除すると、 残った標本は「低・中所得層中心」になる。 ここで健康と消費を相関分析すると、 高所得層の効果が消えて推定が歪む。 MAR の場合 listwise は 不偏ではない。 MI / FIML が必要。
IterativeImputer は変数間の 条件付き分布 を反復的に学習する MICE 系で、 連続変数 + 多変量正規に近い場合に強い。 KNNImputer は 距離に基づく局所平均で、 サンプル数が小さく非線形構造(クラスタ性)がある場合に有効だが、 高次元では距離が劣化する。 SSDSE-B-2026 のような 47 行 × 数十列の表では IterativeImputer の方が扱いやすい。
💡 採点ガイド: 5 問以上正答できれば「実務適用フェーズ」に進める。 3-4 問なら本ページの 🌍 ケーススタディ + 📐 数式定義に戻る。 2 問以下なら 🎨 直感セクションから読み直すこと。
3 つのメカニズム(MCAR / MAR / MNAR)は、 単に「ランダムか否か」では区別しきれない。 観測値と欠損値の 分布のずれ を視覚的に確認することで、 仮定の妥当性が見える。 ここでは SSDSE-B-2026 を素材にした典型図を 3 枚並べ、 「何を見れば次のステップが決まるか」を示す。
📌 視覚的診断のチェックリスト: (1) 欠損あり/なし群のヒストグラム重ね合わせ、 (2) 散布図上での欠損行マーカー、 (3) missingno の matrix / heatmap、 (4) ロジスティック回帰で「欠損 vs 観測変数」を回帰し有意性を見る。 4 つすべて見て初めて MAR の妥当性を主張できる。
欠損機構 (MCAR / MAR / MNAR) は単独で扱う概念ではなく、 上流の欠損パターン可視化 (missingno)、 並列の補完手法選択 (mean / 多重代入 / モデルベース)、 下流の感度分析と組み合わせて初めて妥当な分析になる。 機構を誤判定すると補完がバイアスを生む。
SSDSE-B-2026 にあえて欠損を入れて検証する場合、 例えば「高所得県ほど一部列が欠損」(MNAR) を仮定して、 上流で欠損パターン行列を可視化、 中段で MICE 多重代入、 下流で補完ありと完全ケース分析の結果を比較、 という三段構成で頑健性を確認する。
欠損機構の判定と補完方針は、 (1) 観測変数間に欠損依存があるか (Little MCAR 検定)、 (2) 欠損が結果変数と関係するか、 (3) 補完前後で結論が変わるか、 で決まる。 MCAR なら listwise OK、 MAR なら MICE、 MNAR は感度分析必須。
実務では「完全ケース / 平均補完 / MICE / Pattern-mixture」の 4 法を必ず並べ、 結論が変わらないことを確認するのが標準で、 結論が手法依存なら欠損機構自体を再検討する。
下は 架空データ(実在の統計ではありません)です。 200 人分の x=年齢 と y=所得型スコア(年齢とともに増える相関あり)を生成しています。 欠損メカニズムを 3 つのボタンで切り替え、 欠損率スライダーを動かすと、 観測できる点(濃い点)と欠損した点(グレーの中空◯)、 そして 完全平均・観測平均・回帰代入後平均 の 3 本の水平線がリアルタイムで動きます。
ねらい:MCAR では観測平均が完全平均とほぼ一致(不偏)、 MAR / MNAR では観測平均が下にズレる(バイアス)ことを体感してください。 グラフ上を左右にドラッグしても欠損率を変えられます(タッチ対応)。
同じ欠損データに 3 つの対処法を当てた結果です(数値は上のスライダー設定でリアルタイム更新)。 平均代入 は平均は観測平均のまま動かず、 さらに ばらつき(標準偏差)を過小評価 する点に注目。
| 手法 | 推定した平均 | 真値とのズレ | 標準偏差 | 性質 |
|---|
真値(完全平均)= / 完全データの標準偏差= 。 リストワイズ削除は観測平均と同じ値になります(欠損行を捨てるだけのため)。
このページ上部の対話ウィジェットは、欠測メカニズムが 平均の推定 に与える偏りを扱いました。ここでは重複を避け、別の角度から掘り下げます。すなわち欠測は 変数どうしの関係(相関係数・回帰の傾き) にも独立に効き、しかもその効き方は「どの処理をするか」で真逆になる、という点です。数値は data/raw/SSDSE-B-2026.csv(2023 年・47 都道府県)の 実測値 を用い、欠測を起こす操作だけを「架空の欠損シナリオ」として明示します。
MCAR / MAR / MNAR は「抜けた理由がデータのどこに書いてあるか」の階段として捉えると腹落ちします。
重要なのは、この階段が 「何を推定したいか」ごとに別々に効く ことです。同じ欠測でも、平均は大きく偏るのに回帰の傾きはほとんど偏らない、という組み合わせが普通に起こります。「欠測メカニズム=一つの偏りの向き」ではありません。
実データで確かめます。x = 15歳未満人口(A1301)、y = 出生数(A4101)は 2023 年 47 都道府県で ほぼ完全な線形関係(真の相関 0.9977、傾き 0.0549、出生数の真の平均 15,474 人)にあります。ここに 架空の欠損シナリオ を入れます:「15歳未満人口が多い県ほど y が抜けやすい」(欠測確率が観測列 x に依存 = MAR、seed=42、14/47 件=29.8% を欠損)。この 同じ欠測データ を 2 通りに処理した実測結果が下表です。
| 処理 | 傾き(真値 0.0549) | 相関(真値 0.9977) | y の平均(真値 15,474) |
|---|---|---|---|
| 完全ケース削除 欠測行を捨てる |
0.0558 ほぼ不偏 |
0.9976 ほぼ不偏 |
13,335 −2,139 下振れ |
| 平均代入 欠測 y を観測平均で埋める |
0.0314 −43% 縮小 |
0.7486 0.998→0.749 に崩壊 |
13,335 見かけ上は保たれるが… |
同じ MAR 欠測なのに、削除は「平均」を壊し、平均代入は「関係」を壊す という真逆の結果です。落とし穴を 3 つに整理します。
なぜ削除で傾きが不偏だったのか。この架空シナリオでは欠測が x のみに依存し、各 x における y|x の条件付き分布は保存されました。回帰は「x を与えたときの y」を推定するので、x 軸上でサンプルが偏っても傾きは不偏です(ただし高 x 側が減るぶん傾きの分散は増える)。逆に y に依存する欠測(MNAR)や、傾き自体に依存する欠測 なら傾きも偏ります。「削除は常にダメ/代入は常に安全」という単純な標語は成立しません。
目的で必要な仮定が変わる。純粋な 予測 なら(木モデルの欠損分岐など)平均代入や単純補完でも実害が小さいことがありますが、推論(相関・係数・信頼区間)では上表のように補完法が結論を左右します。補完の良し悪しは「平均が合うか」だけでなく「関係・分散・不確実性まで再現できるか」で判断すべきで、これが単一代入より多重代入(MI)が推奨される理由です。
📝 再現手順(架空の欠損シナリオ・実データ):data/raw/SSDSE-B-2026.csv を pd.read_csv(encoding='cp932', skiprows=[1]) で読み、df[df['SSDSE-B-2026']==2023] の A1301(x) と A4101(y) を取得。np.random.RandomState(42) で「欠測確率 = 0.15 + 0.6×(x を 0–1 正規化)」の一様乱数判定により 14 件を y-欠測させ、(1) 欠測行削除 と (2) 観測平均での代入 の各々で np.polyfit の傾き・np.corrcoef の相関を算出したものが上表です。乱数で欠測させる操作のみが架空で、元の x・y と真値は実測値です。