93 const std::vector<std::vector<double>>& data)
96 throw std::invalid_argument(
97 "statcpp::analyze_missing_patterns: empty data");
101 std::size_t n_rows = data.size();
102 std::size_t n_cols = data[0].size();
106 std::size_t total_missing = 0;
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");
113 for (std::size_t j = 0; j < n_cols; ++j) {
122 rate /=
static_cast<double>(n_rows);
125 static_cast<double>(n_rows * n_cols);
128 std::vector<std::vector<uint8_t>> unique_patterns;
129 std::vector<std::size_t> pattern_counts;
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;
147 for (std::size_t i = 0; i < unique_patterns.size(); ++i) {
148 if (unique_patterns[i] == pattern) {
157 unique_patterns.push_back(pattern);
158 pattern_counts.push_back(1);
180 const std::vector<std::vector<double>>& data)
182 std::vector<std::vector<double>> indicator;
183 indicator.reserve(data.size());
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);
191 indicator.push_back(std::move(ind_row));
210 const std::vector<std::vector<double>>& data)
213 throw std::invalid_argument(
"statcpp::test_mcar_simple: empty data");
216 std::size_t n_cols = data[0].size();
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");
230 double total_chi_sq = 0.0;
231 std::size_t total_df = 0;
233 for (std::size_t j = 0; j < n_cols; ++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);
242 for (std::size_t k = 0; k < n_cols; ++k) {
243 if (j == k)
continue;
246 std::vector<double> obs_k;
247 std::vector<double> miss_j_subset;
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]);
256 if (obs_k.size() < 5)
continue;
259 std::vector<double> obs_when_j_missing;
260 std::vector<double> obs_when_j_observed;
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]);
266 obs_when_j_observed.push_back(obs_k[i]);
270 if (obs_when_j_missing.size() < 2 || obs_when_j_observed.size() < 2) {
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);
280 double n1 =
static_cast<double>(obs_when_j_missing.size());
281 double n2 =
static_cast<double>(obs_when_j_observed.size());
283 double se = std::sqrt(var1 / n1 + var2 / n2);
285 double t_stat = (mean1 - mean2) / se;
286 total_chi_sq += t_stat * t_stat;
293 result.
df = total_df;
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)));
301 result.
p_value = 0.5 * std::erfc(z / std::sqrt(2.0));
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.";
331 const std::vector<std::vector<double>>& data)
335 if (mcar_result.is_mcar) {
376 const std::vector<std::vector<double>>& data,
377 std::size_t target_col,
378 const std::vector<std::size_t>& predictor_cols)
380 std::size_t n = data.size();
381 std::vector<double> result;
385 std::vector<std::vector<double>> complete_cases;
386 for (
const auto& row : data) {
388 for (std::size_t col : predictor_cols) {
389 if (
is_na(row[col])) {
395 complete_cases.push_back(row);
399 if (complete_cases.empty()) {
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]);
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]);
416 if (predictor_cols.empty()) {
418 for (
const auto& row : complete_cases) {
419 m += row[target_col];
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]);
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]);
436 double x_mean =
mean(x_vals.begin(), x_vals.end());
437 double y_mean =
mean(y_vals.begin(), y_vals.end());
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;
448 double beta = (var_x > 1e-10) ? cov_xy / var_x : 0.0;
449 double alpha = y_mean -
beta * x_mean;
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]);
457 result.push_back(y_mean);
460 result.push_back(row[target_col]);
481 const std::vector<std::vector<double>>& data,
483 unsigned int seed = 0)
486 throw std::invalid_argument(
487 "statcpp::multiple_imputation_pmm: empty data");
491 throw std::invalid_argument(
492 "statcpp::multiple_imputation_pmm: m must be >= 2 for Rubin's rules");
495 std::size_t n_rows = data.size();
496 std::size_t n_cols = data[0].size();
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");
511 std::mt19937 rng(seed == 0 ? std::random_device{}() : seed);
513 for (std::size_t imp = 0; imp < m; ++imp) {
518 for (std::size_t j = 0; j < n_cols; ++j) {
520 std::vector<std::size_t> missing_indices;
521 std::vector<std::size_t> observed_indices;
522 std::vector<double> observed_values;
524 for (std::size_t i = 0; i < n_rows; ++i) {
525 if (
is_na(imputed[i][j])) {
526 missing_indices.push_back(i);
528 observed_indices.push_back(i);
529 observed_values.push_back(imputed[i][j]);
533 if (missing_indices.empty() || observed_values.empty()) {
538 std::vector<std::size_t> predictor_cols;
539 for (std::size_t k = 0; k < n_cols; ++k) {
541 predictor_cols.push_back(k);
549 constexpr std::size_t k_donors = 5;
551 for (std::size_t idx : missing_indices) {
552 double pred_val = cond_means[idx];
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);
564 std::partial_sort(distances.begin(),
565 distances.begin() + std::min(k_donors, distances.size()),
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;
573 imputed[idx][j] = imputed[donor_idx][j];
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);
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) {
597 means_j[imp] =
mean(col_values.begin(), col_values.end());
598 vars_j[imp] =
var(col_values.begin(), col_values.end(), 1);
609 for (
double mj : means_j) {
613 result.
between_vars[j] = b /
static_cast<double>(m - 1);
617 (1.0 + 1.0 /
static_cast<double>(m)) * result.
between_vars[j];
625 (1.0 + 1.0 /
static_cast<double>(m)) * result.
between_vars[j] /
647 const std::vector<std::vector<double>>& data,
649 unsigned int seed = 0)
652 throw std::invalid_argument(
653 "statcpp::multiple_imputation_bootstrap: empty data");
657 throw std::invalid_argument(
658 "statcpp::multiple_imputation_bootstrap: m must be >= 2 for Rubin's rules");
661 std::size_t n_rows = data.size();
662 std::size_t n_cols = data[0].size();
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");
676 std::mt19937 rng(seed == 0 ? std::random_device{}() : seed);
678 for (std::size_t imp = 0; imp < m; ++imp) {
680 std::vector<std::vector<double>> boot_sample;
681 boot_sample.reserve(n_rows);
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)]);
689 std::vector<double> col_means(n_cols);
690 std::vector<double> col_stds(n_cols);
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]);
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;
708 std::normal_distribution<double> normal(0.0, 1.0);
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])) {
714 imputed[i][j] = col_means[j] + col_stds[j] * normal(rng);
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);
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) {
739 means_j[imp] =
mean(col_values.begin(), col_values.end());
740 vars_j[imp] =
var(col_values.begin(), col_values.end(), 1);
747 for (
double mj : means_j) {
751 result.
between_vars[j] = b /
static_cast<double>(m - 1);
754 (1.0 + 1.0 /
static_cast<double>(m)) * result.
between_vars[j];
759 (1.0 + 1.0 /
static_cast<double>(m)) * result.
between_vars[j] /
811 const std::vector<double>& data,
812 const std::vector<double>& delta_values)
815 throw std::invalid_argument(
816 "statcpp::sensitivity_analysis_pattern_mixture: empty data");
825 std::vector<double> observed;
826 std::size_t n_missing = 0;
827 for (
double val : data) {
829 observed.push_back(val);
835 if (observed.empty()) {
836 throw std::invalid_argument(
837 "statcpp::sensitivity_analysis_pattern_mixture: all values are missing");
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;
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;
853 for (
double delta : delta_values) {
856 double imputed_mean = obs_mean + delta;
857 double overall_mean = obs_mean * prop_obs + imputed_mean * prop_miss;
861 double overall_var = obs_var;
868 "Pattern mixture model sensitivity analysis. "
869 "delta represents the hypothesized difference between "
870 "missing and observed values. delta=0 corresponds to MAR assumption.";
889 const std::vector<double>& data,
890 const std::vector<double>& phi_values)
893 throw std::invalid_argument(
894 "statcpp::sensitivity_analysis_selection_model: empty data");
903 std::vector<double> observed;
904 std::size_t n_missing = 0;
905 for (
double val : data) {
907 observed.push_back(val);
913 if (observed.empty()) {
914 throw std::invalid_argument(
915 "statcpp::sensitivity_analysis_selection_model: all values are missing");
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);
931 for (
double phi : phi_values) {
934 double adjustment = -phi * obs_std * 0.5;
935 double imputed_mean = obs_mean + adjustment;
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;
942 double overall_mean = obs_mean * prop_obs + imputed_mean * prop_miss;
943 double overall_var = obs_var;
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.";
986 const std::vector<double>& data,
987 double threshold = 0.0,
988 double delta_min = -5.0,
989 double delta_max = 5.0,
990 std::size_t n_points = 100)
994 result.
found =
false;
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);
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];
1010 if ((prev <= threshold && curr > threshold) ||
1011 (prev >= threshold && curr < threshold)) {
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;
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) +
".";
1030 "No tipping point found in the specified range. "
1031 "The conclusion is robust to MNAR assumptions within this range.";
1064 const std::vector<std::vector<double>>& data)
1069 for (
const auto& row : data) {
1070 bool is_complete =
true;
1071 for (
double val : row) {
1073 is_complete =
false;
1086 static_cast<double>(result.
n_complete) /
static_cast<double>(data.size());
1101 const std::vector<std::vector<double>>& data)
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));
1110 for (std::size_t i = 0; i < n_cols; ++i) {
1111 for (std::size_t j = i + 1; j < n_cols; ++j) {
1113 std::vector<double> x_vals, y_vals;
1114 for (
const auto& row : data) {
1116 x_vals.push_back(row[i]);
1117 y_vals.push_back(row[j]);
1121 if (x_vals.size() >= 2) {
1123 y_vals.begin(), y_vals.end());
1124 corr_matrix[i][j] = r;
1125 corr_matrix[j][i] = r;
1127 corr_matrix[i][j] =
NA;
1128 corr_matrix[j][i] =
NA;
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.
@ 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.