diff --git a/Algorithms.Tests/MachineLearning/LinearRegressionTests.cs b/Algorithms.Tests/MachineLearning/LinearRegressionTests.cs new file mode 100644 index 00000000..c43fc41b --- /dev/null +++ b/Algorithms.Tests/MachineLearning/LinearRegressionTests.cs @@ -0,0 +1,85 @@ +using Algorithms.MachineLearning; + +namespace Algorithms.Tests.MachineLearning; + +/// +/// Unit tests for the LinearRegression class. +/// +public class LinearRegressionTests +{ + [Test] + public void Fit_ThrowsException_WhenInputIsNull() + { + var lr = new LinearRegression(); + Assert.Throws(() => lr.Fit(null!, new List { 1 })); + Assert.Throws(() => lr.Fit(new List { 1 }, null!)); + } + + [Test] + public void Fit_ThrowsException_WhenInputIsEmpty() + { + var lr = new LinearRegression(); + Assert.Throws(() => lr.Fit(new List(), new List())); + } + + [Test] + public void Fit_ThrowsException_WhenInputLengthsDiffer() + { + var lr = new LinearRegression(); + Assert.Throws(() => lr.Fit(new List { 1 }, new List { 2, 3 })); + } + + [Test] + public void Fit_ThrowsException_WhenXVarianceIsZero() + { + var lr = new LinearRegression(); + Assert.Throws(() => lr.Fit(new List { 1, 1, 1 }, new List { 2, 3, 4 })); + } + + [Test] + public void Predict_ThrowsException_IfNotFitted() + { + var lr = new LinearRegression(); + Assert.Throws(() => lr.Predict(1.0)); + Assert.Throws(() => lr.Predict(new List { 1.0 })); + } + + [Test] + public void FitAndPredict_WorksForSimpleData() + { + // y = 2x + 1 + var x = new List { 1, 2, 3, 4 }; + var y = new List { 3, 5, 7, 9 }; + var lr = new LinearRegression(); + lr.Fit(x, y); + Assert.That(lr.IsFitted, Is.True); + Assert.That(lr.Intercept, Is.EqualTo(1.0).Within(1e-6)); + Assert.That(lr.Slope, Is.EqualTo(2.0).Within(1e-6)); + Assert.That(lr.Predict(5), Is.EqualTo(11.0).Within(1e-6)); + } + + [Test] + public void FitAndPredict_WorksForNegativeSlope() + { + // y = -3x + 4 + var x = new List { 0, 1, 2 }; + var y = new List { 4, 1, -2 }; + var lr = new LinearRegression(); + lr.Fit(x, y); + Assert.That(lr.Intercept, Is.EqualTo(4.0).Within(1e-6)); + Assert.That(lr.Slope, Is.EqualTo(-3.0).Within(1e-6)); + Assert.That(lr.Predict(3), Is.EqualTo(-5.0).Within(1e-6)); + } + + [Test] + public void Predict_List_WorksCorrectly() + { + var x = new List { 1, 2, 3 }; + var y = new List { 2, 4, 6 }; + var lr = new LinearRegression(); + lr.Fit(x, y); // y = 2x + var predictions = lr.Predict(new List { 4, 5 }); + Assert.That(predictions[0], Is.EqualTo(8.0).Within(1e-6)); + Assert.That(predictions[1], Is.EqualTo(10.0).Within(1e-6)); + } +} diff --git a/Algorithms/MachineLearning/LinearRegression.cs b/Algorithms/MachineLearning/LinearRegression.cs new file mode 100644 index 00000000..4e586376 --- /dev/null +++ b/Algorithms/MachineLearning/LinearRegression.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.MachineLearning; + +/// +/// Implements simple linear regression for one independent variable (univariate). +/// Linear regression is a supervised learning algorithm used to model the relationship +/// between a scalar dependent variable (Y) and an independent variable (X). +/// The model fits a line: Y = a + bX, where 'a' is the intercept and 'b' is the slope. +/// +public class LinearRegression +{ + // Intercept (a) and slope (b) of the fitted line + public double Intercept { get; private set; } + + public double Slope { get; private set; } + + public bool IsFitted { get; private set; } + + /// + /// Fits the linear regression model to the provided data. + /// + /// List of independent variable values. + /// List of dependent variable values. + /// Thrown if input lists are null, empty, or of different lengths. + public void Fit(IList x, IList y) + { + if (x == null || y == null) + { + throw new ArgumentException("Input data cannot be null."); + } + + if (x.Count == 0 || y.Count == 0) + { + throw new ArgumentException("Input data cannot be empty."); + } + + if (x.Count != y.Count) + { + throw new ArgumentException("Input lists must have the same length."); + } + + // Calculate means + double xMean = x.Average(); + double yMean = y.Average(); + + // Calculate slope (b) and intercept (a) + double numerator = 0.0; + double denominator = 0.0; + for (int i = 0; i < x.Count; i++) + { + numerator += (x[i] - xMean) * (y[i] - yMean); + denominator += (x[i] - xMean) * (x[i] - xMean); + } + + const double epsilon = 1e-12; + if (Math.Abs(denominator) < epsilon) + { + throw new ArgumentException("Variance of X must not be zero."); + } + + Slope = numerator / denominator; + Intercept = yMean - Slope * xMean; + IsFitted = true; + } + + /// + /// Predicts the output value for a given input using the fitted model. + /// + /// Input value. + /// Predicted output value. + /// Thrown if the model is not fitted. + public double Predict(double x) + { + if (!IsFitted) + { + throw new InvalidOperationException("Model must be fitted before prediction."); + } + + return Intercept + Slope * x; + } + + /// + /// Predicts output values for a list of inputs using the fitted model. + /// + /// List of input values. + /// List of predicted output values. + /// Thrown if the model is not fitted. + public IList Predict(IList xValues) + { + if (!IsFitted) + { + throw new InvalidOperationException("Model must be fitted before prediction."); + } + + return xValues.Select(Predict).ToList(); + } +} diff --git a/README.md b/README.md index cb1358d0..37165565 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ find more than one implementation for the same objective but using different alg * [SoftMax Function](./Algorithms/Numeric/SoftMax.cs) * [RecommenderSystem](./Algorithms/RecommenderSystem) * [CollaborativeFiltering](./Algorithms/RecommenderSystem/CollaborativeFiltering) + * [Machine Learning](./Algorithms/MachineLearning) + * [Linear Regression](./Algorithms/MachineLearning/LinearRegression.cs) * [Searches](./Algorithms/Search) * [A-Star](./Algorithms/Search/AStar/) * [Binary Search](./Algorithms/Search/BinarySearcher.cs)