From 3f90613ff7a130dd85dd257bb540544fe3f8f336 Mon Sep 17 00:00:00 2001 From: Christoph Ruegg Date: Wed, 25 Sep 2013 09:46:45 +0200 Subject: [PATCH] Basic direct linear regression (simple, multiple, weighted, local) --- src/FSharp/Fit.fs | 4 +- src/Numerics/Fit.cs | 70 ++------ .../LinearRegression/MultipleRegression.cs | 169 ++++++++++++++++++ .../LinearRegression/SimpleRegression.cs | 88 +++++++++ src/Numerics/LinearRegression/Util.cs | 52 ++++++ .../LinearRegression/WeightedRegression.cs | 97 ++++++++++ src/Numerics/Numerics.csproj | 4 + src/UnitTests/FitTests.cs | 10 +- 8 files changed, 430 insertions(+), 64 deletions(-) create mode 100644 src/Numerics/LinearRegression/MultipleRegression.cs create mode 100644 src/Numerics/LinearRegression/SimpleRegression.cs create mode 100644 src/Numerics/LinearRegression/Util.cs create mode 100644 src/Numerics/LinearRegression/WeightedRegression.cs diff --git a/src/FSharp/Fit.fs b/src/FSharp/Fit.fs index f14dfce9..2fd45512 100644 --- a/src/FSharp/Fit.fs +++ b/src/FSharp/Fit.fs @@ -40,8 +40,8 @@ module Fit = let private tofs (f:Func<_,_>) = fun a -> f.Invoke(a) /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, - /// returning its best fitting parameters as [a, b] array. - let line x y = let p = Fit.Line(x,y) in (p.[0],p.[1]) + /// returning its best fitting parameters as (a, b) tuple. + let line x y = Fit.Line(x,y) /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, /// returning a function y' for the best fitting line. diff --git a/src/Numerics/Fit.cs b/src/Numerics/Fit.cs index d0bedb42..f68006b4 100644 --- a/src/Numerics/Fit.cs +++ b/src/Numerics/Fit.cs @@ -31,7 +31,7 @@ using System; using System.Linq; using MathNet.Numerics.LinearAlgebra.Double; -using MathNet.Numerics.Properties; +using MathNet.Numerics.LinearRegression; namespace MathNet.Numerics { @@ -42,44 +42,12 @@ namespace MathNet.Numerics { /// /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, - /// returning its best fitting parameters as [a, b] array. + /// returning its best fitting parameters as [a, b] array, + /// where a is the intercept and b the slope. /// - public static double[] Line(double[] x, double[] y) + public static Tuple Line(double[] x, double[] y) { - if (x == null) throw new ArgumentNullException("x"); - if (y == null) throw new ArgumentNullException("y"); - if (x.Length != y.Length) throw new ArgumentException(Resources.ArgumentVectorsSameLength); - if (x.Length <= 1) throw new ArgumentException(string.Format(Resources.ArrayTooSmall, 2)); - - // First Pass: Mean (Less robust but faster than ArrayStatistics.Mean) - double mx = 0.0; - double my = 0.0; - for (int i = 0; i < x.Length; i++) - { - mx += x[i]; - my += y[i]; - } - mx /= x.Length; - my /= y.Length; - - // Second Pass: Covariance/Variance - double covariance = 0.0; - double variance = 0.0; - for (int i = 0; i < x.Length; i++) - { - double diff = x[i] - mx; - covariance += diff*(y[i] - my); - variance += diff*diff; - } - - var b = covariance/variance; - return new[] {my - b*mx, b}; - - // General Solution: - //return DenseMatrix - // .OfColumns(x.Length, 2, new[] {DenseVector.Create(x.Length, i => 1.0), new DenseVector(x)}) - // .QR(QRMethod.Thin).Solve(new DenseVector(y)) - // .ToArray(); + return SimpleRegression.Fit(x, y); } /// @@ -88,9 +56,9 @@ namespace MathNet.Numerics /// public static Func LineFunc(double[] x, double[] y) { - var parameters = Line(x, y); - double a = parameters[0], b = parameters[1]; - return z => a + b*z; + var parameters = SimpleRegression.Fit(x, y); + double intercept = parameters.Item1, slope = parameters.Item2; + return z => intercept + slope*z; } /// @@ -99,10 +67,8 @@ namespace MathNet.Numerics /// public static double[] Polynomial(double[] x, double[] y, int order) { - return DenseMatrix - .OfColumns(x.Length, order + 1, Enumerable.Range(0, order + 1).Select(j => DenseVector.Create(x.Length, i => Math.Pow(x[i], j)))) - .QR().Solve(new DenseVector(y)) - .ToArray(); + var design = DenseMatrix.OfColumns(x.Length, order + 1, Enumerable.Range(0, order + 1).Select(j => DenseVector.Create(x.Length, i => Math.Pow(x[i], j)))); + return MultipleRegression.QR(design, new DenseVector(y)).ToArray(); } /// @@ -121,10 +87,8 @@ namespace MathNet.Numerics /// public static double[] LinearCombination(double[] x, double[] y, params Func[] functions) { - return DenseMatrix - .OfColumns(x.Length, functions.Length, functions.Select(f => DenseVector.Create(x.Length, i => f(x[i])))) - .QR().Solve(new DenseVector(y)) - .ToArray(); + var design = DenseMatrix.OfColumns(x.Length, functions.Length, functions.Select(f => DenseVector.Create(x.Length, i => f(x[i])))); + return MultipleRegression.QR(design, new DenseVector(y)).ToArray(); } /// @@ -143,10 +107,7 @@ namespace MathNet.Numerics /// public static double[] LinearMultiDim(double[][] x, double[] y, params Func[] functions) { - return DenseMatrix - .OfRows(x.Length, functions.Length, x.Select(xi => functions.Select(f => f(xi)))) - .QR().Solve(new DenseVector(y)) - .ToArray(); + return MultipleRegression.QR(x.Select(xi => functions.Select(f => f(xi)).ToArray()).ToArray(), y); } /// @@ -165,10 +126,7 @@ namespace MathNet.Numerics /// public static double[] LinearGeneric(T[] x, double[] y, params Func[] functions) { - return DenseMatrix - .OfRows(x.Length, functions.Length, x.Select(xi => functions.Select(f => f(xi)))) - .QR().Solve(new DenseVector(y)) - .ToArray(); + return MultipleRegression.QR(x.Select(xi => functions.Select(f => f(xi)).ToArray()).ToArray(), y); } /// diff --git a/src/Numerics/LinearRegression/MultipleRegression.cs b/src/Numerics/LinearRegression/MultipleRegression.cs new file mode 100644 index 00000000..40655644 --- /dev/null +++ b/src/Numerics/LinearRegression/MultipleRegression.cs @@ -0,0 +1,169 @@ +// +// Math.NET Numerics, part of the Math.NET Project +// http://numerics.mathdotnet.com +// http://github.com/mathnet/mathnet-numerics +// http://mathnetnumerics.codeplex.com +// +// Copyright (c) 2009-2013 Math.NET +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using MathNet.Numerics.LinearAlgebra; + +namespace MathNet.Numerics.LinearRegression +{ + public static class MultipleRegression + { + /// + /// Find the model parameters β such that X*β with predictor X becomes as close to response Y as possible, with least squares residuals. + /// Uses the cholesky decomposition of the normal equations. + /// + /// Predictor matrix X + /// Response vector Y + /// Best fitting vector for model parameters β + public static Vector NormalEquations(Matrix x, Vector y) where T : struct, IEquatable, IFormattable + { + return x.TransposeThisAndMultiply(x).Cholesky().Solve(x.Transpose()*y); + } + + /// + /// Find the model parameters β such that their linear combination with all predictor-arrays in X become as close to their response in Y as possible, with least squares residuals. + /// Uses the cholesky decomposition of the normal equations. + /// + /// List of predictor-arrays. + /// List of responses + /// True if an intercept should be added as first artificial perdictor value. Default = false. + /// Best fitting list of model parameters β for each element in the predictor-arrays. + public static T[] NormalEquations(T[][] x, T[] y, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var predictor = Matrix.Build.DenseMatrixOfRowArrays(x); + if (intercept) + { + predictor = predictor.InsertColumn(0, Vector.Build.DenseVector(predictor.RowCount, Vector.One)); + } + var response = Matrix.Build.DenseVector(y); + return predictor.TransposeThisAndMultiply(predictor).Cholesky().Solve(predictor.Transpose()*response).ToArray(); + } + + /// + /// Find the model parameters β such that their linear combination with all predictor-arrays in X become as close to their response in Y as possible, with least squares residuals. + /// Uses the cholesky decomposition of the normal equations. + /// + /// Sequence of predictor-arrays and their response. + /// True if an intercept should be added as first artificial perdictor value. Default = false. + /// Best fitting list of model parameters β for each element in the predictor-arrays. + public static T[] NormalEquations(IEnumerable> samples, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var xy = samples.UnpackSinglePass(); + return NormalEquations(xy.Item1, xy.Item2, intercept); + } + + /// + /// Find the model parameters β such that X*β with predictor X becomes as close to response Y as possible, with least squares residuals. + /// Uses an orthogonal decomposition and is therefore more numerically stable than the normal equations but also slower. + /// + /// Predictor matrix X + /// Response vector Y + /// Best fitting vector for model parameters β + public static Vector QR(Matrix x, Vector y) where T : struct, IEquatable, IFormattable + { + return x.QR().Solve(y); + } + + /// + /// Find the model parameters β such that their linear combination with all predictor-arrays in X become as close to their response in Y as possible, with least squares residuals. + /// Uses an orthogonal decomposition and is therefore more numerically stable than the normal equations but also slower. + /// + /// List of predictor-arrays. + /// List of responses + /// True if an intercept should be added as first artificial perdictor value. Default = false. + /// Best fitting list of model parameters β for each element in the predictor-arrays. + public static T[] QR(T[][] x, T[] y, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var predictor = Matrix.Build.DenseMatrixOfRowArrays(x); + if (intercept) + { + predictor = predictor.InsertColumn(0, Vector.Build.DenseVector(predictor.RowCount, Vector.One)); + } + return predictor.QR().Solve(Matrix.Build.DenseVector(y)).ToArray(); + } + + /// + /// Find the model parameters β such that their linear combination with all predictor-arrays in X become as close to their response in Y as possible, with least squares residuals. + /// Uses an orthogonal decomposition and is therefore more numerically stable than the normal equations but also slower. + /// + /// Sequence of predictor-arrays and their response. + /// True if an intercept should be added as first artificial perdictor value. Default = false. + /// Best fitting list of model parameters β for each element in the predictor-arrays. + public static T[] QR(IEnumerable> samples, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var xy = samples.UnpackSinglePass(); + return QR(xy.Item1, xy.Item2, intercept); + } + + /// + /// Find the model parameters β such that X*β with predictor X becomes as close to response Y as possible, with least squares residuals. + /// Uses a singular value decomposition and is therefore more numerically stable (especially if ill-conditioned) than the normal equations or QR but also slower. + /// + /// Predictor matrix X + /// Response vector Y + /// Best fitting vector for model parameters β + public static Vector Svd(Matrix x, Vector y) where T : struct, IEquatable, IFormattable + { + return x.Svd().Solve(y); + } + + /// + /// Find the model parameters β such that their linear combination with all predictor-arrays in X become as close to their response in Y as possible, with least squares residuals. + /// Uses a singular value decomposition and is therefore more numerically stable (especially if ill-conditioned) than the normal equations or QR but also slower. + /// + /// List of predictor-arrays. + /// List of responses + /// True if an intercept should be added as first artificial perdictor value. Default = false. + /// Best fitting list of model parameters β for each element in the predictor-arrays. + public static T[] Svd(T[][] x, T[] y, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var predictor = Matrix.Build.DenseMatrixOfRowArrays(x); + if (intercept) + { + predictor = predictor.InsertColumn(0, Vector.Build.DenseVector(predictor.RowCount, Vector.One)); + } + return predictor.Svd().Solve(Matrix.Build.DenseVector(y)).ToArray(); + } + + /// + /// Find the model parameters β such that their linear combination with all predictor-arrays in X become as close to their response in Y as possible, with least squares residuals. + /// Uses a singular value decomposition and is therefore more numerically stable (especially if ill-conditioned) than the normal equations or QR but also slower. + /// + /// Sequence of predictor-arrays and their response. + /// True if an intercept should be added as first artificial perdictor value. Default = false. + /// Best fitting list of model parameters β for each element in the predictor-arrays. + public static T[] Svd(IEnumerable> samples, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var xy = samples.UnpackSinglePass(); + return Svd(xy.Item1, xy.Item2, intercept); + } + } +} diff --git a/src/Numerics/LinearRegression/SimpleRegression.cs b/src/Numerics/LinearRegression/SimpleRegression.cs new file mode 100644 index 00000000..081e831d --- /dev/null +++ b/src/Numerics/LinearRegression/SimpleRegression.cs @@ -0,0 +1,88 @@ +// +// Math.NET Numerics, part of the Math.NET Project +// http://numerics.mathdotnet.com +// http://github.com/mathnet/mathnet-numerics +// http://mathnetnumerics.codeplex.com +// +// Copyright (c) 2009-2013 Math.NET +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using MathNet.Numerics.Properties; + +namespace MathNet.Numerics.LinearRegression +{ + public static class SimpleRegression + { + /// + /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, + /// returning its best fitting parameters as (a, b) tuple, + /// where a is the intercept and b the slope. + /// + /// Predictor (independent) + /// Response (dependent) + public static Tuple Fit(double[] x, double[] y) + { + if (x.Length != y.Length) throw new ArgumentException(Resources.ArgumentVectorsSameLength); + if (x.Length <= 1) throw new ArgumentException(string.Format(Resources.ArrayTooSmall, 2)); + + // First Pass: Mean (Less robust but faster than ArrayStatistics.Mean) + double mx = 0.0; + double my = 0.0; + for (int i = 0; i < x.Length; i++) + { + mx += x[i]; + my += y[i]; + } + mx /= x.Length; + my /= y.Length; + + // Second Pass: Covariance/Variance + double covariance = 0.0; + double variance = 0.0; + for (int i = 0; i < x.Length; i++) + { + double diff = x[i] - mx; + covariance += diff*(y[i] - my); + variance += diff*diff; + } + + var b = covariance/variance; + return new Tuple(my - b*mx, b); + } + + /// + /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, + /// returning its best fitting parameters as (a, b) tuple, + /// where a is the intercept and b the slope. + /// + /// Predictor-Response samples as tuples + public static Tuple Fit(IEnumerable> samples) + { + var xy = samples.UnpackSinglePass(); + return Fit(xy.Item1, xy.Item2); + } + } +} diff --git a/src/Numerics/LinearRegression/Util.cs b/src/Numerics/LinearRegression/Util.cs new file mode 100644 index 00000000..f1c2a85f --- /dev/null +++ b/src/Numerics/LinearRegression/Util.cs @@ -0,0 +1,52 @@ +// +// Math.NET Numerics, part of the Math.NET Project +// http://numerics.mathdotnet.com +// http://github.com/mathnet/mathnet-numerics +// http://mathnetnumerics.codeplex.com +// +// Copyright (c) 2009-2013 Math.NET +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MathNet.Numerics.LinearRegression +{ + internal static class Util + { + public static Tuple UnpackSinglePass(this IEnumerable> samples) + { + var u = new List(); + var v = new List(); + + foreach (var tuple in samples) + { + u.Add(tuple.Item1); + v.Add(tuple.Item2); + } + + return new Tuple(u.ToArray(), v.ToArray()); + } + } +} diff --git a/src/Numerics/LinearRegression/WeightedRegression.cs b/src/Numerics/LinearRegression/WeightedRegression.cs new file mode 100644 index 00000000..d615ad21 --- /dev/null +++ b/src/Numerics/LinearRegression/WeightedRegression.cs @@ -0,0 +1,97 @@ +// +// Math.NET Numerics, part of the Math.NET Project +// http://numerics.mathdotnet.com +// http://github.com/mathnet/mathnet-numerics +// http://mathnetnumerics.codeplex.com +// +// Copyright (c) 2009-2013 Math.NET +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using MathNet.Numerics.LinearAlgebra; +using MathNet.Numerics.LinearAlgebra.Storage; + +namespace MathNet.Numerics.LinearRegression +{ + public static class WeightedRegression + { + /// + /// Weighted Linear Regression using normal equations. + /// + public static Vector Weighted(Matrix x, Vector y, Matrix w) where T : struct, IEquatable, IFormattable + { + return x.TransposeThisAndMultiply(w * x).Cholesky().Solve(x.Transpose() * (w * y)); + } + + /// + /// Weighted Linear Regression using normal equations. + /// + /// True if an intercept should be added as first artificial perdictor value. Default = false. + public static T[] Weighted(T[][] x, T[] y, T[] w, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var predictor = Matrix.Build.DenseMatrixOfRowArrays(x); + if (intercept) + { + predictor = predictor.InsertColumn(0, Vector.Build.DenseVector(predictor.RowCount, Vector.One)); + } + var response = Matrix.Build.DenseVector(y); + var weights = Matrix.Build.DiagonalMatrix(new DiagonalMatrixStorage(predictor.RowCount, predictor.RowCount, w)); + return predictor.TransposeThisAndMultiply(weights * predictor).Cholesky().Solve(predictor.Transpose() * (weights * response)).ToArray(); + } + + /// + /// Weighted Linear Regression using normal equations. + /// + /// True if an intercept should be added as first artificial perdictor value. Default = false. + public static T[] Weighted(IEnumerable> samples, T[] w, bool intercept = false) where T : struct, IEquatable, IFormattable + { + var xy = samples.UnpackSinglePass(); + return Weighted(xy.Item1, xy.Item2, w, intercept); + } + + /// + /// Locally-Weighted Linear Regression using normal equations. + /// + public static Vector Local(Matrix x, Vector y, Vector t, Func, Vector, T> kernel) where T : struct, IEquatable, IFormattable + { + // TODO: Kernel definition is a bit weird as it includes computing the difference norm + // We can make this more common once we change the norm to always be of type double around LA. + + var w = Matrix.Build.DenseMatrix(x.RowCount, x.RowCount); + for (int i = 0; i < x.RowCount; i++) + { + w.At(i, i, kernel(t, x.Row(i))); + } + return Weighted(x, y, w); + } + + public static Func, Vector, double> GaussianKernel(double radius) + { + // TODO: see above... + var d = -2.0*radius*radius; + return (t, x) => Math.Exp(Distance.SSD(x, t)/d); + } + } +} diff --git a/src/Numerics/Numerics.csproj b/src/Numerics/Numerics.csproj index 4a99efb3..80b7e2ee 100644 --- a/src/Numerics/Numerics.csproj +++ b/src/Numerics/Numerics.csproj @@ -132,6 +132,10 @@ + + + + diff --git a/src/UnitTests/FitTests.cs b/src/UnitTests/FitTests.cs index 38e301d0..c2444ff6 100644 --- a/src/UnitTests/FitTests.cs +++ b/src/UnitTests/FitTests.cs @@ -45,9 +45,8 @@ namespace MathNet.Numerics.UnitTests var y = x.Select(z => 4.0 - 1.5*z).ToArray(); var resp = Fit.Line(x, y); - Assert.AreEqual(2, resp.Length); - Assert.AreEqual(4.0, resp[0], 1e-12); - Assert.AreEqual(-1.5, resp[1], 1e-12); + Assert.AreEqual(4.0, resp.Item1, 1e-12); + Assert.AreEqual(-1.5, resp.Item2, 1e-12); var resf = Fit.LineFunc(x, y); foreach (var z in Enumerable.Range(-3, 10)) @@ -66,9 +65,8 @@ namespace MathNet.Numerics.UnitTests var y = new[] {4.986, 2.347, 2.061, -2.995, -2.352, -5.782}; var resp = Fit.Line(x, y); - Assert.AreEqual(2, resp.Length); - Assert.AreEqual(7.01013, resp[0], 1e-4); - Assert.AreEqual(-2.08551, resp[1], 1e-4); + Assert.AreEqual(7.01013, resp.Item1, 1e-4); + Assert.AreEqual(-2.08551, resp.Item2, 1e-4); var resf = Fit.LineFunc(x, y); foreach (var z in Enumerable.Range(-3, 10))