## TL;DR
Comparisons involving `pd.NA` return `pd.NA`, not True or False, so build masks with `.isna()` / `.notna()` explicitly and use `fillna` before comparing when you need a plain boolean. Nullable Int64 propagates missingness where float64 quietly coerced it.

```text
pandas nullable Int64 NA comparison gotchas
```

## Use this when
- A boolean mask on an Int64 column contains NA and filtering breaks
- `NA == NA` does not behave like you expected
- Sorting or groupby acts oddly around missing integers

## Not for this skill when
- You are dealing with float NaN, which follows different (numpy) rules
- Chained assignment silently does nothing
- You are migrating string columns to Arrow dtype

## Steps

1. See the gotcha in action so you stop trusting the comparison:

```python
s = pd.Series([1, None, 3], dtype="Int64")
print(s == 1)
print((s == 1).tolist())
```
Expected output: `[True, NA, False]`. The NA row is neither True nor False, and boolean indexing with NA in the mask raises.

2. Write NA-safe filters by combining the comparison with an explicit NA check:

```python
mask = s.eq(1).fillna(False)
print(s[mask].tolist())
```
Expected output: `[1]`. `fillna(False)` turns the undecided NA into False, giving you a clean boolean mask.

3. When you need NA to count as equal, say so explicitly:

```python
print(s.eq(1) | s.isna())
```
Expected output: `[True, True, False]`. There is no comparison operator that treats NA as equal to a value, you have to OR in the `isna()` branch.

4. For aggregations, decide what NA means before you aggregate:

```python
print(s.sum())          # skips NA by default
print(s.sum(min_count=1))  # NA-aware variant
```
Expected output: `4` for the plain sum (NA skipped). Check `skipna` on every aggregation you use in a pipeline, the default is skip but not everywhere.

5. Sort and group deliberately around NA:

```python
print(s.sort_values(na_position="first").tolist())
print(s.value_counts(dropna=False).to_dict())
```
Expected output: NA placement is explicit in the sort, and `value_counts` shows the NA bucket when `dropna=False`. Defaults hide NA, which is how it leaks into downstream joins.

## Variant phrasings

### pandas Int64 boolean indexing NA error
Filtering with a mask that contains NA raises. Apply step 2: `fillna(False)` on the mask before indexing.

### pd.NA == pd.NA returns NA
Correct and by design. NA means unknown, and unknown == unknown is unknown. Use `.isna()` on both sides when you need to match missings.

### nullable integer groupby dropping NA keys
`groupby` drops NA keys by default. Pass `dropna=False` to keep the NA group visible.

## Why it happens
Nullable Int64 uses `pd.NA` with three-valued Kleene logic: any comparison with NA yields NA. Float columns historically used NaN, where `nan == nan` is False but the mask at least stays boolean. The nullable dtypes are stricter and more correct, which means code written against float-NaN habits breaks in new ways.

## Edge cases
- `s == s` is not all-True when NA is present, never use self-equality to find duplicates on nullable columns.
- Merging on nullable Int64 keys: NA keys never match, same as NaN.
- `astype("Int64")` on a float column with NaN converts NaN to NA silently, audit downstream comparisons.
- `fillna` with a value outside the int range upcasts the column, check the dtype after filling.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_fRUjrc1LZ0D6QgvxL2_3jA
