Skip to content

Commit 44c70af

Browse files
committed
Multiple edge case bug fixes. Added Yeo-Johnson transformation, Unimodality test, new methods in TimeSeries class, and fixed minor bugs in distributions and MCMC sampling methods.
1 parent 3d40102 commit 44c70af

72 files changed

Lines changed: 2551 additions & 1129 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

4.8.1/Numerics/Data/Statistics/BoxCox.cs

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,23 @@ public class BoxCox
5252
/// Fit the transformation parameters using maximum likelihood estimation.
5353
/// </summary>
5454
/// <param name="values">The list of values to transform.</param>
55-
/// <param name="lambda1">Output. The transformation exponent. Range -5 to +5.</param>
56-
/// <param name="lambda2">Output. The transformation shift for negative values.</param>
55+
/// <param name="lambda">Output. The transformation exponent. Range -5 to +5.</param>
5756
/// <remarks>
5857
/// https://www.rdocumentation.org/packages/EnvStats/versions/2.4.0/topics/boxcox
5958
/// </remarks>
60-
public static void FitLambda(IList<double> values, out double lambda1, out double lambda2)
59+
public static void FitLambda(IList<double> values, out double lambda)
6160
{
62-
int n = values.Count;
63-
double l1 = 0d;
64-
double l2 = 0d;
65-
double min = Statistics.Minimum(values);
66-
if (min <= 0d) l2 = 0d - min + Tools.DoubleMachineEpsilon;
67-
68-
Func<double, double> func = lambda =>
69-
{
70-
return LogLikelihood(values, lambda, l2);
71-
};
72-
7361
// Solve with Brent
74-
var brent = new BrentSearch(func, -5d, 5d);
62+
var brent = new BrentSearch((x) => { return LogLikelihood(values, x); }, -5d, 5d);
7563
brent.Maximize();
76-
l1 = brent.BestParameterSet.Values[0];
77-
// Set parameters
78-
lambda1 = l1;
79-
lambda2 = l2;
64+
if (brent.Status == OptimizationStatus.Success)
65+
{
66+
lambda = brent.BestParameterSet.Values[0];
67+
}
68+
else
69+
{
70+
lambda = double.NaN;
71+
}
8072
}
8173

