numbarrow.core.mapinarrow_factory
Overview
Factory for PySpark mapInArrow UDF functions. Bridges PySpark’s
Arrow-based batch processing with Numba JIT-compiled functions by
converting each pyarrow.RecordBatch column through the adapter
layer before passing data to a user-supplied computation function.
Usage:
from numbarrow.core.mapinarrow_factory import Nullable, make_mapinarrow_func
def my_func(data_dict, bitmap_dict, broadcasts):
# data_dict: {name: np.ndarray} for a uniform column, or
# {name: {field: np.ndarray}} for a struct column
# bitmap_dict: the same shape, each leaf a uint8 bitmap or None where
# the column carries no validity buffer; for a struct
# column the struct-level validity is folded into each
# field's bitmap
# broadcasts: {key: value}
result = data_dict["input_col"] * broadcasts["scale"]
# result is null wherever input_col is, so input_col's bitmap goes out
# with it; a bare array would carry no nulls out, and a result with
# nulls of its own packs them: np.packbits(valid, bitorder="little")
return {"output_col": Nullable(result, bitmap_dict["input_col"])}
udf = make_mapinarrow_func(my_func, broadcasts={"scale": 1.5})
df_out = df_in.mapInArrow(udf, output_schema)
Every name in data_dict is also a key of bitmap_dict, so a batch that
happens to contain no nulls is indexable exactly like one that does.
On the way out a bare array carries no nulls: a row that came in null goes out
valid, holding whatever the UDF computed from the placeholder under the null,
which is 0, 0.0 or '' in a batch from Spark.
Nullable(data, bitmap) carries nulls out, the bitmap being in the layout
bitmap_dict hands out. A result that is null exactly where one input
column is, as in the example, passes that column’s bitmap through,
Nullable(result, bitmap_dict[column]), or bitmap_dict[column][field]
for a struct field; any other result builds its own, for instance
np.packbits(valid, bitorder="little") from a boolean array valid. A
packed bitmap carries no row count, so a bitmap the batch handed out is
accepted only on a column of the length it covers, the batch’s rows for a
column’s own bitmap and the flattened elements for a struct field’s; one that
resizes the column needs a bitmap of its own.
For a StructArray column the struct-level validity is folded into each
field’s bitmap, so a row that is null as a whole is visible to one is_null
call per field. For a ListArray of structs the fold covers the flattened
struct elements, NOT the outer list rows: a null outer row can be reported
nowhere and also shifts the element-to-row mapping, so a list column whose
null_count is non-zero raises NotImplementedError.
A struct or list-of-struct column’s fields sit under the column’s own name,
data_dict[column][field], so a field never shares a namespace with another
column or with another struct’s fields.
See test_mapinarrow_spark.py in the test suite for a complete runnable example.
Module
Factory for PySpark mapInArrow UDF functions.
Bridges PySpark’s Arrow-based batch processing with Numba JIT-compiled functions
by converting each pyarrow.RecordBatch column through
arrow_array_adapter() before passing the data
to a user-supplied computation function.
- class numbarrow.core.mapinarrow_factory.Nullable(data: object, bitmap: ndarray | None)[source]
Bases:
NamedTupleAn output column with its validity,
Nullable(data, bitmap).datais anything a column may be: an ndarray, a list, apyarrow.Arrayor a numpy record array.bitmapis a packed uint8 validity bitmap in the layoutbitmap_dicthands out,(rows + 7) // 8bytes with a set bit for a valid row, orNone, which is whatbitmap_dictholds for a column with no validity buffer. A bare array carries no nulls out of a UDF; this does.- bitmap: ndarray | None
Alias for field number 1
- data: object
Alias for field number 0
- numbarrow.core.mapinarrow_factory.make_mapinarrow_func(main_func: Callable, input_columns: list[str] | None = None, broadcasts: dict | None = None, output_schema: Schema | None = None)[source]
Creates a function that can be given as an argument to mapInArrow
- Parameters:
main_func –
called once per
pyarrow.RecordBatchasmain_func(data_dict, bitmap_dict, broadcasts), returning a dict that maps each output column’s name to an ndarray, a list or apyarrow.Array, from which a PyArrowRecordBatchis built. A numpy record array becomes a struct column, one child per field. A null comes out of a list, a tuple or an object array holdingNone, apyarrow.Array, a numpy masked array, and aNullable,Nullable(data, bitmap), whosebitmapis a packed uint8 validity bitmap in the layoutbitmap_dicthands out,(rows + 7) // 8bytes with a set bit for a valid row, orNone. A bare array carries no nulls out: a row that came in null goes out valid, holding whatever the UDF computed from the placeholder under the null. A result that is null exactly where one input column is passes that column’s bitmap through,{"out": Nullable(result, bitmap_dict["value"])}, orbitmap_dict["column"]["field"]for a struct field, and any other result builds its own in that layout, the oneis_null()reads, for instancenp.packbits(valid, bitorder="little")from a boolean array. A bitmap that is not an ndarray, or is one of another length or dtype, raises naming the column, and so does a bitmap the batch handed out on a column whose row count is not the count that bitmap covers, the batch’s rows for a column’s own bitmap and the flattened elements for a struct field’s, since a packed bitmap cannot tell row counts apart inside one byte.Spark binds the columns of that batch to the declared output schema by POSITION, not by name, and checks nothing about their names: it reads each Arrow vector through the accessor its declared type expects, so an int64 column declared as a timestamp reads as a timestamp, and two columns whose types share an accessor family swap silently when the dict is built in the other order. A column read through the accessor of another family fails in the JVM with
java.lang.UnsupportedOperationExceptionwhatever its width, as float64 underLongTypeand int64 underDoubleTypedo, both 64 bits wide, and so does a width mismatch inside one family, such as int32 underLongType. Build the returned dict in the order the output schema declares, or passoutput_schemaand let Arrow bind it by name instead.data_dictmaps each selected column’s name to its data. A column of a uniform type maps to one array. A struct or list-of-struct column maps to a dict of its fields,data_dict[column][field], so a field never shares a namespace with another column or with another struct’s fields.bitmap_dicthas the same shape: a uint8 aligned array of bitmap data, orNonewhere the column carries no validity buffer, and for a struct column a dict of those keyed by field. Every key ofdata_dictis present, so a null-free batch is indexable exactly like a batch containing nulls.For a
StructArraycolumn the struct-level validity is folded into each field’s bitmap, so oneis_null()call per field sees both a null field and a row that is null as a whole.For a
ListArrayof structs the fold covers the flattened struct elements, NOT the outer list rows. A null outer row can be reported nowhere, and because the adapter returns the flattened elements with no offsets, such a row also shifts the element-to-row mapping, so a list column whosenull_countis non-zero raisesNotImplementedError.input_columns – optional list of column names that will be expected to be needed for in data_dict for the calculation done by main_func. When not given, all columns in the iterated over PySpark DataFrame will be used. Names are matched exactly; a name the batch does not have raises
KeyErrorlisting the batch’s columns, since Spark’s case-insensitive projection may have spelled it differently, and a name the batch carries more than once, as an unaliased join produces, raisesValueError.broadcasts – optional dictionary of broadcast values
output_schema –
optional
pyarrow.Schemafor the batch that is yielded. When given, the dict returned bymain_funcis bound to it BY NAME, so insertion order stops deciding, and every column is built with its declared type rather than inferred and cast: a string column declaredlarge_stringis built as one, a list of dicts declaredstructis built field by field, and a list of dicts or of pairs declaredmapbecomes a map, which no inferred type can be cast to. A name the schema declares but the dict omits raisesKeyError, a key the schema does not name raisesValueError, and a dict key that no declared struct field has raisesValueErrortoo, since Arrow matches struct fields by exact name and would otherwise fill the column with nulls.What the declared type refuses is what
pa.arrayrefuses, and that depends on the shape the column arrives in. For an ndarray of a numeric or datetime dtype, or apyarrow.Array, an integer out of the declared type’s range, a float with a fraction into an integer type and a timestamp unit change that drops digits all raisepyarrow.ArrowInvalid. A Python list, and any other sequence of Python objects, an object-dtype ndarray included, goes throughpa.array’s sequence converter instead: an integer out of the declared type’s range still raisespyarrow.ArrowInvalidand one beyond int64 altogether raisesOverflowError, but a float’s fraction and a timestamp’s extra digits are dropped silently. So the lossy conversions that pass without a word are a timestamp intodate32ordate64, which floors to the day,float64intofloat32, which overflows toinf, and, from a list alone, a fraction into an integer type and a timestamp unit change that drops digits.Left as
Nonethe batch is built from the dict alone: insertion order decides, and every type is inferred from the value, so a unicode or bytes array comes backstringorbinarywhatever type went in, adatetime64array comes back a naivetimestampof its unit, except a day-unit one, which comes backdate32, and an object array holding onlyNonecomes backnull.