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.
- CleanRemove duplicates, fix typos and impossible values.
- Fill gapsDecide what to do with missing values.
- Handle outliersFind extreme values and decide if they're real.
- Choose X and yPick the features and the target to predict.
- EncodeTurn text categories into numbers.
- SplitSeparate training data from test data.
- 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 ID | Age | City | Contract | Monthly bill (£) | Months with us | Left? |
|---|---|---|---|---|---|---|
| C001 | 34 | Leeds | Monthly | 45 | 6 | Yes |
| C002 | 52 | London | 2-year | 30 | 40 | No |
| C003 | (blank) | Newcastle | 1-year | 38 | 18 | No |
| C004 | 29 | london | Monthly | 55 | 3 | Yes |
| C005 | 250 | Leeds | 1-year | 35 | 24 | No |
| C002 | 52 | London | 2-year | 30 | 40 | No |
| C006 | 41 | Manchester | Monthly | (blank) | 9 | Yes |
Can you spot the problems? The red cells are all issues we need to fix before any model can use this data.
| Problem | Where | Why it's a problem |
|---|---|---|
| Missing value | C003 age, C006 bill | Most algorithms crash or give errors when a value is empty |
| Inconsistent text | "london" vs "London" | The computer sees these as two different cities |
| Impossible value | Age 250 | Nobody is 250 years old; probably a typing error |
| Duplicate row | C002 appears twice | The model would count this customer twice and give them too much weight |
| Text columns | City, 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:
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:
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.
| Option | What it means | When to use it |
|---|---|---|
| Remove the row | Delete any row with a missing value | When only a few rows are affected and you have lots of data |
| Remove the column | Delete the whole column | When most of the column is empty (for example, over 60%) |
| Fill it in (called imputation) | Replace the gap with a sensible value | Most 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:
In our dataset, the missing age is filled with the median age (37.5) and the missing bill with the median bill (£38).
# 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.
When you find an outlier, don't delete it straight away. First ask: is it a mistake, or is it real?
| Situation | Example | What to do |
|---|---|---|
| It's a mistake | Age 250, someone typed an extra zero | Fix it if you can, or treat it as missing |
| It's real but rare | A business customer with a £180 bill | Keep it, but consider limiting (capping) extreme values |
| It's the whole point | A fraudulent £9,000 card payment | Definitely 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 ID | Age | City | Contract | Monthly bill (£) | Months with us | Left? |
|---|---|---|---|---|---|---|
| C001 | 34 | Leeds | Monthly | 45 | 6 | Yes |
| C002 | 52 | London | 2-year | 30 | 40 | No |
| C003 | 37.5 | Newcastle | 1-year | 38 | 18 | No |
| C004 | 29 | London | Monthly | 55 | 3 | Yes |
| C005 | 37.5 | Leeds | 1-year | 35 | 24 | No |
| C006 | 41 | Manchester | Monthly | 38 | 9 | Yes |
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 size | Encoded |
|---|---|
| Small | 1 |
| Medium | 2 |
| Large | 3 |
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
| Leeds | London | Manch. | Newc. |
|---|---|---|---|
| 1 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 |
| 0 | 0 | 0 | 1 |
| 0 | 0 | 1 | 0 |
| Label encoding | One-hot encoding | |
|---|---|---|
| Use when | Categories have an order (small, medium, large) | Categories have no order (cities, colours) |
| Number of columns | Stays as one column | One new column per category |
| Watch out for | Creating a fake order | Too 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
The two most common methods
| Normalisation (min-max) | Standardisation (z-score) | |
|---|---|---|
| What it does | Squeezes every value into the range 0 to 1 | Centres values around 0, measured in "how far from average" |
| Formula | (value − min) ÷ (max − min) | (value − mean) ÷ standard deviation |
| Result | Smallest = 0, largest = 1 | Average = 0; most values between −2 and +2 |
| Good for | Data with no big outliers; images; neural networks | Most cases; handles outliers better |
| scikit-learn | MinMaxScaler() | 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 feature | Why it helps |
|---|---|---|
| Date of birth | Age | A model can't do much with "1998-04-12", but age is meaningful |
| Date joined | Months as a customer | New customers often behave differently from loyal ones |
| Purchase date | Day of the week, month | Shopping patterns change at weekends and near Christmas |
| Total spent, number of orders | Average order value | Separates "many small orders" from "a few big orders" |
| Height, weight | BMI | One combined number that captures both |
| Postcode | Region | Thousands 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.
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.
| Setting | What it does | Typical value |
|---|---|---|
test_size | How much data goes into the test set | 0.2 (20%) or 0.25 |
random_state | Shuffles the same way every time, so your results can be repeated | Any fixed number, e.g. 42 |
stratify=y | Keeps the same mix of answers (e.g. % who left) in both sets | Use 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.
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))
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.30Notice 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
- 01Introduction to Machine LearningWhat ML is and how it works
- 02Preparing Data for Machine LearningYou are here
- 03Linear RegressionPredicting numbers
- 04Logistic RegressionPredicting yes or no
- 05Evaluating ModelsAccuracy, precision, recall, overfitting, cross-validation
- 06K-Nearest NeighboursLearning from similar examples
- 07Decision TreesFlowcharts that learn
- 08Random Forest and Gradient BoostingMany models working together
- 09Support Vector MachinesFinding the best boundary
- 10Naive BayesProbability-based classification
- 11Clustering with K-MeansFinding groups without labels
- 12Dimensionality Reduction with PCASimplifying big datasets
- 13Capstone ProjectBuild and present a full ML project
Next module
Linear Regression