NaN and Missing Data

The value that is not equal to itself, and the family of functions that exist because of it.

Overview

Not equal to itself

NaN is a floating-point value defined by the IEEE 754 standard. Its defining property is that every comparison involving NaN returns False, including equality with itself. That property is not a NumPy design choice. It is a rule built into the hardware.

The consequence is that the usual ways of checking for a value do not work. a == np.nan is False even when a is NaN. The in keyword does not find NaN in a list reliably. The only correct way to test for NaN is np.isnan, which was designed to handle the special case. pandas wraps this in pd.isna, which also handles None and pd.NA.

NaN propagates through arithmetic. Any operation that touches a NaN produces NaN. The sum of a thousand numbers plus one NaN is NaN. The mean of a clean dataset with one corrupted entry is NaN. That propagation is a safety feature. It ensures that missing data does not silently disappear from the result. But it also means that a single NaN in a large array can make the entire result useless.

NumPy provides nan-aware aggregation functions for exactly this reason. np.nansum ignores NaN values and sums the rest. np.nanmean computes the mean of the non-NaN values. np.nanstd, np.nanmin, np.nanmax do the same for their respective statistics. The regular versions propagate NaN. The nan-aware versions skip it. Choosing between them depends on whether you want missing data to be loud or quiet.

NaN is a float. Integer arrays cannot hold NaN. If you need missing values in an integer array, you must either use a float dtype and accept the type change, or use a masked array, or use pandas nullable integer types. That limitation is the reason why a pandas integer column with one missing value turns into float64.

This module covers NaN arithmetic, detection with np.isnan, the nan-aware aggregation functions, and the practical consequences for integer and boolean arrays. It also covers np.inf, which behaves differently from NaN in comparisons and arithmetic.

np.nan is a floating-point value defined by the IEEE 754 standard to represent "not a number". Its defining property is that each compare involving it's False, including equality with itself.

nan == nan is False. nan < 1 is False. nan > 1 is False. The only compare that returns True is !=. Only because it's the negation of a False.

The direct hands-on result: arr == np.nan gives an all-False mask, no matter how many NaNs the array holds. It fails silently. This Is the worst way to fail.

np.isnan(arr) is the correct test, and it returns a proper boolean mask.

The rule isn't a NumPy quirk. It falls out of NaN meaning "the result of an undefined step". Two undefined results have no reason to be the same thing.

Worth knowing

np.nan != np.nan. Every comparison with NaN is False, so arr == np.nan never matches — use np.isnan.
NaN is a float. An integer array cannot hold one, so a single missing value promotes the whole column to float.
One NaN makes sum, mean and max return NaN. The nan* family ignores them instead.
nanmean divides by the count of real values, not the array length.
np.isnan(a).any(axis=1) finds rows with missing data; ~ keeps the clean ones.
inf is separate from NaN. np.isfinite is the check that catches both.

NaN and Missing Data

The value that isn't equal to itself. The functions that exist because of it.

NaN is a float, and it is not equal to itself

Every comparison with NaN is False, including nan == nan.

example_01.pyNumPy
Output

NaN needs a float dtype

There is no integer NaN, so introducing one promotes the whole array.

example_02.pyNumPy
Output

One NaN poisons a whole reduction

Silently, and the result still looks like a number.

example_03.pyNumPy
Output

Finding and counting

isnan gives a mask, and everything you already know about masks applies.

example_04.pyNumPy
Output

Filling: drop, constant, or interpolate

Three strategies, and the choice matters more than the code.

example_05.pyNumPy
Output

inf is a separate thing

Division by zero gives infinity and a warning, not an exception.

example_06.pyNumPy
Output

NaN is a float

There's no int NaN. The int types have no bit pattern reserved for it.

So assigning np.nan into an int array raises. Any array literal holding np.nan comes out as float.

This is why a table column with one missing value silently turns float. Why identifiers end up displayed as 1001.0 instead of 1001. There's no way around it within a plain NumPy int array. The options are to accept float, to use a sentinel value like -1 and doc it, or to carry a split boolean mask alongside the data.

Pandas handles this with nullable int types. Plain NumPy doesn't have them.

One NaN poisons everything

a.sum() on an array holding a single NaN returns NaN. So does mean, max, min, std and every other reduction.

This is correct — the sum truly is unknown — but it's dangerous. The result is still a number-shaped thing that flows onward through the pipeline and turns all downstream into NaN too. By the time you notice, the origin can be far away.