8274
/// <summary>
@@ -85,21 +77,20 @@ public static void FitLambda(IList<double> values, out double lambda1, out doubl
8577
/// </summary>
8678
/// <param name="values">The list of values to transform.</param>
8779
/// <param name="lambda1">The transformation exponent. Range -5 to +5.</param>
88-
/// <param name="lambda2">The transformation shift for negative values.</param>
8980
/// <returns>
9081
/// The value of log-likelihood function evaluated at the given values and lambdas.
9182
/// </returns>
92-
public static double LogLikelihood(IList<double> values, double lambda1, double lambda2)
83+
public static double LogLikelihood(IList<double> values, double lambda1)
9384
{
9485
int n = values.Count;
9586
var y = new double[n];
9687
double mu = 0d;
9788
var sumX = 0d;
9889
for (int i = 0; i < n; i++)
9990
{
100-
y[i] = Transform(values[i], lambda1, lambda2);
91+
y[i] = Transform(values[i], lambda1);
10192
mu += y[i];
102-
sumX += Math.Log(values[i] + lambda2);
93+
sumX += Math.Log(values[i]);
10394
}
10495
mu = mu / n;
10596
double sse = 0d;
@@ -110,57 +101,78 @@ public static double LogLikelihood(IList<double> values, double lambda1, double
110101
return ll;
111102
}
112103

104+
/// <summary>
105+
/// Computes the Log-Jacobian used to adjust the log-likelihood function.
106+
/// </summary>
107+
/// <param name="values">The list of values to transform.</param>
108+
/// <param name="lambda">The transformation exponent. Range -5 to +5.</param>
109+
/// <returns>Returns the Log Jacobian.</returns>
110+
public static double LogJacobian(IList<double> values, double lambda)
111+
{
112+
double logJacobianSum = 0d;
113+
int n = values.Count;
114+
115+
for (int i = 0; i < n; i++)
116+
{
117+
double xi = values[i];
118+
119+
if (xi <= 0)
120+
return double.NegativeInfinity; // Box-Cox undefined for non-positive values
121+
122+
// For Box-Cox: log derivative is (lambda - 1) * log(x)
123+
logJacobianSum += (lambda - 1d) * Math.Log(xi);
124+
}
125+
126+
return logJacobianSum;
127+
}
128+
113129
/// <summary>
114130
/// Returns the Box-Cox transformation of the value.
115131
/// </summary>
116132
/// <param name="value">The value to transform.</param>
117-
/// <param name="lambda1">The transformation exponent. Range -5 to +5.</param>
118-
/// <param name="lambda2">The transformation shift for negative values.</param>
119-
public static double Transform(double value, double lambda1, double lambda2 = 0.0d)
133+
/// <param name="lambda">The transformation exponent. Range -5 to +5.</param>
134+
public static double Transform(double value, double lambda)
120135
{
121-
if (Math.Abs(lambda1) > 5d) return double.NaN;
122-
if (lambda1 == 0d) return Math.Log(value + lambda2);
123-
return (Math.Pow(value + lambda2, lambda1) - 1.0d) / lambda1;
136+
if (Math.Abs(lambda) > 5d) return double.NaN;
137+
if (Math.Abs(lambda) < 1e-8) return Math.Log(value);
138+
return (Math.Pow(value, lambda) - 1.0d) / lambda;
124139
}
125140

126141
/// <summary>
127142
/// Returns the Box-Cox transformation of each value in the list.
128143
/// </summary>
129144
/// <param name="values">The list of values to transform.</param>
130-
/// <param name="lambda1">The transformation exponent. Range -5 to +5.</param>
131-
/// <param name="lambda2">The transformation shift for negative values.</param>
132-
public static List<double> Transform(IList<double> values, double lambda1, double lambda2 = 0.0d)
145+
/// <param name="lambda">The transformation exponent. Range -5 to +5.</param>
146+
public static List<double> Transform(IList<double> values, double lambda)
133147
{
134148
var newValues = new List<double>();
135149
for (int i = 0; i < values.Count; i++)
136-
newValues.Add(Transform(values[i], lambda1, lambda2));
150+
newValues.Add(Transform(values[i], lambda));
137151
return newValues;
138152
}
139153

140154
/// <summary>
141155
/// Returns the reverse of the Box-Cox transformed value.
142156
/// </summary>
143157
/// <param name="value">The value to reverse transform.</param>
144-
/// <param name="lambda1">The transformation exponent. Range -5 to +5.</param>
145-
/// <param name="lambda2">The transformation shift for negative values.</param>
146-
public static double ReverseTransform(double value, double lambda1, double lambda2 = 0.0d)
158+
/// <param name="lambda">The transformation exponent. Range -5 to +5.</param>
159+
public static double InverseTransform(double value, double lambda)
147160
{
148-
if (Math.Abs(lambda1) > 5d) return double.NaN;
149-
if (lambda1 == 0d) return Math.Exp(value) - lambda2;
150-
return Math.Pow(value * lambda1 + 1.0d, 1.0d / lambda1) - lambda2;
161+
if (Math.Abs(lambda) > 5d) return double.NaN;
162+
if (Math.Abs(lambda) < 1e-8) return Math.Exp(value);
163+
return Math.Pow(value * lambda + 1.0d, 1.0d / lambda);
151164
}
152165

153166
/// <summary>
154-
/// Returns the reverse of each Box-Cox transformed value in the list.
167+
/// Returns the inverse of each Box-Cox transformed value in the list.
155168
/// </summary>
156169
/// <param name="values">The list of values to reverse transform.</param>
157-
/// <param name="lambda1">The transformation exponent. Range -5 to +5.</param>
158-
/// <param name="lambda2">The transformation shift for negative values.</param>
159-
public static List<double> ReverseTransform(IList<double> values, double lambda1, double lambda2 = 0.0d)
170+
/// <param name="lambda">The transformation exponent. Range -5 to +5.</param>
171+
public static List<double> InverseTransform(IList<double> values, double lambda)
160172
{
161173
var newValues = new List<double>();
162174
for (int i = 0; i < values.Count; i++)
163-
newValues.Add(ReverseTransform(values[i], lambda1, lambda2));
175+
newValues.Add(InverseTransform(values[i], lambda));
164176
return newValues;
165177
}
166178
}

4.8.1/Numerics/Data/Statistics/Correlation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ public static double Spearman(IList<double> sample1, IList<double> sample2)
117117
throw new ArgumentOutOfRangeException(nameof(sample1), "The sample arrays must be the same length.");
118118
}
119119

120-
var rank1 = Statistics.RanksInplace(sample1.ToArray());
121-
var rank2 = Statistics.RanksInplace(sample2.ToArray());
120+
var rank1 = Statistics.RanksInPlace(sample1.ToArray());
121+
var rank2 = Statistics.RanksInPlace(sample2.ToArray());
122122
return Pearson(rank1, rank2);
123123
}
124124

4.8.1/Numerics/Data/Statistics/GoodnessOfFit.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,28 +113,6 @@ public static double[] AICWeights(IList<double> aicValues)
113113
return weights;
114114
}
115115

