Handle Missing Values with fillna and dropna

Master fillna() and dropna() to clean missing values in pandas—learn when to fill vs drop with hands-on examples and troubleshooting tips.

Focus: handle missing values with fillna and dropna

Sponsored

You've spent hours cleaning data, only to discover that a handful of NaN values just threw off your entire analysis. Maybe your averages are skewed, your plots have gaps, or your machine learning model refuses to run. Missing data is one of the most common — and most annoying — issues in real-world datasets. But with pandas, you have two powerful tools to handle missing values: fillna() and dropna(). By the end of this lesson, you'll know exactly when to fill gaps with sensible values and when to drop rows altogether, turning messy data into a clean, analysis-ready DataFrame.

The problem this lesson solves

Real-world data is rarely perfect. Surveys skip questions, sensors fail, and log files omit fields. In pandas, missing data is represented as NaN (Not a Number) or None. If you ignore them, you'll get:

  • Wrong summary statisticsmean() and sum() silently skip NaN, giving you a biased result.
  • Broken visualizations — gaps or jagged lines in your charts.
  • Errors in downstream code — many functions, especially in scikit-learn, simply refuse to operate on data that contains NaN.

So the problem is: how do you make missing values disappear in a way that doesn't distort your analysis or crash your pipeline? This lesson gives you the two core strategies: fill the gap with a substitute value, or drop the incomplete row or column.

Core concept / mental model

Think of your dataset as a spreadsheet with a few empty cells. You have two choices: you can write something in the blank cell (filling it) or cut out the entire row that contains the blank (dropping it). Both are valid, but they answer different questions:

  • Fill — "I believe the missing value is close to what I already have, so I'll estimate it."
  • Drop — "I don't trust this row enough, so I'll remove it entirely."

Pandas gives you two methods that map directly to these choices:

  • DataFrame.fillna(value, method, ...) — replaces every NaN with a specified value, a computed statistic like the mean, or a forward/backward fill.
  • DataFrame.dropna(axis, how, thresh, ...) — removes rows (or columns) that contain missing values.

The key is knowing when to use which. If you've got 1000 rows and only 5 missing values, dropping is trivial. But if 30% of your rows are missing a column, dropping would throw away too much valuable data — you need to fill.

How it works step by step

1. Detect missing values first

Before you fix anything, you need to find out where the gaps are. Use isna() or its alias isnull() to get a boolean mask, and then summarize with sum() to see the count per column.

2. Decide on a strategy

Ask yourself: Is the missing data random, or is there a pattern? If it's random and sparse, dropping might be fine. If it's systematic (e.g., older entries lack a new column), filling is better.

3. Apply dropna() or fillna()

For dropna():

  • Set axis=0 (default) to drop rows, axis=1 to drop columns.
  • Use how='any' (default) to drop if any value is missing, how='all' to drop only if all values are missing.
  • Use thresh=n to keep rows with at least n non-missing values.

For fillna():

  • Pass a constant: df.fillna(0).
  • Pass a per-column dict: df.fillna({'age': 30, 'income': 0}).
  • Use a statistic: df.fillna(df.mean()).
  • Use forward fill: df.fillna(method='ffill') to carry the last known value forward, or method='bfill' to use the next value.
  • Use inplace=True to modify the DataFrame directly (though chaining is safer).

4. Verify your result

After filling or dropping, run df.isna().sum().sum() to ensure no missing values remain. If you're filling, check that your new values don't distort your data (e.g., mean before vs after).

Hands-on walkthrough

Let's start with a small DataFrame that simulates a common scenario: sales data with missing values in different columns.

import pandas as pd
import numpy as np

# Create a DataFrame with missing values
df = pd.DataFrame({
    'product': ['A', 'B', 'C', 'D', 'E'],
    'price': [100, np.nan, 150, 200, np.nan],
    'quantity': [5, 10, np.nan, 8, 12],
    'region': ['North', 'South', np.nan, 'East', 'West']
})

print(df)
print("\nMissing count per column:")
print(df.isna().sum())

Output:

  product  price  quantity region
0       A  100.0       5.0  North
1       B    NaN      10.0  South
2       C  150.0       NaN    NaN
3       D  200.0       8.0  East
4       E    NaN      12.0  West

Missing count per column:
price      2
quantity   1
region     1
dtype: int64

Now, let's apply different strategies:

Option 1: Drop rows with any missing value

# Drop any row that has at least one NaN
df_drop = df.dropna()
print(df_drop)

Output:

  product  price  quantity region
0       A  100.0       5.0  North
3       D  200.0       8.0  East

We lost 3 out of 5 rows — not great if this were a real dataset.

Option 2: Fill with column means

# Fill numeric columns with their mean, and categorical with a placeholder
df_filled = df.copy()
df_filled['price'].fillna(df['price'].mean(), inplace=True)
df_filled['quantity'].fillna(df['quantity'].mean(), inplace=True)
df_filled['region'].fillna('Unknown', inplace=True)

print(df_filled)

Output:

  product       price  quantity  region
0       A  100.000000       5.0  North
1       B  150.000000      10.0  South
2       C  150.000000       8.75  Unknown
3       D  200.000000       8.0  East
4       E  150.000000      12.0  West

Now we kept all rows, but the filled values are estimates (mean price is 150, mean quantity is 8.75).

Option 3: Forward fill for time-series-like data

# Add a date column and sort by it

df_t = pd.DataFrame({
    'date': pd.date_range('2025-01-01', periods=5, freq='D'),
    'temperature': [22, np.nan, 24, np.nan, 26]
})

