Streaming, Serialization, and IPC¶
Writing and Reading Streams¶
Arrow defines two types of binary formats for serializing record batches:
Streaming format: for sending an arbitrary length sequence of record batches. The format must be processed from start to end, and does not support random access
File or Random Access format: for serializing a fixed number of record batches. Supports random access, and thus is very useful when used with memory maps
To follow this section, make sure to first read the section on Memory and IO.
Using streams¶
First, let’s create a small record batch:
In [1]: import pyarrow as pa
In [2]: data = [
...: pa.array([1, 2, 3, 4]),
...: pa.array(['foo', 'bar', 'baz', None]),
...: pa.array([True, None, False, True])
...: ]
...:
In [3]: batch = pa.record_batch(data, names=['f0', 'f1', 'f2'])
In [4]: batch.num_rows
Out[4]: 4
In [5]: batch.num_columns
Out[5]: 3
Now, we can begin writing a stream containing some number of these batches. For
this we use RecordBatchStreamWriter
, which can write to a
writeable NativeFile
object or a writeable Python object. For convenience,
this one can be created with new_stream()
:
In [6]: sink = pa.BufferOutputStream()
In [7]: with pa.ipc.new_stream(sink, batch.schema) as writer:
...: for i in range(5):
...: writer.write_batch(batch)
...:
Here we used an in-memory Arrow buffer stream (sink
),
but this could have been a socket or some other IO sink.
When creating the StreamWriter
, we pass the schema, since the schema
(column names and types) must be the same for all of the batches sent in this
particular stream. Now we can do:
In [8]: buf = sink.getvalue()
In [9]: buf.size
Out[9]: 1984
Now buf
contains the complete stream as an in-memory byte buffer. We can
read such a stream with RecordBatchStreamReader
or the
convenience function pyarrow.ipc.open_stream
:
In [10]: with pa.ipc.open_stream(buf) as reader:
....: schema = reader.schema
....: batches = [b for b in reader]
....:
In [11]: schema
Out[11]:
f0: int64
f1: string
f2: bool
In [12]: len(batches)
Out[12]: 5
We can check the returned batches are the same as the original input:
In [13]: batches[0].equals(batch)
Out[13]: True
An important point is that if the input source supports zero-copy reads
(e.g. like a memory map, or pyarrow.BufferReader
), then the returned
batches are also zero-copy and do not allocate any new memory on read.
Writing and Reading Random Access Files¶
The RecordBatchFileWriter
has the same API as
RecordBatchStreamWriter
. You can create one with
new_file()
:
In [14]: sink = pa.BufferOutputStream()
In [15]: with pa.ipc.new_file(sink, batch.schema) as writer:
....: for i in range(10):
....: writer.write_batch(batch)
....:
In [16]: buf = sink.getvalue()
In [17]: buf.size
Out[17]: 4226
The difference between RecordBatchFileReader
and
RecordBatchStreamReader
is that the input source must have a
seek
method for random access. The stream reader only requires read
operations. We can also use the open_file()
method to open a file:
In [18]: with pa.ipc.open_file(buf) as reader:
....: num_record_batches = reader.num_record_batches
....:
In [19]: b = reader.get_batch(3)
Because we have access to the entire payload, we know the number of record batches in the file, and can read any at random.
In [20]: num_record_batches
Out[20]: 10
In [21]: b.equals(batch)
Out[21]: True
Reading from Stream and File Format for pandas¶
The stream and file reader classes have a special read_pandas
method to
simplify reading multiple record batches and converting them to a single
DataFrame output:
In [22]: with pa.ipc.open_file(buf) as reader:
....: df = reader.read_pandas()
....:
In [23]: df[:5]
Out[23]:
f0 f1 f2
0 1 foo True
1 2 bar None
2 3 baz False
3 4 None True
4 1 foo True
Efficiently Writing and Reading Arrow Data¶
Being optimized for zero copy and memory mapped data, Arrow allows to easily read and write arrays consuming the minimum amount of resident memory.
When writing and reading raw Arrow data, we can use the Arrow File Format or the Arrow Streaming Format.
To dump an array to file, you can use the new_file()
which will provide a new RecordBatchFileWriter
instance
that can be used to write batches of data to that file.
For example to write an array of 10M integers, we could write it in 1000 chunks of 10000 entries:
In [24]: BATCH_SIZE = 10000
In [25]: NUM_BATCHES = 1000
In [26]: schema = pa.schema([pa.field('nums', pa.int32())])
In [27]: with pa.OSFile('bigfile.arrow', 'wb') as sink:
....: with pa.ipc.new_file(sink, schema) as writer:
....: for row in range(NUM_BATCHES):
....: batch = pa.record_batch([pa.array(range(BATCH_SIZE), type=pa.int32())], schema)
....: writer.write(batch)
....:
record batches support multiple columns, so in practice we always write the
equivalent of a Table
.
Writing in batches is effective because we in theory need to keep in memory only the current batch we are writing. But when reading back, we can be even more effective by directly mapping the data from disk and avoid allocating any new memory on read.
Under normal conditions, reading back our file will consume a few hundred megabytes of memory:
In [28]: with pa.OSFile('bigfile.arrow', 'rb') as source:
....: loaded_array = pa.ipc.open_file(source).read_all()
....:
In [29]: print("LEN:", len(loaded_array))
LEN: 10000000
In [30]: print("RSS: {}MB".format(pa.total_allocated_bytes() >> 20))
RSS: 38MB
To more efficiently read big data from disk, we can memory map the file, so that Arrow can directly reference the data mapped from disk and avoid having to allocate its own memory. In such case the operating system will be able to page in the mapped memory lazily and page it out without any write back cost when under pressure, allowing to more easily read arrays bigger than the total memory.
In [31]: with pa.memory_map('bigfile.arrow', 'rb') as source:
....: loaded_array = pa.ipc.open_file(source).read_all()
....:
In [32]: print("LEN:", len(loaded_array))
LEN: 10000000
In [33]: print("RSS: {}MB".format(pa.total_allocated_bytes() >> 20))
RSS: 0MB
Note
Other high level APIs like read_table()
also provide a
memory_map
option. But in those cases, the memory mapping can’t help with
reducing resident memory consumption. See Reading Parquet and Memory Mapping for details.
Arbitrary Object Serialization¶
Warning
The custom serialization functionality is deprecated in pyarrow 2.0, and will be removed in a future version.
While the serialization functions in this section utilize the Arrow stream
protocol internally, they do not produce data that is compatible with the
above ipc.open_file
and ipc.open_stream
functions.
For arbitrary objects, you can use the standard library pickle
functionality instead. For pyarrow objects, you can use the IPC
serialization format through the pyarrow.ipc
module, as explained
above.
PyArrow serialization was originally meant to provide a higher-performance
alternative to pickle
thanks to zero-copy semantics. However,
pickle
protocol 5 gained support for zero-copy using out-of-band
buffers, and can be used instead for similar benefits.
In pyarrow
we are able to serialize and deserialize many kinds of Python
objects. As an example, consider a dictionary containing NumPy arrays:
In [34]: import numpy as np
In [35]: data = {
....: i: np.random.randn(500, 500)
....: for i in range(100)
....: }
....:
We use the pyarrow.serialize
function to convert this data to a byte
buffer:
In [36]: buf = pa.serialize(data).to_buffer()
In [37]: type(buf)
Out[37]: pyarrow.lib.Buffer
In [38]: buf.size
Out[38]: 200028928
pyarrow.serialize
creates an intermediate object which can be converted to
a buffer (the to_buffer
method) or written directly to an output stream.
pyarrow.deserialize
converts a buffer-like object back to the original
Python object:
In [39]: restored_data = pa.deserialize(buf)
In [40]: restored_data[0]
Out[40]:
array([[ 0.46699622, -2.11602638, -0.56904213, ..., 1.15544883,
0.3554366 , 1.15533181],
[-1.88494427, 1.19107613, 1.3125915 , ..., -0.86588752,
1.47378327, -0.22359666],
[ 1.10013479, 0.70936348, 0.85277265, ..., 0.4853728 ,
0.87684054, 1.19171109],
...,
[ 1.66414845, 0.48667611, -0.1908364 , ..., -0.60828816,
-0.33246996, 0.43652564],
[ 0.70968349, -0.8510139 , -1.40881027, ..., 0.30935225,
-1.06945996, -1.407847 ],
[ 0.4359112 , -0.23702384, 0.9009462 , ..., -0.5146575 ,
-0.19030145, -0.6146166 ]])
Serializing Custom Data Types¶
If an unrecognized data type is encountered when serializing an object,
pyarrow
will fall back on using pickle
for converting that type to a
byte string. There may be a more efficient way, though.
Consider a class with two members, one of which is a NumPy array:
class MyData:
def __init__(self, name, data):
self.name = name
self.data = data
We write functions to convert this to and from a dictionary with simpler types:
def _serialize_MyData(val):
return {'name': val.name, 'data': val.data}
def _deserialize_MyData(data):
return MyData(data['name'], data['data']
then, we must register these functions in a SerializationContext
so that
MyData
can be recognized:
context = pa.SerializationContext()
context.register_type(MyData, 'MyData',
custom_serializer=_serialize_MyData,
custom_deserializer=_deserialize_MyData)
Lastly, we use this context as an additional argument to pyarrow.serialize
:
buf = pa.serialize(val, context=context).to_buffer()
restored_val = pa.deserialize(buf, context=context)
The SerializationContext
also has convenience methods serialize
and
deserialize
, so these are equivalent statements:
buf = context.serialize(val).to_buffer()
restored_val = context.deserialize(buf)
Component-based Serialization¶
For serializing Python objects containing some number of NumPy arrays, Arrow
buffers, or other data types, it may be desirable to transport their serialized
representation without having to produce an intermediate copy using the
to_buffer
method. To motivate this, suppose we have a list of NumPy arrays:
In [41]: import numpy as np
In [42]: data = [np.random.randn(10, 10) for i in range(5)]
The call pa.serialize(data)
does not copy the memory inside each of these
NumPy arrays. This serialized representation can be then decomposed into a
dictionary containing a sequence of pyarrow.Buffer
objects containing
metadata for each array and references to the memory inside the arrays. To do
this, use the to_components
method:
In [43]: serialized = pa.serialize(data)
In [44]: components = serialized.to_components()
The particular details of the output of to_components
are not too
important. The objects in the 'data'
field are pyarrow.Buffer
objects,
which are zero-copy convertible to Python memoryview
objects:
In [45]: memoryview(components['data'][0])
Out[45]: <memory at 0x7f0d89739400>
A memoryview can be converted back to a Arrow Buffer
with
pyarrow.py_buffer
:
In [46]: mv = memoryview(components['data'][0])
In [47]: buf = pa.py_buffer(mv)
An object can be reconstructed from its component-based representation using
deserialize_components
:
In [48]: restored_data = pa.deserialize_components(components)
In [49]: restored_data[0]
Out[49]:
array([[ 1.44649633, 0.93194922, -1.77486972, -1.1853281 , 0.27405308,
0.13004041, 0.38774913, -1.43172251, -0.72651926, -1.52870033],
[ 0.91640599, 0.04590635, 0.03235188, -1.05644207, 1.3421809 ,
-0.10093171, -0.9761181 , -0.12497654, -0.96269071, 1.02257454],
[-1.73360158, 1.18685463, 0.37393424, 0.21182226, -0.10115813,
0.72712082, 0.45806757, -1.94600472, 1.18081289, 1.49462241],
[ 1.31973191, -0.18583713, -0.55702181, -0.40404604, -0.11571783,
1.25700011, 1.80732565, 1.51276195, -0.16940765, 0.43530112],
[ 0.2241836 , -1.24907952, 0.78111059, 2.18569872, -1.08166882,
1.38768866, -0.34120458, -0.6547015 , -1.18979897, 1.20659172],
[-0.18461613, 0.16573596, 0.55557632, -1.1756573 , 0.25936376,
0.97016504, -0.59199487, 0.66065634, -0.86926986, 1.38161322],
[ 0.03004867, -1.74395182, 1.78663219, -1.00968874, 0.03191733,
-1.49166984, -0.10192021, -1.18528804, -0.32832038, -0.5606902 ],
[ 0.5984967 , -0.38410479, -1.44223429, 0.09066406, -0.30213887,
-0.74912914, -0.80802927, -0.25725153, -1.19534629, 0.46082082],
[ 0.86466015, 0.69910702, -0.18465602, 0.52114178, -2.00259641,
-0.48502901, 0.9559934 , 0.75547104, 0.82136872, -0.756342 ],
[-1.18141542, -0.04550835, 0.03554301, 0.7910252 , 0.09900296,
-0.70504705, -1.22476942, 1.40271341, -1.17917136, 1.96561697]])
deserialize_components
is also available as a method on
SerializationContext
objects.