Python & Data Science
Machine Learning Under review

Feature Scaling: Why Your Model Might Be Ignoring Half Your Data

Fresh from the Naive Bayes spam-filter task, Sam returns to the main HomeMatch sellability model. Since the sellability classifier and the price model draw on the same raw listing features, Sam prototypes the scaling fix on price data first, where the effect is easiest to see. Right away he notices the issue: square footage sits in the thousands, while bedroom count barely reaches single digits. One feature will drown out the other.

The Tale of Two Rulers: Why Units Can Trick Your Model

Imagine you’re building a machine learning model to predict house prices. You have two main inputs: the size in square feet and the number of bedrooms.

In your dataset, a typical house might have 2,500 square feet and 3 bedrooms. Think like a computer for a second. It doesn’t know what a bedroom is, and it has no feel for a spacious living room. It only sees the numbers: 2,500 and 3.

So what’s happening? To the math inside your model, a 1% change in square footage (25 feet) looks more significant than adding a whole extra bedroom. Because 25 is bigger than 1, the model treats square footage as the only thing that matters. It’s like measuring a marathon with a ruler marked in miles for one leg and inches for the other. If one feature has a bigger “voice” just because its units are larger, is the model actually learning the truth?

Here’s what those raw numbers look like in Python.

import pandas as pd
import matplotlib.pyplot as plt

# Creating a dummy house dataset
data = {
    'Square_Feet': [1200, 2500, 1800, 4200, 3100],
    'Bedrooms': [2, 3, 2, 5, 4],
    'Price': [250000, 500000, 350000, 850000, 620000]
}

df = pd.DataFrame(data)

print("Raw Data Ranges:")
print(df[['Square_Feet', 'Bedrooms']].describe().loc[['min', 'max']])

# Visualizing the massive gap in scale
df[['Square_Feet', 'Bedrooms']].plot(kind='box')
plt.title("The Scale Gap: Square Feet vs Bedrooms")
plt.show()
  • import pandas as pd and import matplotlib.pyplot as plt bring in the two workhorse libraries — pandas for tabular data manipulation and matplotlib for basic plotting.
  • data = {...} creates a Python dictionary with three keys: Square_Feet (ranging 1,200–4,200), Bedrooms (ranging 2–5), and Price (ranging $250k–$850k) — the deliberate gap between the first two columns’ ranges is the whole point of the demonstration.
  • df = pd.DataFrame(data) converts the dictionary into a pandas DataFrame — a table where each key becomes a column.
  • df[['Square_Feet', 'Bedrooms']].describe() generates summary statistics (count, mean, std, min, quartiles, max) for just those two columns — .loc[['min', 'max']] slices the output to show only the minimum and maximum rows, making the range gap obvious.
  • df[['Square_Feet', 'Bedrooms']].plot(kind='box') creates a box plot for the two columns side by side — because square feet ranges in the thousands and bedrooms in single digits, the bedroom box will appear as a flat line at the bottom of the plot, visually confirming the scale mismatch.

When you run this, the Square_Feet column stretches into the thousands while Bedrooms sits flat at the bottom. Square feet range across 3,000 units. Bedrooms vary by 3. This is the hardest part for beginners to spot — your model isn’t biased because the data is bad. It’s biased because the rulers are different.

Distance-Based Models: Where Scaling is Life or Death

Think of it this way: if you walk 1 mile North and 1 inch East, where are you? You’re basically just 1 mile North. The inch barely registered.

Algorithms like K-Nearest Neighbors (KNN) or Support Vector Machines (SVM) calculate the “distance” between data points on a map. If one axis (like square feet) is stretched to 4,000 and the other (bedrooms) only goes to 5, the map becomes incredibly tall and skinny.

So when KNN looks for the “nearest” house to yours, it will only care about square footage. A house with 5 bedrooms and 2,000 square feet will look “closer” to a 1-bedroom house with 2,000 square feet than to another 5-bedroom house with 2,500 square feet. The model is effectively ignoring bedrooms entirely.

from sklearn.neighbors import KNeighborsRegressor

# Let's try to predict price using unscaled data
X = df[['Square_Feet', 'Bedrooms']]
y = df['Price']

knn = KNeighborsRegressor(n_neighbors=2)
knn.fit(X, y)

