CadetX
CX Learn | Complete Machine Learning Guide
Scikit-Learn · Python
CadetX logo CadetX CX Learn
ML-101 · Module 2 Beginner about 30 minutes 12 Lessons Prereq: Module 1, basic pandas helps

Data Preparation & Feature Engineering

A model is only as good as the data you feed it. In this module you'll learn how to take messy, real-world data and turn it into clean numbers a machine learning model can learn from.

  • Level: Beginner
  • Time: about 30 minutes
  • Needs: Module 1, basic pandas helps

By the end of this module you will be able to

  • Explain why data preparation matters so much in machine learning
  • Find and fix duplicates, typos, missing values and outliers
  • Separate your data into features (X) and a target (y)
  • Turn text columns into numbers using label and one-hot encoding
  • Scale features using normalisation and standardisation
  • Split data into training and test sets without "leaking" information

2.1Why data preparation matters

Imagine a chef preparing a meal. Before any cooking starts, they wash the vegetables, throw away anything rotten, and chop everything to the right size. If they skip this, even the best recipe in the world will produce a bad meal.

Machine learning is the same. The algorithm is the recipe, but the data is the ingredients. Real-world data is almost always messy: it has gaps, typos, repeated rows and strange values. Models also only understand numbers, so any text has to be converted first.

Garbage in, garbage out

This is a famous saying in computing. If you train a model on bad data, you will get bad predictions, no matter how clever the algorithm is. Good data preparation is often the single biggest thing you can do to improve a model.

Every data preparation job follows roughly the same steps. This module walks through each one in order.

  1. CleanRemove duplicates, fix typos and impossible values.
  2. Fill gapsDecide what to do with missing values.
  3. Handle outliersFind extreme values and decide if they're real.
  4. Choose X and yPick the features and the target to predict.
  5. EncodeTurn text categories into numbers.
  6. SplitSeparate training data from test data.
  7. ScalePut number columns on a similar range.

2.2Meet our messy dataset

Throughout this module we'll use one small example. A UK broadband company wants to predict which customers will leave (this is called churn). Here is a sample of their data, exactly as it came out of their system.

Customer IDAgeCityContractMonthly bill (£)Months with usLeft?
C00134LeedsMonthly456Yes
C00252London2-year3040No
C003(blank)Newcastle1-year3818No
C00429londonMonthly553Yes
C005250Leeds1-year3524No
C00252London2-year3040No
C00641ManchesterMonthly(blank)9Yes

Can you spot the problems? The red cells are all issues we need to fix before any model can use this data.

ProblemWhereWhy it's a problem
Missing valueC003 age, C006 billMost algorithms crash or give errors when a value is empty
Inconsistent text"london" vs "London"The computer sees these as two different cities
Impossible valueAge 250Nobody is 250 years old; probably a typing error
Duplicate rowC002 appears twiceThe model would count this customer twice and give them too much weight
Text columnsCity, Contract, Left?Models need numbers, not words

2.3Fixing duplicates and inconsistent values

The easiest problems to fix come first.

Duplicates

A duplicate is a row that appears more than once. It often happens when data is copied or joined together from different systems. In pandas, one line removes them:

clean.py
df = df.drop_duplicates()

Inconsistent text

To a computer, "London", "london" and "LONDON " (with a space at the end) are three completely different values. We fix this by making them all follow the same format:

clean.py
df["city"] = df["city"].str.strip().str.title()   # " london" -> "London"

Impossible values

An age of 250, a price of -£40, or a date in the year 2099 can't be right. Use your common sense and knowledge of the business to set rules. Here, any age over 100 is clearly a mistake, so we treat it as missing and fix it in the next step.

Always ask "does this make sense?"

A quick way to spot impossible values is to look at the smallest and largest value in every number column using df.describe(). If the minimum age is -3 or the maximum is 250, something is wrong.

2.4Dealing with missing values

Missing values are one of the most common problems in real data. A customer skipped a question on a form, a sensor stopped working, or a system didn't save a value. You have three main choices.

