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
For more context: Awkward is like numpy but for arrays of very arbitrary (dynamic) structure.
I don't know much yet about that library (I've just seen this SciPy 2020 presentation), but now I could imagine using xarray for dealing with labelled collections of geometrical / geospatial objects like polylines or polygons.
At this stage, any integration between xarray and awkward arrays would be something highly experimental, but I think this might be an interesting case for flexible arrays (and possibly flexible indexes) mentioned in the roadmap. There is some discussion here: scikit-hep/awkward#27.
Does anyone see any other potential use case?
cc @pydata/xarray
Reacted by Florian R. Hölzlwimmer, Markus Löning, Iacopo, Cove Geary, Wei Ji, Andrea Pierré, Bas Nijholt and Yunus Sevinchan
I think that xarray should offer a "compatibility test toolkit" to any numpy-like, NEP18-compatible library that wants to integrate with it.
Instead of having a module full of tests specifically for pint, one for sparse, one for cupy, one for awkward, etc. etc. etc. those projects could just write a minimal test module like this:
importxarrayimportsparsexarray.testing.test_nep18_module(
sparse,
# TODO: lambda to create an array# TODO: list of xfails
)
which would automatically expand into a comprehensive suite of tests thanks to pytest parameterize/fixture magic.
this would allow developers of numpy-like libraries to just test their package vs what's expected from a generic NEP-18 compliant package.
Reacted by Florian R. Hölzlwimmer, Tom Nicholas and Benda Xu
I've been using Xarray with argopy recently, and the immediate value I see is the documentation of columns, which is semi-lacking in Awkward (one user has been passing this information through an Awkward tree as a scikit-hep/awkward#422). I should also look into Xarray's indexing, which I've always seen as being the primary difference between NumPy and Pandas; Awkward Array has no indexing, though every node has an optional Identities which would be used to track such information through Awkward manipulations—Identities would have a bijection with externally supplied indexes. They haven't been used for anything yet.
Although the elevator pitch for Xarray is "n-dimensional Pandas," it's rather different, isn't it? The contextual metadata is more extensive than anything I've seen in Pandas, and Xarray can be partitioned for out-of-core analysis: Xarray wraps Dask, unlike Dask's array collection, which wraps NumPy. I had troubles getting Pandas to wrap Awkward array (scikit-hep/awkward#350 ), but maybe these won't be issues for Xarray.
One last thing (in this very rambly message): the main difficulty I think we would have in that is that Awkward Arrays don't have shape and dtype, since those define a rectilinear array of numbers. The data model is Datashape plus union types. There is a sense in which ndim is defined: the number of nested lists before reaching the first record, which may split it into different depths for each field, but even this can be ill-defined with union types:
So I actually think we can do this, with some caveats.
I recently found a cool dataset with ragged-like data which has rekindled my interest in this interfacing, and given me a real example to try it out with.
As far as I understand it the main problem is that awkward arrays don't define a shape or dtype attribute. Instead they follow a different model (the "datashape" model). Xarray expects shape and dtype to be defined, and given that those attributes are in the data API standard, this is a pretty reasonable expectation for most cases.
(There is a useful discussion here on the data-apis consortium repo about why awkward arrays don't define these attributes in general.)
Conceptually though, it seems to me that shape and dtype do make sense for Awkward arrays, at least for some subset of them, because Awkward's "type" is clearly related to the normal notion of shape and dtype.
Let's take an Awkward array that can be coerced directly to a numpy array:
Here there is a clear correspondence: the first axis of the awkward array has length 2, and because in this case the second axis has a consistent length of 3, we can coerce this to a numpy array with shape=(2,3). The dtype also makes sense, because in this case the awkward array only contains data of one type, an int64.
Now imagine a "ragged" (or "jagged") array, which is like a numpy array except that the lengths along one (or more) of the axes can be variable. Awkward allows this, e.g.
However we still conceptually have a "shape". It's either (2, "var"), where "var" means a variable length across the other axes, or alternatively we could say the shape is (2, 4), where 4 is simply the maximum length along the variable-length axis. The latter interpretation is kind of similar to sparse arrays.
In the second case you can still read off the dtype too. However awkward also allows "Union types", which basically means that one array can contain data of multiple numpy dtypes. Unfortunately this seems to completely break the numpy / xarray model, but we can completely ignore this problem if we simply say that xarray should only try to wrap awkward arrays with non-Union types. I think that's okay - a ragged-length array with a fixed dtype would still be extremely useful!
So if we want to wrap an (non-union type) awkward array instance like ragged in xarray we have to do one of two things:
Generalise xarray to allow for variable-length dimensions
This seems hard. Xarray's whole model is built assuming that dims has type Mapping[Hashable, int]. It also breaks our normal concept of alignment, which we need to put coordinate variables in DataArrays alongside data variables.
It would also mean a big change to xarray in order to support one unusual type of array, that goes beyond the data API standard. That breaks xarray's general design philosophy of providing a general wrapper and delegating to domain-specific array implementations / backends / etc. for specificity.
Expose a version of shape and dtype on Awkward arrays
This doesn't seem as hard, at least for non-union type awkward arrays. In fact this crude monkey-patching seems to mostly work:
In [1]: fromawkwardimportArray, num
...: importnumpyasnpIn [2]: defget_dtype(self) ->np.dtype:
...: if"Union"instr(self.type):
...: raiseValueError("awkward arrays with Union types can't be expressed in terms of a single numpy dtype")
...:
...: datatype=str(self.type).split(" * ")[-1]
...:
...: ifdatatype=="string":
...: returnnp.dtype("str")
...: else:
...: returnnp.dtype(datatype)
...:
In [3]: defget_shape(self):
...: if"Union"instr(self.type):
...: raiseValueError("awkward arrays with Union types can't be expressed in terms of a single numpy dtype")
...:
...: lengths=str(self.type).split(" * ")[:-1]
...:
...: foraxisinrange(self.ndim):
...: iflengths[axis] =="var":
...: lengths[axis] =np.max(num(self, axis))
...: else:
...: lengths[axis] =int(lengths[axis])
...:
...: returntuple(lengths)
...:
In [4]: defget_size(self):
...: returnnp.prod(get_shape(self))
...:
In [5]: setattr(Array, 'dtype', property(get_dtype))
...: setattr(Array, 'shape', property(get_shape))
...: setattr(Array, 'size', property(get_size))
Now if we make the same ragged array but with the monkey-patched class, we have a sensible return value for dtype, shape, and size, which means that the xarray constructors will accept our Array now!
In [6]: ragged=Array([[1, 2, 3, 100], [4, 5, 6]])
In [7]: importxarrayasxrIn [8]: da=xr.DataArray(ragged, dims=['x', 't'])
In [17]: daOut[17]:
<xarray.DataArray (x: 2, t: 4)><Array [[1, 2, 3, 100], [4, 5, 6]] type='2 * var * int64'>Dimensionswithoutcoordinates: x, tIn [18]: da.dtypeOut[18]: dtype('int64')
In [19]: da.sizeOut[19]: 8In [20]: da.shapeOut[20]: (2, 4)
Promising...
Let's try indexing:
In [21]: da.isel(t=2)
Out[21]:
<xarray.DataArray (x: 2)><Array [3, 6] type='2 * int64'>Dimensionswithoutcoordinates: xIn [22]: da.isel(t=4)
---------------------------------------------------------------------------ValueErrorTraceback (mostrecentcalllast)
InputIn [22], in<cellline: 1>()
---->1da.isel(t=4)
...
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/highlevel.py:991, inArray.__getitem__(self, where)
579""" 580 Args: 581 where (many types supported; see below): Index of positions to (...) 988 have the same dimension as the array being indexed. 989 """990ifnothasattr(self, "_tracers"):
-->991tmp=ak._util.wrap(self.layout[where], self._behavior)
992else:
993tmp=ak._connect._jax.jax_utils._jaxtracers_getitem(self, where)
ValueError: inListOffsetArray64attemptingtoget4, indexoutofrange
(https://github.com/scikit-hep/awkward-1.0/blob/1.8.0/src/cpu-kernels/awkward_NumpyArray_getitem_next_at.cpp#L21)
That's what should happen - xarray delegates the indexing to the underlying array, which throws an error if there is a problem.
Arithmetic also seems to work
In [23]: da*2Out[23]:
<xarray.DataArray (x: 2, t: 4)><Array [[2, 4, 6, 200], [8, 10, 12]] type='2 * var * int64'>Dimensionswithoutcoordinates: x, t
But we hit snags with numpy functions
In [24]: np.mean(da)
---------------------------------------------------------------------------TypeErrorTraceback (mostrecentcalllast)
InputIn [24], in<cellline: 1>()
---->1np.mean(da)
File<__array_function__internals>:180, inmean(*args, **kwargs)
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/numpy/core/fromnumeric.py:3430, inmean(a, axis, dtype, out, keepdims, where)
3428pass3429else:
->3430returnmean(axis=axis, dtype=dtype, out=out, **kwargs)
3432return_methods._mean(a, axis=axis, dtype=dtype,
3433out=out, **kwargs)
File~/Documents/Work/Code/xarray/xarray/core/_reductions.py:1478, inDataArrayReductions.mean(self, dim, skipna, keep_attrs, **kwargs)
1403defmean(
1404self,
1405dim: None|Hashable|Sequence[Hashable] =None,
(...)
1409**kwargs: Any,
1410 ) ->DataArray:
1411""" 1412 Reduce this DataArray's data by applying ``mean`` along some dimension(s). 1413 (...) 1476 array(nan) 1477 """->1478returnself.reduce(
1479duck_array_ops.mean,
1480dim=dim,
1481skipna=skipna,
1482keep_attrs=keep_attrs,
1483**kwargs,
1484 )
File~/Documents/Work/Code/xarray/xarray/core/dataarray.py:2930, inDataArray.reduce(self, func, dim, axis, keep_attrs, keepdims, **kwargs)
2887defreduce(
2888self: T_DataArray,
2889func: Callable[..., Any],
(...)
2895**kwargs: Any,
2896 ) ->T_DataArray:
2897"""Reduce this array by applying `func` along some dimension(s). 2898 2899 Parameters (...) 2927 summarized data and the indicated dimension(s) removed. 2928 """->2930var=self.variable.reduce(func, dim, axis, keep_attrs, keepdims, **kwargs)
2931returnself._replace_maybe_drop_dims(var)
File~/Documents/Work/Code/xarray/xarray/core/variable.py:1854, inVariable.reduce(self, func, dim, axis, keep_attrs, keepdims, **kwargs)
1852data=func(self.data, axis=axis, **kwargs)
1853else:
->1854data=func(self.data, **kwargs)
1856ifgetattr(data, "shape", ()) ==self.shape:
1857dims=self.dimsFile~/Documents/Work/Code/xarray/xarray/core/duck_array_ops.py:579, inmean(array, axis, skipna, **kwargs)
577return_to_pytimedelta(mean_timedeltas, unit="us") +offset578else:
-->579return_mean(array, axis=axis, skipna=skipna, **kwargs)
File~/Documents/Work/Code/xarray/xarray/core/duck_array_ops.py:341, in_create_nan_agg_method.<locals>.f(values, axis, skipna, **kwargs)
339withwarnings.catch_warnings():
340warnings.filterwarnings("ignore", "All-NaN slice encountered")
-->341returnfunc(values, axis=axis, **kwargs)
342exceptAttributeError:
343ifnotis_duck_dask_array(values):
File<__array_function__internals>:180, inmean(*args, **kwargs)
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/highlevel.py:1434, inArray.__array_function__(self, func, types, args, kwargs)
1417def__array_function__(self, func, types, args, kwargs):
1418""" 1419 Intercepts attempts to pass this Array to those NumPy functions other 1420 than universal functions that have an Awkward equivalent. (...) 1432 See also #__array_ufunc__. 1433 """->1434returnak._connect._numpy.array_function(func, types, args, kwargs)
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/_connect/_numpy.py:43, inarray_function(func, types, args, kwargs)
41returnout42else:
--->43returnfunction(*args, **kwargs)
TypeError: mean() gotanunexpectedkeywordargument'dtype'
This seems fixable though.
In fact I think if we changed #6845 (@dcherian) then this alternative would already work
In [25]: importawkwardasakIn [26]: ak.mean(da)
---------------------------------------------------------------------------ValueErrorTraceback (mostrecentcalllast)
InputIn [26], in<cellline: 1>()
---->1ak.mean(da)
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/operations/reducers.py:971, inmean(x, weight, axis, keepdims, mask_identity)
969withnp.errstate(invalid="ignore"):
970ifweightisNone:
-->971sumw=count(x, axis=axis, keepdims=keepdims, mask_identity=mask_identity)
972sumwx=sum(x, axis=axis, keepdims=keepdims, mask_identity=mask_identity)
973else:
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/operations/reducers.py:79, incount(array, axis, keepdims, mask_identity)
10defcount(array, axis=None, keepdims=False, mask_identity=False):
11""" 12 Args: 13 array: Data in which to count elements. (...) 77 to turn the None values into something that would be counted. 78 """--->79layout=ak.operations.convert.to_layout(
80array, allow_record=False, allow_other=False81 )
82ifaxisisNone:
84defreduce(xs):
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/operations/convert.py:1917, into_layout(array, allow_record, allow_other, numpytype)
1914returnfrom_iter([array], highlevel=False)
1916elifisinstance(array, Iterable):
->1917returnfrom_iter(array, highlevel=False)
1919elifnotallow_other:
1920raiseTypeError(
1921f"{array} cannot be converted into an Awkward Array"1922+ak._util.exception_suffix(__file__)
1923 )
File~/miniconda3/envs/hummingbirds/lib/python3.10/site-packages/awkward/operations/convert.py:891, infrom_iter(iterable, highlevel, behavior, allow_record, initial, resize)
889out=ak.layout.ArrayBuilder(initial=initial, resize=resize)
890forxiniterable:
-->891out.fromiter(x)
892layout=out.snapshot()
893returnak._util.maybe_wrap(layout, behavior, highlevel)
ValueError: cannotconvert<xarray.DataArray ()>array(1) (type DataArray) toanarrayelement
(https://github.com/scikit-hep/awkward-1.0/blob/1.8.0/src/python/content.cpp#L974)
Suggestion: How about awkward offer a specialized array class which uses the same fast code underneath but disallows Union types, and follows the array API standard, implementing shape, dtype etc. as described above. That should then "just work" in xarray, in the same way that sparse arrays already do.
missing data (through masks and indexes, not NaNs)
record structures
heterogeneous unions
You're interested in a subset of this type system, but that subset doesn't just exclude unions, it also excludes records. If you have an xarray, you don't need top-level records since those could just be the columns of an xarray, but some data source might provide records nested within variable-length lists (very common in HEP) or other nesting. It would have to be explicitly excluded.
That leaves the possibility of missing lists and missing numeric primitives. Missing lists could be turned into empty lists (Google projects like Protocol Buffers often make that equivalence) and missing numbers could be turned into NaN if you're willing to lose integer-ness.
Here's a way to determine if an array (Python type ak.Array) is in that subset and to pre-process it, ensuring that you only have numbers, variable-length, and regular-length lists (in Awkward version 2, so note the "_v2"):
It should catch all the cases and doesn't rely on string-processing the type's DataShape representation.
Given that you're working within that subset, it would be possible to define shape with some token for the variable-length dimensions and dtype. I can follow up with another message (I have to deal with something else at the moment).
Oh, if you're replacing variable-length dimensions with the maximum length in that dimension, what about actually padding the array with ak.pad_none?
The above would have to be expanded to get every axis, but it makes all nested lists have the length of the longest one by padding with None, then replaces those None values with np.nan. That uses all the memory of a padded array, but it's what people use now if they want to convert Awkward data into non-Awkward data (maybe passing the final step to ak.to_numpy).
You mentioned union arrays, but for completeness, the type system in Awkward Array has
...
Here's a way to determine if an array (Python type ak.Array) is in that subset
(FWIW I find the "behavior" stuff very confusing in general, even after reading the docs page on it. I don't really understand why I can't just reimplement my monkey-patched example above by subclassing ak.Array, or should I be wrapping it?)
it would be possible to define shape with some token for the variable-length dimensions and dtype.
How would I do this without monkey-patching?
All I really want (and I hazard all that most xarray users want) is to be able to import some class from awkward that offers only the simplest possible Ragged Array, that conforms to the data API standard (i.e. defines shape and dtype).
Oh, if you're replacing variable-length dimensions with the maximum length in that dimension, what about actually padding the array with ak.pad_none?
What's the benefit of doing this over just using ak.num on each axis like I did above?
That uses all the memory of a padded array, but it's what people use now if they want to convert Awkward data into non-Awkward data (maybe passing the final step to ak.to_numpy).
The passing on of behavior is just to not break applications that depend on it. I did that just for correctness.
Monkey-patching will add the desired properties to the ak.Array class (a part of the problem I haven't addressed yet), though it would do so globally for all ak.Arrays, including those that are not simple ragged arrays. The function I wrote above would take a general array and simplify it to a ragged array or die trying.
Ragged array is not a specialized subset of types within Awkward Array. There are ak.* functions that would take you out of this subset. However (thinking it through...) I don't think slices, ufuncs, or reducers would take you out of this subset.
Given that you have an array of only list-type, regular-type, and numpy-type (which the prepare function, above, guarantees), here's a one-pass function to get the dtype and shape:
(This recursively_apply is a Swiss Army knife for restructuring or getting data out of layouts that we use internally all over the codebase, and intend to make public in v2: scikit-hep/awkward#516.)
To answer your question about monkey-patching, I think it would be best to make a wrapper. You don't want to give allak.Array instances properties named shape and dtype, since those properties won't make sense for general types. This is exactly the reason we had to back off on making ak.Array inherit from pandas.api.extensions.ExtensionArray: Pandas wanted it to have methods with names and behaviors that would have been misleading for Awkward Arrays. We think we'll be able to reintroduce Awkward Arrays as Pandas columns by wrapping them—that's what we're doing differently this time.
It keeps an _array (ak.Array), performs all internal operations on the ak.Array level (unwrapping RaggedArrays if necessary), but returns RaggedArrays (if the output is not scalar). It handles only the operations you want it to: this one handles all the complex slicing, NumPy ufuncs, and one reducer, sum.
Thus, it can act as a gatekeeper of what kinds of operations are allowed: ak.* won't recognize RaggedArray, which is good because some ak.* functions would take you out of this "ragged array" subset of types. You can add some non-ufunc NumPy functions with __array_function__, but only the ones that make sense for this subset of types.
I meant to say something earlier about why we go for full generality in types: it's because some of the things we want to do, such as ak.cartesian, require more complex types, and as soon as one function needs it, the whole space needs to be enlarged. For the first year of Awkward Array use, most users wanted it for plain ragged arrays (based on their bug-reports and questions), but after about a year, they were asking about missing values and records, too, because you eventually need them unless you intend to work within a narrow set of functions.
Union arrays are still not widely used, but they can come from some file formats. Some GeoJSON files that I looked at had longitude, latitude points in different list depths because some were points and some were polygons, disambiguated by a string label. That's not good to work with (we can't handle that in Numba, for instance), but if you select all points with some slice, put them in one array, and select all polygons with another slice, putting them in their own array, these each become trivial unions, and that's why I added the squashing of trivial unions to the prepare function example above.
Reacted by Tom Nicholas, Deepak Cherian and Dominik Stańczak-Marikin
Ragged array is not a specialized subset of types within Awkward Array. There are ak.* functions that would take you out of this subset. However (thinking it through...) I don't think slices, ufuncs, or reducers would take you out of this subset.
This is an important point which I meant to ask about earlier. We need a RaggedArray class which always returns other RaggedArray instances (i.e. the set of ragged arrays is closed under the set of numpy-like methods / functions that xarray might call upon it).
To answer your question about monkey-patching, I think it would be best to make a wrapper. You don't want to give all ak.Array instances properties named shape and dtype, since those properties won't make sense for general types. This is exactly the reason we had to back off on making ak.Array inherit from pandas.api.extensions.ExtensionArray: Pandas wanted it to have methods with names and behaviors that would have been misleading for Awkward Arrays.
If you want a RaggedArray class that is more specific (i.e. defines more attributes) than awkward.Array, then surely the "correct" thing to do would be be to subclass though? I mean for eventual integration of RaggedArray within awkward's codebase.
Thus, it can act as a gatekeeper of what kinds of operations are allowed: ak.* won't recognize RaggedArray, which is good because some ak.* functions would take you out of this "ragged array" subset of types. You can add some non-ufunc NumPy functions with __array_function__, but only the ones that make sense for this subset of types.
That makes sense. And if you subclassed then I guess you would also need to change those ak.* functions to not accept RaggedArray, so maybe wrapping is better...
Thanks for the wrapping example! I think there is a bug with your .shape method though - if I put your code snippets in a file then they return the wrong results:
In [1]: fromraggedimportRaggedArrayIn [2]: ra=RaggedArray([[1, 2, 3], [4, 5]])
In [3]: ra.ndimOut[3]: 1In [4]: ra.shapeOut[4]: [3]
(I expected 2 and (2, 3) respectively). I think perhaps context["shape"] is being overwritten as it recurses through the data structure, when it should be being appended?
I would really like to try testing the RaggedArray class with our WIP public framework for testing duck array compatiblity (#6894). If we can get a very basic wrapper then I could make a PR to add RaggedArray to awkward, and import xarray's new tests to test it with.
If you want a RaggedArray class that is more specific (i.e. defines more attributes) than awkward.Array, then surely the "correct" thing to do would be be to subclass though?
It shouldn't be a subclass because it doesn't satisfy a substitution principle: ak.combinations(array: ak.Array, n: int) -> ak.Array, but ak.combinations(array: RaggedArray, n: int) -> ⊥ (at best, would raise an exception because RaggedArray isn't closed under ak.combinations).
Since RaggedArray can't be used everywhere that an ak.Array can be used, it shouldn't be a subclass.
I mean for eventual integration of RaggedArray within awkward's codebase.
Oh.......
I hadn't been thinking that RaggedArray is something we'd put in the general Awkward Array library. I was thinking of it only as a way to define "the subset of Awkward Arrays that xarray uses," which would live in xarray.
I don't want to introduce another level of type-specificity to the system, since that would make things harder to understand. (Imagine reading the docs and it says, "You can apply this function to ak.Array, but not to ak.RaggedArray." Or "this is an ak.Array that happens to be ragged, but not a ak.RaggedArray.")
So let me rethink your original idea of adding shape and dtype properties to all ak.Arrays. Perhaps they should raise exceptions when the array is not a ragged array? People don't usually expect properties to raise exceptions, and you really need them to be properties with the exact spellings "shape" and "dtype" to get what you want.
If that point is negotiable, I could introduce an ak.shape_dtype(array) function that returns a shape and dtype if array has the right properties and raise an exception if it doesn't. That would be more normal: you're asking if it satisfies a specific constraint, and if so, to get some information about it. Then we would also be able to deal with the fact that
some people are going to want the shape to specify the maximum of "var" dimensions (what you asked for): "virtually padding",
some people are going to want the shape to specify the minimum of "var" dimensions because that tells you what upper bounds are legal to slice: "virtually truncating",
and some people are going to want the string "var" or maybe None or maybe np.nan in place of "var" dimensions because no integer is correct. Then they would have to deal with the fact that this shape is not a tuple of integers.
Or maybe the best way to present it is with a min_shape and a max_shape, whose items are equal where the array is regular.
Anyway, you can see why I'm loath to add a property to ak.Array that's just named "shape"? That has the potential for misinterpretation. (Pandas wanted arrays to have a shape that is always equal to (len(array),); if we satisfied that constraint, we couldn't satisfy yours!) In fact, "dtype" of a general array would be misleading, too, though a list of unique "dtypes" of all the leaf-nodes could be a useful thing to have. (2 shapes and n dtypes!)
But if I'm providing it as an extra function, or as a trio of properties named min_shape, max_shape, and dtypes which are all spelled differently from the shape and dtype you want, you'd then be forced to wrap it as a RaggedArray type within xarray again, anyway. Which, as a reminder, is what we're doing for Pandas: https://github.com/intake/awkward-pandas lives outside the Awkward codebase and it wraps ak.Array to put them in Series.
So in the end, I just came back to where we started: xarray would own the RaggedArray wrapper. Or it could be a third package, as awkward-pandas is to awkward and pandas.
(I expected 2 and (3, 2) respectively). I think perhaps context["shape"] is being overwritten as it recurses through the data structure, when it should be being appended?
No, I initialized it incorrectly: it should have started as
context= {"shape": [len(array)]}
and then recurse from there. My previous example also had the wrong output, but I didn't count square brackets carefully enough to have caught it. (By the way, not copying the context is why it's called "lateral"; if a copied dict is needed, it's "depth_context". I just went back and checked: yes, they're being handled appropriately.)
I fixed the code that I wrote in the comments above for posterity.
Since RaggedArray can't be used everywhere that an ak.Array can be used, it shouldn't be a subclass.
I see, makes sense.
I hadn't been thinking that RaggedArray is something we'd put in the general Awkward Array library.
Oh I was just thinking if we're building a new class that is tightly coupled to awkward.Array then it should live in awkward. (I also would like someone else to maintain it ideally! 😅 )
I was thinking of it only as a way to define "the subset of Awkward Arrays that xarray uses," which would live in xarray.
I don't think it's within scope of xarray to offer a numpy-like array class in our main library - we don't do this for any other case!
Or it could be a third package, as awkward-pandas is to awkward and pandas.
However we could definitely have a separate awkward-xarray package that lives in xarray-contrib and provides a RaggedArray class. (see pint-xarray for something sort of similar.) That seems fine, all it takes is some keen bean to take our prototypes here and turn them into something usable...
(Imagine reading the docs and it says, "You can apply this function to ak.Array, but not to ak.RaggedArray." Or "this is an ak.Array that happens to be ragged, but not a ak.RaggedArray.")
Yeah that wouldn't be ideal.
(Digression: From my perspective part of the problem is that merely generalising numpy arrays to be ragged would have been useful for lots of people, but awkward.Array goes a lot further. It also generalises the type system, adds things like Records, and possibly adds xarray-like features. That puts awkward.Array in a somewhat ill-defined place within the wider scientific python ecosystem: it's kind of a numpy-like duck array, but can't be treated as one, it's also a more general type system, and it might even get features of higher-level data structures like xarray.)
some people are going to want the shape to specify the maximum of "var" dimensions (what you asked for): "virtually padding",
some people are going to want the shape to specify the minimum of "var" dimensions because that tells you what upper bounds are legal to slice: "virtually truncating",
and some people are going to want the string "var" or maybe None or maybe np.nan in place of "var" dimensions because no integer is correct. Then they would have to deal with the fact that this shape is not a tuple of integers.
That's very interesting. I'm not immediately sure which of those would be best for xarray wrapping - I think it's plausible that we could eventually support any of those options... ((3) through the issues Deepak linked to (#5168, #2801).)
I fixed the code that I wrote in the comments above for posterity.
Thanks for fixing that, and for all the explanations!
I am tempted to suggest that the right way to handle Awkward array is to treat "var" dimensions similar to NumPy's structured dtypes, with shape only handling non-variable dimensions. The uniform dimensions are the only ones for which Xarray's API is going to work properly out of the box, and Awkward array properly already has the right tools for working with ragged dimensions.
Either way, I would definitely encourage figuring out some actual use-cases before building this out :)
Similarly to @TomNicholas' oceanography observation data example, I was thinking about trajectories or more generally any collection of geospatial "features".
An alternative approach to "ragged" arrays (i.e., arrays of lists as described by @SimonHeybrock) would be to have regular arrays with opaque objects as elements. For example, pygeos (now merged in shapely 2.0) provides vectorized operations (numpy ufuncs) for arrays of Geometry objects (points, polylines, polygon, etc.) and is used as a backend for spatial operations in geopandas data frames (side note: that would probably be useful in xarray too).
This approach is more specific than "ragged" arrays (it heavily relies on the tools provided for dealing with the opaque objects) and in some cases it might not be ideal, e.g., when the data collected for the geospatial features is not "geometry invariant" (like for the trajectories of buoys floating and drifting in the oceans).
But perhaps both approach may co-exist in a single xarray Dataset? (e.g., feature geometry as coordinates of shapely object arrays and feature data as ragged arrays, if necessary).
This seems hard. Xarray's whole model is built assuming that dims has type Mapping[Hashable, int]. It also breaks our normal concept of alignment, which we need to put coordinate variables in DataArrays alongside data variables.
Scipp's ragged data can be considered a "partial sorting", to build a sort of "index".
Sounds like a good use case for Xarray explicit indexes to support some of Scipp's features. I can imagine a "ragged" array coordinate coupled with a custom Xarray index to perform data selection (slicing) and alignment along the "regular" dimension(s) of the ragged array.
Just adding another use-case for this discussion, Argo float data. These are oceanographic instruments that vertically profile the ocean, and the length of each profile changes: https://argopy.readthedocs.io/en/latest/data_fetching.html
Reacted by Tom Nicholas, Angus Hollands and Guillaume Maze
jpivarski commented on Aug 10 It shouldn't be a subclass because it doesn't satisfy a substitution principle: ak.combinations(array: ak.Array, n: int) -> ak.Array, but ak.combinations(array: RaggedArray, n: int) -> ⊥ (at best, would raise an exception because RaggedArray isn't closed under ak.combinations).
A question from the perspective of Zarr (v3), does it make sense to think of this potential RaggedArray as a base extension that AwkwardArray could then build on top of? (i.e. the reverse) Or more something to keep separate and it's just a matter of the same library could be used to read (de/serialize) either?
More generally, ❤️ for all of this, and interested to see how I/we can possibly help.
Thank you for the detailed discussion and thank you @TomNicholas for pointing it out to me. I read the thread last week and have been digesting it. There are many details that go over my head and will keep re-reading them to develop a better understanding of the problem.
Two weeks ago I started working part-time on CloudDrift. This is an NSF EarthCube-funded project led by @selipot. @philippemiron was the lead developer in the first year of the project and he laid the foundation of the data structure that we need and example notebooks. The project's purpose is to make working with Lagrangian data (primarily ocean but generalizable to other kinds) easier for scientists who consume such data while also optimizing the storage of such data. This is use case 1 in Tom's list of use cases here.
Clouddrift currently provides an implementation of a RaggedArray class. Once instantiated with user-provided data (a collection of variable-length arrays, either manually or from dataset-specific adapters), this class allows you to get either an awkward.Array or an xarray.Dataset, and from there store to a parquet file (via awkward) or a NetCDF file (via xarray). On either end (awkward or xarray), you get the indexing convenience that comes with these libraries, and once indexed you get the NumPy set of functionality. So, RaggedArray serves as an intermediate structure to get you to an awkward.Array or an xarray.Dataset representations of the data, but it does not itself wrap either.
Other goals of the project include providing example and tutorial notebooks, writing adapters for canonical ocean Lagrangian datasets, writing methods for oceanographic diagnostics, and more general developer/scientist advocacy kind of work.
I am very much interested in making our RaggedArray class more generally useful in other fields and use cases. I am also interested in designing and implementing it toward a closer integration with xarray, since there seems to be an appetite for that. clouddrift.RaggedArray becoming part of xarray (via core or contrib or otherwise) would be a success story for us. However, I will need help from all of you here given your deep understanding of the internals of awkward and xarray to make it work. I'll be paid half of my day-job salary to work on this for the next two years. So, at least you know that somebody will be committing time to it, but again, I will need guidance.
What do you think should be the next step? Should we plan a video call to explore options?
This sounds good to me! RaggedArray can be a well-defined subset of types that have clear use-cases (the ones @TomNicholas listed). The thing I was worried about is that most functions in Awkward Array ignore the boundary line between RaggedArray and non-RaggedArray; defining it as a new type with its own collection of functions (or just methods) in CloudDrift isolates it in a way that you can ensure that your functions stay within that boundary.
To represent a RaggedArray without wrapping an Awkward Array, you can store it as a sequence of n offsets arrays for depth-n lists of lists and numerical contents. (Whereas an Awkward Array is a tree of 1D buffers, RaggedArray can be a sequence of 1D buffers.) We can then make sure that when you do want to convert it to and from Awkward Arrays, you can do so in a zero-copy way. That way, if you want to define some functions, particularly __getitem__ with complex slices, by calling the corresponding Awkward function, you can do it by converting and then converting back, knowing that the conversion overhead is all O(1).
(Same for xarray.Dataset!)
I'm in favor of a video call meeting to discuss this. In general, I'm busiest on U.S. mornings, on Wednesday and Thursday, but perhaps you can send a when2meet or equivalent poll?
One thing that could be discussed in writing (maybe more easily) is what data types you would consider in scope for RaggedArray. (I've reminded myself of the use-cases above, but it doesn't fully answer this question.)
That is,
You'll want the numerical data, the end of your sequence of 1D buffers, to be arbitrary NumPy types or some reasonable subset. That's a given.
You'll want ragged arrays of those. Ragged arrays can be represented in several ways:
* offsets buffer whose length is 1 more than the length of the array. Every neighboring pair of integers is the starting and stopping index of the content of a nested list. The integers must be non-decreasing since they are the cumulative sum of list lengths. This is pyarrow's ListArray and Awkward's ListOffsetArray.
* starts and stops buffers of the same length as the length of the array. Every starts[i] and stops[i] is the starting and stopping index of the content of a nested list. These may be equivalent to an offsets buffer or they can be in a random order, not cover all of the content, or cover the content multiple times. The value of such a thing is that reordering, filtering, or duplicating the set of lists is not an operation that needs to propagate through every level of the sequence of buffers, so it's good for intermediate calculations. pyarrow has no equivalent, but it's Awkward's ListArray.
* a parents buffer with the same length as the content; each parents[j] indicates which list j the content[j] belongs to. They may be contiguous or not. This is a Normal Form in database renormalization, and it's useful for some operations that propagate upward, such as reducers (sum, max, etc.). Neither pyarrow nor Awkward have this as a native type. It can't encode empty lists at the end of an array, so another integer would be needed to preserve that information.
Will you want regular arrays? If some dimensions of the array are variable-length (ragged) and some are fixed-length, that can be accomplished by adding a node without any offsets/starts/stops/parents buffer, just an integer size. Multiple fixed dimensions could be multiple nested nodes (which is easier) or a tuple of integers shape. Is the nested data constrained to be contiguous or would you also have strides? (They can't be byte-strides, as in NumPy, they have to count numbers of items.) Can the size be zero? If so, you'll need another integer for the length of this array. This is pyarrow's FixedSizeListArray and Awkward's RegularArray (neither of which have strides, and only Awkward allows size=0).
Will you want to allow for missing data? Only missing numerical values or also missing lists? Some functions naturally return missing data, such as max of an axis with variable length lists, some of which can be zero length. There's a variety of ways to represent missing data, though in a system of only nested lists, a bit-mask or byte-mask is probably best. All pyarrow node types are potentially missing, represented by a bit-mask, and Awkward as four node types for missing data, including BitMaskedArray and ByteMaskedArray.
You don't want record-types or union-types, so the only questions are how to implement (2) and whether you want (3) and (4). Including a type, such as missing data, allows for more function return values but obliges you to consider that type for all function arguments. You'll want to choose carefully how you close your system.
(Maybe this block of details can be copied to an issue where you're doing the development of RaggedArray in CloudDrift. It got longer than I had intended.)
A possibly relevant distinction that had not occurred to me previously is the example by @milancurcic: If I understand this correctly then this type of data is essentially an array of variable-length time-series (essentially a list of lists?), i.e., there is an order within each inner list. This is conceptually different from the data I am typically dealing with, where each inner list is a list of records without specific ordering.
That sounds extremely exciting @milancurcic ! Someone dedicated who wants to make a widely-useful tool is exactly what is needed.
I think there are many technical questions (and tbh I didn't really follow a lot of the details of your last comment @jpivarski), but the answers to those will likely depend on intended use cases.
I'm happy to attend a video call to discuss this, and think that organising one with people interested in ragged arrays and xarray across disciplines would be a sensible next step. (You should also advertise such a meeting on the pangeo discourse - we could start a new pangeo working group like this if it goes well.)
What do you think should be the next step? Should we plan a video call to explore options?
Everyone who is interested in this, but particularly @milancurcic, please fill out this poll: https://www.when2meet.com/?17481732-uGwNn and we'll meet by Zoom (URL to be distributed later) to talk about RaggedArray. I'll pick a best time from these responses on Monday.
@milancurcic, @joshmoore, and I are all available on Thursday, November 3 at 11am U.S. Central (12pm U.S. Eastern/Florida, 5pm Central European/Germany: note the unusual U.S.-Europe difference this week, 16:00 UTC). Let's meet then!
I sent a Google calendar invitation to both of you at that time, which contains a Zoom URL. If anyone else is interested, let me know and I'll send you the Zoom URL as well (just not on a public GitHub comment).
Activity
jpivarski commentedon Jul 29, 2020
I'm linking myself here, to follow this: @jpivarski.
crusaderky commentedon Aug 2, 2020
I think that xarray should offer a "compatibility test toolkit" to any numpy-like, NEP18-compatible library that wants to integrate with it.
Instead of having a module full of tests specifically for pint, one for sparse, one for cupy, one for awkward, etc. etc. etc. those projects could just write a minimal test module like this:
which would automatically expand into a comprehensive suite of tests thanks to pytest parameterize/fixture magic.
this would allow developers of numpy-like libraries to just test their package vs what's expected from a generic NEP-18 compliant package.
jpivarski commentedon Oct 12, 2020
Copied from https://gitter.im/pangeo-data/Lobby :
I've been using Xarray with argopy recently, and the immediate value I see is the documentation of columns, which is semi-lacking in Awkward (one user has been passing this information through an Awkward tree as a scikit-hep/awkward#422). I should also look into Xarray's indexing, which I've always seen as being the primary difference between NumPy and Pandas; Awkward Array has no indexing, though every node has an optional Identities which would be used to track such information through Awkward manipulations—Identities would have a bijection with externally supplied indexes. They haven't been used for anything yet.
Although the elevator pitch for Xarray is "n-dimensional Pandas," it's rather different, isn't it? The contextual metadata is more extensive than anything I've seen in Pandas, and Xarray can be partitioned for out-of-core analysis: Xarray wraps Dask, unlike Dask's array collection, which wraps NumPy. I had troubles getting Pandas to wrap Awkward array (scikit-hep/awkward#350 ), but maybe these won't be issues for Xarray.
One last thing (in this very rambly message): the main difficulty I think we would have in that is that Awkward Arrays don't have shape and dtype, since those define a rectilinear array of numbers. The data model is Datashape plus union types. There is a sense in which ndim is defined: the number of nested lists before reaching the first record, which may split it into different depths for each field, but even this can be ill-defined with union types:
So if we wanted to have an Xarray of Awkward Arrays, we'd have to take stock of all the assumptions Xarray makes about the arrays it contains.
TomNicholas commentedon Jul 30, 2022
So I actually think we can do this, with some caveats.
I recently found a cool dataset with ragged-like data which has rekindled my interest in this interfacing, and given me a real example to try it out with.
As far as I understand it the main problem is that awkward arrays don't define a
shapeordtypeattribute. Instead they follow a different model (the "datashape" model). Xarray expectsshapeanddtypeto be defined, and given that those attributes are in the data API standard, this is a pretty reasonable expectation for most cases.(There is a useful discussion here on the data-apis consortium repo about why awkward arrays don't define these attributes in general.)
Conceptually though, it seems to me that
shapeanddtypedo make sense for Awkward arrays, at least for some subset of them, because Awkward's "type" is clearly related to the normal notion ofshapeanddtype.Let's take an Awkward array that can be coerced directly to a numpy array:
Here there is a clear correspondence: the first axis of the awkward array has length 2, and because in this case the second axis has a consistent length of 3, we can coerce this to a numpy array with
shape=(2,3). The dtype also makes sense, because in this case the awkward array only contains data of one type, anint64.Now imagine a "ragged" (or "jagged") array, which is like a numpy array except that the lengths along one (or more) of the axes can be variable. Awkward allows this, e.g.
but a direct coercion to numpy will fail.
However we still conceptually have a "shape". It's either
(2, "var"), where "var" means a variable length across the other axes, or alternatively we could say the shape is(2, 4), where4is simply the maximum length along the variable-length axis. The latter interpretation is kind of similar to sparse arrays.In the second case you can still read off the dtype too. However awkward also allows "Union types", which basically means that one array can contain data of multiple numpy dtypes. Unfortunately this seems to completely break the numpy / xarray model, but we can completely ignore this problem if we simply say that xarray should only try to wrap awkward arrays with non-Union types. I think that's okay - a ragged-length array with a fixed dtype would still be extremely useful!
So if we want to wrap an (non-union type) awkward array instance like
raggedin xarray we have to do one of two things:This seems hard. Xarray's whole model is built assuming that
dimshas typeMapping[Hashable, int]. It also breaks our normal concept of alignment, which we need to put coordinate variables in DataArrays alongside data variables.It would also mean a big change to xarray in order to support one unusual type of array, that goes beyond the data API standard. That breaks xarray's general design philosophy of providing a general wrapper and delegating to domain-specific array implementations / backends / etc. for specificity.
shapeanddtypeon Awkward arraysThis doesn't seem as hard, at least for non-union type awkward arrays. In fact this crude monkey-patching seems to mostly work:
Now if we make the same ragged array but with the monkey-patched class, we have a sensible return value for
dtype,shape, andsize, which means that the xarray constructors will accept our Array now!Promising...
Let's try indexing:
That's what should happen - xarray delegates the indexing to the underlying array, which throws an error if there is a problem.
Arithmetic also seems to work
But we hit snags with numpy functions
This seems fixable though.
In fact I think if we changed #6845 (@dcherian) then this alternative would already work
Suggestion: How about awkward offer a specialized array class which uses the same fast code underneath but disallows Union types, and follows the array API standard, implementing
shape,dtypeetc. as described above. That should then "just work" in xarray, in the same way thatsparsearrays already do.Am I missing anything here? @jpivarski
tl;dr We probably could support awkward arrays, at least instances where all values have the same dtype.
dcherian commentedon Aug 2, 2022
Cool experiment Tom.
This is somewhat similar to supporting nan-shaped dask arrays (#5168, #2801).
jpivarski commentedon Aug 2, 2022
Hi! I will be looking deeply into this when I get back from traveling (next week). Just to let you know that I saw this and I'm interested. Thanks!
jpivarski commentedon Aug 8, 2022
You mentioned union arrays, but for completeness, the type system in Awkward Array has
issubclass(dtype.type, (np.bool_, np.number, np.datetime64, np.timedelta64))(including complex)You're interested in a subset of this type system, but that subset doesn't just exclude unions, it also excludes records. If you have an xarray, you don't need top-level records since those could just be the columns of an xarray, but some data source might provide records nested within variable-length lists (very common in HEP) or other nesting. It would have to be explicitly excluded.
That leaves the possibility of missing lists and missing numeric primitives. Missing lists could be turned into empty lists (Google projects like Protocol Buffers often make that equivalence) and missing numbers could be turned into NaN if you're willing to lose integer-ness.
Here's a way to determine if an
array(Python typeak.Array) is in that subset and to pre-process it, ensuring that you only have numbers, variable-length, and regular-length lists (in Awkward version 2, so note the "_v2"):It should catch all the cases and doesn't rely on string-processing the type's DataShape representation.
Given that you're working within that subset, it would be possible to define
shapewith some token for the variable-length dimensions anddtype. I can follow up with another message (I have to deal with something else at the moment).Oh, if you're replacing variable-length dimensions with the maximum length in that dimension, what about actually padding the array with ak.pad_none?
The above would have to be expanded to get every
axis, but it makes all nested lists have the length of the longest one by padding withNone, then replaces thoseNonevalues withnp.nan. That uses all the memory of a padded array, but it's what people use now if they want to convert Awkward data into non-Awkward data (maybe passing the final step to ak.to_numpy).TomNicholas commentedon Aug 8, 2022
That's very helpful, thank you!
(FWIW I find the "behavior" stuff very confusing in general, even after reading the docs page on it. I don't really understand why I can't just reimplement my monkey-patched example above by subclassing
ak.Array, or should I be wrapping it?)How would I do this without monkey-patching?
All I really want (and I hazard all that most xarray users want) is to be able to import some class from
awkwardthat offers only the simplest possible Ragged Array, that conforms to the data API standard (i.e. definesshapeanddtype).What's the benefit of doing this over just using
ak.numon each axis like I did above?I can see that this might be useful in xarray's
.to_numpymethods though.This is exciting though @jpivarski !
jpivarski commentedon Aug 8, 2022
The passing on of behavior is just to not break applications that depend on it. I did that just for correctness.
Monkey-patching will add the desired properties to the
ak.Arrayclass (a part of the problem I haven't addressed yet), though it would do so globally for allak.Arrays, including those that are not simple ragged arrays. The function I wrote above would take a general array and simplify it to a ragged array or die trying.Ragged array is not a specialized subset of types within Awkward Array. There are
ak.*functions that would take you out of this subset. However (thinking it through...) I don't think slices, ufuncs, or reducers would take you out of this subset.More later...
jpivarski commentedon Aug 8, 2022
Given that you have an array of only list-type, regular-type, and numpy-type (which the
preparefunction, above, guarantees), here's a one-pass function to get the dtype and shape:Here's the application on an array of mixed regular and irregular lists:
(This
recursively_applyis a Swiss Army knife for restructuring or getting data out of layouts that we use internally all over the codebase, and intend to make public in v2: scikit-hep/awkward#516.)To answer your question about monkey-patching, I think it would be best to make a wrapper. You don't want to give all
ak.Arrayinstances properties namedshapeanddtype, since those properties won't make sense for general types. This is exactly the reason we had to back off on makingak.Arrayinherit frompandas.api.extensions.ExtensionArray: Pandas wanted it to have methods with names and behaviors that would have been misleading for Awkward Arrays. We think we'll be able to reintroduce Awkward Arrays as Pandas columns by wrapping them—that's what we're doing differently this time.Here's a start of a wrapper:
It keeps an
_array(ak.Array), performs all internal operations on theak.Arraylevel (unwrapping RaggedArrays if necessary), but returns RaggedArrays (if the output is not scalar). It handles only the operations you want it to: this one handles all the complex slicing, NumPy ufuncs, and one reducer,sum.Thus, it can act as a gatekeeper of what kinds of operations are allowed:
ak.*won't recognize RaggedArray, which is good because someak.*functions would take you out of this "ragged array" subset of types. You can add some non-ufunc NumPy functions with__array_function__, but only the ones that make sense for this subset of types.I meant to say something earlier about why we go for full generality in types: it's because some of the things we want to do, such as ak.cartesian, require more complex types, and as soon as one function needs it, the whole space needs to be enlarged. For the first year of Awkward Array use, most users wanted it for plain ragged arrays (based on their bug-reports and questions), but after about a year, they were asking about missing values and records, too, because you eventually need them unless you intend to work within a narrow set of functions.
Union arrays are still not widely used, but they can come from some file formats. Some GeoJSON files that I looked at had longitude, latitude points in different list depths because some were points and some were polygons, disambiguated by a string label. That's not good to work with (we can't handle that in Numba, for instance), but if you select all points with some slice, put them in one array, and select all polygons with another slice, putting them in their own array, these each become trivial unions, and that's why I added the squashing of trivial unions to the
preparefunction example above.TomNicholas commentedon Aug 9, 2022
Thanks for the huge response there @jpivarski !
This is an important point which I meant to ask about earlier. We need a
RaggedArrayclass which always returns otherRaggedArrayinstances (i.e. the set of ragged arrays is closed under the set of numpy-like methods / functions that xarray might call upon it).If you want a
RaggedArrayclass that is more specific (i.e. defines more attributes) thanawkward.Array, then surely the "correct" thing to do would be be to subclass though? I mean for eventual integration ofRaggedArraywithin awkward's codebase.That makes sense. And if you subclassed then I guess you would also need to change those
ak.*functions to not acceptRaggedArray, so maybe wrapping is better...Thanks for the wrapping example! I think there is a bug with your
.shapemethod though - if I put your code snippets in a file then they return the wrong results:(I expected
2and(2, 3)respectively). I think perhapscontext["shape"]is being overwritten as it recurses through the data structure, when it should be being appended?I would really like to try testing the
RaggedArrayclass with our WIP public framework for testing duck array compatiblity (#6894). If we can get a very basic wrapper then I could make a PR to addRaggedArrayto awkward, and import xarray's new tests to test it with.jpivarski commentedon Aug 10, 2022
It shouldn't be a subclass because it doesn't satisfy a substitution principle:
ak.combinations(array: ak.Array, n: int) -> ak.Array, butak.combinations(array: RaggedArray, n: int) -> ⊥(at best, would raise an exception becauseRaggedArrayisn't closed underak.combinations).Since
RaggedArraycan't be used everywhere that anak.Arraycan be used, it shouldn't be a subclass.Oh.......
I hadn't been thinking that RaggedArray is something we'd put in the general Awkward Array library. I was thinking of it only as a way to define "the subset of Awkward Arrays that xarray uses," which would live in xarray.
I don't want to introduce another level of type-specificity to the system, since that would make things harder to understand. (Imagine reading the docs and it says, "You can apply this function to ak.Array, but not to ak.RaggedArray." Or "this is an ak.Array that happens to be ragged, but not a ak.RaggedArray.")
So let me rethink your original idea of adding
shapeanddtypeproperties to all ak.Arrays. Perhaps they should raise exceptions when the array is not a ragged array? People don't usually expect properties to raise exceptions, and you really need them to be properties with the exact spellings "shape" and "dtype" to get what you want.If that point is negotiable, I could introduce an
ak.shape_dtype(array)function that returns a shape and dtype ifarrayhas the right properties and raise an exception if it doesn't. That would be more normal: you're asking if it satisfies a specific constraint, and if so, to get some information about it. Then we would also be able to deal with the fact thatshapeto specify the maximum of "var" dimensions (what you asked for): "virtually padding",shapeto specify the minimum of "var" dimensions because that tells you what upper bounds are legal to slice: "virtually truncating","var"or maybeNoneor maybenp.nanin place of "var" dimensions because no integer is correct. Then they would have to deal with the fact that thisshapeis not a tuple of integers.Or maybe the best way to present it is with a
min_shapeand amax_shape, whose items are equal where the array is regular.Anyway, you can see why I'm loath to add a property to ak.Array that's just named "
shape"? That has the potential for misinterpretation. (Pandas wanted arrays to have ashapethat is always equal to(len(array),); if we satisfied that constraint, we couldn't satisfy yours!) In fact, "dtype" of a general array would be misleading, too, though a list of unique "dtypes" of all the leaf-nodes could be a useful thing to have. (2 shapes and n dtypes!)But if I'm providing it as an extra function, or as a trio of properties named
min_shape,max_shape, anddtypeswhich are all spelled differently from theshapeanddtypeyou want, you'd then be forced to wrap it as a RaggedArray type within xarray again, anyway. Which, as a reminder, is what we're doing for Pandas: https://github.com/intake/awkward-pandas lives outside the Awkward codebase and it wraps ak.Array to put them in Series.So in the end, I just came back to where we started: xarray would own the RaggedArray wrapper. Or it could be a third package, as awkward-pandas is to awkward and pandas.
No, I initialized it incorrectly: it should have started as
and then recurse from there. My previous example also had the wrong output, but I didn't count square brackets carefully enough to have caught it. (By the way, not copying the context is why it's called "lateral"; if a copied dict is needed, it's "depth_context". I just went back and checked: yes, they're being handled appropriately.)
I fixed the code that I wrote in the comments above for posterity.
TomNicholas commentedon Aug 10, 2022
I see, makes sense.
Oh I was just thinking if we're building a new class that is tightly coupled to
awkward.Arraythen it should live inawkward. (I also would like someone else to maintain it ideally! 😅 )I don't think it's within scope of xarray to offer a numpy-like array class in our main library - we don't do this for any other case!
However we could definitely have a separate
awkward-xarraypackage that lives in xarray-contrib and provides aRaggedArrayclass. (see pint-xarray for something sort of similar.) That seems fine, all it takes is some keen bean to take our prototypes here and turn them into something usable...Yeah that wouldn't be ideal.
(Digression: From my perspective part of the problem is that merely generalising numpy arrays to be ragged would have been useful for lots of people, but
awkward.Arraygoes a lot further. It also generalises the type system, adds things like Records, and possibly adds xarray-like features. That putsawkward.Arrayin a somewhat ill-defined place within the wider scientific python ecosystem: it's kind of a numpy-like duck array, but can't be treated as one, it's also a more general type system, and it might even get features of higher-level data structures like xarray.)That's very interesting. I'm not immediately sure which of those would be best for xarray wrapping - I think it's plausible that we could eventually support any of those options... ((3) through the issues Deepak linked to (#5168, #2801).)
Thanks for fixing that, and for all the explanations!
shoyer commentedon Aug 10, 2022
I am tempted to suggest that the right way to handle Awkward array is to treat "var" dimensions similar to NumPy's structured dtypes, with
shapeonly handling non-variable dimensions. The uniform dimensions are the only ones for which Xarray's API is going to work properly out of the box, and Awkward array properly already has the right tools for working with ragged dimensions.Either way, I would definitely encourage figuring out some actual use-cases before building this out :)
9 remaining items
benbovy commentedon Aug 25, 2022
Similarly to @TomNicholas' oceanography observation data example, I was thinking about trajectories or more generally any collection of geospatial "features".
An alternative approach to "ragged" arrays (i.e., arrays of lists as described by @SimonHeybrock) would be to have regular arrays with opaque objects as elements. For example, pygeos (now merged in shapely 2.0) provides vectorized operations (numpy ufuncs) for arrays of
Geometryobjects (points, polylines, polygon, etc.) and is used as a backend for spatial operations in geopandas data frames (side note: that would probably be useful in xarray too).This approach is more specific than "ragged" arrays (it heavily relies on the tools provided for dealing with the opaque objects) and in some cases it might not be ideal, e.g., when the data collected for the geospatial features is not "geometry invariant" (like for the trajectories of buoys floating and drifting in the oceans).
But perhaps both approach may co-exist in a single xarray Dataset? (e.g., feature geometry as coordinates of shapely object arrays and feature data as ragged arrays, if necessary).
Sounds like a good use case for Xarray explicit indexes to support some of Scipp's features. I can imagine a "ragged" array coordinate coupled with a custom Xarray index to perform data selection (slicing) and alignment along the "regular" dimension(s) of the ragged array.
pbranson commentedon Aug 25, 2022
Just adding another use-case for this discussion, Argo float data. These are oceanographic instruments that vertically profile the ocean, and the length of each profile changes:
https://argopy.readthedocs.io/en/latest/data_fetching.html
jakirkham commentedon Sep 20, 2022
cc @ivirshup @joshmoore (who may be interested in this as well)
joshmoore commentedon Sep 22, 2022
Definitely interested and interestedly watching 🍿
A question from the perspective of Zarr (v3), does it make sense to think of this potential
RaggedArrayas a base extension thatAwkwardArraycould then build on top of? (i.e. the reverse) Or more something to keep separate and it's just a matter of the same library could be used to read (de/serialize) either?More generally, ❤️ for all of this, and interested to see how I/we can possibly help.
milancurcic commentedon Oct 18, 2022
Hi All,
Thank you for the detailed discussion and thank you @TomNicholas for pointing it out to me. I read the thread last week and have been digesting it. There are many details that go over my head and will keep re-reading them to develop a better understanding of the problem.
Two weeks ago I started working part-time on CloudDrift. This is an NSF EarthCube-funded project led by @selipot. @philippemiron was the lead developer in the first year of the project and he laid the foundation of the data structure that we need and example notebooks. The project's purpose is to make working with Lagrangian data (primarily ocean but generalizable to other kinds) easier for scientists who consume such data while also optimizing the storage of such data. This is use case 1 in Tom's list of use cases here.
Clouddrift currently provides an implementation of a
RaggedArrayclass. Once instantiated with user-provided data (a collection of variable-length arrays, either manually or from dataset-specific adapters), this class allows you to get either anawkward.Arrayor anxarray.Dataset, and from there store to a parquet file (via awkward) or a NetCDF file (via xarray). On either end (awkward or xarray), you get the indexing convenience that comes with these libraries, and once indexed you get the NumPy set of functionality. So,RaggedArrayserves as an intermediate structure to get you to anawkward.Arrayor anxarray.Datasetrepresentations of the data, but it does not itself wrap either.Other goals of the project include providing example and tutorial notebooks, writing adapters for canonical ocean Lagrangian datasets, writing methods for oceanographic diagnostics, and more general developer/scientist advocacy kind of work.
I am very much interested in making our
RaggedArrayclass more generally useful in other fields and use cases. I am also interested in designing and implementing it toward a closer integration with xarray, since there seems to be an appetite for that.clouddrift.RaggedArraybecoming part of xarray (via core or contrib or otherwise) would be a success story for us. However, I will need help from all of you here given your deep understanding of the internals of awkward and xarray to make it work. I'll be paid half of my day-job salary to work on this for the next two years. So, at least you know that somebody will be committing time to it, but again, I will need guidance.What do you think should be the next step? Should we plan a video call to explore options?
jpivarski commentedon Oct 18, 2022
This sounds good to me!
RaggedArraycan be a well-defined subset of types that have clear use-cases (the ones @TomNicholas listed). The thing I was worried about is that most functions in Awkward Array ignore the boundary line betweenRaggedArrayand non-RaggedArray; defining it as a new type with its own collection of functions (or just methods) in CloudDrift isolates it in a way that you can ensure that your functions stay within that boundary.To represent a
RaggedArraywithout wrapping an Awkward Array, you can store it as a sequence of n offsets arrays for depth-n lists of lists and numerical contents. (Whereas an Awkward Array is a tree of 1D buffers,RaggedArraycan be a sequence of 1D buffers.) We can then make sure that when you do want to convert it to and from Awkward Arrays, you can do so in a zero-copy way. That way, if you want to define some functions, particularly__getitem__with complex slices, by calling the corresponding Awkward function, you can do it by converting and then converting back, knowing that the conversion overhead is all O(1).(Same for
xarray.Dataset!)I'm in favor of a video call meeting to discuss this. In general, I'm busiest on U.S. mornings, on Wednesday and Thursday, but perhaps you can send a when2meet or equivalent poll?
One thing that could be discussed in writing (maybe more easily) is what data types you would consider in scope for
RaggedArray. (I've reminded myself of the use-cases above, but it doesn't fully answer this question.)That is,
*
offsetsbuffer whose length is 1 more than the length of the array. Every neighboring pair of integers is the starting and stopping index of the content of a nested list. The integers must be non-decreasing since they are the cumulative sum of list lengths. This is pyarrow'sListArrayand Awkward'sListOffsetArray.*
startsandstopsbuffers of the same length as the length of the array. Everystarts[i]andstops[i]is the starting and stopping index of the content of a nested list. These may be equivalent to anoffsetsbuffer or they can be in a random order, not cover all of the content, or cover the content multiple times. The value of such a thing is that reordering, filtering, or duplicating the set of lists is not an operation that needs to propagate through every level of the sequence of buffers, so it's good for intermediate calculations. pyarrow has no equivalent, but it's Awkward'sListArray.* a
parentsbuffer with the same length as the content; eachparents[j]indicates which listjthecontent[j]belongs to. They may be contiguous or not. This is a Normal Form in database renormalization, and it's useful for some operations that propagate upward, such as reducers (sum, max, etc.). Neither pyarrow nor Awkward have this as a native type. It can't encode empty lists at the end of an array, so another integer would be needed to preserve that information.offsets/starts/stops/parentsbuffer, just an integersize. Multiple fixed dimensions could be multiple nested nodes (which is easier) or a tuple of integersshape. Is the nested data constrained to be contiguous or would you also havestrides? (They can't be byte-strides, as in NumPy, they have to count numbers of items.) Can thesizebe zero? If so, you'll need another integer for the length of this array. This is pyarrow'sFixedSizeListArrayand Awkward'sRegularArray(neither of which havestrides, and only Awkward allowssize=0).maxof anaxiswith variable length lists, some of which can be zero length. There's a variety of ways to represent missing data, though in a system of only nested lists, a bit-mask or byte-mask is probably best. All pyarrow node types are potentially missing, represented by a bit-mask, and Awkward as four node types for missing data, includingBitMaskedArrayandByteMaskedArray.You don't want record-types or union-types, so the only questions are how to implement (2) and whether you want (3) and (4). Including a type, such as missing data, allows for more function return values but obliges you to consider that type for all function arguments. You'll want to choose carefully how you close your system.
(Maybe this block of details can be copied to an issue where you're doing the development of
RaggedArrayin CloudDrift. It got longer than I had intended.)SimonHeybrock commentedon Oct 19, 2022
A possibly relevant distinction that had not occurred to me previously is the example by @milancurcic: If I understand this correctly then this type of data is essentially an array of variable-length time-series (essentially a list of lists?), i.e., there is an order within each inner list. This is conceptually different from the data I am typically dealing with, where each inner list is a list of records without specific ordering.
TomNicholas commentedon Oct 21, 2022
That sounds extremely exciting @milancurcic ! Someone dedicated who wants to make a widely-useful tool is exactly what is needed.
I think there are many technical questions (and tbh I didn't really follow a lot of the details of your last comment @jpivarski), but the answers to those will likely depend on intended use cases.
I'm happy to attend a video call to discuss this, and think that organising one with people interested in ragged arrays and xarray across disciplines would be a sensible next step. (You should also advertise such a meeting on the pangeo discourse - we could start a new pangeo working group like this if it goes well.)
SimonHeybrock commentedon Oct 24, 2022
Also note the Ragged Array Summit on Scientific Python.
jpivarski commentedon Oct 28, 2022
Everyone who is interested in this, but particularly @milancurcic, please fill out this poll: https://www.when2meet.com/?17481732-uGwNn and we'll meet by Zoom (URL to be distributed later) to talk about RaggedArray. I'll pick a best time from these responses on Monday.
Thanks!
jpivarski commentedon Oct 31, 2022
@milancurcic, @joshmoore, and I are all available on Thursday, November 3 at 11am U.S. Central (12pm U.S. Eastern/Florida, 5pm Central European/Germany: note the unusual U.S.-Europe difference this week, 16:00 UTC). Let's meet then!
I sent a Google calendar invitation to both of you at that time, which contains a Zoom URL. If anyone else is interested, let me know and I'll send you the Zoom URL as well (just not on a public GitHub comment).
TomNicholas commentedon Nov 3, 2022
I should be able to join today as well @jpivarski ! Will need the zoom address
jpivarski commentedon Nov 3, 2022
Send me an email address, and I'll send you the Zoom URL. The email that you have listed here:
http://tom-nicholas.com/contact/
doesn't work (bounced back).
TomNicholas commentedon Nov 3, 2022
Oops - use thomas dot nicholas at columbia dot edu please!
jpivarski commentedon Dec 30, 2023
Follow up on this topic at scikit-hep/ragged#6