# Predict for a 2000 sqft house with 2 bedrooms
test_house = [[2000, 2]]
prediction = knn.predict(test_house)
print(f"Predicted Price: ${prediction[0]:,.2f}")
  • from sklearn.neighbors import KNeighborsRegressor imports the regression variant of KNN — unlike the classifier (which predicts a class label), the regressor predicts a continuous value by averaging the target values of the k nearest neighbors.
  • X = df[['Square_Feet', 'Bedrooms']] extracts the two feature columns into a DataFrame X — these are the unscaled values, so square footage (1,200–4,200) will dominate the distance calculation.
  • y = df['Price'] sets the target variable — the house prices Sam’s model is trying to predict.
  • KNeighborsRegressor(n_neighbors=2) creates a model that finds the 2 closest training samples (by Euclidean distance) and averages their prices — with n_neighbors=2, the prediction is the mean of the two nearest houses’ prices.
  • knn.fit(X, y) stores the training data — KNN is a “lazy learner” that doesn’t really learn anything at fit time; it just memorizes the data points and their labels for later distance computations.
  • test_house = [[2000, 2]] is a new data point: 2,000 square feet, 2 bedrooms — the double brackets create a 2D array (1 sample × 2 features) as required by scikit-learn’s API.
  • knn.predict(test_house) computes the Euclidean distance from this test point to all 5 training points — because square footage is in the thousands, the 500-square-foot differences between houses will dwarf any bedroom differences, so the “nearest” neighbors are determined almost entirely by square footage.
  • prediction[0]:,.2f formats the result with a comma thousands separator and two decimal places — the prediction will be the average price of the 2 training houses closest in square footage, regardless of bedrooms.

In this unscaled model, the prediction is almost entirely dictated by whichever houses in the training set had similar square footage, regardless of how many bedrooms they had.

Gradient Descent: Helping the Model Find the Bottom Faster

So what if you aren’t using distance-based models? If you’re using Linear Regression or a Neural Network, scaling still matters—but for a different reason: speed.

Most models learn using an optimizer called Gradient Descent. Think of it like hiking down a mountain in the dark. You feel the slope with your feet and take a step in the steepest direction. With unscaled data, that “mountain” of error looks like a long, narrow, steep-walled valley. The model bounces off the side walls, zig-zagging back and forth in a slow path to the bottom.

Scaling turns that skinny valley into a round bowl. No matter where the model starts, it can head straight for the center. The model trains faster and is less likely to get stuck somewhere strange.

Standardization vs. Normalization: Which Tool to Grab?

Now that we know we need to scale, how do we do it? Two main tools handle this: Standardization and Normalization.

  1. Standardization (StandardScaler): Centers your data around an average of zero. It measures how many “standard deviations” a value sits from the mean. This is usually the better choice if your data has outliers (extreme values).
  2. Normalization (MinMaxScaler): Squeezes every value into a tight range between 0 and 1.

Here’s the thing. One massive outlier—say, a mansion with 50,000 square feet—forces Normalization to compress ordinary houses into a narrow band: with the mansion setting the new maximum, the five original houses land between 0.00 and 0.06 instead of spreading across their old 0 to 1 range. Standardization handles this better.

Standardization (Z-score) and Normalization (Min-Max)

Standardization transforms each feature so it has mean 0 and standard deviation 1:

z=xμσz = \frac{x - \mu}{\sigma}

Normalization (Min-Max scaling) rescales each feature to the range [0,1][0, 1]:

xnorm=xxminxmaxxminx_{\text{norm}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}}

Plain EnglishStatistical symbolPython equivalent
Raw feature valuexxdf['Square_Feet']
Mean of the featureμ\mudf['Square_Feet'].mean()
Standard deviationσ\sigmadf['Square_Feet'].std()
Standardized value (z-score)z=xμσz = \frac{x - \mu}{\sigma}StandardScaler().fit_transform(...)
Minimum of the featurexminx_{\min}df['Square_Feet'].min()
Maximum of the featurexmaxx_{\max}df['Square_Feet'].max()
Normalized value (min-max)xnorm=xxminxmaxxminx_{\text{norm}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}}MinMaxScaler().fit_transform(...)

We can test this against the data using Scikit-Learn:

from sklearn.preprocessing import StandardScaler, MinMaxScaler

# Standardization
scaler_std = StandardScaler()
df_std = scaler_std.fit_transform(df[['Square_Feet', 'Bedrooms']])

# Normalization
scaler_minmax = MinMaxScaler()
df_minmax = scaler_minmax.fit_transform(df[['Square_Feet', 'Bedrooms']])

