statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
robust.hpp
Go to the documentation of this file.
1
8#pragma once
9
10#include <algorithm>
11#include <cmath>
12#include <cstddef>
13#include <iterator>
14#include <limits>
15#include <stdexcept>
16#include <vector>
17
21
22namespace statcpp {
23
24// ============================================================================
25// Median Absolute Deviation (MAD)
26// ============================================================================
27
41template <typename Iterator>
42double mad(Iterator first, Iterator last)
43{
44 auto n = static_cast<std::size_t>(std::distance(first, last));
45 if (n == 0) {
46 throw std::invalid_argument("statcpp::mad: empty range");
47 }
48
49 // Copy and sort data (for median calculation)
50 std::vector<double> sorted_data;
51 sorted_data.reserve(n);
52 for (auto it = first; it != last; ++it) {
53 sorted_data.push_back(static_cast<double>(*it));
54 }
55 std::sort(sorted_data.begin(), sorted_data.end());
56
57 double med = statcpp::median(sorted_data.begin(), sorted_data.end());
58
59 // Calculate absolute deviations from median
60 std::vector<double> abs_deviations;
61 abs_deviations.reserve(n);
62 for (double val : sorted_data) {
63 abs_deviations.push_back(std::abs(val - med));
64 }
65
66 // Median of absolute deviations
67 std::sort(abs_deviations.begin(), abs_deviations.end());
68 return statcpp::median(abs_deviations.begin(), abs_deviations.end());
69}
70
92template <typename Iterator>
93double mad_scaled(Iterator first, Iterator last)
94{
95 return 1.4826 * mad(first, last);
96}
97
98// ============================================================================
99// Outlier Detection (IQR Method / Tukey's Fences)
100// ============================================================================
101
106 std::vector<double> outliers;
107 std::vector<std::size_t> outlier_indices;
108 double lower_fence;
109 double upper_fence;
110 double q1;
111 double q3;
112 double iqr_value;
113};
114
129template <typename Iterator>
130outlier_detection_result detect_outliers_iqr(Iterator first, Iterator last, double k = 1.5)
131{
132 auto n = static_cast<std::size_t>(std::distance(first, last));
133 if (n == 0) {
134 throw std::invalid_argument("statcpp::detect_outliers_iqr: empty range");
135 }
136
137 // Copy and sort data
138 std::vector<double> sorted_data;
139 sorted_data.reserve(n);
140 for (auto it = first; it != last; ++it) {
141 sorted_data.push_back(static_cast<double>(*it));
142 }
143 std::sort(sorted_data.begin(), sorted_data.end());
144
145 auto q = statcpp::quartiles(sorted_data.begin(), sorted_data.end());
146 double iqr_val = q.q3 - q.q1;
147
148 double lower_fence = q.q1 - k * iqr_val;
149 double upper_fence = q.q3 + k * iqr_val;
150
152 result.lower_fence = lower_fence;
153 result.upper_fence = upper_fence;
154 result.q1 = q.q1;
155 result.q3 = q.q3;
156 result.iqr_value = iqr_val;
157
158 // Detect outliers
159 std::size_t idx = 0;
160 for (auto it = first; it != last; ++it, ++idx) {
161 double val = static_cast<double>(*it);
162 if (val < lower_fence || val > upper_fence) {
163 result.outliers.push_back(val);
164 result.outlier_indices.push_back(idx);
165 }
166 }
167
168 return result;
169}
170
171// ============================================================================
172// Z-score Outlier Detection
173// ============================================================================
174
189template <typename Iterator>
190outlier_detection_result detect_outliers_zscore(Iterator first, Iterator last, double threshold = 3.0)
191{
192 auto n = static_cast<std::size_t>(std::distance(first, last));
193 if (n < 2) {
194 throw std::invalid_argument("statcpp::detect_outliers_zscore: need at least 2 elements");
195 }
196
197 double m = statcpp::mean(first, last);
198 double s = statcpp::sample_stddev(first, last, m);
199
200 if (s == 0.0) {
201 throw std::invalid_argument("statcpp::detect_outliers_zscore: zero standard deviation");
202 }
203
205 result.lower_fence = m - threshold * s;
206 result.upper_fence = m + threshold * s;
207 result.q1 = 0.0; // Z-score method does not use quartiles
208 result.q3 = 0.0;
209 result.iqr_value = 0.0;
210
211 std::size_t idx = 0;
212 for (auto it = first; it != last; ++it, ++idx) {
213 double val = static_cast<double>(*it);
214 double z = (val - m) / s;
215 if (std::abs(z) > threshold) {
216 result.outliers.push_back(val);
217 result.outlier_indices.push_back(idx);
218 }
219 }
220
221 return result;
222}
223
238template <typename Iterator>
239outlier_detection_result detect_outliers_modified_zscore(Iterator first, Iterator last, double threshold = 3.5)
240{
241 auto n = static_cast<std::size_t>(std::distance(first, last));
242 if (n == 0) {
243 throw std::invalid_argument("statcpp::detect_outliers_modified_zscore: empty range");
244 }
245
246 // Copy and sort data
247 std::vector<double> sorted_data;
248 sorted_data.reserve(n);
249 for (auto it = first; it != last; ++it) {
250 sorted_data.push_back(static_cast<double>(*it));
251 }
252 std::sort(sorted_data.begin(), sorted_data.end());
253
254 double med = statcpp::median(sorted_data.begin(), sorted_data.end());
255 double mad_val = mad(first, last);
256
257 if (mad_val == 0.0) {
258 throw std::invalid_argument("statcpp::detect_outliers_modified_zscore: zero MAD");
259 }
260
261 // Modified Z-score = 0.6745 * (x - median) / MAD
262 double scale = 0.6745;
263
265 result.lower_fence = med - threshold * mad_val / scale;
266 result.upper_fence = med + threshold * mad_val / scale;
267 result.q1 = 0.0;
268 result.q3 = 0.0;
269 result.iqr_value = 0.0;
270
271 std::size_t idx = 0;
272 for (auto it = first; it != last; ++it, ++idx) {
273 double val = static_cast<double>(*it);
274 double modified_z = scale * (val - med) / mad_val;
275 if (std::abs(modified_z) > threshold) {
276 result.outliers.push_back(val);
277 result.outlier_indices.push_back(idx);
278 }
279 }
280
281 return result;
282}
283
284// ============================================================================
285// Winsorization
286// ============================================================================
287
302template <typename Iterator>
303std::vector<double> winsorize(Iterator first, Iterator last, double limits = 0.05)
304{
305 auto n = static_cast<std::size_t>(std::distance(first, last));
306 if (n == 0) {
307 throw std::invalid_argument("statcpp::winsorize: empty range");
308 }
309 if (limits < 0.0 || limits >= 0.5) {
310 throw std::invalid_argument("statcpp::winsorize: limits must be in [0, 0.5)");
311 }
312
313 // Copy and sort data
314 std::vector<double> sorted_data;
315 sorted_data.reserve(n);
316 for (auto it = first; it != last; ++it) {
317 sorted_data.push_back(static_cast<double>(*it));
318 }
319 std::sort(sorted_data.begin(), sorted_data.end());
320
321 // Calculate thresholds
322 double lower_percentile = limits;
323 double upper_percentile = 1.0 - limits;
324 double lower_value = statcpp::percentile(sorted_data.begin(), sorted_data.end(), lower_percentile);
325 double upper_value = statcpp::percentile(sorted_data.begin(), sorted_data.end(), upper_percentile);
326
327 // Winsorize
328 std::vector<double> result;
329 result.reserve(n);
330 for (auto it = first; it != last; ++it) {
331 double val = static_cast<double>(*it);
332 if (val < lower_value) {
333 result.push_back(lower_value);
334 } else if (val > upper_value) {
335 result.push_back(upper_value);
336 } else {
337 result.push_back(val);
338 }
339 }
340
341 return result;
342}
343
344// ============================================================================
345// Cook's Distance (for Linear Regression Diagnostics)
346// ============================================================================
347
363inline std::vector<double> cooks_distance(
364 const std::vector<double>& residuals,
365 const std::vector<double>& hat_values,
366 double mse,
367 std::size_t p)
368{
369 if (residuals.size() != hat_values.size()) {
370 throw std::invalid_argument("statcpp::cooks_distance: residuals and hat_values must have same length");
371 }
372 if (residuals.empty()) {
373 throw std::invalid_argument("statcpp::cooks_distance: empty data");
374 }
375 if (mse <= 0) {
376 throw std::invalid_argument("statcpp::cooks_distance: mse must be positive");
377 }
378 if (p == 0) {
379 throw std::invalid_argument("statcpp::cooks_distance: p must be positive");
380 }
381
382 std::vector<double> result;
383 result.reserve(residuals.size());
384
385 for (std::size_t i = 0; i < residuals.size(); ++i) {
386 double h = hat_values[i];
387 if (h >= 1.0) {
388 result.push_back(std::numeric_limits<double>::infinity());
389 continue;
390 }
391
392 double e = residuals[i];
393 double d = (e * e / (static_cast<double>(p) * mse)) *
394 (h / ((1.0 - h) * (1.0 - h)));
395 result.push_back(d);
396 }
397
398 return result;
399}
400
401// ============================================================================
402// DFFITS (Difference in Fits)
403// ============================================================================
404
418inline std::vector<double> dffits(
419 const std::vector<double>& residuals,
420 const std::vector<double>& hat_values,
421 double mse)
422{
423 if (residuals.size() != hat_values.size()) {
424 throw std::invalid_argument("statcpp::dffits: residuals and hat_values must have same length");
425 }
426 if (residuals.empty()) {
427 throw std::invalid_argument("statcpp::dffits: empty data");
428 }
429
430 std::vector<double> result;
431 result.reserve(residuals.size());
432
433 for (std::size_t i = 0; i < residuals.size(); ++i) {
434 double h = hat_values[i];
435 if (h >= 1.0) {
436 result.push_back(std::numeric_limits<double>::infinity());
437 continue;
438 }
439
440 double e = residuals[i];
441 // Studentized residual
442 double se = std::sqrt(mse * (1.0 - h));
443 if (se == 0.0) {
444 result.push_back(std::numeric_limits<double>::infinity());
445 continue;
446 }
447 double t = e / se;
448
449 // DFFITS
450 double dffits_val = t * std::sqrt(h / (1.0 - h));
451 result.push_back(dffits_val);
452 }
453
454 return result;
455}
456
457// ============================================================================
458// Robust Location Estimators
459// ============================================================================
460
474template <typename Iterator>
475double hodges_lehmann(Iterator first, Iterator last)
476{
477 auto n = static_cast<std::size_t>(std::distance(first, last));
478 if (n == 0) {
479 throw std::invalid_argument("statcpp::hodges_lehmann: empty range");
480 }
481
482 std::vector<double> data;
483 data.reserve(n);
484 for (auto it = first; it != last; ++it) {
485 data.push_back(static_cast<double>(*it));
486 }
487
488 // Calculate all pairwise averages
489 std::vector<double> pairwise_means;
490 pairwise_means.reserve(n * (n + 1) / 2);
491
492 for (std::size_t i = 0; i < n; ++i) {
493 for (std::size_t j = i; j < n; ++j) {
494 pairwise_means.push_back((data[i] + data[j]) / 2.0);
495 }
496 }
497
498 std::sort(pairwise_means.begin(), pairwise_means.end());
499 return statcpp::median(pairwise_means.begin(), pairwise_means.end());
500}
501
516template <typename Iterator>
517double biweight_midvariance(Iterator first, Iterator last, double c = 9.0)
518{
519 auto n = static_cast<std::size_t>(std::distance(first, last));
520 if (n < 2) {
521 throw std::invalid_argument("statcpp::biweight_midvariance: need at least 2 elements");
522 }
523
524 // Copy and sort data
525 std::vector<double> data;
526 data.reserve(n);
527 for (auto it = first; it != last; ++it) {
528 data.push_back(static_cast<double>(*it));
529 }
530 std::sort(data.begin(), data.end());
531
532 double med = statcpp::median(data.begin(), data.end());
533 double mad_val = mad(first, last);
534
535 if (mad_val == 0.0) {
536 return 0.0;
537 }
538
539 double num = 0.0;
540 double den = 0.0;
541 std::size_t count = 0;
542
543 for (double x : data) {
544 double u = (x - med) / (c * mad_val);
545 if (std::abs(u) < 1.0) {
546 double u2 = u * u;
547 double w = (1.0 - u2);
548 double w2 = w * w;
549 double w4 = w2 * w2;
550 num += (x - med) * (x - med) * w4;
551 den += w * (1.0 - 5.0 * u2);
552 count++;
553 }
554 }
555
556 if (den == 0.0 || count < 2) {
557 return 0.0;
558 }
559
560 return static_cast<double>(n) * num / (den * den);
561}
562
563} // namespace statcpp
Basic statistical computation functions.
Dispersion and variance calculation functions.
std::vector< double > dffits(const std::vector< double > &residuals, const std::vector< double > &hat_values, double mse)
Calculate DFFITS.
Definition robust.hpp:418
double biweight_midvariance(Iterator first, Iterator last, double c=9.0)
Biweight Midvariance.
Definition robust.hpp:517
double sample_stddev(Iterator first, Iterator last)
Sample standard deviation.
double mad(Iterator first, Iterator last)
Median Absolute Deviation (MAD)
Definition robust.hpp:42
std::vector< double > cooks_distance(const std::vector< double > &residuals, const std::vector< double > &hat_values, double mse, std::size_t p)
Calculate Cook's Distance.
Definition robust.hpp:363
outlier_detection_result detect_outliers_zscore(Iterator first, Iterator last, double threshold=3.0)
Outlier detection using Z-score.
Definition robust.hpp:190
std::vector< double > winsorize(Iterator first, Iterator last, double limits=0.05)
Winsorization.
Definition robust.hpp:303
double percentile(Iterator first, Iterator last, double p)
Return percentile.
double mean(Iterator first, Iterator last)
Arithmetic mean.
double hodges_lehmann(Iterator first, Iterator last)
Hodges-Lehmann estimator.
Definition robust.hpp:475
quartile_result quartiles(Iterator first, Iterator last)
Return quartiles.
double mad_scaled(Iterator first, Iterator last)
Scaled MAD for normal distribution.
Definition robust.hpp:93
double median(Iterator first, Iterator last)
Median (accepts a sorted range)
outlier_detection_result detect_outliers_iqr(Iterator first, Iterator last, double k=1.5)
Outlier detection using IQR method (Tukey's Fences)
Definition robust.hpp:130
double mse(Iterator1 first1, Iterator1 last1, Iterator2 first2)
Mean Squared Error (MSE)
outlier_detection_result detect_outliers_modified_zscore(Iterator first, Iterator last, double threshold=3.5)
Outlier detection using Modified Z-score.
Definition robust.hpp:239
std::size_t count(Iterator first, Iterator last)
Data count.
Order statistics implementation.
Outlier detection result.
Definition robust.hpp:105
double iqr_value
Interquartile range.
Definition robust.hpp:112
std::vector< std::size_t > outlier_indices
Outlier indices.
Definition robust.hpp:107
double q1
First quartile.
Definition robust.hpp:110
double upper_fence
Upper fence.
Definition robust.hpp:109
std::vector< double > outliers
Outliers.
Definition robust.hpp:106
double q3
Third quartile.
Definition robust.hpp:111
double lower_fence
Lower fence.
Definition robust.hpp:108