statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
order_statistics.hpp
Go to the documentation of this file.
1
8#pragma once
9
10#include <algorithm>
11#include <cmath>
12#include <cstddef>
13#include <functional>
14#include <iterator>
15#include <type_traits>
16#include <limits>
17#include <stdexcept>
18#include <utility>
19#include <vector>
20
21namespace statcpp {
22
23// ============================================================================
24// Result Structures
25// ============================================================================
26
31 double q1;
32 double q2;
33 double q3;
34};
35
40 double min;
41 double q1;
42 double median;
43 double q3;
44 double max;
45};
46
47// ============================================================================
48// Linear Interpolation Helper (R type=7 / Excel QUARTILE.INC equivalent)
49// ============================================================================
50
65template <typename Iterator>
66double interpolate_at(Iterator first, std::size_t n, double p)
67{
68 static_assert(
69 std::is_base_of_v<
70 std::random_access_iterator_tag,
71 typename std::iterator_traits<Iterator>::iterator_category>,
72 "statcpp::interpolate_at requires random access iterators");
73
74 double index = p * static_cast<double>(n - 1);
75 auto lo = static_cast<std::size_t>(std::floor(index));
76 double frac = index - static_cast<double>(lo);
77 if (lo + 1 >= n) {
78 return static_cast<double>(*(first + lo));
79 }
80 return static_cast<double>(*(first + lo)) * (1.0 - frac)
81 + static_cast<double>(*(first + lo + 1)) * frac;
82}
83
95template <typename Iterator, typename Projection>
96double interpolate_at(Iterator first, std::size_t n, double p, Projection proj)
97{
98 static_assert(
99 std::is_base_of_v<
100 std::random_access_iterator_tag,
101 typename std::iterator_traits<Iterator>::iterator_category>,
102 "statcpp::interpolate_at requires random access iterators");
103
104 double index = p * static_cast<double>(n - 1);
105 auto lo = static_cast<std::size_t>(std::floor(index));
106 double frac = index - static_cast<double>(lo);
107 if (lo + 1 >= n) {
108 return static_cast<double>(std::invoke(proj, *(first + lo)));
109 }
110 return static_cast<double>(std::invoke(proj, *(first + lo))) * (1.0 - frac)
111 + static_cast<double>(std::invoke(proj, *(first + lo + 1))) * frac;
112}
113
114// ============================================================================
115// Minimum
116// ============================================================================
117
127template <typename Iterator>
128auto minimum(Iterator first, Iterator last)
129{
130 if (first == last) {
131 throw std::invalid_argument("statcpp::minimum: empty range");
132 }
133 return *std::min_element(first, last);
134}
135
147template <typename Iterator, typename Projection>
148auto minimum(Iterator first, Iterator last, Projection proj)
149{
150 if (first == last) {
151 throw std::invalid_argument("statcpp::minimum: empty range");
152 }
153 auto min_val = std::invoke(proj, *first);
154 for (auto it = std::next(first); it != last; ++it) {
155 auto val = std::invoke(proj, *it);
156 if (val < min_val) {
157 min_val = val;
158 }
159 }
160 return min_val;
161}
162
163// ============================================================================
164// Maximum
165// ============================================================================
166
176template <typename Iterator>
177auto maximum(Iterator first, Iterator last)
178{
179 if (first == last) {
180 throw std::invalid_argument("statcpp::maximum: empty range");
181 }
182 return *std::max_element(first, last);
183}
184
196template <typename Iterator, typename Projection>
197auto maximum(Iterator first, Iterator last, Projection proj)
198{
199 if (first == last) {
200 throw std::invalid_argument("statcpp::maximum: empty range");
201 }
202 auto max_val = std::invoke(proj, *first);
203 for (auto it = std::next(first); it != last; ++it) {
204 auto val = std::invoke(proj, *it);
205 if (val > max_val) {
206 max_val = val;
207 }
208 }
209 return max_val;
210}
211
212// ============================================================================
213// Quartiles (Q1, Q2, Q3)
214// ============================================================================
215
229template <typename Iterator>
230quartile_result quartiles(Iterator first, Iterator last)
231{
232 auto n = static_cast<std::size_t>(std::distance(first, last));
233 if (n == 0) {
234 throw std::invalid_argument("statcpp::quartiles: empty range");
235 }
236 return {
237 interpolate_at(first, n, 0.25),
238 interpolate_at(first, n, 0.50),
239 interpolate_at(first, n, 0.75)
240 };
241}
242
254template <typename Iterator, typename Projection>
255quartile_result quartiles(Iterator first, Iterator last, Projection proj)
256{
257 auto n = static_cast<std::size_t>(std::distance(first, last));
258 if (n == 0) {
259 throw std::invalid_argument("statcpp::quartiles: empty range");
260 }
261 return {
262 interpolate_at(first, n, 0.25, proj),
263 interpolate_at(first, n, 0.50, proj),
264 interpolate_at(first, n, 0.75, proj)
265 };
266}
267
268// ============================================================================
269// Percentile
270// ============================================================================
271
284template <typename Iterator>
285double percentile(Iterator first, Iterator last, double p)
286{
287 auto n = static_cast<std::size_t>(std::distance(first, last));
288 if (n == 0) {
289 throw std::invalid_argument("statcpp::percentile: empty range");
290 }
291 if (p < 0.0 || p > 1.0) {
292 throw std::invalid_argument("statcpp::percentile: p must be in [0, 1]");
293 }
294 return interpolate_at(first, n, p);
295}
296
309template <typename Iterator, typename Projection>
310double percentile(Iterator first, Iterator last, double p, Projection proj)
311{
312 auto n = static_cast<std::size_t>(std::distance(first, last));
313 if (n == 0) {
314 throw std::invalid_argument("statcpp::percentile: empty range");
315 }
316 if (p < 0.0 || p > 1.0) {
317 throw std::invalid_argument("statcpp::percentile: p must be in [0, 1]");
318 }
319 return interpolate_at(first, n, p, proj);
320}
321
322// ============================================================================
323// Five-Number Summary
324// ============================================================================
325
339template <typename Iterator>
341{
342 static_assert(
343 std::is_base_of_v<
344 std::random_access_iterator_tag,
345 typename std::iterator_traits<Iterator>::iterator_category>,
346 "statcpp::five_number_summary requires random access iterators");
347
348 auto n = static_cast<std::size_t>(std::distance(first, last));
349 if (n == 0) {
350 throw std::invalid_argument("statcpp::five_number_summary: empty range");
351 }
352 return {
353 static_cast<double>(*first),
354 interpolate_at(first, n, 0.25),
355 interpolate_at(first, n, 0.50),
356 interpolate_at(first, n, 0.75),
357 static_cast<double>(*(first + (n - 1)))
358 };
359}
360
372template <typename Iterator, typename Projection>
373five_number_summary_result five_number_summary(Iterator first, Iterator last, Projection proj)
374{
375 static_assert(
376 std::is_base_of_v<
377 std::random_access_iterator_tag,
378 typename std::iterator_traits<Iterator>::iterator_category>,
379 "statcpp::five_number_summary requires random access iterators");
380
381 auto n = static_cast<std::size_t>(std::distance(first, last));
382 if (n == 0) {
383 throw std::invalid_argument("statcpp::five_number_summary: empty range");
384 }
385 return {
386 static_cast<double>(std::invoke(proj, *first)),
387 interpolate_at(first, n, 0.25, proj),
388 interpolate_at(first, n, 0.50, proj),
389 interpolate_at(first, n, 0.75, proj),
390 static_cast<double>(std::invoke(proj, *(first + (n - 1))))
391 };
392}
393
394// ============================================================================
395// Weighted Median
396// ============================================================================
397
412template <typename Iterator, typename WeightIterator>
413double weighted_median(Iterator first, Iterator last, WeightIterator weight_first, WeightIterator weight_last)
414{
415 if (std::distance(first, last) != std::distance(weight_first, weight_last)) {
416 throw std::invalid_argument("statcpp::weighted_median: data and weight ranges must have the same size");
417 }
418
419 auto n = static_cast<std::size_t>(std::distance(first, last));
420 if (n == 0) {
421 throw std::invalid_argument("statcpp::weighted_median: empty range");
422 }
423
424 // Create value-weight pairs
425 std::vector<std::pair<double, double>> pairs;
426 pairs.reserve(n);
427 auto weight_it = weight_first;
428 for (auto it = first; it != last; ++it, ++weight_it) {
429 double value = static_cast<double>(*it);
430 double weight = static_cast<double>(*weight_it);
431 if (weight < 0.0) {
432 throw std::invalid_argument("statcpp::weighted_median: negative weight");
433 }
434 pairs.emplace_back(value, weight);
435 }
436
437 // Sort by value
438 std::sort(pairs.begin(), pairs.end(),
439 [](const auto& a, const auto& b) { return a.first < b.first; });
440
441 // Sum of weights
442 double total_weight = 0.0;
443 for (const auto& p : pairs) {
444 total_weight += p.second;
445 }
446
447 if (total_weight == 0.0) {
448 throw std::invalid_argument("statcpp::weighted_median: sum of weights is zero");
449 }
450
451 // Calculate cumulative weight to find median.
452 // Use a relative tolerance when testing whether cumulative == half_weight.
453 // Plain == on a floating-point accumulator is unreliable because sequential
454 // additions may produce a value within rounding error of half_weight without
455 // being bit-identical.
456 double cumulative = 0.0;
457 double half_weight = total_weight / 2.0;
458 const double tol = std::numeric_limits<double>::epsilon() * half_weight;
459
460 for (std::size_t i = 0; i < pairs.size(); ++i) {
461 cumulative += pairs[i].second;
462 if (cumulative >= half_weight) {
463 // If cumulative weight is (approximately) exactly half, take average with next positive-weight value
464 if (std::abs(cumulative - half_weight) <= tol) {
465 for (std::size_t j = i + 1; j < pairs.size(); ++j) {
466 if (pairs[j].second > 0.0) {
467 return (pairs[i].first + pairs[j].first) / 2.0;
468 }
469 }
470 }
471 return pairs[i].first;
472 }
473 }
474
475 return pairs.back().first;
476}
477
491template <typename Iterator, typename WeightIterator>
492[[deprecated("Use weighted_median(first, last, weight_first, weight_last) overload for range safety")]]
493double weighted_median(Iterator first, Iterator last, WeightIterator weight_first)
494{
495 auto n = static_cast<std::size_t>(std::distance(first, last));
496 if (n == 0) {
497 throw std::invalid_argument("statcpp::weighted_median: empty range");
498 }
499
500 // Create value-weight pairs
501 std::vector<std::pair<double, double>> pairs;
502 pairs.reserve(n);
503 auto weight_it = weight_first;
504 for (auto it = first; it != last; ++it, ++weight_it) {
505 double value = static_cast<double>(*it);
506 double weight = static_cast<double>(*weight_it);
507 if (weight < 0.0) {
508 throw std::invalid_argument("statcpp::weighted_median: negative weight");
509 }
510 pairs.emplace_back(value, weight);
511 }
512
513 // Sort by value
514 std::sort(pairs.begin(), pairs.end(),
515 [](const auto& a, const auto& b) { return a.first < b.first; });
516
517 // Sum of weights
518 double total_weight = 0.0;
519 for (const auto& p : pairs) {
520 total_weight += p.second;
521 }
522
523 if (total_weight == 0.0) {
524 throw std::invalid_argument("statcpp::weighted_median: sum of weights is zero");
525 }
526
527 // Calculate cumulative weight to find median.
528 // Use a relative tolerance when testing whether cumulative == half_weight.
529 // Plain == on a floating-point accumulator is unreliable because sequential
530 // additions may produce a value within rounding error of half_weight without
531 // being bit-identical.
532 double cumulative = 0.0;
533 double half_weight = total_weight / 2.0;
534 const double tol = std::numeric_limits<double>::epsilon() * half_weight;
535
536 for (std::size_t i = 0; i < pairs.size(); ++i) {
537 cumulative += pairs[i].second;
538 if (cumulative >= half_weight) {
539 // If cumulative weight is (approximately) exactly half, take average with next positive-weight value
540 if (std::abs(cumulative - half_weight) <= tol) {
541 for (std::size_t j = i + 1; j < pairs.size(); ++j) {
542 if (pairs[j].second > 0.0) {
543 return (pairs[i].first + pairs[j].first) / 2.0;
544 }
545 }
546 }
547 return pairs[i].first;
548 }
549 }
550
551 return pairs.back().first;
552}
553
570template <typename Iterator, typename WeightIterator, typename Projection>
571double weighted_median(Iterator first, Iterator last, WeightIterator weight_first, WeightIterator weight_last, Projection proj)
572{
573 if (std::distance(first, last) != std::distance(weight_first, weight_last)) {
574 throw std::invalid_argument("statcpp::weighted_median: data and weight ranges must have the same size");
575 }
576
577 auto n = static_cast<std::size_t>(std::distance(first, last));
578 if (n == 0) {
579 throw std::invalid_argument("statcpp::weighted_median: empty range");
580 }
581
582 // Create value-weight pairs
583 std::vector<std::pair<double, double>> pairs;
584 pairs.reserve(n);
585 auto weight_it = weight_first;
586 for (auto it = first; it != last; ++it, ++weight_it) {
587 double value = static_cast<double>(std::invoke(proj, *it));
588 double weight = static_cast<double>(*weight_it);
589 if (weight < 0.0) {
590 throw std::invalid_argument("statcpp::weighted_median: negative weight");
591 }
592 pairs.emplace_back(value, weight);
593 }
594
595 // Sort by value
596 std::sort(pairs.begin(), pairs.end(),
597 [](const auto& a, const auto& b) { return a.first < b.first; });
598
599 // Sum of weights
600 double total_weight = 0.0;
601 for (const auto& p : pairs) {
602 total_weight += p.second;
603 }
604
605 if (total_weight == 0.0) {
606 throw std::invalid_argument("statcpp::weighted_median: sum of weights is zero");
607 }
608
609 // Calculate cumulative weight to find median.
610 // Use a relative tolerance when testing whether cumulative == half_weight.
611 // Plain == on a floating-point accumulator is unreliable because sequential
612 // additions may produce a value within rounding error of half_weight without
613 // being bit-identical.
614 double cumulative = 0.0;
615 double half_weight = total_weight / 2.0;
616 const double tol = std::numeric_limits<double>::epsilon() * half_weight;
617
618 for (std::size_t i = 0; i < pairs.size(); ++i) {
619 cumulative += pairs[i].second;
620 if (cumulative >= half_weight) {
621 // If cumulative weight is (approximately) exactly half, take average with next positive-weight value
622 if (std::abs(cumulative - half_weight) <= tol) {
623 for (std::size_t j = i + 1; j < pairs.size(); ++j) {
624 if (pairs[j].second > 0.0) {
625 return (pairs[i].first + pairs[j].first) / 2.0;
626 }
627 }
628 }
629 return pairs[i].first;
630 }
631 }
632
633 return pairs.back().first;
634}
635
649template <typename Iterator, typename WeightIterator, typename Projection>
650[[deprecated("Use weighted_median(first, last, weight_first, weight_last, proj) overload for range safety")]]
651double weighted_median(Iterator first, Iterator last, WeightIterator weight_first, Projection proj)
652{
653 auto n = static_cast<std::size_t>(std::distance(first, last));
654 if (n == 0) {
655 throw std::invalid_argument("statcpp::weighted_median: empty range");
656 }
657
658 // Create value-weight pairs
659 std::vector<std::pair<double, double>> pairs;
660 pairs.reserve(n);
661 auto weight_it = weight_first;
662 for (auto it = first; it != last; ++it, ++weight_it) {
663 double value = static_cast<double>(std::invoke(proj, *it));
664 double weight = static_cast<double>(*weight_it);
665 if (weight < 0.0) {
666 throw std::invalid_argument("statcpp::weighted_median: negative weight");
667 }
668 pairs.emplace_back(value, weight);
669 }
670
671 // Sort by value
672 std::sort(pairs.begin(), pairs.end(),
673 [](const auto& a, const auto& b) { return a.first < b.first; });
674
675 // Sum of weights
676 double total_weight = 0.0;
677 for (const auto& p : pairs) {
678 total_weight += p.second;
679 }
680
681 if (total_weight == 0.0) {
682 throw std::invalid_argument("statcpp::weighted_median: sum of weights is zero");
683 }
684
685 // Calculate cumulative weight to find median.
686 // Use a relative tolerance when testing whether cumulative == half_weight.
687 // Plain == on a floating-point accumulator is unreliable because sequential
688 // additions may produce a value within rounding error of half_weight without
689 // being bit-identical.
690 double cumulative = 0.0;
691 double half_weight = total_weight / 2.0;
692 const double tol = std::numeric_limits<double>::epsilon() * half_weight;
693
694 for (std::size_t i = 0; i < pairs.size(); ++i) {
695 cumulative += pairs[i].second;
696 if (cumulative >= half_weight) {
697 // If cumulative weight is (approximately) exactly half, take average with next positive-weight value
698 if (std::abs(cumulative - half_weight) <= tol) {
699 for (std::size_t j = i + 1; j < pairs.size(); ++j) {
700 if (pairs[j].second > 0.0) {
701 return (pairs[i].first + pairs[j].first) / 2.0;
702 }
703 }
704 }
705 return pairs[i].first;
706 }
707 }
708
709 return pairs.back().first;
710}
711
712// ============================================================================
713// Weighted Percentile
714// ============================================================================
715
731template <typename Iterator, typename WeightIterator>
732double weighted_percentile(Iterator first, Iterator last, WeightIterator weight_first, WeightIterator weight_last, double p)
733{
734 if (std::distance(first, last) != std::distance(weight_first, weight_last)) {
735 throw std::invalid_argument("statcpp::weighted_percentile: data and weight ranges must have the same size");
736 }
737
738 if (!(0.0 <= p && p <= 1.0)) {
739 throw std::invalid_argument("statcpp::weighted_percentile: p must be in [0, 1]");
740 }
741
742 auto n = static_cast<std::size_t>(std::distance(first, last));
743 if (n == 0) {
744 throw std::invalid_argument("statcpp::weighted_percentile: empty range");
745 }
746
747 // Create value-weight pairs
748 std::vector<std::pair<double, double>> pairs;
749 pairs.reserve(n);
750 auto weight_it = weight_first;
751 for (auto it = first; it != last; ++it, ++weight_it) {
752 double value = static_cast<double>(*it);
753 double weight = static_cast<double>(*weight_it);
754 if (weight < 0.0) {
755 throw std::invalid_argument("statcpp::weighted_percentile: negative weight");
756 }
757 pairs.emplace_back(value, weight);
758 }
759
760 // Sort by value
761 std::sort(pairs.begin(), pairs.end(),
762 [](const auto& a, const auto& b) { return a.first < b.first; });
763
764 // Sum of weights
765 double total_weight = 0.0;
766 for (const auto& pair : pairs) {
767 total_weight += pair.second;
768 }
769
770 if (total_weight == 0.0) {
771 throw std::invalid_argument("statcpp::weighted_percentile: sum of weights is zero");
772 }
773
774 // Explicit endpoint handling to avoid relying on floating-point loop
775 if (p <= 0.0) return pairs.front().first;
776 if (p >= 1.0) return pairs.back().first;
777
778 // Calculate cumulative weight to find target percentile
779 // For p=0.5, find position at 50% of total weight
780 double target = p * total_weight;
781 const double tol = std::numeric_limits<double>::epsilon() * total_weight;
782 double cumulative = 0.0;
783
784 for (std::size_t i = 0; i < pairs.size(); ++i) {
785 cumulative += pairs[i].second;
786 if (cumulative >= target) {
787 // If cumulative weight is approximately at target, take average with next value
788 if (std::abs(cumulative - target) <= tol && i + 1 < pairs.size()) {
789 return (pairs[i].first + pairs[i + 1].first) / 2.0;
790 }
791 return pairs[i].first;
792 }
793 }
794
795 return pairs.back().first;
796}
797
812template <typename Iterator, typename WeightIterator>
813[[deprecated("Use weighted_percentile(first, last, weight_first, weight_last, p) overload for range safety")]]
814double weighted_percentile(Iterator first, Iterator last, WeightIterator weight_first, double p)
815{
816 if (!(0.0 <= p && p <= 1.0)) {
817 throw std::invalid_argument("statcpp::weighted_percentile: p must be in [0, 1]");
818 }
819
820 auto n = static_cast<std::size_t>(std::distance(first, last));
821 if (n == 0) {
822 throw std::invalid_argument("statcpp::weighted_percentile: empty range");
823 }
824
825 // Create value-weight pairs
826 std::vector<std::pair<double, double>> pairs;
827 pairs.reserve(n);
828 auto weight_it = weight_first;
829 for (auto it = first; it != last; ++it, ++weight_it) {
830 double value = static_cast<double>(*it);
831 double weight = static_cast<double>(*weight_it);
832 if (weight < 0.0) {
833 throw std::invalid_argument("statcpp::weighted_percentile: negative weight");
834 }
835 pairs.emplace_back(value, weight);
836 }
837
838 // Sort by value
839 std::sort(pairs.begin(), pairs.end(),
840 [](const auto& a, const auto& b) { return a.first < b.first; });
841
842 // Sum of weights
843 double total_weight = 0.0;
844 for (const auto& pair : pairs) {
845 total_weight += pair.second;
846 }
847
848 if (total_weight == 0.0) {
849 throw std::invalid_argument("statcpp::weighted_percentile: sum of weights is zero");
850 }
851
852 // Explicit endpoint handling to avoid relying on floating-point loop
853 if (p <= 0.0) return pairs.front().first;
854 if (p >= 1.0) return pairs.back().first;
855
856 // Calculate cumulative weight to find target percentile
857 // For p=0.5, find position at 50% of total weight
858 double target = p * total_weight;
859 const double tol = std::numeric_limits<double>::epsilon() * total_weight;
860 double cumulative = 0.0;
861
862 for (std::size_t i = 0; i < pairs.size(); ++i) {
863 cumulative += pairs[i].second;
864 if (cumulative >= target) {
865 // If cumulative weight is approximately at target, take average with next value
866 if (std::abs(cumulative - target) <= tol && i + 1 < pairs.size()) {
867 return (pairs[i].first + pairs[i + 1].first) / 2.0;
868 }
869 return pairs[i].first;
870 }
871 }
872
873 return pairs.back().first;
874}
875
893template <typename Iterator, typename WeightIterator, typename Projection>
894double weighted_percentile(Iterator first, Iterator last, WeightIterator weight_first, WeightIterator weight_last, double p, Projection proj)
895{
896 if (std::distance(first, last) != std::distance(weight_first, weight_last)) {
897 throw std::invalid_argument("statcpp::weighted_percentile: data and weight ranges must have the same size");
898 }
899
900 if (!(0.0 <= p && p <= 1.0)) {
901 throw std::invalid_argument("statcpp::weighted_percentile: p must be in [0, 1]");
902 }
903
904 auto n = static_cast<std::size_t>(std::distance(first, last));
905 if (n == 0) {
906 throw std::invalid_argument("statcpp::weighted_percentile: empty range");
907 }
908
909 // Create value-weight pairs
910 std::vector<std::pair<double, double>> pairs;
911 pairs.reserve(n);
912 auto weight_it = weight_first;
913 for (auto it = first; it != last; ++it, ++weight_it) {
914 double value = static_cast<double>(std::invoke(proj, *it));
915 double weight = static_cast<double>(*weight_it);
916 if (weight < 0.0) {
917 throw std::invalid_argument("statcpp::weighted_percentile: negative weight");
918 }
919 pairs.emplace_back(value, weight);
920 }
921
922 // Sort by value
923 std::sort(pairs.begin(), pairs.end(),
924 [](const auto& a, const auto& b) { return a.first < b.first; });
925
926 // Sum of weights
927 double total_weight = 0.0;
928 for (const auto& pair : pairs) {
929 total_weight += pair.second;
930 }
931
932 if (total_weight == 0.0) {
933 throw std::invalid_argument("statcpp::weighted_percentile: sum of weights is zero");
934 }
935
936 // Explicit endpoint handling to avoid relying on floating-point loop
937 if (p <= 0.0) return pairs.front().first;
938 if (p >= 1.0) return pairs.back().first;
939
940 // Calculate cumulative weight to find target percentile
941 // For p=0.5, find position at 50% of total weight
942 double target = p * total_weight;
943 const double tol = std::numeric_limits<double>::epsilon() * total_weight;
944 double cumulative = 0.0;
945
946 for (std::size_t i = 0; i < pairs.size(); ++i) {
947 cumulative += pairs[i].second;
948 if (cumulative >= target) {
949 // If cumulative weight is approximately at target, take average with next value
950 if (std::abs(cumulative - target) <= tol && i + 1 < pairs.size()) {
951 return (pairs[i].first + pairs[i + 1].first) / 2.0;
952 }
953 return pairs[i].first;
954 }
955 }
956
957 return pairs.back().first;
958}
959
974template <typename Iterator, typename WeightIterator, typename Projection>
975[[deprecated("Use weighted_percentile(first, last, weight_first, weight_last, p, proj) overload for range safety")]]
976double weighted_percentile(Iterator first, Iterator last, WeightIterator weight_first, double p, Projection proj)
977{
978 if (!(0.0 <= p && p <= 1.0)) {
979 throw std::invalid_argument("statcpp::weighted_percentile: p must be in [0, 1]");
980 }
981
982 auto n = static_cast<std::size_t>(std::distance(first, last));
983 if (n == 0) {
984 throw std::invalid_argument("statcpp::weighted_percentile: empty range");
985 }
986
987 // Create value-weight pairs
988 std::vector<std::pair<double, double>> pairs;
989 pairs.reserve(n);
990 auto weight_it = weight_first;
991 for (auto it = first; it != last; ++it, ++weight_it) {
992 double value = static_cast<double>(std::invoke(proj, *it));
993 double weight = static_cast<double>(*weight_it);
994 if (weight < 0.0) {
995 throw std::invalid_argument("statcpp::weighted_percentile: negative weight");
996 }
997 pairs.emplace_back(value, weight);
998 }
999
1000 // Sort by value
1001 std::sort(pairs.begin(), pairs.end(),
1002 [](const auto& a, const auto& b) { return a.first < b.first; });
1003
1004 // Sum of weights
1005 double total_weight = 0.0;
1006 for (const auto& pair : pairs) {
1007 total_weight += pair.second;
1008 }
1009
1010 if (total_weight == 0.0) {
1011 throw std::invalid_argument("statcpp::weighted_percentile: sum of weights is zero");
1012 }
1013
1014 // Explicit endpoint handling to avoid relying on floating-point loop
1015 if (p <= 0.0) return pairs.front().first;
1016 if (p >= 1.0) return pairs.back().first;
1017
1018 // Calculate cumulative weight to find target percentile
1019 // For p=0.5, find position at 50% of total weight
1020 double target = p * total_weight;
1021 const double tol = std::numeric_limits<double>::epsilon() * total_weight;
1022 double cumulative = 0.0;
1023
1024 for (std::size_t i = 0; i < pairs.size(); ++i) {
1025 cumulative += pairs[i].second;
1026 if (cumulative >= target) {
1027 // If cumulative weight is approximately at target, take average with next value
1028 if (std::abs(cumulative - target) <= tol && i + 1 < pairs.size()) {
1029 return (pairs[i].first + pairs[i + 1].first) / 2.0;
1030 }
1031 return pairs[i].first;
1032 }
1033 }
1034
1035 return pairs.back().first;
1036}
1037
1038} // namespace statcpp
double weighted_percentile(Iterator first, Iterator last, WeightIterator weight_first, WeightIterator weight_last, double p)
Weighted percentile (safe overload)
auto maximum(Iterator first, Iterator last)
Return maximum value.
auto minimum(Iterator first, Iterator last)
Return minimum value.
double weighted_median(Iterator first, Iterator last, WeightIterator weight_first, WeightIterator weight_last)
Weighted median (safe overload)
double interpolate_at(Iterator first, std::size_t n, double p)
Linear interpolation at position.
double percentile(Iterator first, Iterator last, double p)
Return percentile.
quartile_result quartiles(Iterator first, Iterator last)
Return quartiles.
five_number_summary_result five_number_summary(Iterator first, Iterator last)
Return five-number summary.
double q3
Third quartile (75th percentile)
double q2
Second quartile (median, 50th percentile)
double q1
First quartile (25th percentile)