Skip to content

Commit 9e0f585

Browse files
committed
Add comprehensive statistical metrics to NonlinearMinimizationResult
- TStatistics and PValues for parameter significance testing - ConfidenceIntervalHalfWidths for parameter precision - Dependencies to measure parameter correlations - Goodness-of-fit statistics
1 parent a0d434b commit 9e0f585

File tree

2 files changed

+308
-1
lines changed

2 files changed

+308
-1
lines changed

src/Numerics.Tests/OptimizationTests/NonLinearCurveFittingTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,36 @@ public void PollutionWithWeights()
660660
{
661661
AssertHelpers.AlmostEqualRelative(PollutionBest[i], result.MinimizingPoint[i], 4);
662662
}
663+
664+
// Check statistics
665+
AssertHelpers.AlmostEqualRelative(0.908, result.RSquared, 2);
666+
AssertHelpers.AlmostEqualRelative(0.885, result.AdjustedRSquared, 2);
667+
AssertHelpers.AlmostEqualRelative(24.0096, result.StandardError, 2);
668+
669+
// Check parameter statistics (using expected values)
670+
var expectedStdErrors = new double[] { 10.7, 0.064296 };
671+
var expectedTStats = new double[] { 21.045, 6.2333 };
672+
var expectedPValues = new double[] { 3.0134e-05, 0.0033745 };
673+
// Expected confidence interval bounds
674+
var expectedCI = new double[,]
675+
{
676+
{ 195.4650, 254.8788 },
677+
{ 0.2223, 0.5793 }
678+
};
679+
680+
for (var i = 0; i < result.MinimizingPoint.Count; i++)
681+
{
682+
AssertHelpers.AlmostEqualRelative(expectedStdErrors[i], result.StandardErrors[i], 3);
683+
AssertHelpers.AlmostEqualRelative(expectedTStats[i], result.TStatistics[i], 3);
684+
AssertHelpers.AlmostEqualRelative(expectedPValues[i], result.PValues[i], 3);
685+
686+
// Calculate and check confidence interval bounds
687+
var lowerBound = result.MinimizingPoint[i] - result.ConfidenceIntervalHalfWidths[i];
688+
var upperBound = result.MinimizingPoint[i] + result.ConfidenceIntervalHalfWidths[i];
689+
690+
AssertHelpers.AlmostEqualRelative(expectedCI[i, 0], lowerBound, 3);
691+
AssertHelpers.AlmostEqualRelative(expectedCI[i, 1], upperBound, 3);
692+
}
663693
}
664694

665695
#endregion Weighted Nonlinear Regression

src/Numerics/Optimization/NonlinearMinimizationResult.cs

Lines changed: 278 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
using MathNet.Numerics.LinearAlgebra;
1+
using MathNet.Numerics.Distributions;
2+
using MathNet.Numerics.LinearAlgebra;
3+
using System;
4+
using System.Linq;
25

