SedSat3 1.1.6
Sediment Source Apportionment Tool - Advanced statistical methods for environmental pollution research
Loading...
Searching...
No Matches
concentrationset.cpp
Go to the documentation of this file.
1#include "concentrationset.h"
2#include "math.h"
3#include "Utilities.h"
4#include <gsl/gsl_cdf.h>
5#include <gsl/gsl_randist.h>
6
7// ========== Construction and Assignment ==========
8
10 : vector<double>(),
11 fitted_distribution_(),
12 estimated_distribution_()
13{
14}
15
17 : vector<double>(other),
18 fitted_distribution_(other.fitted_distribution_),
19 estimated_distribution_(other.estimated_distribution_)
20{
21}
22
24 : vector<double>(n),
25 fitted_distribution_(),
26 estimated_distribution_()
27{
28}
29
31{
32 if (this == &other) {
33 return *this;
34 }
35
36 vector<double>::operator=(other);
39
40 return *this;
41}
42
43// ========== Data Management ==========
44
46{
47 push_back(value);
48}
49
51{
52 insert(end(), other.begin(), other.end());
53}
54
56{
57 if (other != nullptr) {
58 insert(end(), other->begin(), other->end());
59 }
60}
61
62// ========== Basic Statistics ==========
63
65{
66 if (empty()) {
67 return 0.0;
68 }
69
70 double sum = 0.0;
71 for (const double value : *this) {
72 sum += value;
73 }
74
75 return sum / static_cast<double>(size());
76}
77
78double ConcentrationSet::CalculateStdDev(double mean_value) const
79{
80 if (size() <= 1) {
81 return 0.0;
82 }
83
84 if (mean_value == -999) {
85 mean_value = CalculateMean();
86 }
87
88 double sum = 0.0;
89 for (const double value : *this) {
90 sum += pow(value - mean_value, 2);
91 }
92
93 return sqrt(sum / static_cast<double>(size() - 1));
94}
95
96double ConcentrationSet::CalculateStdDevLog(double mean_value) const
97{
98 if (size() <= 1) {
99 return 0.0;
100 }
101
102 if (mean_value == -999) {
103 mean_value = CalculateMeanLog();
104 }
105
106 double sum = 0.0;
107 for (const double value : *this) {
108 sum += pow(log(value) - mean_value, 2);
109 }
110
111 return sqrt(sum / static_cast<double>(size() - 1));
112}
113
114double ConcentrationSet::CalculateSSE(double mean_value) const
115{
116 if (mean_value == -999) {
117 mean_value = CalculateMean();
118 }
119
120 double sum = 0.0;
121 for (const double value : *this) {
122 sum += pow(value - mean_value, 2);
123 }
124
125 return sum;
126}
127
128double ConcentrationSet::CalculateSSELog(double mean_value) const
129{
130 if (mean_value == -999) {
131 mean_value = CalculateMeanLog();
132 }
133
134 double sum = 0.0;
135 for (const double value : *this) {
136 sum += pow(log(value) - mean_value, 2);
137 }
138
139 return sum;
140}
141
143{
144 if (empty()) {
145 return 0.0;
146 }
147
148 double sum = 0.0;
149 for (const double value : *this) {
150 sum += log(value);
151 }
152
153 return sum / static_cast<double>(size());
154}
155
157{
158 double sum = 0.0;
159 for (const double value : *this) {
160 sum += pow(value, power);
161 }
162
163 return sum;
164}
165
167{
168 double sum = 0.0;
169 for (const double value : *this) {
170 sum += pow(log(value), power);
171 }
172
173 return sum;
174}
175
177{
178 ConcentrationSet transformed(size());
179
180 for (size_t i = 0; i < size(); i++) {
181 transformed[i] = log(at(i));
182 }
183
184 return transformed;
185}
186
188{
189 return aquiutils::Min(*this);
190}
191
193{
194 return aquiutils::Max(*this);
195}
196
197// ========== Distribution Fitting ==========
198
200 distribution_type dist_type)
201{
202 vector<double> parameters;
203
204 // Use fitted distribution type if none specified
205 if (dist_type == distribution_type::none) {
207 }
208 else {
209 fitted_distribution_.SetType(dist_type);
211 }
212
213 // Store data statistics
216
217 // Calculate distribution parameters based on type
218 if (dist_type == distribution_type::normal) {
219 parameters.push_back(CalculateMean());
220 parameters.push_back(CalculateStdDev());
221 }
222 else if (dist_type == distribution_type::lognormal) {
223 parameters.push_back(CalculateMeanLog());
224 parameters.push_back(CalculateStdDevLog());
225 }
226
227 // CRITICAL: Set the parameters on fitted distribution
228 fitted_distribution_.parameters = parameters;
229
230 return parameters;
231}
232
234 const vector<double>& params,
235 distribution_type dist_type) const
236{
237 double log_likelihood = 0.0;
238
239 // Use fitted distribution if no parameters provided
240 if (params.empty() && dist_type == distribution_type::none) {
241 for (const double value : *this) {
242 log_likelihood += log(fitted_distribution_.Eval(value));
243 }
244 }
245 // Use provided parameters and distribution type
246 else if (!params.empty() && dist_type != distribution_type::none) {
247 Distribution dist;
248 dist.parameters = params;
249 dist.distribution = dist_type;
250
251 for (const double value : *this) {
252 log_likelihood += log(dist.Eval(value));
253 }
254 }
255
256 return log_likelihood;
257}
258
260{
261 // Lognormal requires all positive values
262 if (GetMinimum() <= 0) {
264 }
265
266 // For positive data, always use lognormal (based on user's original intent)
268}
269
270// ========== Goodness-of-Fit Testing ==========
271
273{
274 vector<double> sorted_data = QSort(*this);
275 CMBTimeSeries cdf;
276
277 double probability_increment = 1.0 / static_cast<double>(sorted_data.size());
278
279 for (size_t i = 0; i < sorted_data.size(); i++) {
280 double cumulative_probability = (static_cast<double>(i) + 0.5) * probability_increment;
281 cdf.append(sorted_data[i], cumulative_probability);
282 }
283
284 return cdf;
285}
286
288{
289 CMBTimeSeriesSet comparison;
290
291 // Add empirical CDF
292 comparison.append(CreateDataCDF(), "Observed");
293
294 // Generate fitted CDF
295 CMBTimeSeries fitted_cdf;
296 double x_min = comparison[0].mint();
297 double x_max = comparison[0].maxt();
298 double step_size = (x_max - x_min) / 50.0;
299
300 for (double x = x_min; x <= x_max; x += step_size) {
301 double cumulative_prob = 0.0;
302
303 if (dist_type == distribution_type::normal) {
304 cumulative_prob = gsl_cdf_gaussian_P(x - CalculateMean(), CalculateStdDev());
305 }
306 else if (dist_type == distribution_type::lognormal) {
307 if (x > 0) {
308 cumulative_prob = gsl_cdf_lognormal_P(x, CalculateMeanLog(), CalculateStdDevLog());
309 }
310 }
311
312 fitted_cdf.append(x, cumulative_prob);
313 }
314
315 comparison.append(fitted_cdf, "Fitted");
316 comparison.append(comparison[0] - comparison[1], "Error");
317
318 return comparison;
319}
320
322{
323 CMBTimeSeriesSet result;
324
325 // Add empirical CDF
326 result.append(CreateDataCDF(), "Observed");
327
328 // Get distribution parameters
329 vector<double> parameters;
330 if (dist_type == distribution_type::normal) {
331 parameters.push_back(CalculateMean());
332 parameters.push_back(CalculateStdDev());
333 }
334 else if (dist_type == distribution_type::lognormal) {
335 parameters.push_back(CalculateMeanLog());
336 parameters.push_back(CalculateStdDevLog());
337 }
338
339 // Generate fitted PDF
340 CMBTimeSeries fitted_pdf;
341 double x_min = result[0].mint();
342 double x_max = result[0].maxt();
343 double step_size = (x_max - x_min) / 50.0;
344
345 for (double x = x_min; x <= x_max; x += step_size) {
346 fitted_pdf.append(x, Distribution::Eval(x, parameters, dist_type));
347 }
348
349 // Scale observed data for comparison with PDF
350 result["Observed"] = fitted_pdf.maxC() / 2.0;
351 result.append(fitted_pdf, "Fitted");
352
353 return result;
354}
355
357 distribution_type dist_type) const
358{
359 CMBTimeSeriesSet observed_fitted = CreateCDFComparison(dist_type);
360
361 // K-S statistic is maximum absolute difference between CDFs
362 double max_positive_diff = fabs(observed_fitted[2].maxC());
363 double max_negative_diff = fabs(observed_fitted[2].minC());
364
365 return std::max(max_positive_diff, max_negative_diff);
366}
367
368// ========== Transformations ==========
369
371{
372 ConcentrationSet transformed = ApplyBoxCoxTransform(lambda, true);
373
374 double transformed_mean = transformed.CalculateMean();
375 double std_dev = CalculateStdDev();
376
377 double variance_term = 0.0;
378 double jacobian_term = 0.0;
379
380 for (size_t i = 0; i < size(); i++) {
381 variance_term += -1.0 / static_cast<double>(size()) *
382 pow(transformed[i] - transformed_mean, 2);
383 jacobian_term += (lambda - 1.0) * log(at(i) / std_dev);
384 }
385
386 return -static_cast<double>(size()) / 2.0 * log(variance_term) - jacobian_term;
387}
388
390 double lambda,
391 bool normalize) const
392{
393 // Cannot transform negative values
394 if (GetMinimum() < 0) {
395 ConcentrationSet scaled(size());
396 double std_dev = normalize ? CalculateStdDev() : 1.0;
397
398 for (size_t i = 0; i < size(); i++) {
399 scaled[i] = at(i) / std_dev;
400 }
401
402 return scaled;
403 }
404
405 // Scale data by standard deviation if normalizing
406 ConcentrationSet scaled(size());
407 double std_dev = normalize ? CalculateStdDev() : 1.0;
408
409 for (size_t i = 0; i < size(); i++) {
410 scaled[i] = at(i) / std_dev;
411 }
412
413 // Apply Box-Cox transformation
414 ConcentrationSet transformed(size());
415
416 for (size_t i = 0; i < size(); i++) {
417 if (fabs(lambda) > 1e-5) {
418 // Standard Box-Cox: (x^lambda - 1) / lambda
419 transformed[i] = (pow(scaled[i], lambda) - 1.0) / lambda;
420 }
421 else {
422 // When lambda ≈ 0, use log transformation (limiting case)
423 transformed[i] = log(scaled[i]);
424 }
425 }
426
427 return transformed;
428}
429
431 double min_lambda,
432 double max_lambda,
433 int n_intervals) const
434{
435 // Cannot transform negative values or constant data
436 if (GetMinimum() < 0 || GetMinimum() == GetMaximum()) {
437 return 1.0;
438 }
439
440 // Check for NaN values
441 if (!(GetMinimum() == GetMinimum())) {
442 std::cerr << "Warning: NaN detected in concentration data" << std::endl;
443 return 1.0;
444 }
445
446 // Base case: range is sufficiently small
447 if (fabs(min_lambda - max_lambda) < 1e-6) {
448 return (min_lambda + max_lambda) / 2.0;
449 }
450
451 // Evaluate K-S statistic at multiple lambda values
452 vector<double> ks_statistics;
453 double lambda_step = (max_lambda - min_lambda) / static_cast<double>(n_intervals);
454
455 for (double lambda = min_lambda; lambda <= max_lambda; lambda += lambda_step) {
456 ConcentrationSet transformed = ApplyBoxCoxTransform(lambda, true);
457 double ks_stat = transformed.CalculateKolmogorovSmirnovStatistic(
459 );
460 ks_statistics.push_back(ks_stat);
461 }
462
463 // Find index of minimum K-S statistic
464 int min_index = aquiutils::MinElement(ks_statistics);
465
466 // Recursively refine search around minimum
467 int lower_index = std::max(min_index - 1, 0);
468 int upper_index = std::min(min_index + 1, static_cast<int>(ks_statistics.size() - 1));
469
470 double refined_min = min_lambda + lower_index * lambda_step;
471 double refined_max = min_lambda + upper_index * lambda_step;
472
473 return FindOptimalBoxCoxParameter(refined_min, refined_max, n_intervals);
474}
475
476// ========== Utility Functions ==========
477
478vector<unsigned int> ConcentrationSet::CalculateRanks() const
479{
480 return aquiutils::Rank(*this);
481}
482
Collection of time series with labels and observed values.
Time series data container with Interface support.
Manages a collection of concentration measurements with statistical analysis.
double CalculateSSE(double mean_value=-999) const
Calculate sum of squared errors from mean.
ConcentrationSet CreateLogTransformed() const
Create log-transformed copy.
double CalculateMean() const
Calculate arithmetic mean.
double CalculateLogLikelihood(const vector< double > &params=vector< double >(), distribution_type dist_type=distribution_type::none) const
Calculate log-likelihood of data given distribution parameters.
CMBTimeSeriesSet CreateCDFComparison(distribution_type dist_type) const
Create comparison of empirical vs fitted CDF.
double CalculateStdDevLog(double mean_value=-999) const
Calculate standard deviation of log-transformed values.
vector< double > EstimateDistributionParameters(distribution_type dist_type=distribution_type::none)
Estimate distribution parameters from data.
Distribution estimated_distribution_
Distribution being optimized (e.g., in MCMC)
CMBTimeSeries CreateDataCDF() const
Create empirical CDF from data.
double CalculateMeanLog() const
Calculate mean of log-transformed values.
double CalculateBoxCoxLogLikelihood(double lambda) const
Calculate Box-Cox log-likelihood for given lambda.
double CalculateNormLog(double power=2.0) const
Calculate sum of log-transformed values raised to a power.
double CalculateKolmogorovSmirnovStatistic(distribution_type dist_type) const
Calculate Kolmogorov-Smirnov statistic.
vector< unsigned int > CalculateRanks() const
Calculate ranks of values (1 = smallest)
double GetMaximum() const
Get maximum value.
distribution_type SelectBestDistribution()
Select best-fitting distribution (normal vs lognormal)
void AppendSet(const ConcentrationSet &other)
Append all values from another set.
double GetMinimum() const
Get minimum value.
double FindOptimalBoxCoxParameter(double min_lambda, double max_lambda, int n_intervals) const
Find optimal Box-Cox lambda parameter.
ConcentrationSet ApplyBoxCoxTransform(double lambda, bool normalize) const
Apply Box-Cox transformation.
double CalculateNorm(double power=2.0) const
Calculate sum of values raised to a power.
void AppendValue(double value)
Append a single concentration value.
CMBTimeSeriesSet CreateFittedDistribution(distribution_type dist_type) const
Create fitted distribution visualization.
ConcentrationSet & operator=(const ConcentrationSet &other)
double CalculateSSELog(double mean_value=-999) const
Calculate sum of squared errors from log mean.
double CalculateStdDev(double mean_value=-999) const
Calculate standard deviation.
Distribution fitted_distribution_
Distribution fitted to observed data.
Represents a parametric probability distribution for uncertainty quantification.
double Eval(const double &x) const
Evaluate probability density function at a given value.
vector< double > parameters
Distribution parameters vector.
distribution_type distribution
The type of probability distribution.
void SetDataMean(const double &val)
Set the empirical mean from original data.
void SetType(const distribution_type &typ)
Set the distribution type and resize parameters vector.
void SetDataSTDev(const double &val)
Set the empirical standard deviation from original data.
distribution_type
Enumeration of probability distribution types supported in SedSat3.
@ none
No distribution assigned (uninitialized state)
@ lognormal
Lognormal distribution: ln(x) ~ N(μ, σ²), for strictly positive variables.
@ normal
Normal (Gaussian) distribution: p(x) = N(μ, σ²)