statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
missing_data.hpp
Go to the documentation of this file.
1
16#pragma once
17
18#include <algorithm>
19#include <cmath>
20#include <cstddef>
21#include <random>
22#include <stdexcept>
23#include <string>
24#include <utility>
25#include <vector>
26
31
32namespace statcpp {
33
34// ============================================================================
35// Missing Data Pattern Classification (MCAR/MAR/MNAR)
36// ============================================================================
37
47 mcar,
48 mar,
49 mnar,
50 unknown
51};
52
60 double chi_square = 0.0;
61 double p_value = 1.0;
62 std::size_t df = 0;
63 bool is_mcar = true;
64 std::string interpretation;
65};
66
74 std::vector<std::vector<uint8_t>> patterns;
75 std::vector<std::size_t> pattern_counts;
76 std::vector<double> missing_rates;
77 double overall_missing_rate = 0.0;
78 std::size_t n_complete_cases = 0;
79 std::size_t n_patterns = 0;
80};
81
93 const std::vector<std::vector<double>>& data)
94{
95 if (data.empty()) {
96 throw std::invalid_argument(
97 "statcpp::analyze_missing_patterns: empty data");
98 }
99
101 std::size_t n_rows = data.size();
102 std::size_t n_cols = data[0].size();
103
104 // Calculate missing rate per variable
105 result.missing_rates.resize(n_cols, 0.0);
106 std::size_t total_missing = 0;
107
108 for (const auto& row : data) {
109 if (row.size() != n_cols) {
110 throw std::invalid_argument(
111 "statcpp::analyze_missing_patterns: inconsistent row sizes");
112 }
113 for (std::size_t j = 0; j < n_cols; ++j) {
114 if (is_na(row[j])) {
115 ++result.missing_rates[j];
116 ++total_missing;
117 }
118 }
119 }
120
121 for (auto& rate : result.missing_rates) {
122 rate /= static_cast<double>(n_rows);
123 }
124 result.overall_missing_rate = static_cast<double>(total_missing) /
125 static_cast<double>(n_rows * n_cols);
126
127 // Extract missing patterns (implemented with linear search instead of std::map)
128 std::vector<std::vector<uint8_t>> unique_patterns;
129 std::vector<std::size_t> pattern_counts;
130 result.n_complete_cases = 0;
131
132 for (const auto& row : data) {
133 std::vector<uint8_t> pattern(n_cols);
134 bool has_missing = false;
135 for (std::size_t j = 0; j < n_cols; ++j) {
136 pattern[j] = is_na(row[j]) ? 1 : 0;
137 if (pattern[j]) {
138 has_missing = true;
139 }
140 }
141 if (!has_missing) {
142 ++result.n_complete_cases;
143 }
144
145 // Search for existing pattern
146 bool found = false;
147 for (std::size_t i = 0; i < unique_patterns.size(); ++i) {
148 if (unique_patterns[i] == pattern) {
149 ++pattern_counts[i];
150 found = true;
151 break;
152 }
153 }
154
155 // Add if new pattern
156 if (!found) {
157 unique_patterns.push_back(pattern);
158 pattern_counts.push_back(1);
159 }
160 }
161
162 // Store results
163 result.patterns = unique_patterns;
164 result.pattern_counts = pattern_counts;
165 result.n_patterns = result.patterns.size();
166
167 return result;
168}
169
179inline std::vector<std::vector<double>> create_missing_indicator(
180 const std::vector<std::vector<double>>& data)
181{
182 std::vector<std::vector<double>> indicator;
183 indicator.reserve(data.size());
184
185 for (const auto& row : data) {
186 std::vector<double> ind_row;
187 ind_row.reserve(row.size());
188 for (double val : row) {
189 ind_row.push_back(is_na(val) ? 1.0 : 0.0);
190 }
191 indicator.push_back(std::move(ind_row));
192 }
193 return indicator;
194}
195
210 const std::vector<std::vector<double>>& data)
211{
212 if (data.empty()) {
213 throw std::invalid_argument("statcpp::test_mcar_simple: empty data");
214 }
215
216 std::size_t n_cols = data[0].size();
217
218 // Validate consistent row sizes
219 for (const auto& row : data) {
220 if (row.size() != n_cols) {
221 throw std::invalid_argument(
222 "statcpp::test_mcar_simple: inconsistent row sizes");
223 }
224 }
225
226 mcar_test_result result;
227
228 // Check correlation between missingness in each variable and values of other variables
229 // Under MCAR, missingness of one variable is unrelated to values of other variables
230 double total_chi_sq = 0.0;
231 std::size_t total_df = 0;
232
233 for (std::size_t j = 0; j < n_cols; ++j) {
234 // Missing indicator for variable j
235 std::vector<double> missing_j;
236 missing_j.reserve(data.size());
237 for (const auto& row : data) {
238 missing_j.push_back(is_na(row[j]) ? 1.0 : 0.0);
239 }
240
241 // Check correlation with other variables
242 for (std::size_t k = 0; k < n_cols; ++k) {
243 if (j == k) continue;
244
245 // Observed values of variable k (only cases where both are observed)
246 std::vector<double> obs_k;
247 std::vector<double> miss_j_subset;
248
249 for (std::size_t i = 0; i < data.size(); ++i) {
250 if (!is_na(data[i][k])) {
251 obs_k.push_back(data[i][k]);
252 miss_j_subset.push_back(missing_j[i]);
253 }
254 }
255
256 if (obs_k.size() < 5) continue; // Sample size too small
257
258 // Compare means between missing and observed groups
259 std::vector<double> obs_when_j_missing;
260 std::vector<double> obs_when_j_observed;
261
262 for (std::size_t i = 0; i < obs_k.size(); ++i) {
263 if (miss_j_subset[i] > 0.5) {
264 obs_when_j_missing.push_back(obs_k[i]);
265 } else {
266 obs_when_j_observed.push_back(obs_k[i]);
267 }
268 }
269
270 if (obs_when_j_missing.size() < 2 || obs_when_j_observed.size() < 2) {
271 continue;
272 }
273
274 // Calculate two-sample t-test statistic
275 double mean1 = mean(obs_when_j_missing.begin(), obs_when_j_missing.end());
276 double mean2 = mean(obs_when_j_observed.begin(), obs_when_j_observed.end());
277 double var1 = var(obs_when_j_missing.begin(), obs_when_j_missing.end(), 1);
278 double var2 = var(obs_when_j_observed.begin(), obs_when_j_observed.end(), 1);
279
280 double n1 = static_cast<double>(obs_when_j_missing.size());
281 double n2 = static_cast<double>(obs_when_j_observed.size());
282
283 double se = std::sqrt(var1 / n1 + var2 / n2);
284 if (se > 1e-10) {
285 double t_stat = (mean1 - mean2) / se;
286 total_chi_sq += t_stat * t_stat;
287 ++total_df;
288 }
289 }
290 }
291
292 result.chi_square = total_chi_sq;
293 result.df = total_df;
294
295 // Approximate p-value from chi-square distribution (Wilson-Hilferty approximation)
296 if (total_df > 0) {
297 double z = std::pow(total_chi_sq / static_cast<double>(total_df), 1.0 / 3.0) -
298 (1.0 - 2.0 / (9.0 * static_cast<double>(total_df)));
299 z /= std::sqrt(2.0 / (9.0 * static_cast<double>(total_df)));
300 // Upper tail probability of standard normal distribution (approximation)
301 result.p_value = 0.5 * std::erfc(z / std::sqrt(2.0));
302 result.p_value = std::max(0.0, std::min(1.0, result.p_value));
303 } else {
304 result.p_value = 1.0;
305 }
306
307 result.is_mcar = (result.p_value > 0.05);
308 result.interpretation = result.is_mcar
309 ? "MCAR assumption is not rejected (p > 0.05). "
310 "Missing data may be completely random."
311 : "MCAR assumption is rejected (p <= 0.05). "
312 "Missing data is likely MAR or MNAR.";
313
314 return result;
315}
316
331 const std::vector<std::vector<double>>& data)
332{
333 auto mcar_result = test_mcar_simple(data);
334
335 if (mcar_result.is_mcar) {
337 }
338
339 // Distinguishing MAR vs MNAR is difficult with observed data alone
340 // Here we assume MAR (conservative choice)
342}
343
344// ============================================================================
345// Multiple Imputation
346// ============================================================================
347
356 std::vector<std::vector<std::vector<double>>> imputed_datasets;
357 std::size_t m = 0;
358 std::vector<double> pooled_means;
359 std::vector<double> pooled_vars;
360 std::vector<double> within_vars;
361 std::vector<double> between_vars;
362 std::vector<double> fraction_missing_info;
363};
364
375inline std::vector<double> impute_conditional_mean(
376 const std::vector<std::vector<double>>& data,
377 std::size_t target_col,
378 const std::vector<std::size_t>& predictor_cols)
379{
380 std::size_t n = data.size();
381 std::vector<double> result;
382 result.reserve(n);
383
384 // Extract complete cases
385 std::vector<std::vector<double>> complete_cases;
386 for (const auto& row : data) {
387 bool complete = !is_na(row[target_col]);
388 for (std::size_t col : predictor_cols) {
389 if (is_na(row[col])) {
390 complete = false;
391 break;
392 }
393 }
394 if (complete) {
395 complete_cases.push_back(row);
396 }
397 }
398
399 if (complete_cases.empty()) {
400 // If no complete cases, use simple mean imputation
401 std::vector<double> non_na;
402 for (const auto& row : data) {
403 if (!is_na(row[target_col])) {
404 non_na.push_back(row[target_col]);
405 }
406 }
407 double fill_val = non_na.empty() ? 0.0 : mean(non_na.begin(), non_na.end());
408 for (const auto& row : data) {
409 result.push_back(is_na(row[target_col]) ? fill_val : row[target_col]);
410 }
411 return result;
412 }
413
414 // Estimate imputed values using simple linear regression
415 // Y = target_col, X = predictor_cols (simplified version using only first predictor)
416 if (predictor_cols.empty()) {
417 double m = 0.0;
418 for (const auto& row : complete_cases) {
419 m += row[target_col];
420 }
421 m /= static_cast<double>(complete_cases.size());
422 for (const auto& row : data) {
423 result.push_back(is_na(row[target_col]) ? m : row[target_col]);
424 }
425 return result;
426 }
427
428 // Simple regression using first predictor
429 std::size_t pred_col = predictor_cols[0];
430 std::vector<double> x_vals, y_vals;
431 for (const auto& row : complete_cases) {
432 x_vals.push_back(row[pred_col]);
433 y_vals.push_back(row[target_col]);
434 }
435
436 double x_mean = mean(x_vals.begin(), x_vals.end());
437 double y_mean = mean(y_vals.begin(), y_vals.end());
438
439 double cov_xy = 0.0;
440 double var_x = 0.0;
441 for (std::size_t i = 0; i < x_vals.size(); ++i) {
442 double dx = x_vals[i] - x_mean;
443 double dy = y_vals[i] - y_mean;
444 cov_xy += dx * dy;
445 var_x += dx * dx;
446 }
447
448 double beta = (var_x > 1e-10) ? cov_xy / var_x : 0.0;
449 double alpha = y_mean - beta * x_mean;
450
451 // Imputation
452 for (const auto& row : data) {
453 if (is_na(row[target_col])) {
454 if (!is_na(row[pred_col])) {
455 result.push_back(alpha + beta * row[pred_col]);
456 } else {
457 result.push_back(y_mean);
458 }
459 } else {
460 result.push_back(row[target_col]);
461 }
462 }
463
464 return result;
465}
466
481 const std::vector<std::vector<double>>& data,
482 std::size_t m = 5,
483 unsigned int seed = 0)
484{
485 if (data.empty()) {
486 throw std::invalid_argument(
487 "statcpp::multiple_imputation_pmm: empty data");
488 }
489
490 if (m < 2) {
491 throw std::invalid_argument(
492 "statcpp::multiple_imputation_pmm: m must be >= 2 for Rubin's rules");
493 }
494
495 std::size_t n_rows = data.size();
496 std::size_t n_cols = data[0].size();
497
498 // Validate consistent row sizes
499 for (const auto& row : data) {
500 if (row.size() != n_cols) {
501 throw std::invalid_argument(
502 "statcpp::multiple_imputation_pmm: inconsistent row sizes");
503 }
504 }
505
507 result.m = m;
508 result.imputed_datasets.resize(m);
509
510 // Random number generator
511 std::mt19937 rng(seed == 0 ? std::random_device{}() : seed);
512
513 for (std::size_t imp = 0; imp < m; ++imp) {
514 // Copy data
515 auto imputed = data;
516
517 // Impute each column
518 for (std::size_t j = 0; j < n_cols; ++j) {
519 // Collect missing indices
520 std::vector<std::size_t> missing_indices;
521 std::vector<std::size_t> observed_indices;
522 std::vector<double> observed_values;
523
524 for (std::size_t i = 0; i < n_rows; ++i) {
525 if (is_na(imputed[i][j])) {
526 missing_indices.push_back(i);
527 } else {
528 observed_indices.push_back(i);
529 observed_values.push_back(imputed[i][j]);
530 }
531 }
532
533 if (missing_indices.empty() || observed_values.empty()) {
534 continue;
535 }
536
537 // Use other columns as predictor variables
538 std::vector<std::size_t> predictor_cols;
539 for (std::size_t k = 0; k < n_cols; ++k) {
540 if (k != j) {
541 predictor_cols.push_back(k);
542 }
543 }
544
545 // Calculate conditional means
546 auto cond_means = impute_conditional_mean(imputed, j, predictor_cols);
547
548 // PMM: Randomly select from k observations closest to predicted value
549 constexpr std::size_t k_donors = 5;
550
551 for (std::size_t idx : missing_indices) {
552 double pred_val = cond_means[idx];
553
554 // Calculate distance to observed values
555 std::vector<std::pair<double, std::size_t>> distances;
556 distances.reserve(observed_indices.size());
557 for (std::size_t oi = 0; oi < observed_indices.size(); ++oi) {
558 std::size_t obs_idx = observed_indices[oi];
559 double dist = std::abs(observed_values[oi] - pred_val);
560 distances.emplace_back(dist, obs_idx);
561 }
562
563 // Sort by distance
564 std::partial_sort(distances.begin(),
565 distances.begin() + std::min(k_donors, distances.size()),
566 distances.end());
567
568 // Randomly select from k donors
569 std::size_t n_donors = std::min(k_donors, distances.size());
570 std::uniform_int_distribution<std::size_t> dist(0, n_donors - 1);
571 std::size_t donor_idx = distances[dist(rng)].second;
572
573 imputed[idx][j] = imputed[donor_idx][j];
574 }
575 }
576
577 result.imputed_datasets[imp] = std::move(imputed);
578 }
579
580 // Pooling using Rubin's rules
581 result.pooled_means.resize(n_cols, 0.0);
582 result.pooled_vars.resize(n_cols, 0.0);
583 result.within_vars.resize(n_cols, 0.0);
584 result.between_vars.resize(n_cols, 0.0);
585 result.fraction_missing_info.resize(n_cols, 0.0);
586
587 for (std::size_t j = 0; j < n_cols; ++j) {
588 std::vector<double> means_j(m);
589 std::vector<double> vars_j(m);
590
591 for (std::size_t imp = 0; imp < m; ++imp) {
592 std::vector<double> col_values;
593 col_values.reserve(n_rows);
594 for (std::size_t i = 0; i < n_rows; ++i) {
595 col_values.push_back(result.imputed_datasets[imp][i][j]);
596 }
597 means_j[imp] = mean(col_values.begin(), col_values.end());
598 vars_j[imp] = var(col_values.begin(), col_values.end(), 1);
599 }
600
601 // Pooled mean
602 result.pooled_means[j] = mean(means_j.begin(), means_j.end());
603
604 // Within-imputation variance (W)
605 result.within_vars[j] = mean(vars_j.begin(), vars_j.end());
606
607 // Between-imputation variance (B)
608 double b = 0.0;
609 for (double mj : means_j) {
610 double diff = mj - result.pooled_means[j];
611 b += diff * diff;
612 }
613 result.between_vars[j] = b / static_cast<double>(m - 1);
614
615 // Total variance
616 result.pooled_vars[j] = result.within_vars[j] +
617 (1.0 + 1.0 / static_cast<double>(m)) * result.between_vars[j];
618
619 // Fraction of missing information (FMI)
620 // Rubin (1987) definition: FMI = (r + 2/(df+3)) / (r+1)
621 // where r = (1 + 1/m) * B / W
622 // Simplified version: FMI ≈ (1 + 1/m) * B / T
623 if (result.pooled_vars[j] > 1e-10) {
624 result.fraction_missing_info[j] =
625 (1.0 + 1.0 / static_cast<double>(m)) * result.between_vars[j] /
626 result.pooled_vars[j];
627 }
628 }
629
630 return result;
631}
632
647 const std::vector<std::vector<double>>& data,
648 std::size_t m = 5,
649 unsigned int seed = 0)
650{
651 if (data.empty()) {
652 throw std::invalid_argument(
653 "statcpp::multiple_imputation_bootstrap: empty data");
654 }
655
656 if (m < 2) {
657 throw std::invalid_argument(
658 "statcpp::multiple_imputation_bootstrap: m must be >= 2 for Rubin's rules");
659 }
660
661 std::size_t n_rows = data.size();
662 std::size_t n_cols = data[0].size();
663
664 // Validate consistent row sizes
665 for (const auto& row : data) {
666 if (row.size() != n_cols) {
667 throw std::invalid_argument(
668 "statcpp::multiple_imputation_bootstrap: inconsistent row sizes");
669 }
670 }
671
673 result.m = m;
674 result.imputed_datasets.resize(m);
675
676 std::mt19937 rng(seed == 0 ? std::random_device{}() : seed);
677
678 for (std::size_t imp = 0; imp < m; ++imp) {
679 // Create bootstrap sample (from observations)
680 std::vector<std::vector<double>> boot_sample;
681 boot_sample.reserve(n_rows);
682
683 std::uniform_int_distribution<std::size_t> dist(0, n_rows - 1);
684 for (std::size_t i = 0; i < n_rows; ++i) {
685 boot_sample.push_back(data[dist(rng)]);
686 }
687
688 // Calculate statistics for each column
689 std::vector<double> col_means(n_cols);
690 std::vector<double> col_stds(n_cols);
691
692 for (std::size_t j = 0; j < n_cols; ++j) {
693 std::vector<double> non_na;
694 for (const auto& row : boot_sample) {
695 if (!is_na(row[j])) {
696 non_na.push_back(row[j]);
697 }
698 }
699 if (!non_na.empty()) {
700 col_means[j] = mean(non_na.begin(), non_na.end());
701 col_stds[j] = non_na.size() > 1 ?
702 std::sqrt(var(non_na.begin(), non_na.end(), 1)) : 0.0;
703 }
704 }
705
706 // Imputation (stochastic imputation from normal distribution)
707 auto imputed = data;
708 std::normal_distribution<double> normal(0.0, 1.0);
709
710 for (std::size_t i = 0; i < n_rows; ++i) {
711 for (std::size_t j = 0; j < n_cols; ++j) {
712 if (is_na(imputed[i][j])) {
713 // Mean + random noise
714 imputed[i][j] = col_means[j] + col_stds[j] * normal(rng);
715 }
716 }
717 }
718
719 result.imputed_datasets[imp] = std::move(imputed);
720 }
721
722 // Pooling using Rubin's rules
723 result.pooled_means.resize(n_cols, 0.0);
724 result.pooled_vars.resize(n_cols, 0.0);
725 result.within_vars.resize(n_cols, 0.0);
726 result.between_vars.resize(n_cols, 0.0);
727 result.fraction_missing_info.resize(n_cols, 0.0);
728
729 for (std::size_t j = 0; j < n_cols; ++j) {
730 std::vector<double> means_j(m);
731 std::vector<double> vars_j(m);
732
733 for (std::size_t imp = 0; imp < m; ++imp) {
734 std::vector<double> col_values;
735 col_values.reserve(n_rows);
736 for (std::size_t i = 0; i < n_rows; ++i) {
737 col_values.push_back(result.imputed_datasets[imp][i][j]);
738 }
739 means_j[imp] = mean(col_values.begin(), col_values.end());
740 vars_j[imp] = var(col_values.begin(), col_values.end(), 1);
741 }
742
743 result.pooled_means[j] = mean(means_j.begin(), means_j.end());
744 result.within_vars[j] = mean(vars_j.begin(), vars_j.end());
745
746 double b = 0.0;
747 for (double mj : means_j) {
748 double diff = mj - result.pooled_means[j];
749 b += diff * diff;
750 }
751 result.between_vars[j] = b / static_cast<double>(m - 1);
752
753 result.pooled_vars[j] = result.within_vars[j] +
754 (1.0 + 1.0 / static_cast<double>(m)) * result.between_vars[j];
755
756 // Fraction of missing information (FMI): Rubin (1987)
757 if (result.pooled_vars[j] > 1e-10) {
758 result.fraction_missing_info[j] =
759 (1.0 + 1.0 / static_cast<double>(m)) * result.between_vars[j] /
760 result.pooled_vars[j];
761 }
762 }
763
764 return result;
765}
766
767// ============================================================================
768// Sensitivity Analysis for Missing Data
769// ============================================================================
770
778 std::vector<double> delta_values;
779 std::vector<double> estimated_means;
780 std::vector<double> estimated_vars;
781 double original_mean = 0.0;
782 double original_var = 0.0;
783 std::string interpretation;
784};
785
811 const std::vector<double>& data,
812 const std::vector<double>& delta_values)
813{
814 if (data.empty()) {
815 throw std::invalid_argument(
816 "statcpp::sensitivity_analysis_pattern_mixture: empty data");
817 }
818
820 result.delta_values = delta_values;
821 result.estimated_means.reserve(delta_values.size());
822 result.estimated_vars.reserve(delta_values.size());
823
824 // Statistics of observed values
825 std::vector<double> observed;
826 std::size_t n_missing = 0;
827 for (double val : data) {
828 if (!is_na(val)) {
829 observed.push_back(val);
830 } else {
831 ++n_missing;
832 }
833 }
834
835 if (observed.empty()) {
836 throw std::invalid_argument(
837 "statcpp::sensitivity_analysis_pattern_mixture: all values are missing");
838 }
839
840 double obs_mean = mean(observed.begin(), observed.end());
841 double obs_var = observed.size() > 1 ?
842 var(observed.begin(), observed.end(), 1) : 0.0;
843
844 result.original_mean = obs_mean;
845 result.original_var = obs_var;
846
847 double n_obs = static_cast<double>(observed.size());
848 double n_total = static_cast<double>(data.size());
849 double prop_obs = n_obs / n_total;
850 double prop_miss = static_cast<double>(n_missing) / n_total;
851
852 // Estimate for each delta value
853 for (double delta : delta_values) {
854 // Pattern mixture model: E[Y] = E[Y|R=1] * P(R=1) + E[Y|R=0] * P(R=0)
855 // E[Y|R=0] = E[Y|R=1] + delta
856 double imputed_mean = obs_mean + delta;
857 double overall_mean = obs_mean * prop_obs + imputed_mean * prop_miss;
858
859 // Variance estimation (simplified version)
860 // True variance requires additional assumptions, but here we use observed variance
861 double overall_var = obs_var;
862
863 result.estimated_means.push_back(overall_mean);
864 result.estimated_vars.push_back(overall_var);
865 }
866
867 result.interpretation =
868 "Pattern mixture model sensitivity analysis. "
869 "delta represents the hypothesized difference between "
870 "missing and observed values. delta=0 corresponds to MAR assumption.";
871
872 return result;
873}
874
889 const std::vector<double>& data,
890 const std::vector<double>& phi_values)
891{
892 if (data.empty()) {
893 throw std::invalid_argument(
894 "statcpp::sensitivity_analysis_selection_model: empty data");
895 }
896
898 result.delta_values = phi_values; // Store phi as delta
899 result.estimated_means.reserve(phi_values.size());
900 result.estimated_vars.reserve(phi_values.size());
901
902 // Statistics of observed values
903 std::vector<double> observed;
904 std::size_t n_missing = 0;
905 for (double val : data) {
906 if (!is_na(val)) {
907 observed.push_back(val);
908 } else {
909 ++n_missing;
910 }
911 }
912
913 if (observed.empty()) {
914 throw std::invalid_argument(
915 "statcpp::sensitivity_analysis_selection_model: all values are missing");
916 }
917
918 double obs_mean = mean(observed.begin(), observed.end());
919 double obs_var = observed.size() > 1 ?
920 var(observed.begin(), observed.end(), 1) : 0.0;
921 double obs_std = std::sqrt(obs_var);
922
923 result.original_mean = obs_mean;
924 result.original_var = obs_var;
925
926 // Selection model: logit(P(R=1|Y)) = alpha + phi * Y
927 // phi > 0: Higher values more likely to be observed (missing values tend to be lower)
928 // phi < 0: Lower values more likely to be observed (missing values tend to be higher)
929 // phi = 0: MAR
930
931 for (double phi : phi_values) {
932 // Simplified correction: adjust expected value of missing based on phi
933 // If phi is positive, missing values tend to be lower than observed
934 double adjustment = -phi * obs_std * 0.5; // Scaling factor
935 double imputed_mean = obs_mean + adjustment;
936
937 double n_obs = static_cast<double>(observed.size());
938 double n_total = static_cast<double>(data.size());
939 double prop_obs = n_obs / n_total;
940 double prop_miss = static_cast<double>(n_missing) / n_total;
941
942 double overall_mean = obs_mean * prop_obs + imputed_mean * prop_miss;
943 double overall_var = obs_var;
944
945 result.estimated_means.push_back(overall_mean);
946 result.estimated_vars.push_back(overall_var);
947 }
948
949 result.interpretation =
950 "Selection model sensitivity analysis. "
951 "phi represents the dependence of missingness on the outcome value. "
952 "phi=0 corresponds to MAR assumption. "
953 "phi>0 implies missing values tend to be lower than observed values.";
954
955 return result;
956}
957
965 double tipping_point = 0.0;
966 bool found = false;
967 double threshold = 0.0;
968 std::string interpretation;
969};
970
986 const std::vector<double>& data,
987 double threshold = 0.0, // e.g., null hypothesis value
988 double delta_min = -5.0,
989 double delta_max = 5.0,
990 std::size_t n_points = 100)
991{
993 result.threshold = threshold;
994 result.found = false;
995 result.tipping_point = NA;
996
997 std::vector<double> delta_values;
998 double step = (delta_max - delta_min) / static_cast<double>(n_points - 1);
999 for (std::size_t i = 0; i < n_points; ++i) {
1000 delta_values.push_back(delta_min + static_cast<double>(i) * step);
1001 }
1002
1003 auto sens_result = sensitivity_analysis_pattern_mixture(data, delta_values);
1004
1005 // Find point where threshold is crossed
1006 for (std::size_t i = 1; i < sens_result.estimated_means.size(); ++i) {
1007 double prev = sens_result.estimated_means[i - 1];
1008 double curr = sens_result.estimated_means[i];
1009
1010 if ((prev <= threshold && curr > threshold) ||
1011 (prev >= threshold && curr < threshold)) {
1012 // Estimate tipping point by linear interpolation
1013 double delta_prev = sens_result.delta_values[i - 1];
1014 double delta_curr = sens_result.delta_values[i];
1015 double ratio = (threshold - prev) / (curr - prev);
1016 result.tipping_point = delta_prev + ratio * (delta_curr - delta_prev);
1017 result.found = true;
1018 break;
1019 }
1020 }
1021
1022 if (result.found) {
1023 result.interpretation =
1024 "Tipping point found at delta = " + std::to_string(result.tipping_point) +
1025 ". At this value of delta (shift in missing values), "
1026 "the estimated mean crosses the threshold of " +
1027 std::to_string(threshold) + ".";
1028 } else {
1029 result.interpretation =
1030 "No tipping point found in the specified range. "
1031 "The conclusion is robust to MNAR assumptions within this range.";
1032 }
1033
1034 return result;
1035}
1036
1037// ============================================================================
1038// Complete Case Analysis Utilities
1039// ============================================================================
1040
1048 std::vector<std::vector<double>> complete_data;
1049 std::size_t n_complete = 0;
1050 std::size_t n_dropped = 0;
1051 double proportion_complete = 0.0;
1052};
1053
1064 const std::vector<std::vector<double>>& data)
1065{
1066 complete_case_result result;
1067 result.n_dropped = 0;
1068
1069 for (const auto& row : data) {
1070 bool is_complete = true;
1071 for (double val : row) {
1072 if (is_na(val)) {
1073 is_complete = false;
1074 break;
1075 }
1076 }
1077 if (is_complete) {
1078 result.complete_data.push_back(row);
1079 } else {
1080 ++result.n_dropped;
1081 }
1082 }
1083
1084 result.n_complete = result.complete_data.size();
1085 result.proportion_complete = data.empty() ? 0.0 :
1086 static_cast<double>(result.n_complete) / static_cast<double>(data.size());
1087
1088 return result;
1089}
1090
1100inline std::vector<std::vector<double>> correlation_matrix_pairwise(
1101 const std::vector<std::vector<double>>& data)
1102{
1103 if (data.empty()) {
1104 return {};
1105 }
1106
1107 std::size_t n_cols = data[0].size();
1108 std::vector<std::vector<double>> corr_matrix(n_cols, std::vector<double>(n_cols, 1.0));
1109
1110 for (std::size_t i = 0; i < n_cols; ++i) {
1111 for (std::size_t j = i + 1; j < n_cols; ++j) {
1112 // Extract cases where both are observed
1113 std::vector<double> x_vals, y_vals;
1114 for (const auto& row : data) {
1115 if (!is_na(row[i]) && !is_na(row[j])) {
1116 x_vals.push_back(row[i]);
1117 y_vals.push_back(row[j]);
1118 }
1119 }
1120
1121 if (x_vals.size() >= 2) {
1122 double r = pearson_correlation(x_vals.begin(), x_vals.end(),
1123 y_vals.begin(), y_vals.end());
1124 corr_matrix[i][j] = r;
1125 corr_matrix[j][i] = r;
1126 } else {
1127 corr_matrix[i][j] = NA;
1128 corr_matrix[j][i] = NA;
1129 }
1130 }
1131 }
1132
1133 return corr_matrix;
1134}
1135
1136} // namespace statcpp
Basic statistical computation functions.
Correlation and covariance computation functions.
Data wrangling (data manipulation and transformation) functions.
Dispersion and variance calculation functions.
missing_mechanism
Missing mechanism types.
@ mcar
Missing Completely At Random.
@ mar
Missing At Random.
@ mnar
Missing Not At Random.
@ unknown
Cannot be determined.
std::vector< std::vector< double > > correlation_matrix_pairwise(const std::vector< std::vector< double > > &data)
Correlation matrix using available case analysis (pairwise deletion)
std::vector< std::vector< double > > create_missing_indicator(const std::vector< std::vector< double > > &data)
Create missing indicator variables.
tipping_point_result find_tipping_point(const std::vector< double > &data, double threshold=0.0, double delta_min=-5.0, double delta_max=5.0, std::size_t n_points=100)
Tipping point analysis.
double var(Iterator first, Iterator last, std::size_t ddof=0)
Variance (ddof = Delta Degrees of Freedom)
@ complete
Complete linkage.
double beta(double a, double b)
Beta function.
constexpr double NA
Constant representing NA (NaN)
missing_mechanism diagnose_missing_mechanism(const std::vector< std::vector< double > > &data)
Simple diagnosis of missing mechanism.
complete_case_result extract_complete_cases(const std::vector< std::vector< double > > &data)
Extract complete cases.
double mean(Iterator first, Iterator last)
Arithmetic mean.
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
missing_pattern_info analyze_missing_patterns(const std::vector< std::vector< double > > &data)
Analyze missing patterns.
multiple_imputation_result multiple_imputation_bootstrap(const std::vector< std::vector< double > > &data, std::size_t m=5, unsigned int seed=0)
Multiple imputation (simplified Bootstrap EM method)
mcar_test_result test_mcar_simple(const std::vector< std::vector< double > > &data)
Little's MCAR test (simplified version)
sensitivity_analysis_result sensitivity_analysis_pattern_mixture(const std::vector< double > &data, const std::vector< double > &delta_values)
Sensitivity analysis using pattern mixture model.
double pearson_correlation(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Pearson correlation coefficient.
bool is_na(double x)
Check if a value is NA.
std::vector< double > impute_conditional_mean(const std::vector< std::vector< double > > &data, std::size_t target_col, const std::vector< std::size_t > &predictor_cols)
Single imputation by conditional mean.
multiple_imputation_result multiple_imputation_pmm(const std::vector< std::vector< double > > &data, std::size_t m=5, unsigned int seed=0)
Multiple imputation (PMM: Predictive Mean Matching)
sensitivity_analysis_result sensitivity_analysis_selection_model(const std::vector< double > &data, const std::vector< double > &phi_values)
Sensitivity analysis using selection model.
Complete case analysis result.
std::size_t n_complete
Number of complete cases.
std::vector< std::vector< double > > complete_data
Data with only complete cases.
double proportion_complete
Proportion of complete cases.
std::size_t n_dropped
Number of deleted cases.
Little's MCAR test result.
double chi_square
Chi-square statistic.
std::string interpretation
Interpretation.
std::size_t df
Degrees of freedom.
bool is_mcar
Whether MCAR is concluded (p > 0.05)
Missing pattern information.
std::size_t n_patterns
Number of missing patterns.
std::vector< double > missing_rates
Missing rate per variable.
std::size_t n_complete_cases
Number of complete cases.
std::vector< std::vector< uint8_t > > patterns
Missing patterns (1 = missing, 0 = observed)
double overall_missing_rate
Overall missing rate.
std::vector< std::size_t > pattern_counts
Count of each pattern.
Multiple imputation result.
std::vector< std::vector< std::vector< double > > > imputed_datasets
Imputed datasets.
std::vector< double > between_vars
Between-imputation variances.
std::vector< double > fraction_missing_info
Fraction of missing information (FMI)
std::vector< double > within_vars
Within-imputation variances.
std::vector< double > pooled_means
Pooled means.
std::size_t m
Number of imputations.
std::vector< double > pooled_vars
Pooled variances.
Sensitivity analysis result (single parameter)
double original_mean
Original estimated mean.
std::vector< double > estimated_means
Estimated means.
std::string interpretation
Interpretation.
std::vector< double > delta_values
Sensitivity parameter values.
double original_var
Original estimated variance.
std::vector< double > estimated_vars
Estimated variances.
Tipping point analysis result.
std::string interpretation
Interpretation.
double tipping_point
Tipping point (critical delta value)
bool found
Whether tipping point was found.
double threshold
Threshold used.