Repository: Benardi/touvlo

Issue:
Add classes for existing Linear Regression implementations
#### Description
Add classes to encapsulate existing Ridge and Unregularized Linear Regression implementations.

#### Files
* `touvlo/supv/lin_rg.py` - Existing linear regression module
* `tests/supv/test_lin_rg.py` - Test suite for linear regression module

#### Tasks
Include specific tasks in the order they need to be done in. Include links to specific lines of code where the task should happen at.
- [x] Add classes for Ridge and Unregularized Linear Regression 
- [x] Add unit tests for new classes
- [x] Add documentation for new classes

Comments:

Patch:
diff --git a/README.md b/README.md
index 5c8227a..75e7723 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,8 @@
 
 
 <p align="center">
-    <a href="https://travis-ci.org/Benardi/touvlo">
-        <img src="https://api.travis-ci.org/Benardi/touvlo.svg?branch=master"
+    <a href="https://travis-ci.com/Benardi/touvlo">
+        <img src="https://api.travis-ci.com/Benardi/touvlo.svg?branch=master"
             alt="Build Status"/></a>
     <a href="https://touvlo.readthedocs.io/en/latest/?badge=latest">
         <img src="https://readthedocs.org/projects/touvlo/badge/?version=latest"
diff --git a/touvlo/supv/lin_rg.py b/touvlo/supv/lin_rg.py
index df67ba3..a228801 100644
--- a/touvlo/supv/lin_rg.py
+++ b/touvlo/supv/lin_rg.py
@@ -5,8 +5,12 @@
 .. moduleauthor:: Benardi Nunes <benardinunes@gmail.com>
 """
 
-from numpy import zeros, float64
-from numpy.linalg import inv, LinAlgError
+from abc import ABC, abstractmethod
+
+from numpy import zeros, float64, ones, append, identity
+from numpy.linalg import inv
+
+from touvlo.utils import BGD, SGD, MBGD
 
 
 # model hypothesis
@@ -118,13 +122,196 @@ def normal_eqn(X, y):
         numpy.array: Optimized model parameters theta.
     """
     n = X.shape[1]  # number of columns
-    theta = zeros((n + 1, 1), dtype=float64)
+    theta = zeros((n, 1), dtype=float64)
 
-    try:
-        X_T = X.T
-        theta = inv(X_T.dot(X)).dot(X_T).dot(y)
+    X_T = X.T
+    theta = inv(X_T.dot(X)).dot(X_T).dot(y)
 
-    except LinAlgError:
-        pass
+    return theta
+
+
+def reg_normal_eqn(X, y, _lambda):
+    """Produces optimal theta via normal equation.
+
+    Args:
+        X (numpy.array): Features' dataset plus bias column.
+        y (numpy.array): Column vector of expected values.
+        _lambda (float): The regularization hyperparameter.
+
+    Returns:
+        numpy.array: Optimized model parameters theta.
+    """
+    n = X.shape[1]  # number of columns, already has bias
+    theta = zeros((n, 1), dtype=float64)
+    L = identity(n)
+    L[0, 0] = 0
+    X_T = X.T
+
+    theta = inv(X_T.dot(X) + _lambda * L).dot(X_T).dot(y)
 
     return theta