OptionWhat it meansWhen to use it
Remove the rowDelete any row with a missing valueWhen only a few rows are affected and you have lots of data
Remove the columnDelete the whole columnWhen most of the column is empty (for example, over 60%)
Fill it in (called imputation)Replace the gap with a sensible valueMost of the time, so you don't throw away useful data

What value should you fill in?

For number columns, the usual choices are the mean (average) or the median (the middle value when everything is sorted). For text columns, we use the mode (the most common value).

The median is often the safer choice. Look at what happens to the salaries of six people when one of them is a very high earner:

Six salaries: 24k, 26k, 28k, 30k, 32k and 250k. The mean is 65k, which is higher than five of the six people. The median is 29k. £24k£26k£28k£30k£32k£250k Mean = £65k (pulled up by one person) Median = £29k
Figure 1. One very high salary drags the mean up to £65k, which doesn't describe a "typical" person here at all. The median (£29k) is not affected by extreme values.

In our dataset, the missing age is filled with the median age (37.5) and the missing bill with the median bill (£38).

missing.py
# How many missing values in each column?
print(df.isnull().sum())

# Fill numbers with the median, text with the mode
df["age"] = df["age"].fillna(df["age"].median())
df["city"] = df["city"].fillna(df["city"].mode()[0])

2.5Spotting outliers

An outlier is a value that is far away from all the others. The best way to spot them is with a chart called a box plot.

Box plot of monthly bills. Most bills are between 30 and 55 pounds, with one outlier at 180 pounds. £0£50£100£150£200 Outlier: £180 Most customers
Figure 2. The box shows where the middle half of the values sit, the amber line is the median, and the whiskers show the normal range. Anything far outside the whiskers is an outlier.

When you find an outlier, don't delete it straight away. First ask: is it a mistake, or is it real?

SituationExampleWhat to do
It's a mistakeAge 250, someone typed an extra zeroFix it if you can, or treat it as missing
It's real but rareA business customer with a £180 billKeep it, but consider limiting (capping) extreme values
It's the whole pointA fraudulent £9,000 card paymentDefinitely keep it; in fraud detection, outliers are what you're looking for

2.6Features (X) and target (y)

Now our data is clean, we split the columns into two groups. By tradition, data scientists call the features X (capital, because it's a table of many columns) and the target y (small, because it's a single column).

Customer IDAgeCityContractMonthly bill (£)Months with usLeft?
C00134LeedsMonthly456Yes
C00252London2-year3040No
C00337.5Newcastle1-year3818No
C00429LondonMonthly553Yes
C00537.5Leeds1-year3524No
C00641ManchesterMonthly389Yes

Our cleaned data: blue columns are X, the amber column is y, and green cells were fixed in the steps above.

Drop columns that don't help

The Customer ID is just a name tag. "C004" tells us nothing about whether someone will leave, so we remove it. If we kept it, the model might learn silly patterns, such as "IDs ending in 4 leave more often".

Watch out for data leakage

Data leakage happens when a feature secretly contains the answer. Imagine the company also had a column called "Cancellation date". Only customers who left have one, so the model would get 100% accuracy in testing, then fail completely in real life, because for new customers we don't know that date yet. Only use features you would actually have at the moment you need to make the prediction.

2.7Encoding: turning text into numbers

Machine learning models are maths, and you can't do maths with the word "Leeds". So we need to convert text categories into numbers. This is called encoding. There are two main ways.

Label encoding: give each category a number

This works well when the categories have a natural order, such as sizes or education levels.

T-shirt sizeEncoded
Small1
Medium2
Large3

The order makes sense: Large (3) really is bigger than Small (1). The same idea works for our Left? column: Yes = 1, No = 0.

Don't label-encode categories with no order

If we encoded cities as Leeds = 1, London = 2, Manchester = 3, the model would think Manchester is "three times" Leeds, or that London sits "between" them. That's meaningless and can confuse the model.

One-hot encoding: one yes/no column per category

For categories with no order, like cities, we create a new column for each one. Each row gets a 1 in the column for its city and 0 everywhere else.