print("Standardized (First 2 rows):\n", df_std[:2])
print("Normalized (First 2 rows):\n", df_minmax[:2])

# Re-run the earlier KNN prediction, this time on standardized data
knn_scaled = KNeighborsRegressor(n_neighbors=2)
knn_scaled.fit(df_std, y)
test_house_scaled = scaler_std.transform(test_house)
print(f"Scaled Prediction: ${knn_scaled.predict(test_house_scaled)[0]:,.2f}")
  • from sklearn.preprocessing import StandardScaler, MinMaxScaler imports the two most common scaling utilities from scikit-learn — both follow the same fit/transform API pattern.
  • scaler_std = StandardScaler() creates a z-score scaler — it will compute the mean and standard deviation of each column during fit, then subtract the mean and divide by the std during transform.
  • scaler_std.fit_transform(df[['Square_Feet', 'Bedrooms']]) does both steps at once: fit learns the mean and std of each column from the data, then transform applies the z-score formula to every value — the result df_std is a NumPy array (not a DataFrame) where both columns are centered around 0 with unit standard deviation.
  • scaler_minmax = MinMaxScaler() creates a min-max scaler — it learns the minimum and maximum of each column, then rescales values to the [0,1][0, 1] range.
  • scaler_minmax.fit_transform(...) applies the same fit-then-transform pattern — the result df_minmax will have every value between 0 and 1 for both columns, regardless of their original scales.
  • df_std[:2] and df_minmax[:2] print the first 2 rows of each scaled array — the standardized values run about -1.31 and -1.03 for the first row (centered around 0), while the normalized first row is exactly 0.0 on both columns, since that house holds the dataset minimum for both features.
  • The key takeaway from comparing the two outputs: both methods put Square_Feet and Bedrooms on comparable scales, but standardization preserves the shape of the distribution (just recentered) while normalization compresses everything into a fixed range.
  • knn_scaled.fit(df_std, y) retrains the same KNeighborsRegressor from the unscaled example, this time on the standardized columns — scaler_std.transform(test_house) scales the query point the same way before asking for a prediction, so the comparison is apples-to-apples.

Check the numbers. The standardized values sit close to zero — the printed rows run about -1.31 and -1.03 for the smallest house. The normalized values land in the closed range 0 to 1, with the smallest house at exactly 0.0 on both columns and the largest at exactly 1.0. Both methods work. Square feet and bedrooms are on the same scale now. And the earlier KNN prediction actually moves: $425,000 unscaled versus $300,000 scaled, because the second-nearest neighbor flips from a 3-bedroom house to a 2-bedroom one once bedroom count is finally on equal footing with square footage.

The Rule of Thumb: When You Can Safely Skip Scaling

So do we always have to do this? Not quite.

Tree-based models (like Decision Trees, Random Forests, or XGBoost) are the exception. Trees ask simple questions: “Is the square footage greater than 2,000?” They don’t care about the distance between 2,000 and 3,000; they just care about the split point. Because of this, trees are “scale-invariant.”

from sklearn.tree import DecisionTreeRegressor

# Tree on raw data
tree_raw = DecisionTreeRegressor().fit(X, y)
# Tree on scaled data
tree_scaled = DecisionTreeRegressor().fit(df_std, y)

# Scale the same query point before asking the scaled tree to predict
test_house_std = scaler_std.transform(test_house)

print(f"Raw Tree Prediction: {tree_raw.predict(test_house)[0]}")
print(f"Scaled Tree Prediction: {tree_scaled.predict(test_house_std)[0]}")
  • from sklearn.tree import DecisionTreeRegressor imports the regression variant of a decision tree — it learns a set of if-then splitting rules on the features to predict a continuous target.
  • tree_raw = DecisionTreeRegressor().fit(X, y) trains a tree on the unscaled data (X is the raw Square_Feet and Bedrooms) — the tree will find split points like “Square_Feet ≤ 2500” which work identically regardless of scale.
  • tree_scaled = DecisionTreeRegressor().fit(df_std, y) trains a separate tree on the standardized data — the split points will be different numbers (like “feature_0 ≤ -0.12” instead of “Square_Feet ≤ 2500”), but the tree structure and predictions will be equivalent.
  • tree_raw.predict(test_house) predicts the price for the same 2,000 sqft, 2-bedroom query point used earlier — the prediction is the average Price value of all training samples that end up in the same leaf node.
  • The key point: the two trees (tree_raw and tree_scaled) will produce the same predictions (given equivalent data) because decision trees only care about the order of values (for finding optimal splits), not their absolute magnitudes — scaling changes the numbers but not the ordering, so the split decisions are identical.
  • test_house_std = scaler_std.transform(test_house) scales the query point the same way the training data was scaled, so tree_scaled.predict(test_house_std) compares fairly against tree_raw.predict(test_house) — both print the identical value, which is the scale-invariance claim made visible instead of just asserted.