df_t['temperature_ffill'] = df_t['temperature'].fillna(method='ffill')
print(df_t)

Output:

        date  temperature  temperature_ffill
0 2025-01-01         22.0               22.0
1 2025-01-02          NaN               22.0
2 2025-01-03         24.0               24.0
3 2025-01-04          NaN               24.0
4 2025-01-05         26.0               26.0

Pro tip: Use method='ffill' for time series where you want to carry the last known value forward. For the first row with a missing value, it stays NaN — you may need a separate fillna() after.

Compare options / when to choose what

Strategy When to use Pros Cons
dropna() (rows) Missing values are rare (<5%) and random Simple, keeps original data integrity Loses data, can introduce bias
dropna(axis=1) Entire column is mostly empty (e.g., >40% missing) Removes uninformative features Can drop important if misjudged
fillna() (constant) Categorical column, e.g., 'Unknown' Simple, preserves rows Might skew distribution
fillna() (mean/median) Numeric, roughly normal distribution Keeps data, lowers variance distortion Outliers can push mean; use median for skewed data
fillna(method='ffill') Time series or ordered data Captures trend Can propagate bias if values drift

Variations to consider:

  • fillna(method='bfill') — for when the next value is a better predictor (e.g., in reverse-ordered data).
  • df.interpolate() — linear interpolation for time series, a middle ground between ffill and a mean.
  • Use limit in fillna(method='ffill', limit=2) to cap how many consecutive NaN get filled, preventing over-propagation.

Troubleshooting & edge cases

1. inplace not working as expected

  • Problem: df.fillna(0, inplace=True) seems to do nothing.
  • Cause: The column might be of object dtype, or you're using a chain (like df[df['col']>0].fillna()), which works on a copy.
  • Fix: Use df = df.fillna(0) instead of inplace to avoid chained assignment issues.

2. Mean fill on integer columns turns them into float

  • Cause: Mean of integers is a float; pandas promotes the column to float64.
  • Fix: Round after filling: df['col'] = df['col'].fillna(df['col'].mean()).round(), or use median and cast back to int.

3. Forward fill leaves leading NaNs

  • Cause: ffill has no previous value to use.
  • Fix: Chain fillna(method='ffill').fillna(method='bfill') or fill with a constant first.

4. Dropping based on a specific column only

  • Problem: You want to drop rows only if a certain column is NaN.
  • Solution: Use df.dropna(subset=['price']) — this only checks that column.
# Drop rows where 'price' is NaN, regardless of other columns
df_filtered = df.dropna(subset=['price'])
print(df_filtered)

Output:

  product  price  quantity region
0       A  100.0       5.0  North
2       C  150.0       NaN    NaN
3       D  200.0       8.0  East

What you learned & what's next

You now know how to handle missing values with fillna and dropna — the two pillars of data cleaning in pandas. You can detect missing data with isna(), choose a strategy based on how much data is missing, and apply fillna() with constants, statistics, or forward/backward fills, or use dropna() to remove incomplete rows and columns. You also know how to handle common pitfalls like leading NaNs and dtype changes.

You're now ready to tackle more advanced cleaning scenarios. In the next lesson, you'll learn how to handle duplicates — another essential step to ensure your data is truly analysis-ready. You'll use drop_duplicates() to identify and remove duplicate rows, and you'll know how to combine it with your missing-value skills to clean even the messiest datasets.

Next up: Detecting and removing duplicate records to finish your data cleaning toolkit.

Practice recap

Try a quick exercise: load a CSV file of your choice (or use the sales data above), identify missing values, and apply both dropna() and fillna() with different strategies (mean, forward fill, constant). Compare the resulting shapes and summary statistics. You'll see how the choice of strategy affects your analysis — that's the key skill to master before moving to duplicate handling.

Common mistakes

  • Using inplace=True with a chained operation (like df[df['col']>0].fillna()) which silently fills a copy, not the original DataFrame.
  • Filling every column with the same constant (e.g., 0) even when 0 is a meaningful value, thereby masking the fact that data is missing.
  • Dropping rows without checking how much data is lost — if more than 5-10% is missing, consider filling instead.
  • Forgetting that ffill leaves leading NaN values unfilled, causing errors downstream if you assume all gaps are gone.

Variations

  1. Use df.interpolate() for numeric columns to estimate missing values linearly, which often beats a simple mean for time series.
  2. Use fillna(method='bfill') when the next value is the best predictor (e.g., in descending time series).
  3. Combine dropna(thresh=n) to keep rows that have at least n non-null values, striking a balance between dropping and filling.

Real-world use cases

  • Cleaning sensor data from IoT devices, where occasional NaN readings are filled using forward fill to maintain time-series continuity.
  • Preprocessing a survey dataset where missing demographic fields are filled with the mode or 'Unknown' to preserve sample size for regression analysis.
  • Preparing a DataFrame for a machine learning pipeline by dropping rows with missing target values and filling feature gaps with the column median.

Key takeaways

  • Always detect missing values with df.isna().sum() before deciding to fill or drop.
  • Use dropna() when missing values are rare and random; otherwise, filling is safer.
  • fillna() accepts constants, dicts, per-column statistics, and forward/backward fill methods.
  • For time series, prefer method='ffill' or interpolate() over the mean to respect data order.
  • Set thresh=n in dropna() to keep rows with enough valid data instead of dropping entire rows.
  • Always verify your dataset has zero NaNs after cleaning with df.isna().sum().sum().

Sponsored

Sponsored