# NEP 25 — NA support via special dtypes#

Author:

Nathaniel J. Smith <njs@pobox.com>

Status:

Deferred

Type:

Standards Track

Created:

2011-07-08

## Abstract#

Context: this NEP was written as an additional alternative to NEP 12 (NEP 24 is another alternative), which at the time of writing had an implementation that was merged into the NumPy main branch.

To try and make more progress on the whole missing values/masked arrays/… debate, it seems useful to have a more technical discussion of the pieces which we can agree on. This is the second, which attempts to nail down the details of how NAs can be implemented using special dtype’s.

### Rationale#

An ordinary value is something like an integer or a floating point number. A missing value is a placeholder for an ordinary value that is for some reason unavailable. For example, in working with statistical data, we often build tables in which each row represents one item, and each column represents properties of that item. For instance, we might take a group of people and for each one record height, age, education level, and income, and then stick these values into a table. But then we discover that our research assistant screwed up and forgot to record the age of one of our individuals. We could throw out the rest of their data as well, but this would be wasteful; even such an incomplete row is still perfectly usable for some analyses (e.g., we can compute the correlation of height and income). The traditional way to handle this would be to stick some particular meaningless value in for the missing data,e.g., recording this person’s age as 0. But this is very error prone; we may later forget about these special values while running other analyses, and discover to our surprise that babies have higher incomes than teenagers. (In this case, the solution would be to just leave out all the items where we have no age recorded, but this isn’t a general solution; many analyses require something more clever to handle missing values.) So instead of using an ordinary value like 0, we define a special “missing” value, written “NA” for “not available”.

The basic semantics of NA values are as follows. Like any other value, they must be supported by your array’s dtype – you can’t store a floating point number in an array with dtype=int32, and you can’t store an NA in it either. You need an array with dtype=NAint32 or something (exact syntax to be determined). Otherwise, NA values act exactly like any other values. In particular, you can apply arithmetic functions and so forth to them. By default, any function which takes an NA as an argument always returns an NA as well, regardless of the values of the other arguments. This ensures that if we try to compute the correlation of income with age, we will get “NA”, meaning “given that some of the entries could be anything, the answer could be anything as well”. This reminds us to spend a moment thinking about how we should rephrase our question to be more meaningful. And as a convenience for those times when you do decide that you just want the correlation between the known ages and income, then you can enable this behavior by adding a single argument to your function call.

For floating point computations, NAs and NaNs have (almost?) identical behavior. But they represent different things – NaN an invalid computation like 0/0, NA a value that is not available – and distinguishing between these things is useful because in some situations they should be treated differently. (For example, an imputation procedure should replace NAs with imputed values, but probably should leave NaNs alone.) And anyway, we can’t use NaNs for integers, or strings, or booleans, so we need NA anyway, and once we have NA support for all these types, we might as well support it for floating point too for consistency.

## General strategy#

NumPy already has a general mechanism for defining new dtypes and slotting them in so that they’re supported by ndarrays, by the casting machinery, by ufuncs, and so on. In principle, we could implement NA-dtypes just using these existing interfaces. But we don’t want to do that, because defining all those new ufunc loops etc. from scratch would be a huge hassle, especially since the basic functionality needed is the same in all cases. So we need some generic functionality for NAs – but it would be better not to bake this in as a single set of special “NA types”, since users may well want to define new custom dtypes that have their own NA values, and have them integrate well the rest of the NA machinery. Our strategy, therefore, is to avoid the mid-layer mistake by exposing some code for generic NA handling in different situations, which dtypes can selectively use or not as they choose.

Some example use cases:
1. We want to define a dtype that acts exactly like an int32, except that the most negative value is treated as NA.

2. We want to define a parametrized dtype to represent categorical data, and the bit-pattern to be used for NA depends on the number of categories defined, so our code needs to play an active role handling it rather than simply deferring to the standard machinery.

3. We want to define a dtype that acts like an length-10 string and supports NAs. Since our string may hold arbitrary binary values, we want to actually allocate 11 bytes for it, with the first byte a flag indicating whether this string is NA and the rest containing the string content.

4. We want to define a dtype that allows multiple different types of NA data, which print differently and can be distinguished by the new ufunc that we define called `is_na_of_type(...)`, but otherwise takes advantage of the generic NA machinery for most operations.

## dtype C-level API extensions#

The PyArray_Descr struct gains the following new fields:

```void * NA_value;
PyArray_Descr * NA_extends;
int NA_extends_offset;
```

The following new flag values are defined:

```NPY_NA_AUTO_ARRFUNCS
NPY_NA_AUTO_CAST
NPY_NA_AUTO_UFUNC
NPY_NA_AUTO_UFUNC_CHECKED
NPY_NA_AUTO_ALL /* the above flags OR'ed together */
```

The PyArray_ArrFuncs struct gains the following new fields:

```void (*isna)(void * src, void * dst, npy_intp n, void * arr);
void (*clearna)(void * data, npy_intp n, void * arr);
```

We add at least one new convenience macro:

```#define NPY_NA_SUPPORTED(dtype) ((dtype)->f->isna != NULL)
```

The general idea is that anywhere where we used to call a dtype-specific function pointer, the code will be modified to instead:

1. Check for whether the relevant `NPY_NA_AUTO_...` bit is enabled, the NA_extends field is non-NULL, and the function pointer we wanted to call is NULL.

2. If these conditions are met, then use `isna` to identify which entries in the array are NA, and handle them appropriately. Then look up whatever function we were going to call using this dtype on the `NA_extends` dtype instead, and use that to handle the non-NA elements.

For more specifics, see following sections.

Note that if `NA_extends` points to a parametrized dtype, then the dtype object it points to must be fully specified. For example, if it is a string dtype, it must have a non-zero `elsize` field.

