From-scratch guide / Chapter 6 / Introductory

Logistic Regression from Scratch in Python

Binary logistic regression turns a linear score into a probability with the sigmoid function, then learns the weights by minimizing binary cross-entropy. The implementation below exposes the probability, loss gradient, and thresholded prediction instead of calling a model library.

How it works

p(y=1 | x) = 1 / (1 + exp(-w^T x)); L = -mean[y log(p) + (1-y) log(1-p)]

The linear score w^T x becomes a probability p through the sigmoid. Cross-entropy penalizes confident wrong probabilities, and its gradient supplies the weight update.

  1. 1Add a constant feature if the model needs an intercept.
  2. 2Compute linear scores and map them through a numerically stable sigmoid.
  3. 3Use the mean cross-entropy gradient X^T(p - y) / n.
  4. 4Update the weights, then choose a probability threshold for predictions.

Visual intuition

Logistic regression decision boundary changing as a two-class model is fitted
The animation follows a linear decision boundary during fitting. It illustrates model geometry; it does not by itself establish probability calibration or test-set performance.

NumPy implementation

This example is deterministic and uses NumPy for arrays and arithmetic, not a prebuilt optimizer or model implementation.

import numpy as np

def sigmoid(scores):
    scores = np.clip(scores, -500.0, 500.0)
    return 1.0 / (1.0 + np.exp(-scores))

def fit_logistic_regression(X, y, step_size=0.2, iterations=2000):
    X = np.asarray(X, dtype=float)
    y = np.asarray(y, dtype=float)
    weights = np.zeros(X.shape[1])

    for _ in range(iterations):
        probabilities = sigmoid(X @ weights)
        gradient = X.T @ (probabilities - y) / len(y)
        weights = weights - step_size * gradient

    return weights

def predict_proba(X, weights):
    return sigmoid(np.asarray(X, dtype=float) @ weights)

X = np.array([
    [1.0, -2.0], [1.0, -1.0], [1.0, -0.5],
    [1.0,  0.5], [1.0,  1.0], [1.0,  2.0],
])
y = np.array([0, 0, 0, 1, 1, 1])
weights = fit_logistic_regression(X, y)
predictions = (predict_proba(X, weights) >= 0.5).astype(int)

The first column of X is the intercept feature; the second contains the one-dimensional measurements.

Clipping scores keeps exp from overflowing while leaving ordinary score values unchanged.

The training loop is the full vectorized gradient method, and the threshold is applied only after probabilities are computed.

What can go wrong

Scale affects optimization

Features on very different scales can make one global step size inefficient or unstable.

The threshold is a decision choice

Changing 0.5 changes predicted labels; it does not retrain the probability model or fix class imbalance.

Probability code needs numerical care

Extreme scores can overflow exp, and direct log-loss calculations must clip probabilities or use a stable equivalent.

Continue with the book and source

This guide is grounded in Chapter 6, Two-Class Classification, and the official Machine Learning Refined notebooks. Use the chapter for context, then open the exact sources below for the full treatment and exercises.