Before

One text column

City
Leeds
London
Newcastle
Manchester

After one-hot encoding

Four number columns

LeedsLondonManch.Newc.
1000
0100
0001
0010
Figure 3. One-hot encoding turns one text column into several 0/1 columns. It's called "one-hot" because exactly one column is "switched on" (1) in each row.
Label encodingOne-hot encoding
Use whenCategories have an order (small, medium, large)Categories have no order (cities, colours)
Number of columnsStays as one columnOne new column per category
Watch out forCreating a fake orderToo many columns if there are hundreds of categories
In pandas.map({"S":1, "M":2, "L":3})pd.get_dummies()

2.8Feature scaling

Look at two features for five customers: age (roughly 20 to 60) and yearly income (roughly £20,000 to £75,000). Income numbers are about a thousand times bigger than ages.

Many algorithms compare values by their size. To them, a difference of £1,000 in income looks far more important than a difference of 30 years in age, just because the number is bigger. Scaling fixes this by putting all features on a similar range, so each one gets a fair say.

Try it: scale the features

AgeYearly income

The two most common methods

Normalisation (min-max)Standardisation (z-score)
What it doesSqueezes every value into the range 0 to 1Centres values around 0, measured in "how far from average"
Formula(value − min) ÷ (max − min)(value − mean) ÷ standard deviation
ResultSmallest = 0, largest = 1Average = 0; most values between −2 and +2
Good forData with no big outliers; images; neural networksMost cases; handles outliers better
scikit-learnMinMaxScaler()StandardScaler()

Think of it like exam marks

Scoring 45 out of 50 in maths and 70 out of 100 in English can't be compared directly. Turning both into percentages (90% and 70%) puts them on the same scale. That's exactly what normalisation does.

Do all algorithms need scaling? No. Algorithms that measure distances or use gradients (K-Nearest Neighbours, SVM, logistic regression, neural networks) need it. Tree-based algorithms (decision trees, random forests) don't care. You'll see this in each algorithm's module.

2.9Feature engineering: creating better features

Feature engineering means creating new, more useful columns from the ones you already have. It's where your understanding of the business really pays off, and it often improves a model more than switching algorithms.

Original column(s)New featureWhy it helps
Date of birthAgeA model can't do much with "1998-04-12", but age is meaningful
Date joinedMonths as a customerNew customers often behave differently from loyal ones
Purchase dateDay of the week, monthShopping patterns change at weekends and near Christmas
Total spent, number of ordersAverage order valueSeparates "many small orders" from "a few big orders"
Height, weightBMIOne combined number that captures both
PostcodeRegionThousands of postcodes become a handful of useful groups

2.10Splitting your data the right way

In Module 1 you learned that we keep some data hidden as a test set. There's one important rule about when to split: split first, then scale.

Cleaned data is split into a training set and a test set. The scaler learns its settings from the training set only, then applies the same settings to both sets. Cleaned data Training set (80%) Test set (20%) Scaler learns mean and spread Same settings applied
Figure 4. The scaler "studies" the training set only. The test set is then scaled using those same settings, just like new customers will be in real life.

Why does this matter? If you scale before splitting, the scaler looks at the test data too. Information about the "hidden" test set leaks into training, like a student peeking at the exam paper. Your test score will look better than the model really is.

SettingWhat it doesTypical value
test_sizeHow much data goes into the test set0.2 (20%) or 0.25
random_stateShuffles the same way every time, so your results can be repeatedAny fixed number, e.g. 42
stratify=yKeeps the same mix of answers (e.g. % who left) in both setsUse it for classification problems

2.11The full pipeline in Python

Here is everything from this module in one script, using our broadband customers. Read the comments to follow each step.

