Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/Snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
with:
dotnet-version: '10.0.x'
project-names: 'Numerics'
# Snapshot workflow appends .<run_number>-dev, so this tracks the next development patch after v2.1.3.
version: '2.1.4'
# Snapshot workflow appends .<run_number>-dev, so this tracks the next development patch after v2.1.4.
version: '2.1.5'
# PR integration already runs the full multi-target test suite.
run-tests: false
nuget-source: 'https://www.hec.usace.army.mil/nexus/repository/consequences-nuget-public/'
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ cff-version: 1.2.0
message: "If you use this software, please cite our article in the Journal of Open Source Software."
type: software
title: "Numerics: A .NET Library for Numerical Computing, Statistical Analysis, and Risk Assessment"
version: "2.1.3"
date-released: "2026-07-15"
version: "2.1.4"
date-released: "2026-07-17"
license: 0BSD
repository-code: "https://github.com/USACE-RMC/Numerics"
url: "https://github.com/USACE-RMC/Numerics"
Expand Down
82 changes: 71 additions & 11 deletions Numerics/Data/Statistics/BoxCox.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Numerics.Mathematics.Optimization;

Expand Down Expand Up @@ -28,17 +28,35 @@ public class BoxCox
/// </remarks>
public static void FitLambda(IList<double> values, out double lambda)
{
// Solve with Brent
var brent = new BrentSearch((x) => { return LogLikelihood(values, x); }, -5d, 5d);
brent.Maximize();
if (brent.Status == OptimizationStatus.Success)
lambda = double.NaN;
if (!CanFitLambda(values))
return;

// Keep BrentSearch arithmetic finite even when the profile likelihood is undefined.
var brent = new BrentSearch((x) =>
{
lambda = brent.BestParameterSet.Values[0];
}
else
double logLikelihood = LogLikelihood(values, x);
return Tools.IsFinite(logLikelihood) ? logLikelihood : -double.MaxValue;
}, -5d, 5d)
{
lambda = double.NaN;
}
ReportFailure = false,
ComputeHessian = false,
RecordTraces = false
};

brent.Maximize();
if (brent.Status != OptimizationStatus.Success ||
brent.BestParameterSet.Values == null ||
brent.BestParameterSet.Values.Length == 0)
return;

double candidate = brent.BestParameterSet.Values[0];
if (!Tools.IsFinite(candidate) ||
Math.Abs(candidate) > 5d ||
!Tools.IsFinite(LogLikelihood(values, candidate)))
return;

lambda = candidate;
}

/// <summary>
Expand All @@ -52,23 +70,38 @@ public static void FitLambda(IList<double> values, out double lambda)
/// </returns>
public static double LogLikelihood(IList<double> values, double lambda1)
{
if (!CanFitLambda(values) || !Tools.IsFinite(lambda1) || Math.Abs(lambda1) > 5d)
return double.NegativeInfinity;

int n = values.Count;
var y = new double[n];
double mu = 0d;
var sumX = 0d;
for (int i = 0; i < n; i++)
{
y[i] = Transform(values[i], lambda1);
if (!Tools.IsFinite(y[i]))
return double.NegativeInfinity;

mu += y[i];
sumX += Math.Log(values[i]);
}
if (!Tools.IsFinite(mu) || !Tools.IsFinite(sumX))
return double.NegativeInfinity;

mu = mu / n;
double sse = 0d;
for (int i = 0; i < n; i++)
sse += Math.Pow(y[i] - mu, 2d);
if (!Tools.IsFinite(sse) || sse <= 0d)
return double.NegativeInfinity;

double sigma = Math.Sqrt(sse / n);
if (!Tools.IsFinite(sigma) || sigma <= 0d)
return double.NegativeInfinity;

double ll = -n / 2.0d * Tools.LogSqrt2PI - n / 2.0d * Math.Log(sigma * sigma) - 1.0d / (2d * sigma * sigma) * sse + (lambda1 - 1d) * sumX;
return ll;
return Tools.IsFinite(ll) ? ll : double.NegativeInfinity;
}

