Skip to content

Commit c0ffceb

Browse files
authored
More efficient read_xarray implementation with from_map_batched (#63)
I created a lightweight iterator over batches called `from_map_batched`, which uses a pyarrow RecordBatchReader to stream in batches one by one. This is not the ideal of a streaming dataset, which after lots of research, requires a TabeProvider via a FFI. But, it is a memory-efficient improvement over the present implementation. This PR provides other small refactorings and reformattings.
1 parent 0577963 commit c0ffceb

3 files changed

Lines changed: 307 additions & 57 deletions

File tree

xarray_sql/df.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import pandas as pd
66
import pyarrow as pa
77
import xarray as xr
8+
from datafusion.context import ArrowStreamExportable
89

9-
Block = t.Dict[str, slice]
10+
Block = t.Dict[t.Hashable, slice]
1011
Chunks = t.Optional[t.Dict[str, int]]
1112

1213

@@ -54,7 +55,42 @@ def explode(ds: xr.Dataset, chunks: Chunks = None) -> t.Iterator[xr.Dataset]:
5455

5556

5657
def _block_len(block: Block) -> int:
57-
return np.prod([v.stop - v.start for v in block.values()])
58+
return int(np.prod([v.stop - v.start for v in block.values()]))
59+
60+
61+
def from_map_batched(
62+
func: t.Callable[[...], pd.DataFrame],
63+
*iterables,
64+
args: t.Optional[t.Tuple] = None,
65+
schema: pa.Schema = None,
66+
**kwargs,
67+
) -> pa.RecordBatchReader:
68+
"""Create a PyArrow RecordBatchReader by mapping a function over iterables.
69+
70+
This is equivalent to dask's from_map but returns a PyArrow
71+
RecordBatchReader that can be used with DataFusion. It iterates over
72+
RecordBatches which are created via the `func` one-at-a-time.
73+
74+
Args:
75+
func: Function to apply to each element of the iterables. Currently, the function
76+
must return a Pandas DataFrame.
77+
*iterables: Iterable objects to map the function over.
78+
schema: Optional schema needed for the RecordBatchReader.
79+
args: Additional positional arguments to pass to func.
80+
**kwargs: Additional keyword arguments to pass to func.
81+
82+
Returns:
83+
A PyArrow RecordBatchReader containing the stream of RecordBatches.
84+
"""
85+
if args is None:
86+
args = ()
87+
88+
def map_batches():
89+
for items in zip(*iterables):
90+
df = func(*items, *args, **kwargs)
91+
yield pa.RecordBatch.from_pandas(df, schema=schema)
92+
93+
return pa.RecordBatchReader.from_batches(schema, map_batches())
5894

5995

6096
def from_map(
@@ -109,7 +145,12 @@ def from_map(
109145
return pa.concat_tables(results)
110146

111147

112-
def read_xarray(ds: xr.Dataset, chunks: Chunks = None) -> pa.Table:
148+
def pivot(ds: xr.Dataset) -> pd.DataFrame:
149+
"""Converts an xarray Dataset to a pandas DataFrame."""
150+
return ds.to_dataframe().reset_index()
151+
152+
153+
def read_xarray(ds: xr.Dataset, chunks: Chunks = None) -> ArrowStreamExportable:
113154
"""Pivots an Xarray Dataset into a PyArrow Table, partitioned by chunks.
114155
115156
Args:
@@ -128,7 +169,11 @@ def read_xarray(ds: xr.Dataset, chunks: Chunks = None) -> pa.Table:
128169

129170
blocks = list(block_slices(ds, chunks))
130171

131-
def pivot(b: Block) -> pd.DataFrame:
132-
return ds.isel(b).to_dataframe().reset_index()
172+
def pivot_block(b: Block):
173+
return pivot(ds.isel(b))
174+
175+
schema = pa.Schema.from_pandas(pivot_block(blocks[0]))
176+
last_schema = pa.Schema.from_pandas(pivot_block(blocks[-1]))
177+
assert schema == last_schema, "Schemas must be consistent across blocks!"
133178

134-
return from_map(pivot, blocks)
179+
return from_map_batched(pivot_block, blocks, schema=schema)

0 commit comments

Comments
 (0)