prepare_data.py
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. Load the (messy) data
df = pd.DataFrame({
    "customer_id":  ["C001", "C002", "C003", "C004", "C005", "C002", "C006"],
    "age":          [34, 52, np.nan, 29, 250, 52, 41],
    "city":         ["Leeds", "London", "Newcastle", "london", "Leeds", "London", "Manchester"],
    "contract":     ["Monthly", "2-year", "1-year", "Monthly", "1-year", "2-year", "Monthly"],
    "monthly_bill": [45, 30, 38, 55, 35, 30, np.nan],
    "months":       [6, 40, 18, 3, 24, 40, 9],
    "left":         ["Yes", "No", "No", "Yes", "No", "No", "Yes"],
})

# 2. Clean
df = df.drop_duplicates()                                  # remove repeated rows
df["city"] = df["city"].str.title()                        # "london" -> "London"
df.loc[df["age"] > 100, "age"] = np.nan                    # impossible age -> missing
df["age"] = df["age"].fillna(df["age"].median())           # fill with the median
df["monthly_bill"] = df["monthly_bill"].fillna(df["monthly_bill"].median())

# 3. Features (X) and target (y)
X = df.drop(columns=["customer_id", "left"])
y = df["left"].map({"Yes": 1, "No": 0})

# 4. Encode text columns with one-hot encoding
X = pd.get_dummies(X, columns=["city", "contract"], dtype=int)

# 5. Split BEFORE scaling
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=42)

# 6. Scale the number columns (learn from training data only)
num_cols = ["age", "monthly_bill", "months"]
scaler = StandardScaler()
X_train[num_cols] = scaler.fit_transform(X_train[num_cols])
X_test[num_cols] = scaler.transform(X_test[num_cols])

print("Rows after cleaning:", len(df))
print("Training rows:", len(X_train), "| Test rows:", len(X_test))
print("Number of features:", X.shape[1])
print(X_train[num_cols].round(2))
Output
Rows after cleaning: 6
Training rows: 4 | Test rows: 2
Number of features: 10
    age  monthly_bill  months
6  1.07         -0.44   -0.56
2  0.28         -0.44    0.56
4  0.28         -0.82    1.30
3 -1.64          1.71   -1.30

Notice what happened. We started with 7 rows and 7 columns of messy text and numbers. We ended with 6 clean rows and 10 number-only features (3 number columns, plus 4 city columns and 3 contract columns from one-hot encoding). The scaled values sit around 0, just like in the scaling lab. This data is now ready for a model.

A note for later

To keep things simple, we filled the missing values before splitting. In real projects, you should learn fill values (like the median) from the training data only, for the same leakage reason as scaling. scikit-learn's Pipeline does all of this for you automatically, and you'll use it from Module 5 onwards.

SummaryKey takeaways

  • Garbage in, garbage out: clean data matters more than a clever algorithm.
  • Remove duplicates, fix inconsistent text and treat impossible values as errors.
  • Fill missing values with the median (numbers) or mode (text). The median isn't affected by extreme values.
  • Check outliers with a box plot, and ask whether each one is a mistake or real.
  • X is the table of features; y is the target. Remove ID columns and anything that leaks the answer.
  • Use label encoding for ordered categories and one-hot encoding for unordered ones.
  • Scaling gives every feature a fair say. Normalisation maps to 0–1; standardisation centres on 0.
  • Split first, then scale, learning the scaler's settings from the training data only.

Check your understanding

Your machine learning roadmap

  1. 01
    Introduction to Machine LearningWhat ML is and how it works
  2. 02
    Preparing Data for Machine LearningYou are here
  3. 03
    Linear RegressionPredicting numbers
  4. 04
    Logistic RegressionPredicting yes or no
  5. 05
    Evaluating ModelsAccuracy, precision, recall, overfitting, cross-validation
  6. 06
    K-Nearest NeighboursLearning from similar examples
  7. 07
    Decision TreesFlowcharts that learn
  8. 08
    Random Forest and Gradient BoostingMany models working together
  9. 09
    Support Vector MachinesFinding the best boundary
  10. 10
    Naive BayesProbability-based classification
  11. 11
    Clustering with K-MeansFinding groups without labels
  12. 12
    Dimensionality Reduction with PCASimplifying big datasets
  13. 13
    Capstone ProjectBuild and present a full ML project