36
namespace MathNet.Numerics.Optimization
47
{
8+
/// <summary>
9+
/// Represents the result of a nonlinear minimization operation, including
10+
/// the optimal parameters and various statistical measures of fitness.
11+
/// </summary>
512
public class NonlinearMinimizationResult
613
{
14+
/// <summary>
15+
/// The objective model evaluated at the minimum point.
16+
/// </summary>
717
public IObjectiveModel ModelInfoAtMinimum { get; }
818

919
/// <summary>
@@ -16,6 +26,30 @@ public class NonlinearMinimizationResult
1626
/// </summary>
1727
public Vector<double> StandardErrors { get; private set; }
1828

29+
/// <summary>
30+
/// Returns the t-statistics for each parameter (parameter value / standard error).
31+
/// These measure how many standard deviations each parameter is from zero.
32+
/// </summary>
33+
public Vector<double> TStatistics { get; private set; }
34+
35+
/// <summary>
36+
/// Returns the p-values for each parameter based on t-distribution.
37+
/// Lower p-values indicate statistically significant parameters.
38+
/// </summary>
39+
public Vector<double> PValues { get; private set; }
40+
41+
/// <summary>
42+
/// Returns the dependency values for each parameter, measuring how linearly related
43+
/// each parameter is to the others. Values close to 1 indicate high dependency (multicollinearity).
44+
/// </summary>
45+
public Vector<double> Dependencies { get; private set; }
46+
47+
/// <summary>
48+
/// Returns the half-width of the confidence intervals for each parameter at the 95% confidence level.
49+
/// These represent the margin of error for each parameter estimate.
50+
/// </summary>
51+
public Vector<double> ConfidenceIntervalHalfWidths { get; private set; }
52+
1953
/// <summary>
2054
/// Returns the y-values of the fitted model that correspond to the independent values.
2155
/// </summary>
@@ -31,19 +65,69 @@ public class NonlinearMinimizationResult
3165
/// </summary>
3266
public Matrix<double> Correlation { get; private set; }
3367

68+
/// <summary>
69+
/// Returns the residual sum of squares at the minimum point,
70+
/// calculated as F(p) = 1/2 * ∑(residuals²).
71+
/// </summary>
72+
public double MinimizedValue => ModelInfoAtMinimum.Value;
73+
74+
/// <summary>
75+
/// Root Mean Squared Error (RMSE) - measures the average magnitude of errors.
76+
/// </summary>
77+
public double RootMeanSquaredError { get; private set; }
78+
79+
/// <summary>
80+
/// Coefficient of determination (R-Square) - proportion of variance explained by the model.
81+
/// </summary>
82+
public double RSquared { get; private set; }
83+
84+
/// <summary>
85+
/// Adjusted R-Squre - accounts for the number of predictors in the model.
86+
/// </summary>
87+
public double AdjustedRSquared { get; private set; }
88+
89+
/// <summary>
90+
/// Residual standard error of the regression, calculated as sqrt(RSS/df) where RSS is the
91+
/// residual sum of squares and df is the degrees of freedom. This measures the average
92+
/// distance between the observed values and the fitted model.
93+
/// </summary>
94+
public double StandardError { get; private set; }
95+
96+
/// <summary>
97+
/// Pearson correlation coefficient between observed and predicted values.
98+
/// </summary>
99+
public double CorrelationCoefficient { get; private set; }
100+
101+
/// <summary>
102+
/// Number of iterations performed during optimization.
103+
/// </summary>
34104
public int Iterations { get; }
35105

106+
/// <summary>
107+
/// Reason why the optimization algorithm terminated.
108+
/// </summary>
36109
public ExitCondition ReasonForExit { get; }
37110

111+
/// <summary>
112+
/// Creates a new instance of the NonlinearMinimizationResult class.
113+
/// </summary>
114+
/// <param name="modelInfo">The objective model at the minimizing point.</param>
115+
/// <param name="iterations">The number of iterations performed.</param>
116+
/// <param name="reasonForExit">The reason why the algorithm terminated.</param>
38117
public NonlinearMinimizationResult(IObjectiveModel modelInfo, int iterations, ExitCondition reasonForExit)
39118
{
40119
ModelInfoAtMinimum = modelInfo;
41120
Iterations = iterations;
42121
ReasonForExit = reasonForExit;
43122

44123
EvaluateCovariance(modelInfo);
124+
EvaluateGoodnessOfFit(modelInfo);
45125
}
46126

127+
/// <summary>
128+
/// Evaluates the covariance matrix, correlation matrix, standard errors, t-statistics and p-values.
129+
/// </summary>
130+
/// <param name="objective">The objective model at the minimizing point.</param>
47131
void EvaluateCovariance(IObjectiveModel objective)
48132
{
49133
objective.EvaluateAt(objective.Point); // Hessian may be not yet updated.
@@ -66,15 +150,208 @@ void EvaluateCovariance(IObjectiveModel objective)
66150
{
67151
StandardErrors = Covariance.Diagonal().PointwiseSqrt();
68152

153+
// Calculate t-statistics and p-values
154+
TStatistics = Vector<double>.Build.Dense(StandardErrors.Count);
155+
for (var i = 0; i < StandardErrors.Count; i++)
156+
{
157+
TStatistics[i] = StandardErrors[i] > double.Epsilon
158+
? objective.Point[i] / StandardErrors[i]
159+
: double.NaN;
160+
}
161+
162+
// Calculate p-values based on t-distribution with DegreeOfFreedom
163+
PValues = Vector<double>.Build.Dense(TStatistics.Count);
164+
var tDist = new StudentT(0, 1, objective.DegreeOfFreedom);
165+
166+
// Calculate confidence interval half-widths (for 95% confidence)
167+
ConfidenceIntervalHalfWidths = Vector<double>.Build.Dense(StandardErrors.Count);
168+
var tCritical = tDist.InverseCumulativeDistribution(0.975); // Two-tailed, 95% confidence
169+
170+
for (var i = 0; i < TStatistics.Count; i++)
171+
{
172+
// Two-tailed p-value from t-distribution
173+
var tStat = Math.Abs(TStatistics[i]);
174+
PValues[i] = 2 * (1 - tDist.CumulativeDistribution(tStat));
175+
176+
// Calculate confidence interval half-width
177+
ConfidenceIntervalHalfWidths[i] = tCritical * StandardErrors[i];
178+
}
179+
69180
var correlation = Covariance.Clone();
70181
var d = correlation.Diagonal().PointwiseSqrt();
71182
var dd = d.OuterProduct(d);
72183
Correlation = correlation.PointwiseDivide(dd);
184+
185+
// Calculate dependencies (measure of multicollinearity)
186+
Dependencies = Vector<double>.Build.Dense(Correlation.RowCount);
187+
for (var i = 0; i < Correlation.RowCount; i++)
188+
{
189+
// For each parameter, calculate 1 - 1/VIF where VIF is the variance inflation factor
190+
// VIF is calculated as 1/(1-R²) where R² is the coefficient of determination when
191+
// parameter i is regressed against all other parameters
192+
193+
// Extract the correlation coefficients related to parameter i (excluding self-correlation)
194+
var correlations = Vector<double>.Build.Dense(Correlation.RowCount - 1);
195+
var index = 0;
196+
for (var j = 0; j < Correlation.RowCount; j++)
197+
{
198+
if (j != i)
199+
{
200+
correlations[index++] = Correlation[i, j];
201+
}
202+
}
203+
204+
// Calculate the square of the multiple correlation coefficient
205+
// In the simple case, this is the maximum of the squared individual correlations
206+
var maxSquaredCorrelation = 0.0;
207+
for (var j = 0; j < correlations.Count; j++)
208+
{
209+
var squaredCorr = correlations[j] * correlations[j];
210+
if (squaredCorr > maxSquaredCorrelation)
211+
{
212+
maxSquaredCorrelation = squaredCorr;
213+
}
214+
}
215+
216+
// Calculate dependency = 1 - 1/VIF
217+
var maxSquaredCorrelationCapped = Math.Min(maxSquaredCorrelation, 0.9999);
218+
var vif = 1.0 / (1.0 - maxSquaredCorrelationCapped);
219+
Dependencies[i] = 1.0 - 1.0 / vif;
220+
}
73221
}
74222
else
75223
{
76224
StandardErrors = null;
225+
TStatistics = null;
226+
PValues = null;
227+
ConfidenceIntervalHalfWidths = null;
77228
Correlation = null;
229+
Dependencies = null;
230+
}
231+
}
232+
233+
/// <summary>
234+
/// Evaluates goodness of fit statistics like R-squared, RMSE, etc.
235+
/// </summary>
236+
/// <param name="objective">The objective model at the minimizing point.</param>
237+
void EvaluateGoodnessOfFit(IObjectiveModel objective)
238+
{
239+
// Note: GoodnessOfFit class methods do not support weights, so we apply weighting manually here.
240+
241+
// Check if we have the essentials for calculating statistics
242+
var hasResiduals = objective.Residuals != null;
243+
var hasObservations = objective.ObservedY != null;
244+
var hasModelValues = objective.ModelValues != null;
245+
var hasSufficientDof = objective.DegreeOfFreedom >= 1;
246+
247+
// Set values to NaN if we can't calculate them
248+
RootMeanSquaredError = double.NaN;
249+
RSquared = double.NaN;
250+
AdjustedRSquared = double.NaN;
251+
StandardError = double.NaN;
252+
CorrelationCoefficient = double.NaN;
253+
254+
// Need residuals and sufficient DOF for most calculations
255+
if (!hasResiduals || !hasSufficientDof)
256+
{
257+
return;
258+
}
259+
260+
var n = hasObservations ? objective.ObservedY.Count : objective.Residuals.Count;
261+
var dof = objective.DegreeOfFreedom;
262+
263+
// Calculate sum of squared residuals using vector operations
264+
var ssRes = objective.Residuals.DotProduct(objective.Residuals);
265+
266+
// Guard against zero or negative SSR
267+
if (ssRes <= 0)
268+
{
269+
RootMeanSquaredError = 0;
270+
StandardError = 0;
271+
272+
// Only calculate these if we have observations
273+
if (hasObservations)
274+
{
275+
RSquared = 1.0;
276+
AdjustedRSquared = 1.0;
277+
}
278+
279+
// Only calculate if we have model values and observations
280+
if (hasModelValues && hasObservations)
281+
{
282+
CorrelationCoefficient = 1.0;
283+
}
284+
return;
285+
}
286+
287+
// Calculate standard error and RMSE, which only require residuals
288+
StandardError = Math.Sqrt(ssRes / dof);
289+
RootMeanSquaredError = Math.Sqrt(ssRes / n);
290+
291+
// The following statistics require observations
292+
if (!hasObservations)
293+
{
294+
return;
295+
}
296+
297+
// Calculate total sum of squares
298+
double ssTot;
299+
300+
// If weights are present, calculate weighted total sum of squares
301+
if (objective.Weights != null)
302+
{
303+
var weightSum = 0.0;
304+
var weightedSum = 0.0;
305+
306+
for (var i = 0; i < n; i++)
307+
{
308+
var weight = objective.Weights[i, i];
309+
weightSum += weight;
310+
weightedSum += weight * objective.ObservedY[i];
311+
}
312+
313+
// Avoid division by zero
314+
var weightedMean = weightSum > double.Epsilon ? weightedSum / weightSum : 0;
315+
316+
ssTot = 0.0;
317+
for (var i = 0; i < n; i++)
318+
{
319+
var weight = objective.Weights[i, i];
320+
var dev = objective.ObservedY[i] - weightedMean;
321+
ssTot += weight * dev * dev;
322+
}
323+
}
324+
else
325+
{
326+
// Unweighted case - use vector operations for total sum of squares
327+
var yMean = objective.ObservedY.Average();
328+
var deviations = objective.ObservedY.Subtract(yMean);
329+
ssTot = deviations.DotProduct(deviations);
330+
}
331+
332+
// Guard against zero or negative total sum of squares
333+
if (ssTot <= double.Epsilon)
334+
{
335+
RSquared = 0.0;
336+
AdjustedRSquared = 0.0;
337+
}
338+
else
339+
{
340+
// Calculate R-squared directly
341+
RSquared = 1 - (ssRes / ssTot);
342+
343+
// Calculate adjusted R-squared using the ratio of mean squares
344+
AdjustedRSquared = 1 - (ssRes / dof) / (ssTot / (n - 1));
345+
346+
// Ensure adjusted R-squared is not greater than 1 or less than 0
347+
AdjustedRSquared = Math.Min(1.0, Math.Max(0.0, AdjustedRSquared));
348+
}
349+
350+
// Only calculate correlation coefficient if we have model values
351+
if (hasModelValues)
352+
{
353+
// Calculate correlation coefficient between observed and predicted
354+
CorrelationCoefficient = GoodnessOfFit.R(objective.ModelValues, objective.ObservedY);
78355
}
79356
}
80357
}

0 commit comments

Comments
 (0)