In order to handle the case where the NA information is stored in a field next to the real’ data, the ``NA_extends_offset` field is set to a non-zero value; it must point to the location within each element of this dtype where some data of the `NA_extends` dtype is found. For example, if we have are storing 10-byte strings with an NA indicator byte at the beginning, then we have:

```elsize == 11
NA_extends_offset == 1
NA_extends->elsize == 10
```

When delegating to the `NA_extends` dtype, we offset our data pointer by `NA_extends_offset` (while keeping our strides the same) so that it sees an array of data of the expected type (plus some superfluous padding). This is basically the same mechanism that record dtypes use, IIUC, so it should be pretty well-tested.

When delegating to a function that cannot handle “misbehaved” source data (see the `PyArray_ArrFuncs` documentation for details), then we need to check for alignment issues before delegating (especially with a non-zero `NA_extends_offset`). If there’s a problem, when we need to “clean up” the source data first, using the usual mechanisms for handling misaligned data. (Of course, we should usually set up our dtypes so that there aren’t any alignment issues, but someone screws that up, or decides that reduced memory usage is more important to them then fast inner loops, then we should still handle that gracefully, as we do now.)

The `NA_value` and `clearna` fields are used for various sorts of casting. `NA_value` is a bit-pattern to be used when, for example, assigning from np.NA. `clearna` can be a no-op if `elsize` and `NA_extends->elsize` are the same, but if they aren’t then it should clear whatever auxiliary NA storage this dtype uses, so that none of the specified array elements are NA.

### Core dtype functions#

The following functions are defined in `PyArray_ArrFuncs`. The special behavior described here is enabled by the NPY_NA_AUTO_ARRFUNCS bit in the dtype flags, and only enabled if the given function field is not filled in.

`getitem`: Calls `isna`. If `isna` returns true, returns np.NA. Otherwise, delegates to the `NA_extends` dtype.

`setitem`: If the input object is `np.NA`, then runs `memcpy(self->NA_value, data, arr->dtype->elsize);`. Otherwise, calls `clearna`, and then delegates to the `NA_extends` dtype.

`copyswapn`, `copyswap`: FIXME: Not sure whether there’s any special handling to use for these?

`compare`: FIXME: how should this handle NAs? R’s sort function discards NAs, which doesn’t seem like a good option.

`argmax`: FIXME: what is this used for? If it’s the underlying implementation for np.max, then it really needs some way to get a skipna argument. If not, then the appropriate semantics depends on what it’s supposed to accomplish…

`dotfunc`: QUESTION: is it actually guaranteed that everything has the same dtype? FIXME: same issues as for `argmax`.

`scanfunc`: This one’s ugly. We may have to explicitly override it in all of our special dtypes, because assuming that we want the option of, say, having the token “NA” represent an NA value in a text file, we need some way to check whether that’s there before delegating. But `ungetc` is only guaranteed to let us put back 1 character, and we need 2 (or maybe 3 if we actually check for “NA “). The other option would be to read to the next delimiter, check whether we have an NA, and if not then delegate to `fromstr` instead of `scanfunc`, but according to the current API, each dtype might in principle use a totally different rule for defining “the next delimiter”. So… any ideas? (FIXME)

`fromstr`: Easy – check for “NA “, if present then assign `NA_value`, otherwise call `clearna` and delegate.

`nonzero`: FIXME: again, what is this used for? (It seems redundant with using the casting machinery to cast to bool.) Probably it needs to be modified so that it can return NA, though…

`fill`: Use `isna` to check if either of the first two values is NA. If so, then fill the rest of the array with `NA_value`. Otherwise, call `clearna` and then delegate.

`fillwithvalue`: Guess this can just delegate?

`sort`, `argsort`: These should probably arrange to sort NAs to a particular place in the array (either the front or the back – any opinions?)

`scalarkind`: FIXME: I have no idea what this does.

`castdict`, `cancastscalarkindto`, `cancastto`: See section on casting below.

### Casting#

FIXME: this really needs attention from an expert on NumPy’s casting rules. But I can’t seem to find the docs that explain how casting loops are looked up and decided between (e.g., if you’re casting from dtype A to dtype B, which dtype’s loops are used?), so I can’t go into details. But those details are tricky and they matter…

But the general idea is, if you have a dtype with `NPY_NA_AUTO_CAST` set, then the following conversions are automatically allowed:

• Casting from the underlying type to the NA-type: this is performed by the

• usual `clearna` + potentially-strided copy dance. Also, `isna` is

• called to check that none of the regular values have been accidentally

• converted into NA; if so, then an error is raised.

• Casting from the NA-type to the underlying type: allowed in principle, but if `isna` returns true for any of the values that are to be converted, then again, an error is raised. (If you want to get around this, use `np.view(array_with_NAs, dtype=float)`.)

• Casting between the NA-type and other types that do not support NA: this is allowed if the underlying type is allowed to cast to the other type, and is performed by combining a cast to or from the underlying type (using the above rules) with a cast to or from the other type (using the underlying type’s rules).

• Casting between the NA-type and other types that do support NA: if the other type has NPY_NA_AUTO_CAST set, then we use the above rules plus the usual dance with `isna` on one array being converted to `NA_value` elements in the other. If only one of the arrays has NPY_NA_AUTO_CAST set, then it’s assumed that that dtype knows what it’s doing, and we don’t do any magic. (But this is one of the things that I’m not sure makes sense, as per my caveat above.)

### Ufuncs#

All ufuncs gain an additional optional keyword argument, `skipNA=`, which defaults to False.

If `skipNA == True`, then the ufunc machinery unconditionally calls `isna` for any dtype where NPY_NA_SUPPORTED(dtype) is true, and then acts as if any values for which isna returns True were masked out in the `where=` argument (see miniNEP 1 for the behavior of `where=`). If a `where=` argument is also given, then it acts as if the `isna` values had be ANDed out of the `where=` mask, though it does not actually modify the mask. Unlike the other changes below, this is performed unconditionally for any dtype which has an `isna` function defined; the NPY_NA_AUTO_UFUNC flag is not checked.

If NPY_NA_AUTO_UFUNC is set, then ufunc loop lookup is modified so that whenever it checks for the existence of a loop on the current dtype, and does not find one, then it also checks for a loop on the `NA_extends` dtype. If that loop is found, then it uses it in the normal way, with the exceptions that (1) it is only called for values which are not NA according to `isna`, (2) if the output array has NPY_NA_AUTO_UFUNC set, then `clearna` is called on it before calling the ufunc loop, (3) pointer offsets are adjusted by `NA_extends_offset` before calling the ufunc loop. In addition, if NPY_NA_AUTO_UFUNC_CHECK is set, then after evaluating the ufunc loop we call `isna` on the output array, and if there are any NAs in the output which were not in the input, then we raise an error. (The intention of this is to catch cases where, say, we represent NA using the most-negative integer, and then someone’s arithmetic overflows to create such a value by accident.)

FIXME: We should go into more detail here about how NPY_NA_AUTO_UFUNC works when there are multiple input arrays, of which potentially some have the flag set and some do not.

### Printing#

FIXME: There should be some sort of mechanism by which values which are NA are automatically repr’ed as NA, but I don’t really understand how NumPy printing works, so I’ll let someone else fill in this section.

### Indexing#

Scalar indexing like `a[12]` goes via the `getitem` function, so according to the proposal as described above, if a dtype delegates `getitem`, then scalar indexing on NAs will return the object `np.NA`. (If it doesn’t delegate `getitem`, of course, then it can return whatever it wants.)

This seems like the simplest approach, but an alternative would be to add a special case to scalar indexing, where if an `NPY_NA_AUTO_INDEX` flag were set, then it would call `isna` on the specified element. If this returned false, it would call `getitem` as usual; otherwise, it would return a 0-d array containing the specified element. The problem with this is that it breaks expressions like `if a[i] is np.NA: ...`. (Of course, there is nothing nearly so convenient as that for NaN values now, but then, NaN values don’t have their own global singleton.) So for now we stick to scalar indexing just returning `np.NA`, but this can be revisited if anyone objects.

## Python API for generic NA support#

NumPy will gain a global singleton called `numpy.NA`, similar to None, but with semantics reflecting its status as a missing value. In particular, trying to treat it as a boolean will raise an exception, and comparisons with it will produce `numpy.NA` instead of True or False. These basics are adopted from the behavior of the NA value in the R project. To dig deeper into the ideas, http://en.wikipedia.org/wiki/Ternary_logic#Kleene_logic provides a starting point.

Most operations on `np.NA` (e.g., `__add__`, `__mul__`) are overridden to unconditionally return `np.NA`.

The automagic dtype detection used for expressions like ```np.asarray([1, 2, 3])```, `np.asarray([1.0, 2.0. 3.0])` will be extended to recognize the `np.NA` value, and use it to automatically switch to a built-in NA-enabled dtype (which one being determined by the other elements in the array). A simple `np.asarray([np.NA])` will use an NA-enabled float64 dtype (which is analogous to what you get from `np.asarray([])`). Note that this means that expressions like `np.log(np.NA)` will work: first `np.NA` will be coerced to a 0-d NA-float array, and then `np.log` will be called on that.

Python-level dtype objects gain the following new fields:

```NA_supported
NA_value
```

`NA_supported` is a boolean which simply exposes the value of the `NPY_NA_SUPPORTED` flag; it should be true if this dtype allows for NAs, false otherwise. [FIXME: would it be better to just key this off the existence of the `isna` function? Even if a dtype decides to implement all other NA handling itself, it still has to define `isna` in order to make `skipNA=` work correctly.]

`NA_value` is a 0-d array of the given dtype, and its sole element contains the same bit-pattern as the dtype’s underlying `NA_value` field. This makes it possible to determine the default bit-pattern for NA values for this type (e.g., with `np.view(mydtype.NA_value, dtype=int8)`).

We do not expose the `NA_extends` and `NA_extends_offset` values at the Python level, at least for now; they’re considered an implementation detail (and it’s easier to expose them later if they’re needed then unexpose them if they aren’t).

Two new ufuncs are defined: `np.isNA` returns a logical array, with true values where-ever the dtype’s `isna` function returned true. `np.isnumber` is only defined for numeric dtypes, and returns True for all elements which are not NA, and for which `np.isfinite` would return True.

## Builtin NA dtypes#

The above describes the generic machinery for NA support in dtypes. It’s flexible enough to handle all sorts of situations, but we also want to define a few generally useful NA-supporting dtypes that are available by default.

For each built-in dtype, we define an associated NA-supporting dtype, as follows:

• floats: the associated dtype uses a specific NaN bit-pattern to indicate NA (chosen for R compatibility)

• complex: we do whatever R does (FIXME: look this up – two NA floats, probably?)

• signed integers: the most-negative signed value is used as NA (chosen for R compatibility)

• unsigned integers: the most-positive value is used as NA (no R compatibility possible).

• strings: the first byte (or, in the case of unicode strings, first 4 bytes) is used as a flag to indicate NA, and the rest of the data gives the actual string. (no R compatibility possible)

• objects: Two options (FIXME): either we don’t include an NA-ful version, or we use np.NA as the NA bit pattern.

• boolean: we do whatever R does (FIXME: look this up – 0 == FALSE, 1 == TRUE, 2 == NA?)

Each of these dtypes is trivially defined using the above machinery, and are what are automatically used by the automagic type inference machinery (for `np.asarray([True, np.NA, False])`, etc.).

They can also be accessed via a new function `np.withNA`, which takes a regular dtype (or an object that can be coerced to a dtype, like ‘float’) and returns one of the above dtypes. Ideally `withNA` should also take some optional arguments that let you describe which values you want to count as NA, etc., but I’ll leave that for a future draft (FIXME).

FIXME: If `d` is one of the above dtypes, then should `d.type` return?

The NEP also contains a proposal for a somewhat elaborate domain-specific-language for describing NA dtypes. I’m not sure how great an idea that is. (I have a bias against using strings as data structures, and find the already existing strings confusing enough as it is – also, apparently the NEP version of NumPy uses strings like ‘f8’ when printing dtypes, while my NumPy uses object names like ‘float64’, so I’m not sure what’s going on there. `withNA(float64, arg1=value1)` seems like a more pleasant way to print a dtype than “NA[f8,value1]”, at least to me.) But if people want it, then cool.

### Type hierarchy#

FIXME: how should we do subtype checks, etc., for NA dtypes? What does `issubdtype(withNA(float), float)` return? How about `issubdtype(withNA(float), np.floating)`?