You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Boolean indexing with multi-dimensional key arrays #1887
da[key] where key is a boolean labelled array (with any number of dimensions) is made equivalent to da.where(key.reindex_like(ds), drop=True). This matches the existing behavior if key is a 1D boolean array. For multi-dimensional arrays, even though the result is now multi-dimensional, this coupled with automatic skipping of NaNs means that da[key].mean() gives the same result as in NumPy.
da[key] = value where key is a boolean labelled array can be made equivalent to da = da.where(*align(key.reindex_like(da), value.reindex_like(da))) (that is, the three argument form of where).
da[key_0, ..., key_n] where all of key_i are boolean arrays gets handled in the usual way. It is an IndexingError to supply multiple labelled keys if any of them are not already aligned with as the corresponding index coordinates (and share the same dimension name). If they want alignment, we suggest users simply write da[key_0 & ... & key_n].
Reacted by Braham Snyder, tqfjo, Vladyslav Shapran, Maximilian Roos and Shahriar Heidrich
Just wanted to confirm, that boolean indexing is indeed highly relevant, especially for assigning values instead of just selecting them. Here is a use case which I encounter very often:
I'm working with very sparse data (e.g a satellite image of some islands surrounded by water), and I want to modify it using some_vectorized_function(). Of course I could use some_vectorized_function() to process the whole image, but boolean masking allows me to save a lot of computations.
The most important fact here is that image[mask] is just a list of 4 pixels, which I can process and then assign them back into their original place. And as you see, this boolean masking also plays very nice with broadcasting, which allows me to mask a 3D array with a 2D mask.
Unfortunately, nothing like this is currently possible with XArray. If implemented, it would enable some crazy speedups for operations like spatial interpolation, where we don't want to interpolate the whole image, but only some pixels that we care about.
I've added the "good first issue" label — at least the first two bullets of the proposal would be relatively simple to implement, given they're mostly syntactic sugar.
It's worth noting that there is at least one other way boolean indexing could work:
ds[key] could work like ds.stack({key.name: key.dims}).isel({key.name: np.flatnonzero(key.data)}), except without creating a MultiIndex. Arguably this might be more useful and also more consistent with NumPy itself. It's also more similar to the operation @Hoeze wants in N-dimensional boolean indexing #5179.
We can't support both with the same syntax, so we have to make a choice here :).
See also the discussion about what drop_duplicates/unique should do over in #5089.
I've been trying to conceptualize why I think the where equivalence (the original proposal) is better than the stack proposal (the latter). I think it's mostly:
It's simpler
I'm not sure how the setitem would work; da[key] = value?
If someone wants the stack result, it's less work to do original -> where result -> stack result relative to original -> stack result -> where result; which suggests they're more composable?
But I don't do much pointwise indexing — and so maybe we do want to prioritize that
I've been trying to conceptualize why I think the where equivalence (the original proposal) is better than the stack proposal (the latter).
Here are two reasons why I like the stack version:
It's more NumPy like -- boolean indexing in NumPy returns a flat array in the same way
It doesn't need dtype promotion to handle possibly missing values, so it will have more predictable semantics.
As a side note: one nice feature of using isel() for stacking is that it does not create a MultiIndex, which can be expensive. But there's no reason why we necessarily need to do that for stack(). I'll open a new issue to discuss adding an optional parameter.
I'm not sure how the setitem would work; da[key] = value?
To match the semantics of NumPy, value would need to have matching dims/coords to those of da[key]. In other words, it would also need to be stacked.
If someone wants the stack result, it's less work to do original -> where result -> stack result relative to original -> stack result -> where result; which suggests they're more composable?
I'm not quite sure this is true -- it's the difference between needing to call stack() vs unstack().
I'm not quite sure this is true -- it's the difference between needing to call stack() vs unstack().
This was a tiny point so it's fine to discard. I had meant that producing the where result via the stack result requires a stack and unstack. But producing the stack result via a where result requires only one stack — the where result is very cheap.
Current proposal ("stack"), of da[key] and with a dimension of key's name (and probably no multiindex):
In [86]: da.values[key.values]
Out[86]: array([0, 3, 6, 9]) # But the xarray version
The part about this new proposal that is most annoying is that the key needs a name, which we can use to name the new dimension. That's not too hard to do, but it is little annoying -- in practice you would have to write something like da[key.rename('key_name')] much of the time to make this work.
Current proposal ("stack"), of da[key] and with a dimension of key's name (and probably no multiindex):
In [86]: da.values[key.values]
Out[86]: array([0, 3, 6, 9]) # But the xarray version
The part about this new proposal that is most annoying is that the key needs a name, which we can use to name the new dimension. That's not too hard to do, but it is little annoying -- in practice you would have to write something like da[key.rename('key_name')] much of the time to make this work.
IMO, the perfect solution would be masking support.
I.e. da[key] would return the same array with an additional variable da.mask == key:
In [87]: da[key]
Out[87]:
<xarray.DataArray (a: 3, b: 4)>array([[ 0, <NA>, <NA>, 3],
[<NA>, <NA>, 6, <NA>],
[<NA>, 9, <NA>, <NA>]])
dtype: intDimensionswithoutcoordinates: a, b
Then we could have something like da[key].stack(new_dim=["a", "b"], dropna=True):
This could be useful (potentially we can open a different issue). While someone can call .dropna, that coerces to floats (or some type that supports missing) and can allocate more than is needed. Potentially this can be considered along with issues around sparse, e.g. #3245, #4143
Activity
Hoeze commentedon Oct 21, 2019
Since #3206 has been implemented now:
Maybe fancy boolean indexing (
da[boolean_mask]) could return a sparse array as well.shaprann commentedon Dec 14, 2020
Just wanted to confirm, that boolean indexing is indeed highly relevant, especially for assigning values instead of just selecting them. Here is a use case which I encounter very often:
I'm working with very sparse data (e.g a satellite image of some islands surrounded by water), and I want to modify it using
some_vectorized_function(). Of course I could usesome_vectorized_function()to process the whole image, but boolean masking allows me to save a lot of computations.Here is how I would achieve this in numpy:
The most important fact here is that
image[mask]is just a list of 4 pixels, which I can process and then assign them back into their original place. And as you see, this boolean masking also plays very nice with broadcasting, which allows me to mask a 3D array with a 2D mask.Unfortunately, nothing like this is currently possible with XArray. If implemented, it would enable some crazy speedups for operations like spatial interpolation, where we don't want to interpolate the whole image, but only some pixels that we care about.
max-sixty commentedon Mar 21, 2021
I've added the "good first issue" label — at least the first two bullets of the proposal would be relatively simple to implement, given they're mostly syntactic sugar.
shoyer commentedon Apr 20, 2021
It's worth noting that there is at least one other way boolean indexing could work:
ds[key]could work likeds.stack({key.name: key.dims}).isel({key.name: np.flatnonzero(key.data)}), except without creating a MultiIndex. Arguably this might be more useful and also more consistent with NumPy itself. It's also more similar to the operation @Hoeze wants in N-dimensional boolean indexing #5179.We can't support both with the same syntax, so we have to make a choice here :).
See also the discussion about what
drop_duplicates/uniqueshould do over in #5089.max-sixty commentedon Apr 21, 2021
I've been trying to conceptualize why I think the
whereequivalence (the original proposal) is better than thestackproposal (the latter). I think it's mostly:da[key] = value?stackresult, it's less work to do original ->whereresult ->stackresult relative to original ->stackresult ->whereresult; which suggests they're more composable?But I don't do much pointwise indexing — and so maybe we do want to prioritize that
shoyer commentedon Apr 21, 2021
Here are two reasons why I like the
stackversion:As a side note: one nice feature of using
isel()for stacking is that it does not create a MultiIndex, which can be expensive. But there's no reason why we necessarily need to do that forstack(). I'll open a new issue to discuss adding an optional parameter.To match the semantics of NumPy,
valuewould need to have matching dims/coords to those ofda[key]. In other words, it would also need to be stacked.I'm not quite sure this is true -- it's the difference between needing to call
stack()vsunstack().max-sixty commentedon Apr 22, 2021
OK great. To confirm, this is what it would look like:
Context:
Currently
Current proposal ("
stack"), ofda[key]and with a dimension ofkey's name (and probably no multiindex):Previous suggestion ("
where"), for the result ofda[key]:(small follow up I'll put in another message, for clarity)
max-sixty commentedon Apr 22, 2021
This was a tiny point so it's fine to discard. I had meant that producing the
whereresult via thestackresult requires astackandunstack. But producing thestackresult via awhereresult requires only onestack— thewhereresult is very cheap.shoyer commentedon Apr 22, 2021
Yes, this looks right to me.
shoyer commentedon Apr 22, 2021
The part about this new proposal that is most annoying is that the
keyneeds aname, which we can use to name the new dimension. That's not too hard to do, but it is little annoying -- in practice you would have to write something likeda[key.rename('key_name')]much of the time to make this work.max-sixty commentedon Apr 22, 2021
I'm still working through this. Using this to jot down my notes, no need to respond.
One property that seems to be lacking is that if
keychanges fromn-1tondimensions, the behavior changes (also outlined here):...as expected, but now let's make a 2D mask...
Hoeze commentedon Apr 22, 2021
IMO, the perfect solution would be masking support.
I.e.
da[key]would return the same array with an additional variableda.mask == key:Then we could have something like
da[key].stack(new_dim=["a", "b"], dropna=True):Here,
dropna=Truewould allow avoiding to create the cross-product ofa, b.Also, that would avoid all those unnecessary
floatcasts for free.max-sixty commentedon Apr 22, 2021
This could be useful (potentially we can open a different issue). While someone can call
.dropna, that coerces to floats (or some type that supports missing) and can allocate more than is needed. Potentially this can be considered along with issues around sparse, e.g. #3245, #4143matthiasschabel commentedon Jul 22, 2026
This proposal has been sitting around for almost a decade - it still seems to address a real need. Is it just waiting for implementation/test work?