The nan* family exists for this: nansum, nanmean, nanmax, nanmin, nanstd, nanmedian, nanpercentile. Each skips missing values rather than passing them.

Note what nanmean does: it cuts by the count of real values. On four elements with one NaN, it cuts by three. That is often right. It's worth being clear that it's a choice rather than a detail. You're asserting that the missing value is missing at random.

nansum of an all-NaN array returns 0, while nanmean of one returns NaN with a warning. Those are other and defensible choices. Both are worth knowing before you lean on either.

Finding them

np.isnan(a) gives a mask, and all from the masking module applies.

np.isnan(a).sum() counts them. With axis, it counts per row or per column. This Is the first thing to look at when data arrives.

np.isnan(a).any(axis=1) flags rows holding any missing value. ~ inverts it to keep the full ones. That is listwise deletion in one line.

np.where(np.isnan(a)) gives the coords. This Is what you want when you need to know *where* rather than *how many*.

Filling

Three broad plans, and the choice is a statistical choice rather than a coding one.

Drop. a[~np.isnan(a)] for a vector, or the row-wise version above for a table. Simple and unbiased if the data really is missing at random. Throws away maybe a lot if it isn't.

Constant. np.nan_to_num(a) replaces with zero. np.where(np.isnan(a), value, a) with anything else. Filling with zero shifts the mean. Filling with the mean preserves the mean but shrinks the variance. This Quietly misleads anything downstream that cares about spread.

Interpolate. np.interp fills from neighbouring values. This Is the right choice for sorted data like a time series where adjacency is real.

None of these is neutral. each one of them puts numbers into your data that weren't measured. The honest thing is to record which you used.

inf is different

Cutting by zero doesn't raise in NumPy. It makes inf, -inf or nan, and emits a RuntimeWarning.

1/0 gives inf. 0/0 gives nan. They come from other failures and are worth distinguishing.

np.isnan doesn't catch inf. np.isinf doesn't catch NaN. np.isfinite catches both. Is often the check you really want when checking data.

np.errstate is the context manager for suppressing the warnings when the action is intentional. np.nan_to_num replaces NaN and both infinities in one call, with split args for each.

Three ways to represent missing, and their costs

NaN. Works only for floats. Passes on its own through math. This Is both the safety feature and the hazard. Supported by the whole nan* family and understood by each library.

A sentinel value. -1 for a count, -999 for a reading. Works for ints, costs nothing. Is fully a rule. Nothing stops the sentinel being treated as data by code that doesn't know about it. each such bug is silent and makes plausible numbers.

A split boolean mask. clear, works for any dtype. Never gets mistaken for data. Costs a byte per element and needs each step to be written mask-aware. This Is the reason it isn't more common.

np.ma packages the third option, and pandas' nullable dtypes package a version of it with better ergonomics.

For plain NumPy, NaN is the default answer for floats. A documented sentinel is the pragmatic answer for ints when converting to float isn't acceptable. The key part is documenting it. A sentinel that isn't written down anywhere is a bug waiting for a new maintainer.

Comparing arrays that contain NaN

np.array_equal(a, b) returns False if either holds NaN. NaN never equals anything. Including the NaN in the same spot of the other array.

That makes it useless for checking that a math copied a result. This Is just when you want it.

np.array_equal(a, b, equal_nan=True) treats NaNs in matching spots as equal.

np.allclose(a, b, equal_nan=True) does the same with a handling. Is what belongs in a test checking floating-point results.

Both are worth reaching for by default in test code. A NaN appearing in both arrays is often the expected outcome rather than a failure.

NaN in sorting and extremes

np.sort places NaN at the end, on the grounds that it checks greater than all. That is a rule rather than a result. NaN checks are all False.

np.argmax and np.max return NaN if any element is NaN. This Is steady with the reductions but means a single missing value hides the real max. np.nanargmax and np.nanmax skip them.

np.nanargmax raises on an all-NaN slice rather than returning a thing meaningless. This Is the right action and worth catching in code that reduces over groups that might be fully missing.

np.median passes NaN. np.nanmedian doesn't.

The pattern is steady: the plain function passes, the nan version skips. The nan version has an view about the all-missing case.

Tracking a NaN back to its source

The hard part of NaN debugging is that the value flows a long way from where it was created.

The single most good tool is np.errstate(all="raise"). This Converts the step that *makes* an invalid result into an exception. The traceback then points at the division by zero or the square root of a minus number, rather than at the reduction three functions later that returned NaN.

Failing that, bisect: check np.isfinite(x).all() at a few points in the pipeline and narrow down where it first goes false.

