statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
correlation_covariance.hpp
Go to the documentation of this file.
1
11#pragma once
12
13#include <algorithm>
14#include <cmath>
15#include <cstddef>
16#include <functional>
17#include <iterator>
18#include <stdexcept>
19#include <utility>
20#include <vector>
21
23
24namespace statcpp {
25
26// ============================================================================
27// Covariance
28// ============================================================================
29
45template <typename Iterator1, typename Iterator2>
46double population_covariance(Iterator1 first1, Iterator1 last1,
47 Iterator2 first2, Iterator2 last2)
48{
49 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
50 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
51
52 if (n1 == 0 || n2 == 0) {
53 throw std::invalid_argument("statcpp::population_covariance: empty range");
54 }
55 if (n1 != n2) {
56 throw std::invalid_argument("statcpp::population_covariance: ranges must have equal length");
57 }
58
59 double mean_x = statcpp::mean(first1, last1);
60 double mean_y = statcpp::mean(first2, last2);
61
62 double sum = 0.0;
63 auto it1 = first1;
64 auto it2 = first2;
65 for (; it1 != last1; ++it1, ++it2) {
66 sum += (static_cast<double>(*it1) - mean_x) * (static_cast<double>(*it2) - mean_y);
67 }
68
69 return sum / static_cast<double>(n1);
70}
71
86template <typename Iterator1, typename Iterator2>
87double population_covariance(Iterator1 first1, Iterator1 last1,
88 Iterator2 first2, Iterator2 last2,
89 double mean_x, double mean_y)
90{
91 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
92 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
93
94 if (n1 == 0 || n2 == 0) {
95 throw std::invalid_argument("statcpp::population_covariance: empty range");
96 }
97 if (n1 != n2) {
98 throw std::invalid_argument("statcpp::population_covariance: ranges must have equal length");
99 }
100
101 double sum = 0.0;
102 auto it1 = first1;
103 auto it2 = first2;
104 for (; it1 != last1; ++it1, ++it2) {
105 sum += (static_cast<double>(*it1) - mean_x) * (static_cast<double>(*it2) - mean_y);
106 }
107
108 return sum / static_cast<double>(n1);
109}
110
127template <typename Iterator1, typename Iterator2, typename Projection1, typename Projection2>
128double population_covariance(Iterator1 first1, Iterator1 last1,
129 Iterator2 first2, Iterator2 last2,
130 Projection1 proj1, Projection2 proj2)
131{
132 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
133 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
134
135 if (n1 == 0 || n2 == 0) {
136 throw std::invalid_argument("statcpp::population_covariance: empty range");
137 }
138 if (n1 != n2) {
139 throw std::invalid_argument("statcpp::population_covariance: ranges must have equal length");
140 }
141
142 double mean_x = statcpp::mean(first1, last1, proj1);
143 double mean_y = statcpp::mean(first2, last2, proj2);
144
145 double sum = 0.0;
146 auto it1 = first1;
147 auto it2 = first2;
148 for (; it1 != last1; ++it1, ++it2) {
149 sum += (static_cast<double>(std::invoke(proj1, *it1)) - mean_x)
150 * (static_cast<double>(std::invoke(proj2, *it2)) - mean_y);
151 }
152
153 return sum / static_cast<double>(n1);
154}
155
171template <typename Iterator1, typename Iterator2>
172double sample_covariance(Iterator1 first1, Iterator1 last1,
173 Iterator2 first2, Iterator2 last2)
174{
175 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
176 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
177
178 if (n1 == 0 || n2 == 0) {
179 throw std::invalid_argument("statcpp::sample_covariance: empty range");
180 }
181 if (n1 != n2) {
182 throw std::invalid_argument("statcpp::sample_covariance: ranges must have equal length");
183 }
184 if (n1 < 2) {
185 throw std::invalid_argument("statcpp::sample_covariance: need at least 2 elements");
186 }
187
188 double mean_x = statcpp::mean(first1, last1);
189 double mean_y = statcpp::mean(first2, last2);
190
191 double sum = 0.0;
192 auto it1 = first1;
193 auto it2 = first2;
194 for (; it1 != last1; ++it1, ++it2) {
195 sum += (static_cast<double>(*it1) - mean_x) * (static_cast<double>(*it2) - mean_y);
196 }
197
198 return sum / static_cast<double>(n1 - 1);
199}
200
215template <typename Iterator1, typename Iterator2>
216double sample_covariance(Iterator1 first1, Iterator1 last1,
217 Iterator2 first2, Iterator2 last2,
218 double mean_x, double mean_y)
219{
220 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
221 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
222
223 if (n1 == 0 || n2 == 0) {
224 throw std::invalid_argument("statcpp::sample_covariance: empty range");
225 }
226 if (n1 != n2) {
227 throw std::invalid_argument("statcpp::sample_covariance: ranges must have equal length");
228 }
229 if (n1 < 2) {
230 throw std::invalid_argument("statcpp::sample_covariance: need at least 2 elements");
231 }
232
233 double sum = 0.0;
234 auto it1 = first1;
235 auto it2 = first2;
236 for (; it1 != last1; ++it1, ++it2) {
237 sum += (static_cast<double>(*it1) - mean_x) * (static_cast<double>(*it2) - mean_y);
238 }
239
240 return sum / static_cast<double>(n1 - 1);
241}
242
259template <typename Iterator1, typename Iterator2, typename Projection1, typename Projection2>
260double sample_covariance(Iterator1 first1, Iterator1 last1,
261 Iterator2 first2, Iterator2 last2,
262 Projection1 proj1, Projection2 proj2)
263{
264 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
265 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
266
267 if (n1 == 0 || n2 == 0) {
268 throw std::invalid_argument("statcpp::sample_covariance: empty range");
269 }
270 if (n1 != n2) {
271 throw std::invalid_argument("statcpp::sample_covariance: ranges must have equal length");
272 }
273 if (n1 < 2) {
274 throw std::invalid_argument("statcpp::sample_covariance: need at least 2 elements");
275 }
276
277 double mean_x = statcpp::mean(first1, last1, proj1);
278 double mean_y = statcpp::mean(first2, last2, proj2);
279
280 double sum = 0.0;
281 auto it1 = first1;
282 auto it2 = first2;
283 for (; it1 != last1; ++it1, ++it2) {
284 sum += (static_cast<double>(std::invoke(proj1, *it1)) - mean_x)
285 * (static_cast<double>(std::invoke(proj2, *it2)) - mean_y);
286 }
287
288 return sum / static_cast<double>(n1 - 1);
289}
290
302template <typename Iterator1, typename Iterator2>
303double covariance(Iterator1 first1, Iterator1 last1,
304 Iterator2 first2, Iterator2 last2)
305{
306 return sample_covariance(first1, last1, first2, last2);
307}
308
322template <typename Iterator1, typename Iterator2>
323double covariance(Iterator1 first1, Iterator1 last1,
324 Iterator2 first2, Iterator2 last2,
325 double mean_x, double mean_y)
326{
327 return sample_covariance(first1, last1, first2, last2, mean_x, mean_y);
328}
329
345template <typename Iterator1, typename Iterator2, typename Projection1, typename Projection2>
346double covariance(Iterator1 first1, Iterator1 last1,
347 Iterator2 first2, Iterator2 last2,
348 Projection1 proj1, Projection2 proj2)
349{
350 return sample_covariance(first1, last1, first2, last2, proj1, proj2);
351}
352
353// ============================================================================
354// Pearson Correlation Coefficient
355// ============================================================================
356
375template <typename Iterator1, typename Iterator2>
376double pearson_correlation(Iterator1 first1, Iterator1 last1,
377 Iterator2 first2, Iterator2 last2)
378{
379 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
380 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
381
382 if (n1 == 0 || n2 == 0) {
383 throw std::invalid_argument("statcpp::pearson_correlation: empty range");
384 }
385 if (n1 != n2) {
386 throw std::invalid_argument("statcpp::pearson_correlation: ranges must have equal length");
387 }
388 if (n1 < 2) {
389 throw std::invalid_argument("statcpp::pearson_correlation: need at least 2 elements");
390 }
391
392 double mean_x = statcpp::mean(first1, last1);
393 double mean_y = statcpp::mean(first2, last2);
394
395 double sum_xy = 0.0;
396 double sum_x_sq = 0.0;
397 double sum_y_sq = 0.0;
398
399 auto it1 = first1;
400 auto it2 = first2;
401 for (; it1 != last1; ++it1, ++it2) {
402 double dx = static_cast<double>(*it1) - mean_x;
403 double dy = static_cast<double>(*it2) - mean_y;
404 sum_xy += dx * dy;
405 sum_x_sq += dx * dx;
406 sum_y_sq += dy * dy;
407 }
408
409 if (sum_x_sq == 0.0 || sum_y_sq == 0.0) {
410 throw std::invalid_argument("statcpp::pearson_correlation: zero variance in one or both variables");
411 }
412
413 return sum_xy / std::sqrt(sum_x_sq * sum_y_sq);
414}
415
431template <typename Iterator1, typename Iterator2>
432double pearson_correlation(Iterator1 first1, Iterator1 last1,
433 Iterator2 first2, Iterator2 last2,
434 double mean_x, double mean_y)
435{
436 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
437 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
438
439 if (n1 == 0 || n2 == 0) {
440 throw std::invalid_argument("statcpp::pearson_correlation: empty range");
441 }
442 if (n1 != n2) {
443 throw std::invalid_argument("statcpp::pearson_correlation: ranges must have equal length");
444 }
445 if (n1 < 2) {
446 throw std::invalid_argument("statcpp::pearson_correlation: need at least 2 elements");
447 }
448
449 double sum_xy = 0.0;
450 double sum_x_sq = 0.0;
451 double sum_y_sq = 0.0;
452
453 auto it1 = first1;
454 auto it2 = first2;
455 for (; it1 != last1; ++it1, ++it2) {
456 double dx = static_cast<double>(*it1) - mean_x;
457 double dy = static_cast<double>(*it2) - mean_y;
458 sum_xy += dx * dy;
459 sum_x_sq += dx * dx;
460 sum_y_sq += dy * dy;
461 }
462
463 if (sum_x_sq == 0.0 || sum_y_sq == 0.0) {
464 throw std::invalid_argument("statcpp::pearson_correlation: zero variance in one or both variables");
465 }
466
467 return sum_xy / std::sqrt(sum_x_sq * sum_y_sq);
468}
469
487template <typename Iterator1, typename Iterator2, typename Projection1, typename Projection2>
488double pearson_correlation(Iterator1 first1, Iterator1 last1,
489 Iterator2 first2, Iterator2 last2,
490 Projection1 proj1, Projection2 proj2)
491{
492 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
493 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
494
495 if (n1 == 0 || n2 == 0) {
496 throw std::invalid_argument("statcpp::pearson_correlation: empty range");
497 }
498 if (n1 != n2) {
499 throw std::invalid_argument("statcpp::pearson_correlation: ranges must have equal length");
500 }
501 if (n1 < 2) {
502 throw std::invalid_argument("statcpp::pearson_correlation: need at least 2 elements");
503 }
504
505 double mean_x = statcpp::mean(first1, last1, proj1);
506 double mean_y = statcpp::mean(first2, last2, proj2);
507
508 double sum_xy = 0.0;
509 double sum_x_sq = 0.0;
510 double sum_y_sq = 0.0;
511
512 auto it1 = first1;
513 auto it2 = first2;
514 for (; it1 != last1; ++it1, ++it2) {
515 double dx = static_cast<double>(std::invoke(proj1, *it1)) - mean_x;
516 double dy = static_cast<double>(std::invoke(proj2, *it2)) - mean_y;
517 sum_xy += dx * dy;
518 sum_x_sq += dx * dx;
519 sum_y_sq += dy * dy;
520 }
521
522 if (sum_x_sq == 0.0 || sum_y_sq == 0.0) {
523 throw std::invalid_argument("statcpp::pearson_correlation: zero variance in one or both variables");
524 }
525
526 return sum_xy / std::sqrt(sum_x_sq * sum_y_sq);
527}
528
529// ============================================================================
530// Spearman's Rank Correlation Coefficient
531// ============================================================================
532
533namespace detail {
534
545template <typename Iterator>
546std::vector<double> compute_ranks(Iterator first, Iterator last)
547{
548 auto n = static_cast<std::size_t>(std::distance(first, last));
549 if (n == 0) {
550 return {};
551 }
552
553 // Create pairs of index and value
554 std::vector<std::pair<std::size_t, double>> indexed_values;
555 indexed_values.reserve(n);
556 std::size_t idx = 0;
557 for (auto it = first; it != last; ++it, ++idx) {
558 indexed_values.emplace_back(idx, static_cast<double>(*it));
559 }
560
561 // Sort by value
562 std::sort(indexed_values.begin(), indexed_values.end(),
563 [](const auto& a, const auto& b) { return a.second < b.second; });
564
565 // Compute ranks (average rank for ties)
566 std::vector<double> ranks(n);
567 std::size_t i = 0;
568 while (i < n) {
569 std::size_t j = i;
570 // Find range of elements with the same value
571 while (j < n && indexed_values[j].second == indexed_values[i].second) {
572 ++j;
573 }
574 // Compute average rank (1-based)
575 double avg_rank = (static_cast<double>(i + 1) + static_cast<double>(j)) / 2.0;
576 for (std::size_t k = i; k < j; ++k) {
577 ranks[indexed_values[k].first] = avg_rank;
578 }
579 i = j;
580 }
581
582 return ranks;
583}
584
595template <typename Iterator, typename Projection>
596std::vector<double> compute_ranks(Iterator first, Iterator last, Projection proj)
597{
598 auto n = static_cast<std::size_t>(std::distance(first, last));
599 if (n == 0) {
600 return {};
601 }
602
603 std::vector<std::pair<std::size_t, double>> indexed_values;
604 indexed_values.reserve(n);
605 std::size_t idx = 0;
606 for (auto it = first; it != last; ++it, ++idx) {
607 indexed_values.emplace_back(idx, static_cast<double>(std::invoke(proj, *it)));
608 }
609
610 std::sort(indexed_values.begin(), indexed_values.end(),
611 [](const auto& a, const auto& b) { return a.second < b.second; });
612
613 std::vector<double> ranks(n);
614 std::size_t i = 0;
615 while (i < n) {
616 std::size_t j = i;
617 while (j < n && indexed_values[j].second == indexed_values[i].second) {
618 ++j;
619 }
620 double avg_rank = (static_cast<double>(i + 1) + static_cast<double>(j)) / 2.0;
621 for (std::size_t k = i; k < j; ++k) {
622 ranks[indexed_values[k].first] = avg_rank;
623 }
624 i = j;
625 }
626
627 return ranks;
628}
629
630} // namespace detail
631
649template <typename Iterator1, typename Iterator2>
650double spearman_correlation(Iterator1 first1, Iterator1 last1,
651 Iterator2 first2, Iterator2 last2)
652{
653 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
654 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
655
656 if (n1 == 0 || n2 == 0) {
657 throw std::invalid_argument("statcpp::spearman_correlation: empty range");
658 }
659 if (n1 != n2) {
660 throw std::invalid_argument("statcpp::spearman_correlation: ranges must have equal length");
661 }
662 if (n1 < 2) {
663 throw std::invalid_argument("statcpp::spearman_correlation: need at least 2 elements");
664 }
665
666 auto ranks_x = detail::compute_ranks(first1, last1);
667 auto ranks_y = detail::compute_ranks(first2, last2);
668
669 return pearson_correlation(ranks_x.begin(), ranks_x.end(),
670 ranks_y.begin(), ranks_y.end());
671}
672
689template <typename Iterator1, typename Iterator2, typename Projection1, typename Projection2>
690double spearman_correlation(Iterator1 first1, Iterator1 last1,
691 Iterator2 first2, Iterator2 last2,
692 Projection1 proj1, Projection2 proj2)
693{
694 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
695 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
696
697 if (n1 == 0 || n2 == 0) {
698 throw std::invalid_argument("statcpp::spearman_correlation: empty range");
699 }
700 if (n1 != n2) {
701 throw std::invalid_argument("statcpp::spearman_correlation: ranges must have equal length");
702 }
703 if (n1 < 2) {
704 throw std::invalid_argument("statcpp::spearman_correlation: need at least 2 elements");
705 }
706
707 auto ranks_x = detail::compute_ranks(first1, last1, proj1);
708 auto ranks_y = detail::compute_ranks(first2, last2, proj2);
709
710 return pearson_correlation(ranks_x.begin(), ranks_x.end(),
711 ranks_y.begin(), ranks_y.end());
712}
713
714// ============================================================================
715// Kendall's Tau (Rank Correlation Coefficient)
716// ============================================================================
717
734template <typename Iterator1, typename Iterator2>
735double kendall_tau(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
736{
737 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
738 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
739
740 if (n1 == 0 || n2 == 0) {
741 throw std::invalid_argument("statcpp::kendall_tau: empty range");
742 }
743 if (n1 != n2) {
744 throw std::invalid_argument("statcpp::kendall_tau: ranges must have equal length");
745 }
746
747 auto n = n1;
748 if (n < 2) {
749 throw std::invalid_argument("statcpp::kendall_tau: need at least 2 elements");
750 }
751
752 // Create pairs
753 std::vector<std::pair<double, double>> pairs;
754 pairs.reserve(n);
755
756 auto it1 = first1;
757 auto it2 = first2;
758 for (std::size_t i = 0; i < n; ++i, ++it1, ++it2) {
759 pairs.push_back({static_cast<double>(*it1), static_cast<double>(*it2)});
760 }
761
762 // Count concordant, discordant, and tied pairs.
763 //
764 // Tie detection uses exact equality (diff == 0.0).
765 // This is correct when the input values are integers or bit-identical doubles
766 // (the common case for statistical data).
767 // If the inputs are the result of floating-point computations, two values that
768 // are mathematically equal may differ by a rounding error and thus not be
769 // detected as tied. Callers should round or quantise such data before passing
770 // it to this function.
771 std::size_t concordant = 0;
772 std::size_t discordant = 0;
773 std::size_t tie_x = 0;
774 std::size_t tie_y = 0;
775 std::size_t tie_xy = 0;
776
777 for (std::size_t i = 0; i < n; ++i) {
778 for (std::size_t j = i + 1; j < n; ++j) {
779 double x_diff = pairs[i].first - pairs[j].first;
780 double y_diff = pairs[i].second - pairs[j].second;
781
782 if (x_diff == 0.0 && y_diff == 0.0) {
783 tie_xy++;
784 } else if (x_diff == 0.0) {
785 tie_x++;
786 } else if (y_diff == 0.0) {
787 tie_y++;
788 } else if ((x_diff > 0.0 && y_diff > 0.0) || (x_diff < 0.0 && y_diff < 0.0)) {
789 concordant++;
790 } else {
791 discordant++;
792 }
793 }
794 }
795
796 // Kendall's tau-b
797 std::size_t n0 = n * (n - 1) / 2;
798 std::size_t tie_count_x = tie_x + tie_xy;
799 std::size_t tie_count_y = tie_y + tie_xy;
800
801 if (n0 == tie_count_x || n0 == tie_count_y) {
802 // All tied case
803 return 0.0;
804 }
805
806 double numerator = static_cast<double>(concordant) - static_cast<double>(discordant);
807 double denominator = std::sqrt(static_cast<double>(n0 - tie_count_x) * static_cast<double>(n0 - tie_count_y));
808
809 return numerator / denominator;
810}
811
828template <typename Iterator1, typename Iterator2, typename Projection1, typename Projection2>
829double kendall_tau(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2,
830 Projection1 proj1, Projection2 proj2)
831{
832 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
833 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
834
835 if (n1 == 0 || n2 == 0) {
836 throw std::invalid_argument("statcpp::kendall_tau: empty range");
837 }
838 if (n1 != n2) {
839 throw std::invalid_argument("statcpp::kendall_tau: ranges must have equal length");
840 }
841
842 auto n = n1;
843 if (n < 2) {
844 throw std::invalid_argument("statcpp::kendall_tau: need at least 2 elements");
845 }
846
847 // Create pairs of projected values
848 std::vector<std::pair<double, double>> pairs;
849 pairs.reserve(n);
850
851 auto it1 = first1;
852 auto it2 = first2;
853 for (std::size_t i = 0; i < n; ++i, ++it1, ++it2) {
854 pairs.push_back({static_cast<double>(std::invoke(proj1, *it1)),
855 static_cast<double>(std::invoke(proj2, *it2))});
856 }
857
858 // Count concordant, discordant, and tied pairs.
859 //
860 // Tie detection uses exact equality (diff == 0.0).
861 // This is correct when the projected values are integers or bit-identical doubles.
862 // For inputs derived from floating-point computations, callers should round or
863 // quantise the data before passing it to this function.
864 std::size_t concordant = 0;
865 std::size_t discordant = 0;
866 std::size_t tie_x = 0;
867 std::size_t tie_y = 0;
868 std::size_t tie_xy = 0;
869
870 for (std::size_t i = 0; i < n; ++i) {
871 for (std::size_t j = i + 1; j < n; ++j) {
872 double x_diff = pairs[i].first - pairs[j].first;
873 double y_diff = pairs[i].second - pairs[j].second;
874
875 if (x_diff == 0.0 && y_diff == 0.0) {
876 tie_xy++;
877 } else if (x_diff == 0.0) {
878 tie_x++;
879 } else if (y_diff == 0.0) {
880 tie_y++;
881 } else if ((x_diff > 0.0 && y_diff > 0.0) || (x_diff < 0.0 && y_diff < 0.0)) {
882 concordant++;
883 } else {
884 discordant++;
885 }
886 }
887 }
888
889 std::size_t n0 = n * (n - 1) / 2;
890 std::size_t tie_count_x = tie_x + tie_xy;
891 std::size_t tie_count_y = tie_y + tie_xy;
892
893 if (n0 == tie_count_x || n0 == tie_count_y) {
894 return 0.0;
895 }
896
897 double numerator = static_cast<double>(concordant) - static_cast<double>(discordant);
898 double denominator = std::sqrt(static_cast<double>(n0 - tie_count_x) * static_cast<double>(n0 - tie_count_y));
899
900 return numerator / denominator;
901}
902
903// ============================================================================
904// Weighted Covariance
905// ============================================================================
906
924template <typename Iterator1, typename Iterator2, typename WeightIterator>
925double weighted_covariance(Iterator1 first1, Iterator1 last1,
926 Iterator2 first2, Iterator2 last2,
927 WeightIterator weight_first)
928{
929 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
930 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
931
932 if (n1 == 0 || n2 == 0) {
933 throw std::invalid_argument("statcpp::weighted_covariance: empty range");
934 }
935 if (n1 != n2) {
936 throw std::invalid_argument("statcpp::weighted_covariance: ranges must have the same size");
937 }
938
939 // Compute weighted mean
940 double sum_weights = 0.0;
941 double sum_wx = 0.0;
942 double sum_wy = 0.0;
943 auto it1 = first1;
944 auto it2 = first2;
945 auto weight_it = weight_first;
946
947 for (; it1 != last1; ++it1, ++it2, ++weight_it) {
948 double x = static_cast<double>(*it1);
949 double y = static_cast<double>(*it2);
950 double w = static_cast<double>(*weight_it);
951
952 if (w < 0.0) {
953 throw std::invalid_argument("statcpp::weighted_covariance: negative weight");
954 }
955
956 sum_weights += w;
957 sum_wx += w * x;
958 sum_wy += w * y;
959 }
960
961 if (sum_weights == 0.0) {
962 throw std::invalid_argument("statcpp::weighted_covariance: sum of weights is zero");
963 }
964
965 double mean_x = sum_wx / sum_weights;
966 double mean_y = sum_wy / sum_weights;
967
968 // Compute weighted covariance
969 double sum_w_sq = 0.0;
970 double cov = 0.0;
971 it1 = first1;
972 it2 = first2;
973 weight_it = weight_first;
974
975 for (; it1 != last1; ++it1, ++it2, ++weight_it) {
976 double x = static_cast<double>(*it1);
977 double y = static_cast<double>(*it2);
978 double w = static_cast<double>(*weight_it);
979
980 cov += w * (x - mean_x) * (y - mean_y);
981 sum_w_sq += w * w;
982 }
983
984 // Bessel correction for frequency weights.
985 // The formula W / (W^2 - sum(w_i^2)) yields the unbiased estimate when weights
986 // represent repeat counts (frequency weights). It reduces to n/(n-1) when all
987 // weights equal 1.
988 // For precision weights (inverse-variance), a different correction is required.
989 double denominator = sum_weights * sum_weights - sum_w_sq;
990 if (denominator <= 0.0) {
991 throw std::invalid_argument(
992 "statcpp::weighted_covariance: insufficient effective sample size for Bessel correction");
993 }
994 double correction = sum_weights / denominator;
995 return cov * correction;
996}
997
1017template <typename Iterator1, typename Iterator2, typename WeightIterator,
1018 typename Projection1, typename Projection2>
1019double weighted_covariance(Iterator1 first1, Iterator1 last1,
1020 Iterator2 first2, Iterator2 last2,
1021 WeightIterator weight_first,
1022 Projection1 proj1, Projection2 proj2)
1023{
1024 auto n1 = static_cast<std::size_t>(std::distance(first1, last1));
1025 auto n2 = static_cast<std::size_t>(std::distance(first2, last2));
1026
1027 if (n1 == 0 || n2 == 0) {
1028 throw std::invalid_argument("statcpp::weighted_covariance: empty range");
1029 }
1030 if (n1 != n2) {
1031 throw std::invalid_argument("statcpp::weighted_covariance: ranges must have the same size");
1032 }
1033
1034 // Compute weighted mean
1035 double sum_weights = 0.0;
1036 double sum_wx = 0.0;
1037 double sum_wy = 0.0;
1038 auto it1 = first1;
1039 auto it2 = first2;
1040 auto weight_it = weight_first;
1041
1042 for (; it1 != last1; ++it1, ++it2, ++weight_it) {
1043 double x = static_cast<double>(std::invoke(proj1, *it1));
1044 double y = static_cast<double>(std::invoke(proj2, *it2));
1045 double w = static_cast<double>(*weight_it);
1046
1047 if (w < 0.0) {
1048 throw std::invalid_argument("statcpp::weighted_covariance: negative weight");
1049 }
1050
1051 sum_weights += w;
1052 sum_wx += w * x;
1053 sum_wy += w * y;
1054 }
1055
1056 if (sum_weights == 0.0) {
1057 throw std::invalid_argument("statcpp::weighted_covariance: sum of weights is zero");
1058 }
1059
1060 double mean_x = sum_wx / sum_weights;
1061 double mean_y = sum_wy / sum_weights;
1062
1063 // Compute weighted covariance
1064 double sum_w_sq = 0.0;
1065 double cov = 0.0;
1066 it1 = first1;
1067 it2 = first2;
1068 weight_it = weight_first;
1069
1070 for (; it1 != last1; ++it1, ++it2, ++weight_it) {
1071 double x = static_cast<double>(std::invoke(proj1, *it1));
1072 double y = static_cast<double>(std::invoke(proj2, *it2));
1073 double w = static_cast<double>(*weight_it);
1074
1075 cov += w * (x - mean_x) * (y - mean_y);
1076 sum_w_sq += w * w;
1077 }
1078
1079 // Bessel correction for frequency weights.
1080 // The formula W / (W^2 - sum(w_i^2)) yields the unbiased estimate when weights
1081 // represent repeat counts (frequency weights). It reduces to n/(n-1) when all
1082 // weights equal 1.
1083 // For precision weights (inverse-variance), a different correction is required.
1084 double denominator = sum_weights * sum_weights - sum_w_sq;
1085 if (denominator <= 0.0) {
1086 throw std::invalid_argument(
1087 "statcpp::weighted_covariance: insufficient effective sample size for Bessel correction");
1088 }
1089 double correction = sum_weights / denominator;
1090 return cov * correction;
1091}
1092
1093} // namespace statcpp
Basic statistical computation functions.
std::vector< double > compute_ranks(Iterator first, Iterator last)
Helper function to compute ranks.
double spearman_correlation(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Spearman's rank correlation coefficient.
double sample_covariance(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Sample covariance (unbiased covariance)
auto sum(Iterator first, Iterator last)
Sum.
double weighted_covariance(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, WeightIterator weight_first)
Weighted covariance.
double mean(Iterator first, Iterator last)
Arithmetic mean.
double covariance(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Covariance (alias for sample_covariance)
double pearson_correlation(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Pearson correlation coefficient.
double population_covariance(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Population covariance.
double kendall_tau(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Kendall's rank correlation coefficient (tau-b)