+
+
+class AbstractLinearRegression(ABC):
+    """Represents the definitons common to all Linear Regressions.
+
+    Args:
+        theta (numpy.array): Column vector of model's parameters.
+            Defaults to None
+
+    Attributes:
+        theta (numpy.array): Column vector of model's parameters.
+    """
+
+    def __init__(self, theta=None):
+        self.theta = theta
+
+    def _add_bias_term(self, X):
+        m = len(X)
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+        return X
+
+    def predict(self, X):
+        """Computes prediction vector.
+
+        Args:
+            X (numpy.array): Features' dataset.
+        Returns:
+            numpy.array: vector with a prediction for each example.
+        """
+        X = self._add_bias_term(X)
+        return predict(X, self.theta)
+
+    @abstractmethod
+    def _normal_fit(self, X, y):
+        pass
+
+    @abstractmethod
+    def _gradient_fit(self, grad_descent, X, y, alpha, num_iters):
+        pass
+
+    def fit(self, X, y, strategy='BGD', alpha=0.1,
+            num_iters=100, **kwargs):
+        """Adjusts model parameters to training data.
+
+        Args:
+            X (numpy.array): Features' dataset.
+            y (numpy.array): Column vector of expected values.
+            strategy (str) : Which optimization strategy should be employed:
+
+             * 'BGD': Performs Batch Gradient Descent
+             * 'SGD': Performs Stochastic Gradient Descent
+             * 'MBGD': Performs Mini-Batch Gradient Descent
+             * 'normal_equation': Employs Normal Equation
+            alpha (float): Learning rate or _step size of the optimization.
+            num_iters (int): Number of times the optimization will be
+                             performed.
+
+        Returns:
+            None
+        """
+        if isinstance(strategy, str) and strategy == 'BGD':
+            self._gradient_fit(BGD, X, y, alpha, num_iters, **kwargs)
+
+        elif isinstance(strategy, str) and strategy == 'SGD':
+            self._gradient_fit(SGD, X, y, alpha, num_iters, **kwargs)
+
+        elif isinstance(strategy, str) and strategy == 'MBGD':
+            self._gradient_fit(MBGD, X, y, alpha, num_iters, **kwargs)
+
+        elif isinstance(strategy, str) and strategy == 'normal_equation':
+            self._normal_fit(X, y)
+        else:
+            raise ValueError(
+                ("'%s' (type '%s') was passed. " % (strategy, type(strategy)),
+                 "The strategy parameter for the fit function should ",
+                 "be 'BGD' or 'SGD' or 'MBGD' or 'normal_equation'."))
+
+
+class LinearRegression(AbstractLinearRegression):
+    """Represents an Unregularized Linear Regression model
+
+    Args:
+        theta (numpy.array): Column vector of model's parameters.
+            Defaults to None
+
+    Attributes:
+        theta (numpy.array): Column vector of model's parameters.
+    """
+
+    def __init__(self, theta=None):
+        super().__init__(theta)
+
+    def _normal_fit(self, X, y):
+        X = self._add_bias_term(X)
+        self.theta = normal_eqn(X, y)
+
+    def _gradient_fit(self, grad_descent, X, y, alpha,
+                      num_iters, **kwargs):
+        if self.theta is None:
+            _, n = X.shape
+            self.theta = zeros((n + 1, 1), dtype=float64)
+
+        X = self._add_bias_term(X)
+        self.theta = grad_descent(X, y, grad, self.theta, alpha,
+                                  num_iters, **kwargs)
+
+    def cost(self, X, y):
+        """Computes the cost function J for Linear Regression.
+
+        Args:
+            X (numpy.array): Features' dataset plus bias column.
+            y (numpy.array): Column vector of expected values.
+
+        Returns:
+            float: Computed cost.
+        """
+        X = self._add_bias_term(X)
+        return cost_func(X, y, self.theta)
+
+
+class RidgeLinearRegression(AbstractLinearRegression):
+    """Represents a Ridge Linear Regression model
+
+    Args:
+        theta (numpy.array): Column vector of model's parameters.
+            Defaults to None
+        _lambda (float): The regularization hyperparameter.
+
+    Attributes:
+        theta (numpy.array): Column vector of model's parameters.
+        _lambda (float): The regularization hyperparameter.
+    """
+
+    def __init__(self, theta=None, _lambda=0):
+        super().__init__(theta)
+        self._lambda = _lambda
+
+    def _normal_fit(self, X, y):
+        X = self._add_bias_term(X)
+        self.theta = reg_normal_eqn(X, y, self._lambda)
+
+    def _gradient_fit(self, grad_descent, X, y, alpha,
+                      num_iters, **kwargs):
+        if self.theta is None:
+            _, n = X.shape
+            self.theta = zeros((n + 1, 1), dtype=float64)
+
+        X = self._add_bias_term(X)
+        self.theta = grad_descent(X, y, reg_grad, self.theta, alpha,
+                                  num_iters, _lambda=self._lambda, **kwargs)
+
+    def cost(self, X, y, **kwargs):
+        """Computes the regularized cost function J for
+            Ridge Linear Regression.
+
+        Args:
+            X (numpy.array): Features' dataset plus bias column.
+            y (numpy.array): Column vector of expected values.
+
+        Returns:
+            float: Computed cost.
+        """
+        X = self._add_bias_term(X)
+        return reg_cost_func(X, y, self.theta, self._lambda, **kwargs)
diff --git a/touvlo/unsupv/kmeans.py b/touvlo/unsupv/kmeans.py
index 7a9de32..0302a23 100644
--- a/touvlo/unsupv/kmeans.py
+++ b/touvlo/unsupv/kmeans.py
@@ -169,8 +169,7 @@ def elbow_method(X, K_values, max_iters, n_inits):
         n_inits (int): Number of random initialization.
 
     Returns:
-        (list(int), list(float)): A 2-tuple of K_values, a list of possible
-            numbers of centroids, and cost_values, a computed cost for each K.
+        (list(float)): a list of cost values for each K.
     """
     cost_values = []
     for K in K_values:
@@ -178,4 +177,4 @@ def elbow_method(X, K_values, max_iters, n_inits):
         cost = cost_function(X, idx, centroids)
         cost_values.append(cost)
 
-    return K_values, cost_values
+    return cost_values


Test patch:
diff --git a/tests/supv/test_lin_rg.py b/tests/supv/test_lin_rg.py
index 001047e..4f452b3 100644
--- a/tests/supv/test_lin_rg.py
+++ b/tests/supv/test_lin_rg.py
@@ -3,9 +3,11 @@ import os
 import pytest
 from numpy.testing import assert_allclose
 from numpy import ones, zeros, float64, array, append, genfromtxt
+from numpy.linalg import LinAlgError
 
 from touvlo.supv.lin_rg import (normal_eqn, cost_func, reg_cost_func, grad,
-                                reg_grad, predict, h)
+                                reg_grad, predict, h, LinearRegression,
+                                RidgeLinearRegression, reg_normal_eqn)
 from touvlo.utils import numerical_grad
 
 TESTDATA1 = os.path.join(os.path.dirname(__file__), 'data1.csv')
@@ -33,7 +35,7 @@ class TestLinearRegression:
 
         y = data1[:, -1:]
         X = data1[:, :-1]
-        m, n = X.shape
+        m, _ = X.shape
         intercept = ones((m, 1), dtype=int)
         X = append(intercept, X, axis=1)
 
@@ -44,7 +46,7 @@ class TestLinearRegression:
     def test_normal_eqn_data2(self, data2):
         y = data2[:, -1:]
         X = data2[:, :-1]
-        m, n = X.shape
+        m, _ = X.shape
         intercept = ones((m, 1), dtype=int)
         X = append(intercept, X, axis=1)
 
@@ -52,6 +54,81 @@ class TestLinearRegression:
                         normal_eqn(X, y),
                         rtol=0, atol=0.001)
 
+    def test_reg_normal_eqn_data1_1(self, data1):
+
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        m, _ = X.shape
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+        _lambda = 0
+
+        assert_allclose([[-3.896], [1.193]],
+                        reg_normal_eqn(X, y, _lambda),
+                        rtol=0, atol=0.001)
+
+    def test_reg_normal_eqn_data1_2(self, data1):
+
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        m, _ = X.shape
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+        _lambda = 1
+
+        assert_allclose([[-3.889], [1.192]],
+                        reg_normal_eqn(X, y, _lambda),
+                        rtol=0, atol=0.001)
+
+    def test_reg_normal_eqn_data2(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        m, _ = X.shape
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+        _lambda = 100
+
+        assert_allclose([[74104.492], [135.249], [-1350.731]],
+                        reg_normal_eqn(X, y, _lambda),
+                        rtol=0, atol=0.001)
+
+    def test_normal_eqn_singular(self, data2):
+        y = array([[0], [0], [0]])
+        X = array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+        m, _ = X.shape
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+
+        with pytest.raises(LinAlgError) as excinfo:
+            normal_eqn(X, y)
+        msg = excinfo.value.args[0]
+        assert msg == ("Singular matrix")
+
+    def test_reg_normal_eqn_singular1(self, data2):
+        y = array([[0], [0], [0]])
+        X = array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+        m, _ = X.shape
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+        _lambda = 0
+
+        with pytest.raises(LinAlgError) as excinfo:
+            reg_normal_eqn(X, y, _lambda),
+        msg = excinfo.value.args[0]
+        assert msg == ("Singular matrix")
+
+    def test_reg_normal_eqn_singular2(self, data2):
+        y = array([[0], [0], [0]])
+        X = array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+        m, _ = X.shape
+        intercept = ones((m, 1), dtype=int)
+        X = append(intercept, X, axis=1)
+        _lambda = 0.1
+
+        assert_allclose([[0], [0], [0], [0]],
+                        reg_normal_eqn(X, y, _lambda),
+                        rtol=0, atol=0.001)
+
 # COST FUNCTION
 
     def test_cost_func_data1_1(self, data1):
@@ -505,7 +582,7 @@ class TestLinearRegression:
 
     def test_h_1(self):
         X = array([[3.5]])
-        m, n = X.shape
+        m, _ = X.shape
         intercept = ones((m, 1), dtype=float64)
         X = append(intercept, X, axis=1)
         theta = array([[-3.6303], [1.1664]])
@@ -546,3 +623,288 @@ class TestLinearRegression:
         assert_allclose([[-0.2]],
                         h(X, theta),
                         rtol=0, atol=0.001)
+
+# LINEAR REGRESSION CLASS
+
+    def test_LinearRegression_constructor1(self, data1):
+        theta = array([[1.], [0.6], [1.]])
+        lr = LinearRegression(theta)
+
+        assert_allclose(array([[1.], [0.6], [1.]]),
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_LinearRegression_constructor2(self):
+        lr = LinearRegression()
+
+        assert lr.theta is None
+
+    def test_LinearRegression_cost_data1_1(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        _, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = LinearRegression(theta)
+
+        assert_allclose([[10.266]],
+                        lr.cost(X, y),
+                        rtol=0, atol=0.001)
+
+    def test_LinearRegression_normal_fit(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        lr = LinearRegression()
+        lr.fit(X, y, strategy="normal_equation")
+
+        assert_allclose([[-3.896], [1.193]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_LinearRegression_fit_BGD(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        lr = LinearRegression()
+        lr.fit(X, y, strategy="BGD", alpha=1, num_iters=1)
+
+        assert_allclose([[340412.659], [764209128.191], [1120367.702]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_LinearRegression_fit_SGD(self, err):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = LinearRegression()
+        lr.fit(X, y, strategy="SGD", alpha=1, num_iters=1)
+
+        assert_allclose(array([[2.3], [11.2], [-11.7], [-2.2]]),
+                        lr.theta,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+    def test_LinearRegression_fit_MBGD1(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        m = len(X)
+        lr = LinearRegression()
+        lr.fit(X, y, strategy="MBGD", alpha=1, num_iters=1, b=m)
+
+        assert_allclose([[340412.659], [764209128.191], [1120367.702]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_LinearRegression_fit_MBGD2(self, err):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = LinearRegression()
+        lr.fit(X, y, strategy="MBGD", alpha=1, num_iters=1, b=1)
+
+        assert_allclose(array([[2.3], [11.2], [-11.7], [-2.2]]),
+                        lr.theta,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+    def test_LinearRegression_predict(self):
+        X = array([[3.5]])
+        theta = array([[-3.6303], [1.1664]])
+        lr = LinearRegression(theta)
+
+        assert_allclose([[0.4521]],
+                        lr.predict(X),
+                        rtol=0, atol=0.001)
+
+# RIDGE LINEAR REGRESSION CLASS
+
+    def test_RidgeLinearRegression_constructor1(self, data1):
+        theta = ones((3, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=13.50)
+
+        assert lr._lambda == 13.50
+        assert_allclose(array([[1.], [1.], [1.]]),
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_constructor2(self):
+        lr = RidgeLinearRegression()
+
+        assert lr.theta is None
+        assert lr._lambda == 0
+
+    def test_RidgeLinearRegression_cost_data1_1(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        _, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=0)
+
+        assert_allclose([[10.266]],
+                        lr.cost(X, y),
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_cost_data1_2(self, data1):
+
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        _, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=100)
+
+        assert_allclose([[10.781984]],
+                        lr.cost(X, y),
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_cost_data2_1(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        _, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=0)
+
+        assert_allclose([[64828197300.798]],
+                        lr.cost(X, y),
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_cost_data2_2(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        _, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=1000000)
+
+        assert_allclose([[64828218577.393623]],
+                        lr.cost(X, y),
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_normal_fit_data1_1(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        lr = RidgeLinearRegression(_lambda=0)
+        lr.fit(X, y, strategy="normal_equation")
+
+        assert_allclose([[-3.896], [1.193]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_normal_fit_data1_2(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        lr = RidgeLinearRegression(_lambda=1)
+        lr.fit(X, y, strategy="normal_equation")
+
+        assert_allclose([[-3.889], [1.192]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_fit_BGD1(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        lr = RidgeLinearRegression(_lambda=0)
+        lr.fit(X, y, strategy="BGD", alpha=1, num_iters=1)
+
+        assert_allclose([[340412.659], [764209128.191], [1120367.702]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_fit_BGD2(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        _, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=100)
+
+        lr.fit(X, y, strategy="BGD", alpha=1, num_iters=1)
+
+        assert_allclose([[-2.321], [-24.266]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_fit_SGD1(self, err):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = RidgeLinearRegression(_lambda=0)
+        lr.fit(X, y, strategy="SGD", alpha=1, num_iters=1)
+
+        assert_allclose(array([[2.3], [11.2], [-11.7], [-2.2]]),
+                        lr.theta,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+    def test_RidgeLinearRegression_fit_SGD2(self, err):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = RidgeLinearRegression(_lambda=10)
+        lr.fit(X, y, strategy="SGD", alpha=1, num_iters=1)
+
+        assert_allclose(array([[8.3], [-0.8], [132.3], [123.8]]),
+                        lr.theta,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+    def test_RidgeLinearRegression_fit_MBGD1(self, data2):
+        y = data2[:, -1:]
+        X = data2[:, :-1]
+        m = len(X)
+        lr = RidgeLinearRegression(_lambda=0)
+        lr.fit(X, y, strategy="MBGD", alpha=1, num_iters=1, b=m)
+
+        assert_allclose([[340412.659], [764209128.191], [1120367.702]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_fit_MBGD2(self, err):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = RidgeLinearRegression(_lambda=0)
+        lr.fit(X, y, strategy="MBGD", alpha=1, num_iters=1, b=1)
+
+        assert_allclose(array([[2.3], [11.2], [-11.7], [-2.2]]),
+                        lr.theta,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+    def test_RidgeLinearRegression_fit_MBGD3(self, data1):
+        y = data1[:, -1:]
+        X = data1[:, :-1]
+        m, n = X.shape
+        theta = ones((n + 1, 1), dtype=float64)
+        lr = RidgeLinearRegression(theta, _lambda=100)
+        lr.fit(X, y, strategy="MBGD", alpha=1, num_iters=1, b=m)
+
+        assert_allclose([[-2.321], [-24.266]],
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_fit_MBGD4(self, data1):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = RidgeLinearRegression(_lambda=10)
+        lr.fit(X, y, strategy="MBGD", alpha=1, num_iters=1, b=1)
+
+        assert_allclose(array([[8.3], [-0.8], [132.3], [123.8]]),
+                        lr.theta,
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_fit_unknown(self, data1):
+        X = array([[0, 1, 2], [-1, 5, 3], [2, 0, 1]])
+        y = array([[0.3], [1.2], [0.5]])
+        lr = RidgeLinearRegression(_lambda=10)
+
+        with pytest.raises(ValueError) as excinfo:
+            lr.fit(X, y, strategy="oompa_loompa")
+
+        msg = excinfo.value.args[0]
+        assert msg == ("'oompa_loompa' (type '<class 'str'>') was passed. ",
+                       'The strategy parameter for the fit function should ',
+                       "be 'BGD' or 'SGD' or 'MBGD' or 'normal_equation'.")
+
+    def test_RidgeLinearRegression_predict1(self):
+        X = array([[3.5]])
+        theta = array([[-3.6303], [1.1664]])
+        lr = RidgeLinearRegression(theta)
+
+        assert_allclose([[0.4521]],
+                        lr.predict(X),
+                        rtol=0, atol=0.001)
+
+    def test_RidgeLinearRegression_predict2(self):
+        X = array([[3.5]])
+        theta = array([[-3.6303], [1.1664]])
+        lr = RidgeLinearRegression(theta, _lambda=10)
+
+        assert_allclose([[0.4521]],
+                        lr.predict(X),
+                        rtol=0, atol=0.001)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index f59f102..2671f02 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,7 +5,7 @@ from numpy import array, cos, sin, exp
 from numpy.testing import assert_allclose
 
 from touvlo.utils import (numerical_grad, g_grad, BGD, SGD,
-                          MBGD, mean_normlztn)
+                          MBGD, mean_normlztn, feature_normalize)
 
 
 class TestLogisticRegression:
@@ -386,3 +386,20 @@ class TestLogisticRegression:
         assert_allclose(Y_mean,
                         array([[4.5], [3], [4], [3], [3]]),
                         rtol=0, atol=0.001, equal_nan=False)
+
+    def test_feature_normalize(self, err):
+        X = array([[1.5, 8., 7.5], [1., 6., 9.]])
+        X_norm, mu, sigma = feature_normalize(X)
+
+        assert_allclose(array([1.25, 7, 8.25]),
+                        mu,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+        assert_allclose(array([0.35355339, 1.41421356, 1.06066017]),
+                        sigma,
+                        rtol=0, atol=0.001, equal_nan=False)
+
+        assert_allclose(array([[0.707, 0.707, -0.707],
+                               [-0.707, -0.707, 0.707]]),
+                        X_norm,
+                        rtol=0, atol=0.001, equal_nan=False)
diff --git a/tests/unsupv/test_kmeans.py b/tests/unsupv/test_kmeans.py
index eec059d..189d62a 100644
--- a/tests/unsupv/test_kmeans.py
+++ b/tests/unsupv/test_kmeans.py
@@ -5,7 +5,7 @@ from numpy.testing import assert_allclose, assert_almost_equal
 from touvlo.unsupv.kmeans import (find_closest_centroids, euclidean_dist,
                                   compute_centroids, init_centroids,
                                   cost_function, run_kmeans,
-                                  run_intensive_kmeans)
+                                  run_intensive_kmeans, elbow_method)
 
 
 class TestKmeans:
@@ -180,3 +180,16 @@ class TestKmeans:
         assert all(idx < K)
         assert all(idx > -1)
         assert len(centroids) == K
+
+    def test_elbow_method(self):
+        X = array([[5.89562, 2.89844], [5.61754, 2.59751], [5.63176, 3.04759],
+                   [5.50259, 3.11869], [6.48213, 2.55085], [7.30279, 3.38016],
+                   [6.99198, 2.98707], [4.82553, 2.77962], [6.11768, 2.85476],
+                   [0.94049, 5.71557]])
+        max_iters = 6
+        n_inits = 1
+        K_values = [1, 2, 3, 4]
+
+        cost_values = elbow_method(X, K_values, max_iters, n_inits)
+
+        assert all(cost >= 0 for cost in cost_values)