/// <summary>
Expand Down Expand Up @@ -146,5 +179,32 @@ public static List<double> InverseTransform(IList<double> values, double lambda)
newValues.Add(InverseTransform(values[i], lambda));
return newValues;
}

/// <summary>
/// Determines whether a sample can support Box-Cox lambda fitting.
/// </summary>
/// <param name="values">The sample values to inspect.</param>
/// <returns><see langword="true"/> when the sample is finite, positive, and non-degenerate.</returns>
private static bool CanFitLambda(IList<double> values)
{
if (values == null || values.Count < 2)
return false;

double first = values[0];
if (!Tools.IsFinite(first) || first <= 0d)
return false;

bool hasDifferentValue = false;
for (int i = 1; i < values.Count; i++)
{
double value = values[i];
if (!Tools.IsFinite(value) || value <= 0d)
return false;
if (value != first)
hasDifferentValue = true;
}

return hasDifferentValue;
}
}
}
89 changes: 73 additions & 16 deletions Numerics/Data/Statistics/YeoJohnson.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Numerics.Mathematics.Optimization;
using Numerics.Mathematics.Optimization;
using System;
using System.Collections.Generic;

Expand All @@ -24,17 +24,35 @@ public class YeoJohnson
/// <param name="lambda">Output. The transformation exponent. Range -5 to +5.</param>
public static void FitLambda(IList<double> values, out double lambda)
{
// Solve with Brent
var brent = new BrentSearch((x) => { return LogLikelihood(values, x); }, -5d, 5d);
brent.Maximize();
if (brent.Status == OptimizationStatus.Success)
lambda = double.NaN;
if (!CanFitLambda(values))
return;

// Keep BrentSearch arithmetic finite even when the profile likelihood is undefined.
var brent = new BrentSearch((x) =>
{
lambda = brent.BestParameterSet.Values[0];
}
else
double logLikelihood = LogLikelihood(values, x);
return Tools.IsFinite(logLikelihood) ? logLikelihood : -double.MaxValue;
}, -5d, 5d)
{
lambda = double.NaN;
}
ReportFailure = false,
ComputeHessian = false,
RecordTraces = false
};

brent.Maximize();
if (brent.Status != OptimizationStatus.Success ||
brent.BestParameterSet.Values == null ||
brent.BestParameterSet.Values.Length == 0)
return;

double candidate = brent.BestParameterSet.Values[0];
if (!Tools.IsFinite(candidate) ||
Math.Abs(candidate) > 5d ||
!Tools.IsFinite(LogLikelihood(values, candidate)))
return;

lambda = candidate;
}

/// <summary>
Expand Down Expand Up @@ -62,6 +80,9 @@ public static double FitLambda(IList<double> values)
/// </returns>
public static double LogLikelihood(IList<double> values, double lambda)
{
if (!CanFitLambda(values) || !Tools.IsFinite(lambda) || Math.Abs(lambda) > 5d)
return double.NegativeInfinity;

int n = values.Count;
var transformed = new double[n];
double sum = 0d;
Expand All @@ -72,8 +93,13 @@ public static double LogLikelihood(IList<double> values, double lambda)
{
double xi = values[i];
double yi = Transform(xi, lambda);
if (!Tools.IsFinite(yi))
return double.NegativeInfinity;

transformed[i] = yi;
sum += yi;
if (!Tools.IsFinite(sum))
return double.NegativeInfinity;

// Compute derivative dT/dy for log-Jacobian
double dTdy;
Expand All @@ -91,6 +117,8 @@ public static double LogLikelihood(IList<double> values, double lambda)
logJacobianSum += Math.Log(dTdy);
else
return double.NegativeInfinity; // log-likelihood undefined
if (!Tools.IsFinite(logJacobianSum))
return double.NegativeInfinity;
}

// Compute mean and SSE
Expand All @@ -101,14 +129,16 @@ public static double LogLikelihood(IList<double> values, double lambda)
double resid = transformed[i] - mu;
sse += resid * resid;
}
if (!Tools.IsFinite(sse) || sse <= 0d)
return double.NegativeInfinity;

