Logistic Regression — The Threshold That Missed 3% Cancers
3 out of 100 malignant patients were sent home - the default 0.5 threshold failed.
20+ years shipping production ML systems and the infrastructure behind them. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Logistic Regression predicts a probability between 0 and 1 using the sigmoid function
- The linear part computes log-odds, which are exponentiated and squeezed via sigmoid
- Coefficients are log-odds ratios — interpretable for regulated industries
- Feature scaling is mandatory — unscaled data makes gradient descent crawl
- Decision threshold is a business decision, not a fixed 0.5
- Accuracy is a trap — always check precision, recall, and confusion matrix
Imagine a doctor looking at your test results and saying 'there's a 92% chance this is benign.' They're not predicting a number like your height — they're predicting a probability that tips into a yes-or-no answer. Logistic Regression is exactly that: it takes a bunch of measurements, runs them through a special S-shaped curve, and squeezes the result into a probability between 0 and 1. Once that probability crosses a threshold (usually 0.5), the model commits to an answer. It's less like a ruler and more like a confident doctor making a call.
Every day, your email provider quietly decides whether to drop a message into your inbox or your spam folder. Your bank flags a transaction as fraud or lets it through. A hospital algorithm predicts whether a tumour is malignant or benign. All of these are binary decisions — yes or no, 0 or 1 — and Logistic Regression is one of the most reliable, interpretable, and battle-tested tools for making them. It's been doing this job since the 1950s and it's still the first model data scientists reach for when the stakes are high and the explanation matters.
The core problem Logistic Regression solves is one that Linear Regression cannot: predicting a bounded probability. If you used ordinary linear regression to classify emails, nothing stops it from predicting a 'spam probability' of 2.7 or -0.4 — which is meaningless. Logistic Regression wraps its output in a sigmoid function that mathematically constrains every prediction to live between 0 and 1, giving you an actual probability you can act on.
By the end of this article you'll understand not just how to call LogisticRegression().fit() in scikit-learn, but why the sigmoid function exists, what the coefficients are actually telling you about the real world, how to tune the decision threshold for different business goals, and exactly what questions an interviewer will ask you to separate the practitioners from the people who just skimmed a tutorial.
Why Logistic Regression Is a Linear Classifier, Not a Regression
Logistic regression predicts the probability that an input belongs to a binary class by passing a linear combination of features through the logistic (sigmoid) function. The core mechanic: compute z = w·x + b, then output σ(z) = 1 / (1 + e⁻ᶻ), which squashes any real number into a (0,1) probability. Despite the name, it's a classification algorithm — the 'regression' refers to fitting a linear decision boundary, not predicting a continuous value.
Training maximizes log-likelihood via gradient descent, not least squares. The loss function is cross-entropy: -[y log(ŷ) + (1-y) log(1-ŷ)]. This penalizes confident wrong predictions heavily — a 0.97 probability on a false positive costs far more than 0.51. The decision threshold is a separate hyperparameter; default 0.5 is rarely optimal. In production, you tune this threshold using precision-recall or ROC curves, not accuracy alone.
Use logistic regression when you need interpretable probabilities, fast inference, or a strong baseline. It's the go-to for binary classification on linearly separable or near-separable data — spam detection, churn prediction, medical diagnosis. It scales to millions of features with L1/L2 regularization and trains in minutes on a single machine. For non-linear boundaries, add feature crosses or kernel tricks, but know that deep nets will outperform once data exceeds ~100k examples with complex interactions.
The Sigmoid Function — Why Logistic Regression Uses This Specific Curve
Linear Regression gives you a straight line. That's great for predicting house prices, but terrible for predicting probabilities — because a straight line extends to infinity in both directions and probability must stay between 0 and 1.
The sigmoid function (also called the logistic function, which is where the algorithm gets its name) is the mathematical fix. Its formula is σ(z) = 1 / (1 + e^(-z)). Feed it any real number — whether it's -1000 or +1000 — and it maps the output to the range (0, 1). Large positive inputs push the output close to 1. Large negative inputs push it close to 0. Right at zero, you get exactly 0.5.
The input z is itself a linear combination of your features: z = w₀ + w₁x₁ + w₂x₂ + ... — exactly like Linear Regression. So Logistic Regression is really just Linear Regression with its output passed through the sigmoid. That single design decision makes the output interpretable as a probability, which is the foundation everything else builds on.
The model learns the weights (w values) by maximising the likelihood that the predicted probabilities match the actual labels in your training data — a process called Maximum Likelihood Estimation, optimised via gradient descent.
Training on Real Data — Breast Cancer Classification End-to-End
Theory only sticks when you see it on real data. We'll use scikit-learn's built-in Breast Cancer dataset — 569 tumour samples, each described by 30 numeric features (mean radius, texture, smoothness, etc.), labelled as malignant (0) or benign (1). The goal is to predict the label from the measurements.
There are a few things to get right here that tutorials often skip. First, feature scaling matters enormously for Logistic Regression because gradient descent converges far faster when all features live on a similar scale. If 'mean area' is in the thousands and 'mean fractal dimension' is near 0.05, the loss surface is elongated and training is sluggish. StandardScaler fixes this.
Second, you should always look at your model's coefficients after training. Each coefficient tells you how much the log-odds of the positive class change for a one-unit increase in that feature. A large positive coefficient means that feature is a strong predictor of benign; a large negative one means it predicts malignant. That interpretability is exactly why doctors, banks and regulators often prefer Logistic Regression over a black-box neural network — you can explain every decision.
Third, accuracy alone is a dangerous metric for medical data. A model that predicts 'benign' for every sample gets ~63% accuracy on this dataset without learning anything. Always check precision, recall and the confusion matrix.
scaler.fit_transform() on your entire dataset before splitting leaks test-set statistics into training — a subtle form of data leakage that inflates your reported accuracy. Always fit the scaler on X_train, then use .transform() (not .fit_transform()) on X_test.Tuning the Decision Threshold — When 0.5 Is the Wrong Cut-Off
Most tutorials treat the 0.5 threshold as sacred. It isn't. The threshold is a business decision, not a mathematical constant, and understanding when to move it separates good practitioners from great ones.
Consider the breast cancer case: a False Negative (predicting benign when the tumour is actually malignant) sends a patient home without treatment. A False Positive (flagging benign as malignant) means an unnecessary biopsy — uncomfortable, but survivable. These mistakes are not equal. You should tolerate more False Positives to drive False Negatives toward zero, which means lowering your threshold below 0.5 so the model cries 'malignant' sooner.
Conversely, in a spam filter, a False Positive (blocking a legitimate email) is worse than a False Negative (letting spam through). Here you'd raise the threshold.
The ROC curve plots True Positive Rate against False Positive Rate across every possible threshold. The area under it (AUC-ROC) tells you how well the model separates classes regardless of threshold — it's the metric to optimise during model selection. The Precision-Recall curve is more informative when your classes are heavily imbalanced.
The code below shows how to find the threshold that maximises recall for malignant detection — exactly the kind of analysis you'd run before deploying a medical model.
Maximum Likelihood Estimation and Log-Loss — How Logistic Regression Learns
You've seen the sigmoid and the coefficients. But how does the model actually find those coefficients? The answer is Maximum Likelihood Estimation (MLE). Logistic Regression doesn't minimise squared error (like Linear Regression does) — it maximises the probability of seeing the observed data given the parameters.
Mathematically, MLE finds the weights w that maximise the product of predicted probabilities for each training sample. For a binary classification task, this product is:
L(w) = ∏ P(y=1 | x)^y · (1 - P(y=1 | x))^{(1-y)}
Taking the logarithm turns the product into a sum, which is easier to optimise. The negative of that sum is called log-loss (binary cross-entropy). The model uses gradient descent to minimise log-loss. This is why Logistic Regression uses log-loss instead of MSE: log-loss is convex with respect to the weights, which guarantees that gradient descent will find the global optimum.
Convexity matters because it means you're never stuck in a local minimum. With MSE and sigmoid, the loss surface has hills and valleys — gradient descent can get trapped. Log-loss is a smooth bowl shape. That's the mathematical guarantee you need for a stable training process.
In scikit-learn, you don't see this — it's wrapped inside the fit() method. But understanding the loss function is crucial for debugging: if your loss is not decreasing smoothly, check the learning rate (not exposed in sklearn's default LogisticRegression, but you control it via tol and max_iter) or consider a different solver.
- Convex functions have one global minimum — no local minima to trap you.
- MSE applied to a sigmoid produces a non-convex landscape — that's why linear regression + rounding fails.
- Scikit-learn's default solver (lbfgs) assumes convexity and may converge faster than other solvers.
- If your loss curve is jagged or increasing, you might have a bug in feature scaling or a too-high learning rate (not exposed in sklearn's default API).
Regularisation — L1 (Lasso) and L2 (Ridge) in Logistic Regression
Logistic Regression without regularisation can overfit, especially when you have many features or highly correlated predictors. Regularisation adds a penalty term to the loss function that discourages large coefficients. Scikit-learn's LogisticRegression uses L2 regularisation by default (controlled by the C parameter).
L2 (Ridge) adds the squared sum of coefficients to the loss. It shrinks all coefficients toward zero but rarely makes them exactly zero. Use L2 when you expect all features to contribute some signal, or when features are correlated (it handles multicollinearity gracefully).
L1 (Lasso) adds the absolute sum of coefficients. It can drive some coefficients to exactly zero, performing automatic feature selection. Use L1 when you have many irrelevant features and want a sparse model. The trade-off: L1 can be unstable with highly correlated features — it might pick one and drop the other arbitrarily.
ElasticNet combines L1 and L2 penalties. In scikit-learn, you can use LogisticRegression with penalty='elasticnet' and set the l1_ratio parameter. This gives you the best of both worlds: sparsity from L1 and stability from L2.
The C parameter controls the inverse of regularisation strength. Lower C = more regularisation (simpler model). Tune C via cross-validation — too high C leads to overfitting, too low C underfits. This is the most important hyperparameter to tune for Logistic Regression.
Feature Importance — Why Coefficients Tell You More Than Accuracy
You trained a logistic regression. The confusion matrix looks good. Now what? If you deploy without understanding which features actually drive the decision, you're flying blind. Logistic regression gives you something most black-box models don't: interpretable coefficients.
The sign tells you direction. Positive coefficient means higher feature values push probability toward class 1. The magnitude tells you impact — but only after scaling. If you've got age in years and income in dollars, raw coefficients are incomparable. Standardise your features first, or use odds ratios.
Odds ratio = exp(coef). An odds ratio of 2.0 means a one-unit increase in that feature doubles the odds of the positive class. This is how you explain to a product manager why "hours worked per week" matters more than "education level" for predicting income >$50K. They don't care about log-odds. They care about actionable levers.
Production teams waste weeks tuning hyperparameters when the real insight is in the coefficients. Read them. Read them before you touch the decision threshold.
Multicollinearity — The Silent Killer of Coefficient Stability
Logistic regression assumes your features are independent. In the real world, they're not. Hours worked per week and income? Correlated. Education years and job type? Correlated. When two features carry similar information, the model distributes coefficient weight between them unpredictably.
This isn't just a stats textbook problem. I've seen a production model flip coefficient signs across retraining runs because age and years_of_experience had a correlation of 0.89. One week age was positive, the next it was negative. The model's accuracy stayed the same, but every stakeholder lost trust.
Diagnose it with Variance Inflation Factor (VIF). VIF > 5 means that feature is heavily explained by other features. VIF > 10 means you're in trouble. Drop one of the correlated features, combine them into a ratio, or use L1 regularisation (Lasso) which can zero out one of them.
Don't trust feature importance from a logistic regression with multicollinearity. Trust the VIF scores first.
Model Building in Scikit-learn — Why Defaults Won't Save You
You don't need a PhD to fit a logistic regression in scikit-learn. You need to know which knobs to turn and why the defaults will stab you in production.
First, the class_weight parameter. Default is 'None', which assumes your classes are balanced. Real-world fraud or churn datasets? You'll have 99% negative, 1% positive. Without class_weight='balanced', your model learns to predict everything negative and hits 99% accuracy while catching zero fraud. Senior engineers catch this before the pipeline breaks.
Second, solver choice. 'lbfgs' is the modern default — fast, handles L2 regularization, converges reliably on small-to-medium data. For high-dimensional sparse data (think NLP with 50k features), switch to 'saga' — it supports L1 penalty and multiclass multinomial. Never use 'liblinear' unless your data is tiny; it's a noob trap.
Third, C is the inverse of regularization strength. C=1.0 is default, but you should cross-validate between 0.01 and 100. Why? Because your feature scales matter. If one feature is purchase_amount (range $1 to $10k) and another is click_count (0 to 50), the default regularization penalizes the smaller feature unfairly. Scale your data with StandardScaler before fitting, or watch your coefficients lie to you.
Disadvantages of Logistic Regression — The 3 Hard Walls You'll Hit
Logistic regression is your starting gun, not your finishing line. It fails hard in three common production scenarios, and pretending otherwise costs you.
First, linear decision boundary. Logistic regression draws a straight line (or hyperplane) through feature space. If your data has XOR patterns — think credit risk where being both high-income AND high-debt is dangerous but either alone is safe — you need polynomial features, decision trees, or neural nets. You can engineer interactions manually, but that's guessing, not learning.
Second, multicollinearity kills coefficient interpretability. When two features are highly correlated (e.g., income and credit score), the model can't tell which one matters. Coefficients explode in opposite directions, making feature importance analysis useless. Senior engineers run variance inflation factor (VIF) checks before trusting coefficients.
Third, logistic regression can't learn complex feature interactions natively. If the signal lives in combinations of three or more features (e.g., age income geography), you need manual feature crossing or a model that builds hierarchies. XGBoost or a shallow neural net will crush LR on these problems. Know when to walk away from the tried-and-true.
Bottom line: LR is a fast, interpretable baseline. If you need non-linear boundaries, interaction learning, or robustness to collinearity, swap it out before your stakeholders ask why the model is stupid.
Ordinal Logistic Regression — When Your Target Has a Natural Order
Standard logistic regression expects a binary outcome. Ordinal logistic regression extends this to categorical targets with an inherent ranking — like education level (high school, bachelor, master) or survey responses (poor, fair, good, excellent). The model assumes proportional odds: the effect of a feature is constant across all thresholds between categories. For example, the coefficient for 'years of experience' shifts the log-odds of moving from any lower category to any higher one by the same amount. This assumption must be verified via a Brant test or likelihood-ratio comparison against a model that relaxes it (e.g., multinomial). Fit using mord or statsmodels.miscmodels.ordinal_model. Output is a set of intercepts (thresholds) plus one shared coefficient vector. Predict class probabilities across all levels. Violating the proportional odds assumption biases coefficients and misranks predictions. Always test it — most practitioners miss this and get misleading feature importances.
Multinomial Logistic Regression — Why Softmax Replaces Sigmoid for Multi-Class
When you have more than two unordered classes — like classifying iris species — binary logistic regression fails. Multinomial logistic regression uses the softmax function to estimate probabilities across K categories: each outcome gets its own coefficient vector, and softmax normalizes so probabilities sum to 1. The model is trained using maximum likelihood with cross-entropy loss. Scikit-learn's LogisticRegression(multi_class='multinomial', solver='lbfgs') handles this directly. The reference category matters: coefficients are interpreted as log-odds relative to the baseline class (typically the first). Regularization still applies — use L2 to avoid overfitting with many categories. A critical drawback: the number of parameters grows linearly with K, requiring more data. For high-dimensional problems (e.g., text classification with 1000 classes), consider alternatives like naive Bayes or hierarchical softmax. Never use multinomial when classes are ordinal — you'd waste information about natural ordering.
The Cancer Model That Missed a Malignant Tumour Because of a Bad Threshold
predict_proba() to get raw probabilities, then tuned the threshold so that recall for malignant cases was above 99.5%. The new threshold of 0.18 meant the model flagged more borderline cases — but the false negative rate dropped to near zero. Precision fell from 98% to 91%, but no malignant tumour was missed.- Never deploy a binary classifier without explicitly setting the decision threshold based on the business cost matrix.
- Accuracy is dangerous when classes are imbalanced or costs are asymmetric — always compute confusion matrix and per-class recall.
- AUC-ROC tells you the model's ranking quality, not the optimal threshold — that's a separate business decision.
from sklearn.preprocessing import StandardScaler; X_scaled = scaler.fit_transform(X)model = LogisticRegression(max_iter=1000, solver='lbfgs'); model.fit(X_scaled, y)| File | Command / Code | Purpose |
|---|---|---|
| sigmoid_intuition.py | def sigmoid(z): | The Sigmoid Function |
| breast_cancer_logistic.py | from sklearn.datasets import load_breast_cancer | Training on Real Data |
| threshold_tuning.py | from sklearn.datasets import load_breast_cancer | Tuning the Decision Threshold |
| log_loss_demo.py | from sklearn.linear_model import LogisticRegression | Maximum Likelihood Estimation and Log-Loss |
| regularisation_compare.py | from sklearn.linear_model import LogisticRegression | Regularisation |
| FeatureImportance.py | from sklearn.linear_model import LogisticRegression | Feature Importance |
| MulticollinearityCheck.py | from statsmodels.stats.outliers_influence import variance_inflation_factor | Multicollinearity |
| build_logistic_model.py | from sklearn.linear_model import LogisticRegression | Model Building in Scikit-learn |
| vif_check.py | from statsmodels.stats.outliers_influence import variance_inflation_factor | Disadvantages of Logistic Regression |
| OrdinalLogisticExample.py | from statsmodels.miscmodels.ordinal_model import OrderedModel | Ordinal Logistic Regression |
| MultinomialExample.py | from sklearn.datasets import load_iris | Multinomial Logistic Regression |
Key takeaways
Interview Questions on This Topic
Why does Logistic Regression use log-loss (binary cross-entropy) instead of mean squared error as its loss function?
Frequently Asked Questions
20+ years shipping production ML systems and the infrastructure behind them. Written from production experience, not tutorials.
That's Algorithms. Mark it forged?
10 min read · try the examples if you haven't