quickstats
Quickly compute simple statistics
Loading...
Searching...
No Matches
MultipleQuantiles.hpp
Go to the documentation of this file.
1#ifndef QUICKSTATS_MULTIPLE_QUANTILES_HPP
2#define QUICKSTATS_MULTIPLE_QUANTILES_HPP
3
4#include <type_traits>
5#include <stdexcept>
6#include <cstddef>
7#include <vector>
8#include <optional>
9#include <cassert>
10#include <limits>
11
12#include "sanisizer/sanisizer.hpp"
13
14#include "SingleQuantile.hpp"
15#include "utils.hpp"
16
22namespace quickstats {
23
32template<class Output_ = double>
34public:
43 template<typename Quantiles_>
44 MultipleQuantilesFixedNumber(const std::size_t num_total, const Quantiles_& quantiles) :
45 my_len(num_total),
46 my_stacks(sanisizer::cast<I<decltype(my_stacks.size())> >(quantiles.size()))
47 {
48 static_assert(std::is_floating_point<Output_>::value);
49
50 if (num_total <= 0) {
51 throw std::runtime_error("'num_total' should be positive");
52 }
53 const std::size_t num_m1 = num_total - 1;
54
55 const auto num_quantiles = quantiles.size();
56 Output_ last_quantile = -1;
57
58 for (I<decltype(num_quantiles)> q = 0; q < num_quantiles; ++q) {
59 const Output_ quantile = quantiles[q];
60 auto& current = my_stacks[q];
61 configure_single_quantile(quantile, num_m1, current.upper_index, current.upper_fraction, current.skip_lower);
62
63 if (quantile < last_quantile) {
64 throw std::runtime_error("elements of 'quantiles' should be sorted");
65 }
66 last_quantile = quantile;
67 }
68 }
69
70private:
71 std::size_t my_len;
72
73 struct Configuration {
74 std::size_t upper_index;
75 Output_ upper_fraction;
76 bool skip_lower;
77 };
78
79 std::vector<Configuration> my_stacks;
80
81 // We initialize the upper/lower boundaries of each quantile calculation to avoid GCC complaining about uninitialized values.
82 // All boundaries should always be initialized before use but it seems that GCC can't figure that out.
83 // For safety's sake, we use a very low number to force a nonsensical result if our initialization assumption is wrong.
84 static constexpr Output_ mock_init() {
85 return std::numeric_limits<Output_>::lowest();
86 }
87
88public:
92 std::size_t get_num_total() const {
93 return my_len;
94 }
95
96public:
112 template<typename Input_, class OutputFun_>
113 void operator()(Input_* const ptr, OutputFun_ output) const {
114 const auto end = ptr + my_len;
115 const auto num_quantiles = my_stacks.size();
116
117 std::size_t last_index = 0;
118 std::size_t last_index_p1 = 0; // yes, this is a deliberate starting value for 'last_index + 1'.
119 Output_ lower_val = mock_init(), upper_val = mock_init();
120
121 for (I<decltype(num_quantiles)> q = 0; q < num_quantiles; ++q) {
122 const auto& curstack = my_stacks[q];
123 const auto curindex = curstack.upper_index;
124
125 // Only need to search if 'cur_index > last_index' (i.e., 'cur_index >= last_index_p1'),
126 // otherwise 'cur_index == last_index' and we can re-use existing lower/upper_val.
127 // The exception is if at the first iteration of this loop where 'last_index_p1 == 0 <= curindex',
128 // where this condition is always true to force a search to initialize 'upper_val'.
129 if (curindex >= last_index_p1) {
130 const auto target = ptr + curindex;
131
132 // +1, as we only need to search after the 'last_index'; everything before or at 'last_index' is known to be too small.
133 // Again, the exception is at the first iteration of this loop, where we need to search from the start of the array to initialize 'upper_val'.
134 std::nth_element(ptr + last_index_p1, target, end);
135 upper_val = *target;
136
137 if (curstack.skip_lower) {
138 output(q, upper_val);
139 } else {
140 // No + 1, as 'ptr[last_index]' might be the maximum.
141 lower_val = *std::max_element(ptr + last_index, target);
142 output(q, interpolate(lower_val, upper_val, curstack.upper_fraction));
143 }
144
145 last_index = curindex;
146 last_index_p1 = curindex + 1;
147
148 } else {
149 if (curstack.skip_lower) {
150 output(q, upper_val);
151 } else {
152 // It is implicitly guaranteed that 'lower_val' is initialized at this point.
153 // It's impossible for an earlier quantile to have 'skip_lower = true' while a later quantile has 'skip_lower = false'.
154 output(q, interpolate(lower_val, upper_val, curstack.upper_fraction));
155 }
156 }
157 }
158 }
159
178 template<typename Input_, class OutputFun_>
179 void operator()(const std::size_t num_non_zero, Input_* const values, OutputFun_ output) const {
180 assert(num_non_zero <= my_len);
181 const auto num_quantiles = my_stacks.size();
182
183 if (num_non_zero == my_len) {
184 operator()(values, std::move(output));
185 return;
186 } else if (num_non_zero == 0) {
187 for (I<decltype(num_quantiles)> q = 0; q < num_quantiles; ++q) {
188 output(q, 0);
189 }
190 return;
191 }
192
193 std::size_t num_negative = 0;
194 for (std::size_t i = 0; i < num_non_zero; ++i) {
195 num_negative += (values[i] < 0);
196 }
197
198 std::size_t last_index = 0;
199 std::size_t last_index_p1 = 0; // see comments for the dense case for an explanation.
200 I<decltype(num_quantiles)> q = 0;
201
202 // Processing all quantiles where both upper and lower values are negative.
203 {
204 Output_ lower_val = mock_init(), upper_val = mock_init();
205 for (; q < num_quantiles; ++q) {
206 const auto& curstack = my_stacks[q];
207 const auto curindex = curstack.upper_index;
208 if (curindex >= num_negative) {
209 break;
210 }
211
212 if (curindex >= last_index_p1) {
213 const auto target = values + curindex;
214 std::nth_element(values + last_index_p1, target, values + num_non_zero);
215 upper_val = *target;
216
217 if (curstack.skip_lower) {
218 output(q, upper_val);
219 } else {
220 lower_val = *std::max_element(values + last_index, target);
221 output(q, interpolate(lower_val, upper_val, curstack.upper_fraction));
222 }
223
224 last_index = curindex;
225 last_index_p1 = curindex + 1;
226
227 } else {
228 if (curstack.skip_lower) {
229 output(q, upper_val);
230 } else {
231 output(q, interpolate(lower_val, upper_val, curstack.upper_fraction));
232 }
233 }
234 }
235 }
236
237 // Processing all quantiles where lower value is negative and upper value is zero.
238 if (num_negative) {
239 bool computed = false;
240 Output_ lower_val = mock_init();
241 for (; q < num_quantiles; ++q) {
242 const auto& curstack = my_stacks[q];
243 const auto curindex = curstack.upper_index;
244 if (curindex > num_negative) {
245 break;
246 }
247
248 if (curstack.skip_lower) {
249 // Upper value must be zero.
250 // It can't be positive, as that would imply that there are no structural zeros and 'num_non_zero == my_len'.
251 output(q, 0);
252 continue;
253 }
254
255 if (!computed) { // Only need to search this once given that everyone in this loop has 'upper_index == num_negative'.
256 const auto num_negative_m1 = num_negative - 1;
257 const auto target = values + num_negative_m1;
258
259 // We know that 'curindex >= num_negative' from the exit condition in the previous loop.
260 // This implies that any previous loop iterations would have 'curindex < num_negative' such that 'last_index + 1 <= num_negative'.
261 // If 'last_index + 1 == num_negative', then 'last_index == num_negative - 1', and no search is required.
262 // So, we only search if 'last_index + 1 < num_negative', i.e., 'last_index + 1 <= num_negative - 1'.
263 // (If there were no runs of the previous loop, then 'last_index + 1 == 0' and we force a search anyway.)
264 if (num_negative_m1 >= last_index_p1) {
265 std::nth_element(values + last_index_p1, target, values + num_non_zero);
266 }
267 lower_val = *target;
268
269 last_index = num_negative_m1;
270 last_index_p1 = num_negative;
271 computed = true;
272 }
273
274 output(q, lower_val * (1 - curstack.upper_fraction));
275 }
276 }
277
278 // Processing all quantiles where both the lower and upper values are zero.
279 const std::size_t num_zeros = my_len - num_non_zero;
280 const std::size_t num_not_positive = num_zeros + num_negative;
281 for (; q < num_quantiles; ++q) {
282 if (my_stacks[q].upper_index >= num_not_positive) {
283 break;
284 }
285 output(q, 0);
286 }
287
288 // Processing all quantiles where the lower value is zero and the upper value is positive.
289 {
290 bool computed = false;
291 Output_ upper_val = mock_init();
292 for (; q < num_quantiles; ++q) {
293 const auto& curstack = my_stacks[q];
294 if (curstack.upper_index > num_not_positive) {
295 break;
296 }
297
298 if (!computed) { // Only need to search this once given that everyone in this loop has 'upper_index == num_not_positive'.
299 // The actual target is the first positive value at 'values[num_not_positive - num_zeros]', i.e., 'values[num_negative]'.
300 const auto target = values + num_negative;
301
302 // No point wrapping this in 'num_negative >= last_index + 1', as this will always be true from the exit conditions above.
303 // Or if none of the loops above were iterated over, we would still have 'last_index_p1 == 0', in which case this will also be true.
304 std::nth_element(values + last_index_p1, target, values + num_non_zero);
305
306 last_index = num_negative;
307 last_index_p1 = num_negative + 1;
308 upper_val = *target;
309 computed = true;
310 }
311
312 if (curstack.skip_lower) {
313 output(q, upper_val);
314 } else {
315 output(q, upper_val * curstack.upper_fraction);
316 }
317 }
318 }
319
320 // Processing all quantiles where the upper and lower values are positive.
321 {
322 Output_ lower_val = mock_init(), upper_val = mock_init();
323 for (; q < num_quantiles; ++q) {
324 const auto& curstack = my_stacks[q];
325
326 // This should always be positive as we know that 'upper_index > num_not_positive' and thus 'upper_index - num_zeros > num_negative >= 0'.
327 const std::size_t curindex = curstack.upper_index - num_zeros;
328
329 // Even with subtraction of num_zeros, we can be sure that 'curindex >= last_index + 1' in the first iteration of this loop.
330 // From the clauses above, we know that 'last_index <= num_negative' and 'upper_index > num_not_positive'.
331 // So, subtracting 'num_zeros' gives us 'curindex > last_index' and thus 'curindex >= last_index + 1'.
332 // This ensures that this condition will always be true, and the code will be run, and 'upper_val' will always be set before use.
333 if (curindex >= last_index_p1) {
334 const auto target = values + curindex;
335 std::nth_element(values + last_index_p1, target, values + num_non_zero);
336 upper_val = *target;
337
338 if (curstack.skip_lower) {
339 output(q, upper_val);
340 } else {
341 lower_val = *std::max_element(values + last_index, target);
342 output(q, interpolate(lower_val, upper_val, curstack.upper_fraction));
343 }
344
345 last_index = curindex;
346 last_index_p1 = curindex + 1;
347
348 } else {
349 if (curstack.skip_lower) {
350 output(q, upper_val);
351 } else {
352 output(q, interpolate(lower_val, upper_val, curstack.upper_fraction));
353 }
354 }
355 }
356 }
357 }
358};
359
364template<typename Output_ = double>
371
381template<typename Output_, class QuantilesPointer_>
383public:
391 MultipleQuantilesVariableNumber(const std::size_t max_num_total, QuantilesPointer_ quantiles_ptr, const MultipleQuantilesVariableNumberOptions<Output_>& options) :
392 my_quantiles_ptr(std::move(quantiles_ptr)),
393 my_placeholder(options.placeholder)
394 {
395 if (max_num_total >= 2) {
396 sanisizer::resize(my_choices, max_num_total - 1);
397 }
398 }
399
403 // Back-compatibility.
404 MultipleQuantilesVariableNumber(const std::size_t max_num_total, QuantilesPointer_ quantiles_ptr) :
405 MultipleQuantilesVariableNumber(max_num_total, std::move(quantiles_ptr), {})
406 {}
411private:
412 std::vector<std::optional<MultipleQuantilesFixedNumber<Output_> > > my_choices;
413 QuantilesPointer_ my_quantiles_ptr;
414 Output_ my_placeholder;
415
416 template<class OutputFun_>
417 void fill(const Output_ val, OutputFun_ output) {
418 const auto num_quantiles = my_quantiles_ptr->size();
419 for (I<decltype(num_quantiles)> q = 0; q < num_quantiles; ++q) {
420 output(q, val);
421 }
422 }
423
424public:
428 std::size_t get_max_num_total() const {
429 return sanisizer::sum_unsafe<std::size_t>(my_choices.size(), 1);
430 }
431
432public:
453 template<typename Input_, class OutputFun_>
454 void operator()(const std::size_t num_total, Input_* const ptr, OutputFun_ output) {
455 if (num_total == 0) {
456 fill(my_placeholder, std::move(output));
457 } else if (num_total == 1) {
458 fill(*ptr, std::move(output));
459 } else {
460 assert(sanisizer::is_less_than_or_equal(num_total - 1, my_choices.size()));
461 auto& ocalc = my_choices[num_total - 2];
462 if (!ocalc.has_value()) { // Instantiating them on demand.
463 ocalc.emplace(num_total, *my_quantiles_ptr);
464 }
465 (*ocalc)(ptr, std::move(output));
466 }
467 }
468
494 template<typename Input_, class OutputFun_>
495 void operator()(const std::size_t num_total, const std::size_t num_non_zero, Input_* const values, OutputFun_ output) {
496 if (num_total == 0) {
497 fill(my_placeholder, std::move(output));
498 } else if (num_total == 1) {
499 fill(num_non_zero ? *values : 0, std::move(output));
500 } else {
501 assert(sanisizer::is_less_than_or_equal(num_total - 1, my_choices.size()));
502 auto& ocalc = my_choices[num_total - 2];
503 if (!ocalc.has_value()) { // Instantiating them on demand.
504 ocalc.emplace(num_total, *my_quantiles_ptr);
505 }
506 (*ocalc)(num_non_zero, values, std::move(output));
507 }
508 }
509};
510
511}
512
513#endif
Compute a single quantile.
Calculate multiple quantiles from a fixed number of elements.
Definition MultipleQuantiles.hpp:33
void operator()(Input_ *const ptr, OutputFun_ output) const
Definition MultipleQuantiles.hpp:113
std::size_t get_num_total() const
Definition MultipleQuantiles.hpp:92
void operator()(const std::size_t num_non_zero, Input_ *const values, OutputFun_ output) const
Definition MultipleQuantiles.hpp:179
MultipleQuantilesFixedNumber(const std::size_t num_total, const Quantiles_ &quantiles)
Definition MultipleQuantiles.hpp:44
Calculate multiple quantiles from a variable number of elements.
Definition MultipleQuantiles.hpp:382
void operator()(const std::size_t num_total, const std::size_t num_non_zero, Input_ *const values, OutputFun_ output)
Definition MultipleQuantiles.hpp:495
void operator()(const std::size_t num_total, Input_ *const ptr, OutputFun_ output)
Definition MultipleQuantiles.hpp:454
std::size_t get_max_num_total() const
Definition MultipleQuantiles.hpp:428
MultipleQuantilesVariableNumber(const std::size_t max_num_total, QuantilesPointer_ quantiles_ptr, const MultipleQuantilesVariableNumberOptions< Output_ > &options)
Definition MultipleQuantiles.hpp:391
Quickly compute simple statistics.
Definition mad.hpp:15
constexpr Value_ nan_if_available_else_zero()
Definition utils.hpp:63
Options for MultipleQuantilesVariableNumber.
Definition MultipleQuantiles.hpp:365
Output_ placeholder
Definition MultipleQuantiles.hpp:369
Miscellaneous utilities.