RandLib 2.0.0
Probability distributions, fitting, and heavy-tail tools for C++20
Loading...
Searching...
No Matches
censored.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <randlib/fit/mle.hpp>
4
5#include <algorithm>
6#include <cmath>
7#include <concepts>
8#include <limits>
9#include <ranges>
10#include <span>
11#include <stdexcept>
12#include <type_traits>
13#include <utility>
14#include <vector>
15
16namespace randlib
17{
18
19enum class Censoring
20{
21 Exact,
22 Left,
23 Right,
25};
26
27template <std::floating_point Real>
29{
31 Real lower = 0;
32 Real upper = 0;
33
34 static CensoredObservation exact(Real value) { return {Censoring::Exact, value, value}; }
35 static CensoredObservation left(Real upper) { return {Censoring::Left, -infinity(), upper}; }
36 static CensoredObservation right(Real lower) { return {Censoring::Right, lower, infinity()}; }
38
39private:
40 static constexpr Real infinity() { return std::numeric_limits<Real>::infinity(); }
41};
42
43namespace detail
44{
45
46template <std::floating_point Real>
47void validateCensored(const CensoredObservation<Real> &observation)
48{
49 switch (observation.censoring) {
51 if (!std::isfinite(observation.lower) || observation.lower != observation.upper)
52 throw std::invalid_argument("An exact observation requires one finite value");
53 break;
54 case Censoring::Left:
55 if (!std::isfinite(observation.upper))
56 throw std::invalid_argument("A left-censored observation requires a finite upper bound");
57 break;
59 if (!std::isfinite(observation.lower))
60 throw std::invalid_argument("A right-censored observation requires a finite lower bound");
61 break;
63 if (!std::isfinite(observation.lower) || !std::isfinite(observation.upper) ||
64 !(observation.lower < observation.upper))
65 throw std::invalid_argument("An interval-censored observation requires ordered finite bounds");
66 break;
67 }
68}
69
70template <class Distribution>
71double intervalProbability(const Distribution &distribution, typename Distribution::value_type lower,
72 typename Distribution::value_type upper)
73{
74 const double cdfDifference = distribution.F(upper) - distribution.F(lower);
75 const double survivalDifference = distribution.S(lower) - distribution.S(upper);
76 return std::max(cdfDifference, survivalDifference);
77}
78
79} // namespace detail
80
81template <class Distribution>
82 requires std::floating_point<typename Distribution::value_type> && detail::FittableModel<Distribution>
83FitResult<Distribution>
85 FitOptions options = {})
86{
87 using Real = typename Distribution::value_type;
88 if (observations.empty())
89 throw fit_error(FitErrorCode::TooFewElements, "Censored fitting requires a non-empty sample");
90 if (options.method != Method::MLE)
91 throw fit_error(FitErrorCode::NotApplicable, "Censored fitting supports maximum likelihood only");
92
93 std::vector<Real> initializationSample;
94 initializationSample.reserve(observations.size());
95 for (const auto &observation : observations) {
96 detail::validateCensored(observation);
97 switch (observation.censoring) {
100 initializationSample.push_back(observation.lower);
101 break;
102 case Censoring::Left:
103 initializationSample.push_back(observation.upper);
104 break;
106 initializationSample.push_back(observation.lower + (observation.upper - observation.lower) / 2);
107 break;
108 }
109 }
110
111 const auto logLikelihood = [observations](const Distribution &distribution) {
112 long double result = 0;
113 for (const auto &observation : observations) {
114 switch (observation.censoring) {
115 case Censoring::Exact:
116 result += distribution.logf(observation.lower);
117 break;
118 case Censoring::Left: {
119 const double probability = distribution.F(observation.upper);
120 if (!(probability > 0))
121 return -std::numeric_limits<double>::infinity();
122 result += std::log(probability);
123 break;
124 }
125 case Censoring::Right: {
126 const double probability = distribution.S(observation.lower);
127 if (!(probability > 0))
128 return -std::numeric_limits<double>::infinity();
129 result += std::log(probability);
130 break;
131 }
132 case Censoring::Interval: {
133 const double probability =
134 detail::intervalProbability(distribution, observation.lower, observation.upper);
135 if (!(probability > 0))
136 return -std::numeric_limits<double>::infinity();
137 result += std::log(probability);
138 break;
139 }
140 }
141 }
142 return static_cast<double>(result);
143 };
144 return detail::fitLikelihood<Distribution>(std::span<const Real>(initializationSample), std::move(options),
145 logLikelihood, true);
146}
147
148template <class Distribution, std::ranges::contiguous_range Range>
149 requires std::floating_point<typename Distribution::value_type> && detail::FittableModel<Distribution> &&
150 std::same_as<std::remove_cv_t<std::ranges::range_value_t<Range>>,
151 CensoredObservation<typename Distribution::value_type>> &&
152 (!std::same_as<std::remove_cvref_t<Range>,
153 std::span<const CensoredObservation<typename Distribution::value_type>>>)
154FitResult<Distribution> fit_censored(const Range &observations, FitOptions options = {})
155{
156 using Observation = CensoredObservation<typename Distribution::value_type>;
157 return fit_censored<Distribution>(std::span<const Observation>(observations), std::move(options));
158}
159
160template <class Distribution>
161 requires std::floating_point<typename Distribution::value_type> && detail::FittableModel<Distribution>
162FitResult<Distribution> fit_truncated(std::span<const typename Distribution::value_type> sample,
163 typename Distribution::value_type lower, typename Distribution::value_type upper,
164 FitOptions options = {})
165{
166 using Real = typename Distribution::value_type;
167 if (options.method != Method::MLE)
168 throw fit_error(FitErrorCode::NotApplicable, "Truncated fitting supports maximum likelihood only");
169 if (std::isnan(lower) || std::isnan(upper) || !(lower < upper))
170 throw std::invalid_argument("Truncation bounds should be ordered and not NaN");
171 if (std::ranges::any_of(
172 sample, [lower, upper](Real value) { return !std::isfinite(value) || value < lower || value > upper; }))
173 throw fit_error(FitErrorCode::WrongSample, "Truncated observations should lie within the truncation bounds");
174
175 const auto logLikelihood = [sample, lower, upper](const Distribution &distribution) {
176 const double lowerCdf = std::isinf(lower) ? 0 : distribution.F(lower);
177 const double upperCdf = std::isinf(upper) ? 1 : distribution.F(upper);
178 const double lowerSurvival = std::isinf(lower) ? 1 : distribution.S(lower);
179 const double upperSurvival = std::isinf(upper) ? 0 : distribution.S(upper);
180 const double probability = std::max(upperCdf - lowerCdf, lowerSurvival - upperSurvival);
181 if (!(probability > 0))
182 return -std::numeric_limits<double>::infinity();
183 return distribution.LogLikelihoodFunction(sample) - sample.size() * std::log(probability);
184 };
185 return detail::fitLikelihood<Distribution>(sample, std::move(options), logLikelihood, true);
186}
187
188template <class Distribution, std::ranges::contiguous_range Range>
189 requires std::floating_point<typename Distribution::value_type> && detail::FittableModel<Distribution> &&
190 std::same_as<std::remove_cv_t<std::ranges::range_value_t<Range>>, typename Distribution::value_type> &&
191 (!std::same_as<std::remove_cvref_t<Range>, std::span<const typename Distribution::value_type>>)
192FitResult<Distribution> fit_truncated(const Range &sample, typename Distribution::value_type lower,
193 typename Distribution::value_type upper, FitOptions options = {})
194{
195 using Real = typename Distribution::value_type;
196 return fit_truncated<Distribution>(std::span<const Real>(sample), lower, upper, std::move(options));
197}
198
199} // namespace randlib
Real standardNormal(G &g)
Ziggurat algorithm of Marsaglia and Tsang with 256 layers and an explicit sign bit.
Definition variates.hpp:45
Definition bivariate_normal.hpp:10
@ WrongSample
the sample could not have been drawn from this distribution
@ NotApplicable
the method cannot be applied to this distribution or sample
@ TooFewElements
the sample is too small
FitResult< Distribution > fit_truncated(std::span< const typename Distribution::value_type > sample, typename Distribution::value_type lower, typename Distribution::value_type upper, FitOptions options={})
Definition censored.hpp:162
Censoring
Definition censored.hpp:20
FitResult< Distribution > fit_censored(std::span< const CensoredObservation< typename Distribution::value_type > > observations, FitOptions options={})
Definition censored.hpp:84
Definition censored.hpp:29
Real upper
Definition censored.hpp:32
Censoring censoring
Definition censored.hpp:30
static CensoredObservation right(Real lower)
Definition censored.hpp:36
Real lower
Definition censored.hpp:31
static CensoredObservation exact(Real value)
Definition censored.hpp:34
static CensoredObservation interval(Real lower, Real upper)
Definition censored.hpp:37
static CensoredObservation left(Real upper)
Definition censored.hpp:35
Definition result.hpp:27
Definition result.hpp:36