Loading...
Searching...
No Matches
ranks.hpp
1// Copyright © 2020 Thomas Nagler
2//
3// This file is part of the wdm library and licensed under the terms of
4// the MIT license. For a copy, see the LICENSE file in the root directory
5// or https://github.com/tnagler/wdm/blob/master/LICENSE.
6
7#pragma once
8
9#include "nan_handling.hpp"
10#include "random.hpp"
11#include "utils.hpp"
12
13#include <algorithm>
14#include <memory>
15#include <numeric>
16
17namespace wdm {
18
19namespace impl {
20
27inline std::vector<size_t>
28draw_tie_order(size_t size, random::RandomGenerator& generator)
29{
30 std::vector<size_t> ord(size);
31 std::iota(ord.begin(), ord.end(), 0);
32 if (size > 1)
33 random::shuffle(ord, generator);
34 return ord;
35}
36
49inline std::vector<size_t>
50tie_order(const std::vector<size_t>& group_sizes,
51 std::vector<int> seeds = std::vector<int>())
52{
53 random::RandomGenerator generator(seeds);
54 std::vector<size_t> ord;
55 for (size_t size : group_sizes) {
56 if (size == 0)
57 throw std::runtime_error("tie group sizes must be positive.");
58 auto group = draw_tie_order(size, generator);
59 ord.insert(ord.end(), group.begin(), group.end());
60 }
61 return ord;
62}
63
73inline std::vector<double>
74rank(std::vector<double> x,
75 std::vector<double> weights = std::vector<double>(),
76 std::string ties_method = "min",
77 std::vector<int> seeds = std::vector<int>())
78{
79 if ((ties_method != "min") && (ties_method != "average") &&
80 (ties_method != "first") && (ties_method != "random"))
81 throw std::runtime_error(
82 "ties method must be one of 'min', 'average', 'first', 'random'.");
83
84 // set default weights if necessary
85 size_t n = x.size();
86 if (weights.size() == 0)
87 weights = std::vector<double>(n, 1.0);
88
89 if (weights.size() != n) {
90 throw std::runtime_error("weights and data must have same size.");
91 }
92
93 // NaN-handling
94 std::vector<double> nans;
95 if (utils::any_nan(x)) {
96 nans.resize(n, 0);
97 for (size_t i = 0; i < n; i++) {
98 if (std::isnan(x[i])) {
99 x[i] = std::numeric_limits<double>::max();
100 nans[i] = 1;
101 weights[i] = 0;
102 }
103 }
104 }
105
106 double w_mean =
107 utils::sum(weights) / static_cast<double>(n - utils::sum(nans));
108 for (auto& w : weights) {
109 w = w / w_mean;
110 }
111
112 // permutation that brings 'x' in ascending order
113 std::vector<size_t> perm = utils::get_order(x);
114
115 // all tie groups draw from the same stream, so that they are shuffled
116 // independently of one another
117 std::unique_ptr<random::RandomGenerator> random_gen;
118 if (ties_method == "random")
119 random_gen.reset(new random::RandomGenerator(seeds));
120
121 double w_acc = 0.0, w_batch;
122 for (size_t i = 0, reps; i < n; i += reps) {
123 // find replications
124 reps = 0;
125 w_batch = 0.0;
126 while ((i + reps < n) && (x[perm[i]] == x[perm[i + reps]]))
127 w_batch += weights[perm[i + reps++]];
128
129 // assign min rank
130 for (size_t k = 0; k < reps; ++k)
131 x[perm[i + k]] = w_acc + weights[perm[i]];
132
133 if (reps > 1) {
134 if ((ties_method == "first") || (ties_method == "random")) {
135 // break ties by assigning the cumulative weights, in order of
136 // appearance ("first") or in random order ("random")
137 std::vector<size_t> ord(reps);
138 std::iota(ord.begin(), ord.end(), 0); // 0, 1, 2, ...
139 if (ties_method == "random")
140 ord = draw_tie_order(reps, *random_gen);
141
142 double ww = 0.0;
143 for (size_t k = 0; k < reps; ++k) {
144 ww += weights[perm[i + ord[k]]];
145 x[perm[i + ord[k]]] = w_acc + ww;
146 }
147 } else if (ties_method == "average") {
148 // assign average rank to tied values
149 for (size_t k = 0; k < reps; ++k)
150 x[perm[i + k]] += (w_batch - weights[perm[i]]) / 2;
151 }
152 }
153
154 // accumulate weights for current batch
155 w_acc += w_batch;
156 }
157
158 if (nans.size() == n) {
159 for (size_t i = 0; i < x.size(); i++) {
160 if (nans[i]) {
161 x[i] = NAN;
162 }
163 }
164 }
165
166 return x;
167}
168
178inline std::vector<double>
179rank0(std::vector<double> x,
180 std::vector<double> weights = std::vector<double>(),
181 std::string ties_method = "min")
182{
183 if ((ties_method != "min") && (ties_method != "average") &&
184 (ties_method != "max"))
185 throw std::runtime_error(
186 "ties_method must be either 'min', 'average', or 'max'.");
187
188 // set default weights if necessary
189 size_t n = x.size();
190 if (weights.size() == 0)
191 weights = std::vector<double>(n, 1.0);
192
193 // permutation that brings 'x' in ascending order
194 std::vector<size_t> perm = utils::get_order(x);
195
196 double w_acc = 0.0, w_batch;
197 for (size_t i = 0, reps; i < n; i += reps) {
198 // find replications
199 reps = 0;
200 w_batch = 0.0;
201 while ((i + reps < n) && (x[perm[i]] == x[perm[i + reps]]))
202 w_batch += weights[perm[i + reps++]];
203
204 // assign min rank
205 for (size_t k = 0; k < reps; ++k)
206 x[perm[i + k]] = w_acc;
207
208 // accumulate weights for current batch
209 w_acc += w_batch;
210
211 // assign average rank to tied values
212 if ((ties_method == "average") && (reps > 1)) {
213 std::vector<double> ww(reps);
214 for (size_t k = 0; k < reps; ++k)
215 ww[k] = weights[perm[i + k]];
216 double offset = utils::perm_sum(ww, 2) / w_batch;
217 for (size_t k = 0; k < reps; ++k)
218 x[perm[i + k]] += offset;
219 } else if (ties_method == "max") {
220 // w_acc now holds the weight of everything up to and including the batch
221 for (size_t k = 0; k < reps; ++k)
222 x[perm[i + k]] = w_acc;
223 }
224 }
225
226 return x;
227}
228
235inline std::vector<double>
236bivariate_rank(const std::vector<double>& x,
237 const std::vector<double>& y,
238 std::vector<double> weights = std::vector<double>())
239{
240 utils::check_sizes(x, y, weights);
241 size_t n = x.size();
242 if (weights.size() == 0)
243 weights = std::vector<double>(n, 1.0);
244
245 // Visit the observations by increasing x, and those with equal x by
246 // decreasing y: an observation visited earlier then has a strictly smaller
247 // x whenever it has a strictly smaller y.
248 std::vector<size_t> order(n);
249 std::iota(order.begin(), order.end(), 0);
250 std::stable_sort(order.begin(), order.end(), [&](size_t i, size_t j) {
251 return (x[i] < x[j]) || ((x[i] == x[j]) && (y[i] > y[j]));
252 });
253
254 // The distinct values of y in increasing order, and the weight seen so far
255 // at each.
256 std::vector<double> levels = y;
257 std::sort(levels.begin(), levels.end());
258 levels.erase(std::unique(levels.begin(), levels.end()), levels.end());
259 utils::FenwickTree seen(levels.size());
260
261 std::vector<double> counts(n);
262 for (size_t i : order) {
263 // Which distinct y-value does this observation have?
264 size_t level = static_cast<size_t>(
265 std::lower_bound(levels.begin(), levels.end(), y[i]) - levels.begin());
266 // How much weight has already been seen at smaller y-values?
267 counts[i] = seen.prefix_sum(level);
268 // Record this observation at its y-value.
269 seen.add(level, weights[i]);
270 }
271
272 return counts;
273}
274
277inline double
278median(const std::vector<double>& x,
279 std::vector<double> weights = std::vector<double>())
280{
281 utils::check_sizes(x, x, weights);
282 size_t n = x.size();
283
284 // sort x and weights in x order
285 auto perm = utils::get_order(x);
286 auto xx = x;
287 auto w = weights;
288 for (size_t i = 0; i < n; i++) {
289 xx[i] = x[perm[i]];
290 if (w.size() > 0)
291 w[i] = weights[perm[i]];
292 }
293
294 // compute weighted ranks and the "average rank" (corresponds to the
295 // median)
296 auto ranks = rank0(xx, w, "average");
297 if (weights.size() == 0)
298 weights = std::vector<double>(n, 1.0);
299 double rank_avrg = utils::perm_sum(weights, 2) / utils::sum(weights);
300
301 // weighted median splits data below and above rank_avrg
302 size_t i = 0;
303 while (ranks[i] < rank_avrg)
304 i++;
305 if (ranks[i] == rank_avrg)
306 return xx[i];
307 else
308 return 0.5 * (xx[i - 1] + xx[i]);
309}
310}
311}
Weighted dependence measures.
Definition wdm.hpp:19