double sigmaSq = sse / n;
if (sigmaSq <= 0 || double.IsNaN(sigmaSq))
if (!Tools.IsFinite(sigmaSq) || sigmaSq <= 0)
return double.NegativeInfinity;

double logLikelihood = -n / 2.0 * Tools.LogSqrt2PI - n / 2.0 * Math.Log(sigmaSq) - sse / (2.0 * sigmaSq) + logJacobianSum;

return logLikelihood;
return Tools.IsFinite(logLikelihood) ? logLikelihood : double.NegativeInfinity;
}

/// <summary>
Expand Down Expand Up @@ -160,11 +190,11 @@ public static double Transform(double value, double lambda)
{
return Math.Log(value + 1);
}
else if (value < 0 && lambda != 2)
else if (value < 0 && Math.Abs(lambda - 2d) >= 1E-8)
{
return -(Math.Pow(-value + 1, 2 - lambda) - 1) / (2 - lambda);
}
else if (value < 0 && lambda == 2)
else if (value < 0 && Math.Abs(lambda - 2d) < 1E-8)
{
return -Math.Log(-value + 1);
}
Expand Down Expand Up @@ -216,11 +246,11 @@ public static double InverseTransform(double value, double lambda)
{
return Math.Exp(value) - 1;
}
else if (value < 0 && lambda != 2)
else if (value < 0 && Math.Abs(lambda - 2d) >= 1E-8)
{
return 1 - (Math.Pow(1 - (2 - lambda) * value, 1 / (2 - lambda)));
}
else if (value < 0 && lambda == 2)
else if (value < 0 && Math.Abs(lambda - 2d) < 1E-8)
{
return 1 - Math.Exp(-value);
}
Expand All @@ -242,5 +272,32 @@ public static List<double> InverseTransform(IList<double> values, double lambda)
newValues.Add(InverseTransform(values[i], lambda));
return newValues;
}

/// <summary>
/// Determines whether a sample can support Yeo-Johnson lambda fitting.
/// </summary>
/// <param name="values">The sample values to inspect.</param>
/// <returns><see langword="true"/> when the sample is finite and non-degenerate.</returns>
private static bool CanFitLambda(IList<double> values)
{
if (values == null || values.Count < 2)
return false;

double first = values[0];
if (!Tools.IsFinite(first))
return false;

bool hasDifferentValue = false;
for (int i = 1; i < values.Count; i++)
{
double value = values[i];
if (!Tools.IsFinite(value))
return false;
if (value != first)
hasDifferentValue = true;
}

return hasDifferentValue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,38 @@ public static UnivariateDistributionBase CreateDistribution(UnivariateDistributi
}
}

/// <summary>
/// Attempts to create a distribution that can be initialized without external component distributions.
/// </summary>
/// <param name="distributionType">Distribution type.</param>
/// <param name="distribution">
/// When this method returns <see langword="true"/>, contains a distribution whose
/// <see cref="UnivariateDistributionBase.Type"/> matches <paramref name="distributionType"/>;
/// otherwise, <see langword="null"/>.
/// </param>
/// <returns>
/// <see langword="true"/> when the requested distribution can be created without external components;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// Composite distributions such as <see cref="CompetingRisks"/> and <see cref="Mixture"/>,
/// user-defined distributions, and undefined enumeration values cannot be created by this method.
/// </remarks>
public static bool TryCreateDistribution(UnivariateDistributionType distributionType, out UnivariateDistributionBase? distribution)
{
if (!Enum.IsDefined(typeof(UnivariateDistributionType), distributionType) ||
distributionType == UnivariateDistributionType.CompetingRisks ||
distributionType == UnivariateDistributionType.Mixture ||
distributionType == UnivariateDistributionType.UserDefined)
{
distribution = null;
return false;
}

distribution = CreateDistribution(distributionType);
return true;
}

/// <summary>
/// Create a distribution from XElement.
/// </summary>
Expand Down
Loading
Loading