Which models need scaling — and which can Sam safely skip?

Not every model cares about feature scales. Here’s the breakdown for Sam’s HomeMatch toolkit:

Model typeScaling needed?Why
KNN (Part 8)✅ Yes — criticalDistance-based; large-range features dominate the Euclidean distance calculation
SVM (Part 7)✅ Yes — criticalMargin-based; same distance problem as KNN
Linear / Logistic Regression✅ Yes — for speedGradient descent; unscaled features create elongated cost contours, slowing convergence
Neural Networks✅ Yes — for speedSame gradient descent rationale
Naive Bayes (Part 9)⚠️ DependsGaussianNB assumes features are normally distributed; scaling helps but the bigger issue is distributional fit
Decision Trees (Part 5)❌ NoSplit-based; trees only care about threshold values, not distances
Random Forests (Part 6)❌ NoEnsemble of trees; inherits scale-invariance
XGBoost / LightGBM (Part 4)❌ NoGradient-boosted trees; split-based, scale-invariant

Sam’s rule of thumb: If the model uses distance (KNN, SVM) or gradient descent (linear models, neural nets), scale your features — it’s not optional. If it’s tree-based, you can safely skip this step and save the preprocessing time. When in doubt, scaling rarely hurts and often helps, so when Sam is unsure which model he’ll ultimately deploy, he scales first and asks questions later.

If you’re using a Random Forest, you can usually skip this whole process and save some time. But if you’re ever in doubt, scaling rarely hurts, and it often helps.

Wrapping Up: Your Scaling Checklist

Quick cheat sheet for your next project:

  • Why scale? To give every feature an equal “vote” and help your model learn faster.
  • Use it for: KNN, SVM, Linear/Logistic Regression, and Neural Networks.
  • Skip it for: Decision Trees and Random Forests.
  • Which one? StandardScaler if you have outliers; MinMaxScaler if you need a specific range, like 0 to 1.
  • Key point: Always fit your scaler on training data only, then transform the test data. No peeking at the future.

So scaling is handled. Sam’s model treats all features fairly now, but sellability accuracy is still plateauing. The data itself needs more work. Next, Sam tackles Feature Engineering for tabular data, including how to handle categorical data. Models hate words even more than they hate unscaled numbers.

Check Your Understanding

Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.

Remember What is the difference between Standardization and Normalization, and which one is more robust to outliers?

Understand In your own words, explain why unscaled data makes Gradient Descent take a “zig-zag path,” using the article’s skinny-valley-versus-round-bowl analogy.

Apply Using the article’s “Rule of Thumb” section, would you need to scale features before training an XGBoost model? Justify your answer using the article’s explanation of why tree-based models are “scale-invariant.”

Analyze The article says a house with 5 bedrooms and 2,000 square feet would look “closer” (under unscaled KNN) to a 1-bedroom house with 2,000 square feet than to another 5-bedroom house with 2,500 square feet. Walk through the Euclidean distance calculation conceptually to explain why the 500-square-foot gap dominates the 4-bedroom gap in an unscaled distance metric.

Evaluate The article warns that Normalization will “compress ordinary houses into a narrow band” — squeezed to roughly 0.00–0.06 — if there’s one mansion with 50,000 square feet. Critique relying on Standardization as the fix in this scenario: does StandardScaler fully solve the outlier problem, or does it just make the damage less visually dramatic while the outlier still pulls the mean and standard deviation?

Create Design a scaling decision for a new dataset: a mix of age (18-90), annual_income (20,000-2,000,000, with a few very high earners), and number_of_children (0-6), being fed into a Support Vector Machine. Which scaler would you choose for this dataset and why, given the income column’s likely outliers?


References & Further reading


Apply What You Learned is for Supporter and Insider subscribers.

Subscribe to unlock the exercises on this post.

See plans

Looking for something else?

Search every article by title, summary or topic.