|
28 | 28 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
29 | 29 | */ |
30 | 30 |
|
| 31 | +using Numerics.Data; |
| 32 | +using Numerics.Data.Statistics; |
31 | 33 | using Numerics.Mathematics.Optimization; |
32 | 34 | using Numerics.Sampling.MCMC; |
33 | 35 | using Numerics.Utilities; |
@@ -56,6 +58,51 @@ namespace Numerics.Distributions |
56 | 58 | public class UncertaintyAnalysisResults |
57 | 59 | { |
58 | 60 |
|
| 61 | + /// <summary> |
| 62 | + /// Construct an instance of the UncertaintyAnalysisResults class. |
| 63 | + /// </summary> |
| 64 | + public UncertaintyAnalysisResults() { } |
| 65 | + |
| 66 | + /// <summary> |
| 67 | + /// Constructs a new instance with computed uncertainty metrics. |
| 68 | + /// </summary> |
| 69 | + /// <param name="parentDistribution">The parent (mode/point estimate) distribution.</param> |
| 70 | + /// <param name="sampledDistributions">Array of sampled distributions from posterior or bootstrap.</param> |
| 71 | + /// <param name="probabilities">Array of non-exceedance probabilities for quantile estimation.</param> |
| 72 | + /// <param name="alpha">The confidence level (default = 0.1 for 90% CI).</param> |
| 73 | + /// <param name="minProbability">Minimum probability for mean curve computation (default = 0.001).</param> |
| 74 | + /// <param name="maxProbability">Maximum probability for mean curve computation (default = 1 - 1e-9).</param> |
| 75 | + /// <param name="recordParameterSets">If true, stores all parameter sets from sampled distributions.</param> |
| 76 | + public UncertaintyAnalysisResults(UnivariateDistributionBase parentDistribution, |
| 77 | + UnivariateDistributionBase[] sampledDistributions, |
| 78 | + double[] probabilities, |
| 79 | + double alpha = 0.1, |
| 80 | + double minProbability = 0.001, |
| 81 | + double maxProbability = 1 - 1e-9, |
| 82 | + bool recordParameterSets = false) |
| 83 | + { |
| 84 | + if (parentDistribution == null) |
| 85 | + throw new ArgumentNullException(nameof(parentDistribution)); |
| 86 | + if (sampledDistributions == null || sampledDistributions.Length == 0) |
| 87 | + throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions)); |
| 88 | + if (probabilities == null || probabilities.Length == 0) |
| 89 | + throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities)); |
| 90 | + |
| 91 | + ProcessModeCurve(parentDistribution, probabilities); |
| 92 | + ProcessConfidenceIntervals(sampledDistributions, probabilities, alpha); |
| 93 | + ProcessMeanCurve(sampledDistributions, probabilities, minProbability, maxProbability); |
| 94 | + |
| 95 | + if (recordParameterSets) |
| 96 | + ProcessParameterSets(sampledDistributions); |
| 97 | + |
| 98 | + // Set default values |
| 99 | + AIC = double.NaN; |
| 100 | + BIC = double.NaN; |
| 101 | + DIC = double.NaN; |
| 102 | + RMSE = double.NaN; |
| 103 | + ERL = double.NaN; |
| 104 | + } |
| 105 | + |
59 | 106 | /// <summary> |
60 | 107 | /// The parent probability distribution. |
61 | 108 | /// </summary> |
@@ -302,32 +349,192 @@ public static UncertaintyAnalysisResults FromXElement(XElement xElement) |
302 | 349 | } |
303 | 350 |
|
304 | 351 | /// <summary> |
305 | | - /// Returns uncertainty analysis results for the distribution estimated from MCMC. |
| 352 | + /// Process and set the parent distribution and computed curve (mode, plug-in, point estimate). |
306 | 353 | /// </summary> |
307 | | - /// <param name="results">The MCMC results.</param> |
308 | | - /// <param name="distribution">The parent distribution.</param> |
309 | | - /// <param name="probabilities">List of non-exceedance probabilities.</param> |
| 354 | + /// <param name="parentDistribution">The parent distribution.</param> |
| 355 | + /// <param name="probabilities">Array of non-exceedance probabilities.</param> |
| 356 | + public void ProcessModeCurve(UnivariateDistributionBase parentDistribution, double[] probabilities) |
| 357 | + { |
| 358 | + if (parentDistribution == null) |
| 359 | + throw new ArgumentNullException(nameof(parentDistribution)); |
| 360 | + if (probabilities == null || probabilities.Length == 0) |
| 361 | + throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities)); |
| 362 | + |
| 363 | + ParentDistribution = parentDistribution; |
| 364 | + ModeCurve = ParentDistribution.InverseCDF(probabilities); |
| 365 | + } |
| 366 | + |
| 367 | + /// <summary> |
| 368 | + /// Process and set the confidence intervals from a list of sampled distributions. |
| 369 | + /// </summary> |
| 370 | + /// <param name="sampledDistributions">The list of sampled distributions to process.</param> |
| 371 | + /// <param name="probabilities">Array of non-exceedance probabilities.</param> |
310 | 372 | /// <param name="alpha">The confidence level; Default = 0.1, which will result in the 90% confidence intervals.</param> |
311 | | - public static UncertaintyAnalysisResults FromMCMCResults(MCMCResults results, UnivariateDistributionBase distribution, IList<double> probabilities, double alpha = 0.1) |
| 373 | + public void ProcessConfidenceIntervals(UnivariateDistributionBase[] sampledDistributions, double[] probabilities, double alpha = 0.1) |
| 374 | + { |
| 375 | + if (sampledDistributions == null || sampledDistributions.Length == 0) |
| 376 | + throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions)); |
| 377 | + if (probabilities == null || probabilities.Length == 0) |
| 378 | + throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities)); |
| 379 | + if (alpha <= 0 || alpha >= 1) |
| 380 | + throw new ArgumentOutOfRangeException(nameof(alpha), "Alpha must be between 0 and 1."); |
| 381 | + |
| 382 | + int B = sampledDistributions.Length; |
| 383 | + int p = probabilities.Length; |
| 384 | + double lowerCI = alpha / 2d; |
| 385 | + double upperCI = 1d - alpha / 2d; |
| 386 | + ConfidenceIntervals = new double[p, 2]; |
| 387 | + |
| 388 | + // Loop over probabilities and record percentiles |
| 389 | + for (int i = 0; i < p; i++) |
| 390 | + { |
| 391 | + var XValues = new double[B]; |
| 392 | + |
| 393 | + // Compute quantiles in parallel |
| 394 | + Parallel.For(0, B, idx => { |
| 395 | + XValues[idx] = sampledDistributions[idx]?.InverseCDF(probabilities[i]) ?? double.NaN; |
| 396 | + }); |
| 397 | + |
| 398 | + // Filter valid values and sort |
| 399 | + int validCount = 0; |
| 400 | + for (int j = 0; j < B; j++) |
| 401 | + { |
| 402 | + if (!double.IsNaN(XValues[j])) validCount++; |
| 403 | + } |
| 404 | + |
| 405 | + var validValues = new double[validCount]; |
| 406 | + int writeIdx = 0; |
| 407 | + for (int j = 0; j < B; j++) |
| 408 | + { |
| 409 | + if (!double.IsNaN(XValues[j])) |
| 410 | + validValues[writeIdx++] = XValues[j]; |
| 411 | + } |
| 412 | + |
| 413 | + Array.Sort(validValues); |
| 414 | + |
| 415 | + // Record percentiles for CIs |
| 416 | + ConfidenceIntervals[i, 0] = Statistics.Percentile(validValues, lowerCI, true); |
| 417 | + ConfidenceIntervals[i, 1] = Statistics.Percentile(validValues, upperCI, true); |
| 418 | + } |
| 419 | + } |
| 420 | + |
| 421 | + /// <summary> |
| 422 | + /// Computes the mean (predictive) curve by averaging CDFs across all sampled distributions. |
| 423 | + /// Uses log-spaced quantiles for efficient computation across wide ranges. |
| 424 | + /// </summary> |
| 425 | + /// <param name="sampledDistributions">Array of sampled distributions from posterior or bootstrap.</param> |
| 426 | + /// <param name="probabilities">Array of non-exceedance probabilities for interpolation.</param> |
| 427 | + /// <param name="minProbability">Minimum probability for range determination (default = 0.001).</param> |
| 428 | + /// <param name="maxProbability">Maximum probability for range determination (default = 1 - 1e-9).</param> |
| 429 | + public void ProcessMeanCurve(UnivariateDistributionBase[] sampledDistributions, double[] probabilities, double minProbability = 0.001, double maxProbability = 1 - 1e-9) |
312 | 430 | { |
313 | | - if (results == null|| results.Output == null ||results.Output.Count == 0) return null; |
314 | | - if (results.Output[0].Values.Length != distribution.NumberOfParameters) return null; |
| 431 | + if (sampledDistributions == null || sampledDistributions.Length == 0) |
| 432 | + throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions)); |
| 433 | + if (probabilities == null || probabilities.Length == 0) |
| 434 | + throw new ArgumentException("Probabilities cannot be null or empty.", nameof(probabilities)); |
| 435 | + |
| 436 | + int B = sampledDistributions.Length; |
315 | 437 |
|
316 | | - int realz = results.Output.Count; |
317 | | - var dists = new UnivariateDistributionBase[results.Output.Count]; |
| 438 | + // Compute min and max X values across all distributions |
| 439 | + double minX = double.MaxValue; |
| 440 | + double maxX = double.MinValue; |
| 441 | + object lockObject = new object(); |
318 | 442 |
|
319 | | - Parallel.For(0, realz, idx => |
| 443 | + Parallel.For(0, B, j => |
320 | 444 | { |
321 | | - var d = distribution.Clone(); |
322 | | - d.SetParameters(results.Output[idx].Values); |
323 | | - dists[idx] = d; |
| 445 | + if (sampledDistributions[j] != null) |
| 446 | + { |
| 447 | + var innerMin = sampledDistributions[j].InverseCDF(minProbability); |
| 448 | + var innerMax = sampledDistributions[j].InverseCDF(maxProbability); |
| 449 | + |
| 450 | + lock (lockObject) |
| 451 | + { |
| 452 | + if (innerMin < minX) minX = innerMin; |
| 453 | + if (innerMax > maxX) maxX = innerMax; |
| 454 | + } |
| 455 | + } |
324 | 456 | }); |
325 | 457 |
|
326 | | - // Set up dummy bootstrap analysis |
327 | | - var boot = new BootstrapAnalysis(distribution, ParameterEstimationMethod.MaximumLikelihood, 100, realz); |
328 | | - var uar = boot.Estimate(probabilities, alpha, dists); |
329 | | - uar.ParameterSets = null; |
330 | | - return uar; |
| 458 | + // Create log-spaced quantiles for efficient coverage |
| 459 | + double shift = minX <= 0 ? Math.Abs(minX) + 1d : 0; |
| 460 | + double min = minX + shift; |
| 461 | + double max = maxX + shift; |
| 462 | + int order = (int)Math.Floor(Math.Log10(max) - Math.Log10(min)); |
| 463 | + int bins = Math.Max(200, Math.Min(1000, 100 * order)); |
| 464 | + |
| 465 | + var quantiles = new double[bins]; |
| 466 | + double delta = (Math.Log10(max) - Math.Log10(min)) / (bins - 1); |
| 467 | + |
| 468 | + for (int i = 0; i < bins; i++) |
| 469 | + { |
| 470 | + double logX = Math.Log10(min) + i * delta; |
| 471 | + quantiles[i] = Math.Pow(10, logX) - shift; |
| 472 | + } |
| 473 | + |
| 474 | + // Compute expected probability for each quantile |
| 475 | + var expected = new double[bins]; |
| 476 | + for (int i = 0; i < bins; i++) |
| 477 | + { |
| 478 | + double total = 0d; |
| 479 | + Parallel.For(0, B, () => 0d, (j, loop, sum) => |
| 480 | + { |
| 481 | + if (sampledDistributions[j] != null) |
| 482 | + { |
| 483 | + sum += sampledDistributions[j].CDF(quantiles[i]); |
| 484 | + } |
| 485 | + return sum; |
| 486 | + }, z => Tools.ParallelAdd(ref total, z)); |
| 487 | + expected[i] = total / B; |
| 488 | + } |
| 489 | + |
| 490 | + // Build monotonic interpolation points |
| 491 | + var yVals = new List<double> { quantiles[0] }; |
| 492 | + var xVals = new List<double> { expected[0] }; |
| 493 | + double minY = quantiles[0]; |
| 494 | + double maxY = quantiles[0]; |
| 495 | + |
| 496 | + for (int i = 1; i < bins; i++) |
| 497 | + { |
| 498 | + if (expected[i] > xVals[xVals.Count - 1]) |
| 499 | + { |
| 500 | + minY = Math.Min(minY, quantiles[i]); |
| 501 | + maxY = Math.Max(maxY, quantiles[i]); |
| 502 | + yVals.Add(quantiles[i]); |
| 503 | + xVals.Add(expected[i]); |
| 504 | + } |
| 505 | + } |
| 506 | + |
| 507 | + // Determine if log transform is appropriate |
| 508 | + bool useLogTransform = minY > 0 && (Math.Log10(maxY) - Math.Log10(minY)) > 1; |
| 509 | + |
| 510 | + // Interpolate mean curve at requested probabilities |
| 511 | + var linint = new Linear(xVals, yVals) |
| 512 | + { |
| 513 | + XTransform = Transform.NormalZ, |
| 514 | + YTransform = useLogTransform ? Transform.Logarithmic : Transform.None |
| 515 | + }; |
| 516 | + MeanCurve = linint.Interpolate(probabilities); |
| 517 | + } |
| 518 | + |
| 519 | + /// <summary> |
| 520 | + /// Processes and stores the parameter sets from all sampled distributions. |
| 521 | + /// </summary> |
| 522 | + /// <param name="sampledDistributions">Array of sampled distributions to extract parameters from.</param> |
| 523 | + public void ProcessParameterSets(UnivariateDistributionBase[] sampledDistributions) |
| 524 | + { |
| 525 | + if (sampledDistributions == null || sampledDistributions.Length == 0) |
| 526 | + throw new ArgumentException("Sampled distributions cannot be null or empty.", nameof(sampledDistributions)); |
| 527 | + |
| 528 | + int B = sampledDistributions.Length; |
| 529 | + ParameterSets = new ParameterSet[B]; |
| 530 | + |
| 531 | + Parallel.For(0, B, idx => |
| 532 | + { |
| 533 | + if (sampledDistributions[idx] != null) |
| 534 | + { |
| 535 | + ParameterSets[idx] = new ParameterSet(sampledDistributions[idx].GetParameters, double.NaN); |
| 536 | + } |
| 537 | + }); |
331 | 538 | } |
332 | 539 |
|
333 | 540 | } |
|
0 commit comments