116-
/// <summary>
117-
/// Returns an array of weights based on a list of model BIC values.
118-
/// </summary>
119-
/// <param name="bicValues">The list of model BIC values.</param>
120-
public static double[] BICWeights(IList<double> bicValues)
121-
{
122-
var min = Tools.Min(bicValues);
123-
var weights = new double[bicValues.Count];
124-
var num = new double[bicValues.Count];
125-
double sum = 0;
126-
for (int i = 0; i < bicValues.Count; i++)
127-
{
128-
num[i] = Math.Exp(-0.5 * (bicValues[i] - min));
129-
sum += num[i];
130-
}
131-
for (int i = 0; i < bicValues.Count; i++)
132-
{
133-
weights[i] = num[i] / sum;
134-
}
135-
return weights;
136-
}
137-
138116
/// <summary>
139117
/// Gets the Root Mean Square Error (RMSE) of the model compared to the observed data.
140118
/// </summary>

4.8.1/Numerics/Data/Statistics/HypothesisTests.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
using System.Collections.Generic;
3333
using System.Linq;
3434
using Numerics.Distributions;
35+
using Numerics.MachineLearning;
3536
using Numerics.Mathematics.LinearAlgebra;
3637
using Numerics.Mathematics.SpecialFunctions;
3738

@@ -142,7 +143,7 @@ public static double PairedTtest(IList<double> sample1, IList<double> sample2)
142143
/// <param name="sample2">Data sample 2.</param>
143144
/// <returns>Returns the p-value of the test statistic.</returns>
144145
public static double Ftest(IList<double> sample1, IList<double> sample2)
145-
{
146+
{
146147
var meanVar1 = Statistics.MeanVariance(sample1);
147148
var meanVar2 = Statistics.MeanVariance(sample2);
148149
int n1 = sample1.Count, n2 = sample2.Count;
@@ -224,7 +225,7 @@ public static double WaldWolfowitzTest(IList<double> sample)
224225
R += xn * x1;
225226
double s12 = Tools.Sqr(S1);
226227
double s22 = Tools.Sqr(S2);
227-
double s14 = Tools.Pow(S1,4);
228+
double s14 = Tools.Pow(S1, 4);
228229

229230
eR = (s12 - S2) / (n - 1);
230231
varR = (s22 - S4) / (n - 1) - Tools.Sqr(eR);
@@ -294,7 +295,7 @@ public static double MannKendallTest(IList<double> sample)
294295
int i, j, n = sample.Count;
295296
if (n < 10) throw new ArgumentException("The sample size must be greater than or equal to 10.");
296297

297-
double S = 0, T = 0, varS, z;
298+
double S = 0, T = 0, varS, z;
298299

299300
for (i = 0; i < n - 1; i++)
300301
{
@@ -331,5 +332,40 @@ public static double LinearTrendTest(IList<double> indices, IList<double> sample
331332
return (1 - tdist.CDF(Math.Abs(lm.Parameters[1] / lm.ParameterStandardErrors[1]))) * 2;
332333
}
333334

335+
/// <summary>
336+
/// The Gaussian Mixture Model Unimodality Test.
337+
/// </summary>
338+
/// <param name="sample">Time series sample data.</param>
339+
/// <returns>Returns the p-value of the test statistic.</returns>
340+
public static double UnimodalityTest(IList<double> sample)
341+
{
342+
int n = sample.Count;
343+
if (n < 10) throw new ArgumentException("The sample size must be greater than or equal to 10.");
344+
345+
// Fit 1-component GMM (unimodal)
346+
var gmm1 = new GaussianMixtureModel(sample.ToArray(), 1);
347+
gmm1.Train();
348+
var logLH1 = gmm1.LogLikelihood;
349+
350+
// Fit 2-component GMM (potentially bimodal)
351+
var gmm2 = new GaussianMixtureModel(sample.ToArray(), 2);
352+
gmm2.Train();
353+
var logLH2 = gmm2.LogLikelihood;
354+
355+
// Compute test statistic: Likelihood Ratio Test
356+
double testStat = 2 * (logLH2 - logLH1);
357+
int df = 3;
358+
359+
// Compute p-value from chi-square distribution
360+
var chiSquared = new ChiSquared(df);
361+
double pval = 1.0 - chiSquared.CDF(testStat);
362+
363+
return pval;
364+
365+
}
366+
367+
334368
}
369+
370+
335371
}

4.8.1/Numerics/Data/Statistics/MultipleGrubbsBeckTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,7 @@ private static double GGBCRITP(int N, int R, double ETA)
199199
// The number of low outliers computed by this method is consistent with the results from the FORTRAN code.
200200
// Further testing is necessary to identify any edge cases where the adaptive Simpson's rule might prove insufficient.
201201
var sr = new AdaptiveSimpsonsRule(FGGB, 1E-16, 1 - 1E-16);
202-
sr.RelativeTolerance = 1E-4;
203-
sr.MaxDepth = 50;
202+
sr.MaxDepth = 25;
204203
sr.ReportFailure = false;
205204
sr.Integrate();
206205
return sr.Status != IntegrationStatus.Failure ? sr.Result : double.NaN;

0 commit comments

Comments
 (0)