The usual origins are worth knowing. One of them is almost always it. Division by zero, log of zero or a minus, sqrt of a minus, 0 * inf, inf - inf. An out-of-domain trigonometric inverse where a value drifted just past 1 through rounding. That last one is a classic. A cosine likeness found as 1.0000000002 and passed to arccos gives NaN. np.clip(x, -1, 1) is the fix.

Deciding what to do about missing data

The code is easy. The choice isn't. It's a choice about the data rather than about NumPy.

Why is it missing? Missing at random can be dropped or imputed without bias. Missing because of the value itself. A sensor that fails at extremes, a survey question people skip when the answer is embarrassing. Can't. Dropping those rows biases the result. Imputing them with a mean biases it differently.

How much is missing? A handful of rows out of a hundred thousand can be dropped without much thought. Thirty percent can't. The choice of imputation turns a modelling choice that should be stated.

What does downstream care about? A mean is solid to mean-imputation by build. A variance isn't. Filling with the mean shrinks it. Anything leaning on spread will be quietly wrong.

Is the missingness itself helpful? Often it's. Adding a boolean "was missing" column preserves that data rather than discarding it.

what you choose, count them first and record what you did. np.isnan(a).sum(axis=0) at the point data arrives takes one line and blocks a surprising number of downstream mysteries.

Validating data as it arrives

The cheapest defence against NaN problems is checking at the boundary. Here The data enters the program.

np.isfinite(a).all() answers "is any of this NaN or infinite" in one call. Is the right single check because it catches both.

np.isnan(a).sum(axis=0) gives the count per column. This Turns "there's missing data one spot" into "column 3 is 40% empty". A far more actionable statement.

Doing this at load time, and failing or logging rather than proceeding silently, converts a class of mysterious downstream results into an direct, located error. It costs one line.

NaN and boolean logic

np.isnan needs a float dtype. Calling it on an int or string array raises a TypeError rather than returning all-False. This Surprises people writing plain code.

np.isnan(a) on an object array also fails. The elements may not be numbers at all.

For code that must handle any dtype, guard with np.issubdtype(a.dtype, np.floating) first, or use pd.isna from pandas. This Handles each case including None and NaT.

Comparisons involving NaN, in filtering

Because each compare with NaN is False, NaN values fail each rule. Including the negation of a rule.

a[a > 0] excludes them. a[a <= 0] also excludes them. Two filters that look like an exhaustive split silently drop rows.

Any place where a dataset is split into groups by a numeric rule is a place to ask what happens to the missing values. To add an clear np.isnan branch if they should be handled rather than lost.

Questions people ask about NaN and Missing Data

Why does np.nan == np.nan return False? NaN is defined as unequal to everything, itself included. Test with np.isnan(x) instead of comparing.

How do I ignore NaN in a mean? Use np.nanmean, or the other nan-prefixed reductions such as nansum, nanstd and nanmax.

Can an integer array hold NaN? No. NaN is a float, so an integer array cannot represent it. Convert to float, or use a masked array.

How do I replace NaN with a value? Use np.nan_to_num(a, nan=0.0), or a[np.isnan(a)] = value. Pick the fill with care, since zero changes the mean.

What is the difference between nan and inf? NaN means the value is undefined, such as 0 divided by 0. Inf is an overflow or a divide by a nonzero zero, and it behaves consistently in comparisons.

Are masked arrays better than NaN? They suit data where NaN is a valid value or the dtype is integer. For plain float data, NaN with the nan-functions is simpler and quicker.

The summary

np.isnan to find them, never == np.nan.

np.isfinite to validate, because it catches infinities too.

The nan* family to reduce over them, remembering that skipping is a choice about the data.

np.errstate(all="raise") to find where one was created. This Is the single most good debugging tool here.

equal_nan=True on allclose and array_equal when checking results that legitimately hold them.

np.clip before arccos and similar, to stop rounding drift making NaN from valid data.

And at the start of all: count them, record what you did about them. Prefer being clear over letting a fill value pass silently into an analysis that assumes it was measured.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why does `arr == np.nan` never find anything?

  2. What happens when you assign `np.nan` into an integer array?

  3. What does `a.mean()` return if `a` contains one NaN?

  4. Which check catches both NaN and infinity?

Cheat sheet

NaN and Missing Data

Work with NaN values in NumPy. Learn why NaN breaks comparisons and aggregations, and switch to nanmean and nansum to handle it correctly.

NUMPY · vizlearn.in/numpy/nan_and_missing_data.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.