Missing Values: fillna & dropna
Handle missing values in pandas with fillna and dropna. Practical Python for data science steps, troubleshooting, and next-lesson connection.
Focus: fillna and dropna
You've finally cleaned your dataset, merged a few CSV files, and then you see it: NaN scattered like confetti across your DataFrame. Worse, your regression model just threw a cryptic error, or your group-by summary silently dropped half your rows. Missing values aren't just an inconvenience — they're a silent killer of data integrity. In this lesson, you'll master pandas' two most essential tools for dealing with the void: fillna() for replacing missing values and dropna() for removing them. You'll learn not just how to use them, but when to use each, based on your data and the question you're trying to answer.
The problem this lesson solves
Real-world data is messy. Sensors fail, survey respondents skip questions, and joins between tables produce gaps. If you ignore this, you'll face a cascade of issues:
- Broken computations: Many pandas functions, like
mean()orsum(), returnNaNif any input is missing, or silently exclude those rows, skewing your results. - Errors in Machine Learning: Most scikit-learn estimators refuse to work with
NaNvalues, throwingValueError: Input contains NaN. - Skewed visualizations: Plotting a column with gaps leaves holes in your lines or bars, hiding true trends.
Before pandas, data scientists spent hours writing custom loops to check and handle missing data. The fillna() and dropna() methods abstract away that pain, giving you a clean, expressive, and vectorized way to deal with missingness. Understanding them is non-negotiable for any data scientist working with pandas.
Core concept / mental model
Think of your DataFrame as a spreadsheet where some cells are empty. dropna() is the equivalent of deleting entire rows (or columns) that contain at least one empty cell. fillna() is like filling those empty cells with a post-it note: a default value, the average of the column, or the value from the previous row.
A simple mental model:
dropna()— Out of sight, out of mind. Use when you can afford to lose data and you're confident the missing values are random and not informative.fillna()— Patch it up. Use when you need to keep the row's other data, and you have a sensible substitute for the missing value.
Both methods return a new DataFrame by default, leaving your original data untouched. This is a core pandas principle — you almost never mutate data in place unless you explicitly use inplace=True (which is often discouraged). The how, thresh, and subset parameters give you fine-grained control, and method lets you forward-fill or backward-fill, which is crucial for time-series data.
How it works step by step
Both methods work on the entire DataFrame, or a specific column. Here's the typical flow:
-
Detect missing values — Use
df.isnull()ordf.isna()(they're aliases) to create a boolean mask. Summarize withdf.isnull().sum()to see the count per column. -
Decide on a strategy — Determine whether to drop rows, drop columns, or fill values. This depends on (a) the percentage of missing data, (b) the importance of the column, and (c) whether the missingness is random.
-
Apply
dropna()orfillna()— Choose the right parameters. Fordropna(), decide if you want to drop any row with at least oneNaN(how='any') or only rows where all values are missing (how='all'). Forfillna(), choose a scalar, a method, or a column-specific dictionary. -
Verify the result — Check
df.isnull().sum()again to ensure the data is clean as expected.
Let's see this in action.
Hands-on walkthrough
First, create a sample DataFrame with missing values. In a real scenario, you'd load it from a CSV, but this mirrors the structure.
import pandas as pd
import numpy as np
# Sample dataset with missing values
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [25, np.nan, 30, 29, np.nan],
'salary': [50000, 60000, np.nan, 55000, 52000],
'city': ['NY', 'LA', 'NY', np.nan, 'LA']
})
print(df)
print("\nMissing values per column:\n", df.isnull().sum())
Expected output:
name age salary city
0 Alice 25.0 50000.0 NY
1 Bob NaN 60000.0 LA
2 Charlie 30.0 NaN NY
3 David 29.0 55000.0 NaN
4 Eve NaN 52000.0 LA
Missing values per column:
name 0
age 2
salary 1
city 1
Dropping rows with dropna()
The simplest use: drop every row with any missing value.
# Drop rows where any value is missing (default)
df_drop_any = df.dropna()
print(df_drop_any)
Output:
name age salary city
0 Alice 25.0 50000.0 NY
Only Alice's row survives, because all others contain at least one NaN. That's aggressive — you lost 80% of your data! Usually you'd only do this when missingness is rare.
You can be more targeted with the subset parameter, focusing on specific columns that are critical.
# Drop rows only if 'age' is missing
df_drop_age = df.dropna(subset=['age'])
print(df_drop_age)
Output:
name age salary city
0 Alice 25.0 50000.0 NY
2 Charlie 30.0 NaN NY
3 David 29.0 55000.0 NaN
Now only Bob and Eve (who lack age) are removed, preserving records that might still be useful despite other gaps.
Filling missing values with fillna()
Now let's patch instead of delete. Use a scalar to fill all missing values with 0, or fill each column with its mean.
# Fill all missing values with 0
df_fill_zero = df.fillna(0)
print(df_fill_zero)
# Fill numeric columns with their column mean
df_fill_mean = df.copy()
df_fill_mean['age'] = df_fill_mean['age'].fillna(df_fill_mean['age'].mean())
df_fill_mean['salary'] = df_fill_mean['salary'].fillna(df_fill_mean['salary'].mean())
print(df_fill_mean)
Output (for the mean version):
name age salary city
0 Alice 25.000000 50000.0 NY
1 Bob 28.000000 60000.0 LA
2 Charlie 30.000000 53500.0 NY
3 David 29.000000 55000.0 NaN
4 Eve 28.000000 52000.0 LA
Note that the mean of age is (25 + 30 + 29) / 3 = 28.0, and the mean of salary is (50000 + 60000 + 55000 + 52000) / 4 = 53500.0. Using the column mean is a common imputation trick, but it reduces variance — be aware of that.
For categorical columns, a more honest approach is to fill with a placeholder like 'Unknown' or the mode (most frequent value).
# Fill categorical with mode and numeric columns via dictionary
df_fill_mode = df.copy()
df_fill_mode['city'] = df_fill_mode['city'].fillna(df_fill_mode['city'].mode()[0])
df_fill_mode['age'] = df_fill_mode['age'].fillna(df_fill_mode['age'].median())
print(df_fill_mode)
The missing city becomes 'LA' (the mode), and missing ages become the median (28.0 as well, same result here). Notice how fillna accepts a dictionary that maps column names to values — that's your scalpel for per-column strategies.
Forward-fill for time series
The method parameter is a lifesaver for time-series data (e.g., stock prices, sensor logs). ffill propagates the last valid observation forward, and bfill does the opposite.
# Time series with a missing 'temperature'
time_df = pd.DataFrame({
'time': pd.date_range('2024-01-01', periods=5, freq='h'),
'temp': [71, np.nan, 73, 74, np.nan]
})
# Forward-fill
ffilled = time_df['temp'].fillna(method='ffill')
print(ffilled)
Output:
0 71.0
1 71.0
2 73.0
3 74.0
4 74.0
The missing values inherit the last known temperature, which mimics real-world sensor behavior. Pro tip: method='ffill' is equivalent to method='pad', and it's often used with limit=1 to avoid filling too many gaps in a row.
Blockquote: Pro tip Always inspect
df.isnull().sum()before and after handling missing values. A quick sanity check prevents silent data loss and ensures your imputation worked as expected.
Compare options / when to choose what
| Approach | When to use | Pros | Cons |
|---|---|---|---|
dropna() |
Missing values are few, random, and you can afford to lose rows | Simple, no distortion | Loses data; can bias results if missingness is not random |
fillna(scalar) |
You want a quick placeholder; column is categorical or numeric placeholder is meaningful | Fast, keeps rows | Introduces artificial values; may distort distribution |
fillna(mean/median/mode) |
Numeric columns with moderate missingness; you want a central tendency | Keeps variance lower; widely accepted | Reduces variance; can understate uncertainty |
fillna(method='ffill') |
Time-series data where values change slowly | Preserves temporal continuity | Can create artificial trends if used too aggressively |
dropna(axis='columns') |
A column is mostly empty (e.g., >90%) and is not critical | Removes useless data | Discards potentially valuable info if the column matters |
Key decision guide: If missing values are truly random and you have plenty of data, dropna() is safe. If data is scarce, or you want to keep every observation, use fillna(). For time-series, forward-fill is your best friend — but don't fill more than a few time steps in a row.
Troubleshooting & edge cases
- Error:
ValueError: Fill value must be a scalar, dict or DataFrame— You triedfillnawith a list. Wrap it in a dictionary:df.fillna({'col': [1,2,3]})is invalid; use a scalar or per-column scalars. - Warning:
The 'method' keyword in fillna is deprecated (use .ffill())— In recent pandas versions,method='ffill'is being phased out. Use the explicit.ffill()and.bfill()methods instead. - Data type changes after
fillna(0)— Filling integers with 0 usually preserves dtype, but filling with a float (like the mean) converts the column to float. That's expected — be ready for it. dropna()silently deletes rows — After a drop, your DataFrame index is renumbered (unless you setignore_index=True). Old row numbers are gone, so don't refer to them later.- Filling a column with its own mean using
inplace=True— You might accidentally mutate the original during imputation, making it hard to trace steps. Prefer returning copies.
What you learned & what's next
You've now got two battle-tested methods in your pandas toolkit. You can inspect missing data with isnull(), remove rows or columns with dropna(), and impute gaps with fillna() using scalars, statistics, or forward-fill. You also know when to drop vs. fill, and the operational pitfalls to avoid. This is a standalone skill, but it plugs directly into your next lesson where you'll learn data transformation and feature engineering — turning raw columns into model-ready inputs. With clean data, you can confidently move on to reshaping, encoding categories, and scaling numeric features, all of which assume your data is free of missing values.
Practice recap
Grab a dataset with missing values — the built-in titanic via seaborn is perfect. Compute the missing count per column, then decide for each column whether to drop the rows, fill with a median, or forward-fill. Verify your final DataFrame has zero NaNs, and save the cleaned version to a new CSV. This mirrors the exact workflow you'll use in real projects.
Common mistakes
- Using
dropna()without specifyingsubsetdrops rows with any missing value, potentially losing too much data. Usesubsetto target critical columns instead of nuking everything. - Calling
fillna(df.mean())on a DataFrame that contains non-numeric columns (like strings) raises aTypeError. Always select numeric columns first or use a per-column dictionary. - Forgetting that
dropna()andfillna()return new objects by default. If you don't reassign to a variable or useinplace=True, your original DataFrame stays dirty — and then you wonder why your model still fails. - Using
method='ffill'without alimiton a long series creates a forward-filled chain that can artificially invent a trend. Always considerlimit=1or a small value for day-to-day data.
Variations
- Impute with a model-based approach:
sklearn.impute.SimpleImputerorIterativeImputercan handle more sophisticated strategies like 'most_frequent' or multivariate regression, but they're outside the pandas quick-and-dirty scope. - For time series, pandas also has
.interpolate()which performs linear or spline interpolation between missing points — an alternative to simple forward-fill that preserves trends. - Consider flagging missingness instead of removing it: create an extra column
is_missing(df['age_missing'] = df['age'].isnull()) and keep the NaN in the original — some models can use that signal.
Real-world use cases
- Cleaning customer data where survey responses have missing age and income; you fill income with the column median and drop rows missing the primary key (customer ID).
- Preparing sensor log data from an IoT device; gaps of a few minutes are forward-filled, but longer gaps are dropped to avoid fabricating readings.
- Building a e-commerce sales report; missing 'discount' amounts are filled with 0 (meaning no discount), while rows with missing 'order_id' are dropped as invalid.
Key takeaways
df.isnull()ordf.isna()creates a boolean mask that reveals exactly where missing values live — always check counts before and after.dropna()removes rows (or columns) with missing data; usehow='all'for rows that are completely empty andsubsetto limit the columns you evaluate.fillna()replaces missing values with a scalar, a statistic like the mean/median, a dictionary per column, or a forward-fill method — pick the strategy that matches your data's nature.- Both methods are non-destructive by default; always assign the result or use
inplace=Truedeliberately to avoid silent data confusion. - For time-series data,
.ffill()and.bfill()are more expressive thanfillna(method=...)and support alimitparameter to cap the fill distance. - Choose drop vs. fill based on the proportion of missing data and the importance of the affected column — Dropping is for clean, complete rows; filling keeps your dataset's size but alters its distribution.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.