diff --git a/dascore/utils/pd.py b/dascore/utils/pd.py index e05db70af..f9b721711 100644 --- a/dascore/utils/pd.py +++ b/dascore/utils/pd.py @@ -298,7 +298,9 @@ def _check_misdirected_range_query(key, val, df): base = key[:-4] if key.endswith(("_min", "_max")) else None if base is None or not {f"{base}_min", f"{base}_max"}.issubset(set(df.columns)): return - if not any(x is ... or x is None for x in val): + # Only a two element sequence can be a range. Anything else is a + # membership check, where None may be a legitimate value to match. + if len(val) != 2 or not any(x is ... or x is None for x in val): return msg = ( f"An open bound (... or None) is not valid in the query for column " diff --git a/docs/tutorial/coords.qmd b/docs/tutorial/coords.qmd index f1a09442d..6194a9608 100644 --- a/docs/tutorial/coords.qmd +++ b/docs/tutorial/coords.qmd @@ -166,7 +166,7 @@ sorted_data = data[indexer, :] ### Snap -[`snap`](`dascore.core.coords.BaseCoord.snap`) replaces coordinate values with an evenly sampled range spanning the same minimum and maximum. This intentionally loses interior precision and should be used only when that idealization is acceptable. For a full patch, [`Patch.snap_coords(...)`](`dascore.Patch.snap_coords`) also sorts the selected dimensions and applies the same indexing to the data. +[`snap`](`dascore.core.coords.BaseCoord.snap`) replaces coordinate values with an evenly sampled range spanning the same minimum and maximum. This intentionally loses interior precision and should be used only when that idealization is acceptable. Unlike [`sort`](`dascore.core.coords.BaseCoord.sort`), it returns only a new coordinate and no indexer, because it replaces the values rather than permuting them, so for an unsorted coordinate the snapped values no longer label the samples they used to. When data alignment matters, use [`CoordManager.snap`](`dascore.core.coordmanager.CoordManager.snap`), which sorts the coordinate and its associated array together before snapping, or [`Patch.snap_coords(...)`](`dascore.Patch.snap_coords`), which does the same across the selected dimensions of a patch. ```{python} import numpy as np diff --git a/tests/test_utils/test_pd.py b/tests/test_utils/test_pd.py index fec9b48a2..5b7f25eb4 100644 --- a/tests/test_utils/test_pd.py +++ b/tests/test_utils/test_pd.py @@ -294,6 +294,15 @@ def test_closed_range_on_interval_column_still_isin(self, example_df_2): out = filter_df(example_df_2, bp_min=tuple(vals)) assert np.all(out == example_df_2["bp_min"].isin(vals)) + @pytest.mark.parametrize("val", [[None], [100, None, 125]]) + def test_non_range_shaped_collection_still_isin(self, example_df_2, val): + """ + Only a two element sequence can be a range, so other collections stay + membership checks even when they contain None. + """ + out = filter_df(example_df_2, bp_min=val) + assert np.all(out == example_df_2["bp_min"].isin(val)) + def test_ellipsis_kept_for_non_interval_column(self, example_df_2): """ Columns with no min/max pair are plain isin checks; an ellipsis there