Building Linear Regression from Scratch: A Mathematical Journey with SymPy
Unlocking the Black Box: Understand the Math Behind Machine Learning's Foundational Algorithm.
Building Linear Regression from Scratch: A Mathematical Journey with SymPy

Understand Machine Learning Through Mathematical First Principles
Machine learning often feels like a black box. We import libraries, call fit methods, and get predictions without truly understanding what happens under the hood. In this article, we’ll demystify linear regression by building it from mathematical first principles using symbolic computation.
Why Build from Scratch?
While libraries like scikit-learn are powerful and production-ready, implementing algorithms from scratch offers invaluable insights into how they actually work. Using SymPy for symbolic mathematics allows us to see and manipulate the actual mathematical expressions, making the learning process transparent and intuitive.
The Mathematical Foundation
Linear Regression Formula
Linear regression finds the best straight line that fits through our data. The line can be described with a simple equation:

Where:
- h = The value we want to predict (salary)
- x = The input variable (years of experience)
- m = The slope (how much y changes when x increases by 1)
- c = The intercept (where the line crosses the y-axis)
The model works by finding the values of m and c that make the line fit the data as closely as possible. We do this by minimizing the total squared distance between the actual data points and the predicted line.
Cost Function (Mean Squared Error)
The cost function measures how far our predictions are from the actual values. It’s the function we want to minimize to find the best line.
Formula:

Expanded form:

How it works:
- Calculate errors: For each data point, find the difference between the predicted value (mx + c) and the actual value (y)
- Square the errors: Square each difference to penalize larger errors more heavily and make all errors positive
- Sum squared errors: Add up all the squared differences
- Average the errors: Divide by 2n to normalize the cost (the factor of 2 is for mathematical convenience in derivatives)
- Minimize: Find the values of m and c that minimize J(m, c) to get the best-fitting line
The division by 2n ensures that the cost doesn’t grow with the size of the dataset, making it comparable across different datasets.
Implementation: Step by Step
Setting Up Our Environment
First, let’s import the necessary libraries:
import sympy as sp
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
STEP 1: Define Symbolic Variables
We initialize SymPy for mathematical notation and define our symbolic variables:
sp.init_printing(use_unicode=False, use_latex="mathjax")
m, c, x, y = sp.symbols('m c x y')
STEP 2: Defining the Hypothesis in Symbolic Notation
hypothesis = m * x + c
print("\nStep 2: Hypothesis Formula")
sp.pprint(hypothesis)
This creates our linear equation symbolically, allowing us to manipulate it mathematically.
STEP 3: Defining the Cost Function (MSE)
cost_func = (hypothesis - y)**2
print("\nStep 3: Cost Function (Single Point MSE)")
sp.pprint(cost_func)
This represents the squared error for a single data point.
STEP 4: Computing Gradients with Calculus
Here’s where SymPy shines — we can compute derivatives symbolically:
# Create unevaluated derivative objects for display
grad_m_eqn = sp.Derivative(cost_func, m)
grad_c_eqn = sp.Derivative(cost_func, c)
# Calculate the actual derivative expressions
grad_m_expr = grad_m_eqn.doit()
grad_c_expr = grad_c_eqn.doit()
print("\nStep 4: Symbolic Gradients (Calculus)", end="\n")
print("Partial Derivative for Weight (m):", end="\n")
print("\n\n")
sp.pprint(grad_m_eqn)
print("Evaluates to:")
sp.pprint(grad_m_expr)
print("\nPartial Derivative for Bias (c):")
print("\n\n")
sp.pprint(grad_c_eqn)
print("Evaluates to:")
sp.pprint(grad_c_expr)
These gradients tell us how to adjust m and c to minimize our cost function.
STEP 5: Loading Data with Pandas
print("\nStep 5: Loading Data with Pandas", end="\n")
try:
# Replace 'data.csv' with your actual file path
df = pd.read_csv('../data/Salary_Data.csv')
X_train = df['YearsExperience'].values
Y_train = df['Salary'].values
print("\nData successfully loaded from CSV using Pandas.")
except FileNotFoundError:
print("\nCSV not found. Using placeholder data for demonstration.")
X_train = np.array([1, 2, 3, 4, 5])
Y_train = np.array([3.1, 4.9, 7.2, 8.8, 11.1])
print("X_train = " ,X_train)
print("Y_train = ", Y_train)
STEP 6: Gradient Descent Training
def train_linear_regression(data_x, data_y, lr=0.01, epochs=1000):
curr_m, curr_c = 0.0, 0.0
n = len(data_x)
print(f"\nStep 6: Training (LR={lr}, Epochs={epochs})...")
for epoch in range(epochs):
sum_grad_m = 0
sum_grad_c = 0
for xi, yi in zip(data_x, data_y):
subs = {m: curr_m, c: curr_c, x: xi, y: yi}
sum_grad_m += grad_m_expr.subs(subs)
sum_grad_c += grad_c_expr.subs(subs)
curr_m -= lr * (sum_grad_m / n)
curr_c -= lr * (sum_grad_c / n)
if epoch % 250 == 0:
print(f" Epoch {epoch}: m = {float(curr_m):.4f}, c = {float(curr_c):.4f}")
return float(curr_m), float(curr_c)
This function implements gradient descent:
- We start with random values for m and c (both 0.0)
- For each epoch, we compute the gradients for all data points
- We update m and c in the direction that reduces the cost
- We repeat until convergence
STEP 7: Evaluation and Visualization
def evaluate_and_plot(data_x, data_y, fm, fc):
y_true = np.array(data_y)
y_pred = fm * data_x + fc
# Accuracy Metrics
r2 = 1 - (np.sum((y_true - y_pred)**2) / np.sum((y_true - np.mean(y_true))**2))
mae = np.mean(np.abs(y_true - y_pred))
print("\nStep 7: Accuracy Metrics")
print(f" - R² Score: {r2:.4f}")
print(f" - Mean Absolute Error (MAE): {mae:.4f}")
# Plotting
plt.figure(figsize=(10, 6))
plt.scatter(data_x, data_y, color='red', label='Actual Data')
plt.plot(data_x, y_pred, color='blue', label=f'Model: y={fm:.2f}x + {fc:.2f}')
plt.title('Linear Regression Fit (SymPy + Pandas)')
plt.xlabel('X')
plt.ylabel('Y')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
STEP 8: Computing Total Cost
cost_lambda = sp.lambdify((m, c, x, y), cost_func, 'numpy')
def compute_total_cost(m_val, c_val, x_data, y_data):
# Calculates the average MSE for a grid of m and b values
total_error = 0
for xi, yi in zip(x_data, y_data):
total_error += cost_lambda(m_val, c_val, xi, yi)
return total_error / len(x_data)
STEP 9: Contour Plot Visualization
To truly understand how gradient descent finds the optimal solution, we can visualize the cost function landscape:
def plot_cost_contour(data_x, data_y, fm, fc):
# 1. Create a grid of m and b values around our final solution
m_range = np.linspace(fm - 2, fm + 2, 50)
c_range = np.linspace(fc - 2, fc + 2, 50)
M, C = np.meshgrid(m_range, c_range)
# 2. Compute cost for every point on the grid
Z = np.array([compute_total_cost(mv, cv, data_x, data_y) for mv, cv in zip(np.ravel(M), np.ravel(C))])
Z = Z.reshape(M.shape)
# 3. Plotting
plt.figure(figsize=(8, 6))
cp = plt.contourf(M, C, Z, levels=20, cmap='viridis') # Filled contours
plt.colorbar(cp, label='Cost (MSE)')
# Mark the final optimized point
plt.plot(fm, fc, 'ro', label=f'Minimum (m={fm:.2f}, b={fc:.2f})')
plt.title('Cost Function Contour Landscape')
plt.xlabel('Slope (m)')
plt.ylabel('Intercept (c)')
plt.legend()
plt.show()
final_m, final_c = train_linear_regression(X_train, Y_train)
evaluate_and_plot(X_train, Y_train, final_m, final_c)
plot_cost_contour(X_train, Y_train, final_m, final_c)ape. The red dot marks where gradient descent found the minimum cost — our optimal solution.
Key Insights
1. Transparency Through Symbolic Math
Using SymPy allows us to see the actual mathematical expressions we’re working with, making the algorithm interpretable rather than a black box.
2. Understanding Gradient Descent
By implementing gradient descent from scratch, we understand how the algorithm iteratively finds the optimal parameters by following the slope of the cost function.
3. The Power of Visualization
The contour plot reveals the landscape of the cost function, showing us visually why gradient descent works — it’s literally descending down the slopes to find the valley (minimum cost).
4. From Theory to Practice
This implementation bridges theoretical machine learning concepts with practical coding skills, demonstrating that complex algorithms are built on simple mathematical principles.
Performance Considerations
While our implementation is educational and transparent, production systems typically use optimized libraries like scikit-learn because:
- They’re highly optimized in C/C++
- They handle edge cases and numerical stability
- They include advanced features like regularization
- They’re battle-tested on diverse datasets
However, understanding the fundamentals through implementations like ours makes you a better practitioner who can debug, optimize, and extend these algorithms when needed.
Applications
Linear regression, despite its simplicity, has wide applications:
- Salary Prediction (as demonstrated here)
It’s often the first step in more complex models and remains relevant even in the age of deep learning.
Conclusion
Building machine learning algorithms from scratch using symbolic mathematics provides deep insights that complement the use of high-level libraries. This approach:
- Demystifies how algorithms work
- Strengthens mathematical intuition
- Enables better debugging and optimization
- Builds confidence in applying ML to real-world problems
The complete implementation demonstrates that powerful predictive models can be built from simple mathematical principles, one gradient step at a time.
Tools & Technologies
- Python: Programming language for implementation
- SymPy: For symbolic mathematics and automatic differentiation
- NumPy: For efficient numerical computations
- Pandas: For data manipulation and analysis
- Matplotlib: For comprehensive visualization
Acknowledgments
This work was made possible through the collaboration and guidance of talented colleagues and experienced mentors. Special thanks to our mentors for their invaluable insights and support throughout this learning journey.
Have you implemented machine learning algorithms from scratch? What insights did you gain? Share your experiences in the comments!
About the Authors:
***Harish Gopalakrishnan*** is a Senior Software Engineer at Wipro with expertise in software development and machine learning implementations.
***Primitha Rodrigues*** is an Associate Software Engineer and Data Science Engineer at Optum Technology, specializing in data science and machine learning.
If you found this article helpful, please consider clapping and sharing. Follow us for more deep dives into machine learning fundamentals!
메타데이터
- post_id
- 4ff2742f394f
- slug
- building-linear-regression-from-scratch-a-mathematical-journey-with-sympy-4ff2742f394f
- url
- https://medium.com/@harish_87568/building-linear-regression-from-scratch-a-mathematical-journey-with-sympy-4ff2742f394f
- canonical_url
- https://medium.com/@harish_87568/building-linear-regression-from-scratch-a-mathematical-journey-with-sympy-4ff2742f394f
- author_url
- https://medium.com/@harish_87568
- status
- ok
- fetched_at
- 2026-07-13 19:05:09