This is an automated email from the ASF dual-hosted git repository.
zhengruifeng pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new c9fc60f1bb76 [SPARK-58602][PYTHON][TEST] Add tests for
pa.Array.from_pandas with default arguments
c9fc60f1bb76 is described below
commit c9fc60f1bb766d2e70f7bba6aa49933d516ba318
Author: Spenser Sun <[email protected]>
AuthorDate: Fri Aug 7 13:43:26 2026 +0800
[SPARK-58602][PYTHON][TEST] Add tests for pa.Array.from_pandas with default
arguments
### What changes were proposed in this pull request?
This PR adds a golden-file test pinning the behavior of
`pa.Array.from_pandas` with default arguments (no `mask`, no `type`, no
`safe`), under the SPARK-54936 umbrella for monitoring upstream library
behavior changes.
New file
`python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py`
with `PyArrowArrayFromPandasDefaultTests`, plus the golden files
`golden_pyarrow_array_from_pandas_default.{csv,md}` (97 rows x 2 columns) and
one line in `dev/sparktestsupport/modules.py`.
The source Series are built by three group methods, so the follow-up
non-default tests can reuse the whole inventory or one group:
- `_numpy_backed_sources()` -- numpy storage, so `hasattr(series.array,
"__arrow_array__")` is `False` and PySpark passes `mask=series.isnull()`.
- `_protocol_sources()` -- dtypes implementing `__arrow_array__`, where
PySpark passes `mask=None` because supplying a mask raises. The protocol means
"can export Arrow", not "is stored as Arrow": `Int64` is numpy values plus a
byte mask and `string[python]` is an object ndarray, yet both implement it
alongside the genuinely Arrow-backed `[pyarrow]` dtypes. PySpark does not
distinguish them -- `conversion.py:435` branches only on the protocol -- so
they share one group.
- `_coercion_sources()` -- values that are lossy or ambiguous once a target
type is requested. Their unconverted results are recorded here to give the
follow-up type tests a baseline to be read against.
`from_pandas` does not always return a `pa.Array` -- for a Series backed by
a chunked Arrow array it returns a `pa.ChunkedArray`. A small helper therefore
outputs those cells as `chunked<type>`, since `repr_value` reports the element
type and would render the two identically. Checking who consumes that return
value then showed one place that accepts only an `Array`: the pandas UDF return
path assembles its result with `pa.RecordBatch.from_arrays`, so such a UDF
fails with a raw `TypeE [...]
### Why are the changes needed?
`from_pandas` is the pandas -> Arrow direction, which the previously merged
golden tests under SPARK-54936 do not cover -- they are all Arrow -> pandas.
PySpark depends on it at three production call sites:
`python/pyspark/sql/conversion.py:443` and `:474` (the pandas UDF return leg,
legacy and non-legacy), and `python/pyspark/sql/pandas/conversion.py:117`
(`create_arrow_array_from_pandas`, used by `createDataFrame(pandas_df)`). A
silent change here becomes wrong data on the JVM side [...]
Two behaviors the golden pins that are easy to miss:
- **pandas 3 changed its default temporal resolution.**
`pd.Series([datetime])` infers `datetime64[ns]` on pandas 2 and
`datetime64[us]` on pandas 3, and `Categorical.categories` become `str` (so the
dictionary's values become `large_string`). The rows pin each resolution
explicitly and add deliberately unpinned `:inferred` rows whose only job is to
record which default pandas picks.
- **`object` dtype and `datetime64[ns]` disagree.** In object dtype pyarrow
takes the unit from `datetime.datetime`'s own microsecond resolution rather
than from a numpy dtype, so the same values give `timestamp[us]` instead of
`timestamp[ns]` -- and a sub-microsecond value is dropped silently, while an
explicit `type=pa.timestamp("us")` raises `ArrowInvalid` on that same loss.
### Does this PR introduce _any_ user-facing change?
No. Test-only.
### How was this patch tested?
New golden-file test. It runs without a Spark session, so it exercises
PyArrow and pandas directly.
Validated across **16 environments -- PyArrow 18/19/20/21/22/23/24/25 x
pandas 2 and pandas 3** -- with a fresh virtualenv per combination, running the
committed test file against the committed golden file. All 16 pass. Six cells
legitimately differ on pandas 3 and are carried in the `overrides` dict rather
than by regenerating; disabling the `LooseVersion` guard makes pandas 3 fail on
exactly those six while pandas 2 still passes, so the overrides are
load-bearing.
Also verified: the test passes without `SPARK_GENERATE_GOLDEN_FILES`,
regeneration is byte-identical, and `ruff check` / `ruff format --check` /
`mypy` are clean.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)
Closes #57830 from Spenserrrr/from-pandas-tests.
Authored-by: Spenser Sun <[email protected]>
Signed-off-by: Ruifeng Zheng <[email protected]>
(cherry picked from commit 4c4ce5ff0663d77f08b524cc2488c19c09099f35)
Signed-off-by: Ruifeng Zheng <[email protected]>
---
dev/sparktestsupport/modules.py | 1 +
.../golden_pyarrow_array_from_pandas_default.csv | 99 +++++
.../golden_pyarrow_array_from_pandas_default.md | 100 +++++
.../test_pyarrow_array_from_pandas_default.py | 413 +++++++++++++++++++++
4 files changed, 613 insertions(+)
diff --git a/dev/sparktestsupport/modules.py b/dev/sparktestsupport/modules.py
index 79e2e5a7bc2e..1f1e4604f6d2 100644
--- a/dev/sparktestsupport/modules.py
+++ b/dev/sparktestsupport/modules.py
@@ -537,6 +537,7 @@ pyspark_core = Module(
"pyspark.tests.test_zero_copy_byte_stream",
# unittests for upstream projects
"pyspark.tests.upstream.pyarrow.test_pyarrow_array_cast",
+
"pyspark.tests.upstream.pyarrow.test_pyarrow_array_from_pandas_default",
"pyspark.tests.upstream.pyarrow.test_pyarrow_array_type_inference",
"pyspark.tests.upstream.pyarrow.test_pyarrow_arrow_to_pandas_default",
"pyspark.tests.upstream.pyarrow.test_pyarrow_arrow_to_pandas_non_default",
diff --git
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_default.csv
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_default.csv
new file mode 100644
index 000000000000..725784fe911d
--- /dev/null
+++
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_default.csv
@@ -0,0 +1,99 @@
+test case pandas series arrow array
+int8:standard [0, 1, -1, 127, -128]@Series[int8] [0, 1, -1, 127,
-128]@int8
+int8:empty []@Series[int8] []@int8
+int16:standard [0, 1, -1, 32767, -32768]@Series[int16] [0, 1, -1, 32767,
-32768]@int16
+int16:empty []@Series[int16] []@int16
+int32:standard [0, 1, -1, 2147483647, -2147483648]@Series[int32] [0, 1,
-1, 2147483647, -2147483648]@int32
+int32:empty []@Series[int32] []@int32
+int64:standard [0, 1, -1, 9223372036854775807,
-9223372036854775808]@Series[int64] [0, 1, -1, 9223372036854775807,
-9223372036854775808]@int64
+int64:empty []@Series[int64] []@int64
+int64:nullable [0.0, 1.0, nan]@Series[float64] [0.0, 1.0, None]@float64
+uint8:standard [0, 1, 255]@Series[uint8] [0, 1, 255]@uint8
+uint16:standard [0, 1, 65535]@Series[uint16] [0, 1, 65535]@uint16
+uint32:standard [0, 1, 4294967295]@Series[uint32] [0, 1,
4294967295]@uint32
+uint64:standard [0, 1, 18446744073709551615]@Series[uint64] [0, 1,
18446744073709551615]@uint64
+float32:standard [0.0, 1.5, -1.5]@Series[float32] [0.0, 1.5,
-1.5]@float32
+float32:nullable [0.0, nan, 1.5]@Series[float32] [0.0, None, 1.5]@float32
+float32:empty []@Series[float32] []@float32
+float64:standard [0.0, 1.5, -1.5]@Series[float64] [0.0, 1.5,
-1.5]@float64
+float64:nullable [0.0, nan, 1.5]@Series[float64] [0.0, None, 1.5]@float64
+float64:empty []@Series[float64] []@float64
+bool:standard [True, False, True]@Series[bool] [True, False, True]@bool
+bool:empty []@Series[bool] []@bool
+object:string ['hello', 'world', '']@Series[object] [hello, world, ]@string
+object:string-nullable ['hello', None, 'world']@Series[object] [hello, None,
world]@string
+string:inferred ['hello', 'world']@Series[object] [hello,
world]@string
+object:bytes [b'hello', b'world']@Series[object] [b'hello',
b'world']@binary
+object:empty []@Series[object] []@null
+object:all-null [None, None]@Series[object] [None, None]@null
+object:decimal [Decimal('1.50'), Decimal('-2.25')]@Series[object] [1.50,
-2.25]@decimal128(3, 2)
+list<int64>:standard [[1, 2], [3]]@Series[object] [[1, 2],
[3]]@list<item: int64>
+list<int64>:nullable [[1, 2], None]@Series[object] [[1, 2],
None]@list<item: int64>
+list<int64>:null-element [[1, None], [3]]@Series[object] [[1, None],
[3]]@list<item: int64>
+list<string>:standard [['a', 'b'], ['c']]@Series[object] [['a', 'b'],
['c']]@list<item: string>
+list<list<int64>>:standard [[[1, 2], [3]], [[4]]]@Series[object] [[[1,
2], [3]], [[4]]]@list<item: list<item: int64>>
+list<struct>:standard [[{'a': 1}], [{'a': 2}]]@Series[object] [[{'a': 1}],
[{'a': 2}]]@list<item: struct<a: int64>>
+struct:standard [{'a': 1, 'b': 'x'}]@Series[object] [[('a', 1),
('b', 'x')]]@struct<a: int64, b: string>
+struct:nullable [{'a': 1, 'b': 'x'}, None]@Series[object] [[('a',
1), ('b', 'x')], None]@struct<a: int64, b: string>
+struct<struct>:standard [{'a': {'b': 1}}]@Series[object] [[('a',
{'b': 1})]]@struct<a: struct<b: int64>>
+struct<list<int64>>:standard [{'a': [1, 2]}]@Series[object] [[('a', [1,
2])]]@struct<a: list<item: int64>>
+datetime64[ns]:standard [Timestamp('2024-06-15
18:30:00')]@Series[datetime64[ns]] [2024-06-15 18:30:00]@timestamp[ns]
+datetime64[ns]:nullable [Timestamp('2024-06-15 18:30:00'),
NaT]@Series[datetime64[ns]] [2024-06-15 18:30:00, None]@timestamp[ns]
+datetime64[ns]:empty []@Series[datetime64[ns]] []@timestamp[ns]
+datetime64[ns,tz]:standard [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC')]@Series[datetime64[ns, UTC]] [2024-06-15
18:30:00+00:00]@timestamp[ns, tz=UTC]
+datetime64[ns,tz]:nullable [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC'), NaT]@Series[datetime64[ns, UTC]] [2024-06-15 18:30:00+00:00,
None]@timestamp[ns, tz=UTC]
+timedelta64[ns]:standard [Timedelta('1 days 00:00:00'), Timedelta('0
days 02:00:00')]@Series[timedelta64[ns]] [1 days 00:00:00, 0 days
02:00:00]@duration[ns]
+timedelta64[ns]:nullable [Timedelta('1 days 00:00:00'),
NaT]@Series[timedelta64[ns]] [1 days 00:00:00, None]@duration[ns]
+datetime64[us]:standard [Timestamp('2024-06-15
18:30:00')]@Series[datetime64[us]] [2024-06-15 18:30:00]@timestamp[us]
+datetime64[us]:nullable [Timestamp('2024-06-15 18:30:00'),
NaT]@Series[datetime64[us]] [2024-06-15 18:30:00, None]@timestamp[us]
+datetime64[us]:empty []@Series[datetime64[us]] []@timestamp[us]
+datetime64[us,tz]:standard [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC')]@Series[datetime64[us, UTC]] [2024-06-15
18:30:00+00:00]@timestamp[us, tz=UTC]
+datetime64[us,tz]:nullable [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC'), NaT]@Series[datetime64[us, UTC]] [2024-06-15 18:30:00+00:00,
None]@timestamp[us, tz=UTC]
+timedelta64[us]:standard [Timedelta('1 days 00:00:00'), Timedelta('0
days 02:00:00')]@Series[timedelta64[us]] [1 day, 0:00:00,
2:00:00]@duration[us]
+timedelta64[us]:nullable [Timedelta('1 days 00:00:00'),
NaT]@Series[timedelta64[us]] [1 day, 0:00:00, None]@duration[us]
+datetime64:inferred [Timestamp('2024-06-15
18:30:00')]@Series[datetime64[ns]] [2024-06-15 18:30:00]@timestamp[ns]
+timedelta64:inferred [Timedelta('1 days 00:00:00'), Timedelta('0 days
02:00:00')]@Series[timedelta64[ns]] [1 days 00:00:00, 0 days
02:00:00]@duration[ns]
+datetime64[us]:out-of-ns-range [Timestamp('1500-01-01
00:00:00')]@Series[datetime64[us]] [1500-01-01 00:00:00]@timestamp[us]
+date:standard [datetime.date(2024, 6, 15)]@Series[object]
[2024-06-15]@date32[day]
+time:standard [datetime.time(18, 30, 45)]@Series[object]
[18:30:45]@time64[us]
+object:datetime [datetime.datetime(2024, 6, 15, 18, 30)]@Series[object]
[2024-06-15 18:30:00]@timestamp[us]
+object:datetime-sub-us [Timestamp('2024-01-01
00:00:00.000000123')]@Series[object] [2024-01-01 00:00:00]@timestamp[us]
+object:timedelta [datetime.timedelta(days=1,
seconds=7200)]@Series[object] [1 day, 2:00:00]@duration[us]
+category:standard ['a', 'b', 'a']@Series[category] [a, b,
a]@dictionary<values=string, indices=int8, ordered=0>
+category:nullable ['a', nan, 'b']@Series[category] [a, None,
b]@dictionary<values=string, indices=int8, ordered=0>
+Int8:standard [0, 1, 127, -128]@Series[Int8] [0, 1, 127, -128]@int8
+Int8:nullable [0, 1, <NA>]@Series[Int8] [0, 1, None]@int8
+Int16:standard [0, 1, 32767, -32768]@Series[Int16] [0, 1, 32767,
-32768]@int16
+Int16:nullable [0, 1, <NA>]@Series[Int16] [0, 1, None]@int16
+Int32:standard [0, 1, 2147483647, -2147483648]@Series[Int32] [0, 1,
2147483647, -2147483648]@int32
+Int32:nullable [0, 1, <NA>]@Series[Int32] [0, 1, None]@int32
+Int64:standard [0, 1, 9223372036854775807, -9223372036854775808]@Series[Int64]
[0, 1, 9223372036854775807, -9223372036854775808]@int64
+Int64:nullable [0, 1, <NA>]@Series[Int64] [0, 1, None]@int64
+UInt64:standard [0, 1, 18446744073709551615]@Series[UInt64] [0, 1,
18446744073709551615]@uint64
+Int64:empty []@Series[Int64] []@int64
+Int64:all-null [<NA>, <NA>]@Series[Int64] [None, None]@int64
+Float64:standard [0.0, 1.5]@Series[Float64] [0.0, 1.5]@float64
+Float64:nullable [0.0, <NA>]@Series[Float64] [0.0, None]@float64
+boolean:standard [True, False]@Series[boolean] [True, False]@bool
+boolean:nullable [True, <NA>]@Series[boolean] [True, None]@bool
+string[python]:standard ['hello', 'world']@Series[string] [hello,
world]@string
+string[python]:nullable ['hello', <NA>]@Series[string] [hello,
None]@string
+string[python]:empty []@Series[string] []@string
+int64[pyarrow]:standard [0, 1, -1]@Series[int64[pyarrow]] [0, 1,
-1]@int64
+int64[pyarrow]:nullable [0, 1, <NA>]@Series[int64[pyarrow]] [0, 1,
None]@int64
+int64[pyarrow]:empty []@Series[int64[pyarrow]] []@int64
+double[pyarrow]:nullable [0.0, <NA>]@Series[double[pyarrow]] [0.0,
None]@float64
+bool[pyarrow]:nullable [True, <NA>]@Series[bool[pyarrow]] [True,
None]@bool
+string[pyarrow]:standard ['hello', 'world']@Series[string] [hello,
world]@large_string
+string[pyarrow]:nullable ['hello', <NA>]@Series[string] [hello,
None]@large_string
+string[pyarrow]:empty []@Series[string] []@large_string
+timestamp[us][pyarrow]:standard [Timestamp('2024-01-01
12:00:00')]@Series[timestamp[us][pyarrow]] [2024-01-01
12:00:00]@timestamp[us]
+int64[pyarrow]:single-chunk [1, 2]@Series[int64[pyarrow]] [1, 2]@int64
+int64[pyarrow]:multi-chunk [1, 2, 3]@Series[int64[pyarrow]] [1, 2,
3]@chunked<int64>
+int64:overflow [300, 1]@Series[int64] [300, 1]@int64
+float64:fractional [1.5, 2.5]@Series[float64] [1.5, 2.5]@float64
+float64:infinity [inf, 1.0]@Series[float64] [inf, 1.0]@float64
+float64:precision [1.1234567890123]@Series[float64]
[1.1234567890123]@float64
+datetime64[ns]:sub-us [Timestamp('2024-01-01
00:00:00.000000123')]@Series[datetime64[ns]] [2024-01-01
00:00:00.000000123]@timestamp[ns]
+object:date-then-datetime [datetime.date(2024, 1, 1),
datetime.datetime(2024, 1, 1, 5, 30)]@Series[object] [2024-01-01,
2024-01-01]@date32[day]
+object:datetime-then-date [datetime.datetime(2024, 1, 1, 5, 30),
datetime.date(2024, 1, 1)]@Series[object] ERR@ArrowTypeError
diff --git
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_default.md
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_default.md
new file mode 100644
index 000000000000..502e494d468d
--- /dev/null
+++
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_default.md
@@ -0,0 +1,100 @@
+| test case | pandas series
| arrow array
|
+|---------------------------------|--------------------------------------------------------------------------------------|-----------------------------------------------------------------|
+| int8:standard | [0, 1, -1, 127, -128]@Series[int8]
| [0, 1, -1, 127, -128]@int8
|
+| int8:empty | []@Series[int8]
| []@int8
|
+| int16:standard | [0, 1, -1, 32767, -32768]@Series[int16]
| [0, 1, -1, 32767, -32768]@int16
|
+| int16:empty | []@Series[int16]
| []@int16
|
+| int32:standard | [0, 1, -1, 2147483647,
-2147483648]@Series[int32] | [0, 1, -1,
2147483647, -2147483648]@int32 |
+| int32:empty | []@Series[int32]
| []@int32
|
+| int64:standard | [0, 1, -1, 9223372036854775807,
-9223372036854775808]@Series[int64] | [0, 1, -1,
9223372036854775807, -9223372036854775808]@int64 |
+| int64:empty | []@Series[int64]
| []@int64
|
+| int64:nullable | [0.0, 1.0, nan]@Series[float64]
| [0.0, 1.0, None]@float64
|
+| uint8:standard | [0, 1, 255]@Series[uint8]
| [0, 1, 255]@uint8
|
+| uint16:standard | [0, 1, 65535]@Series[uint16]
| [0, 1, 65535]@uint16
|
+| uint32:standard | [0, 1, 4294967295]@Series[uint32]
| [0, 1, 4294967295]@uint32
|
+| uint64:standard | [0, 1,
18446744073709551615]@Series[uint64] |
[0, 1, 18446744073709551615]@uint64 |
+| float32:standard | [0.0, 1.5, -1.5]@Series[float32]
| [0.0, 1.5, -1.5]@float32
|
+| float32:nullable | [0.0, nan, 1.5]@Series[float32]
| [0.0, None, 1.5]@float32
|
+| float32:empty | []@Series[float32]
| []@float32
|
+| float64:standard | [0.0, 1.5, -1.5]@Series[float64]
| [0.0, 1.5, -1.5]@float64
|
+| float64:nullable | [0.0, nan, 1.5]@Series[float64]
| [0.0, None, 1.5]@float64
|
+| float64:empty | []@Series[float64]
| []@float64
|
+| bool:standard | [True, False, True]@Series[bool]
| [True, False, True]@bool
|
+| bool:empty | []@Series[bool]
| []@bool
|
+| object:string | ['hello', 'world', '']@Series[object]
| [hello, world, ]@string
|
+| object:string-nullable | ['hello', None, 'world']@Series[object]
| [hello, None, world]@string
|
+| string:inferred | ['hello', 'world']@Series[object]
| [hello, world]@string
|
+| object:bytes | [b'hello', b'world']@Series[object]
| [b'hello', b'world']@binary
|
+| object:empty | []@Series[object]
| []@null
|
+| object:all-null | [None, None]@Series[object]
| [None, None]@null
|
+| object:decimal | [Decimal('1.50'),
Decimal('-2.25')]@Series[object] | [1.50,
-2.25]@decimal128(3, 2) |
+| list<int64>:standard | [[1, 2], [3]]@Series[object]
| [[1, 2], [3]]@list<item: int64>
|
+| list<int64>:nullable | [[1, 2], None]@Series[object]
| [[1, 2], None]@list<item: int64>
|
+| list<int64>:null-element | [[1, None], [3]]@Series[object]
| [[1, None], [3]]@list<item: int64>
|
+| list<string>:standard | [['a', 'b'], ['c']]@Series[object]
| [['a', 'b'], ['c']]@list<item:
string> |
+| list<list<int64>>:standard | [[[1, 2], [3]], [[4]]]@Series[object]
| [[[1, 2], [3]], [[4]]]@list<item:
list<item: int64>> |
+| list<struct>:standard | [[{'a': 1}], [{'a': 2}]]@Series[object]
| [[{'a': 1}], [{'a': 2}]]@list<item:
struct<a: int64>> |
+| struct:standard | [{'a': 1, 'b': 'x'}]@Series[object]
| [[('a', 1), ('b', 'x')]]@struct<a:
int64, b: string> |
+| struct:nullable | [{'a': 1, 'b': 'x'}, None]@Series[object]
| [[('a', 1), ('b', 'x')],
None]@struct<a: int64, b: string> |
+| struct<struct>:standard | [{'a': {'b': 1}}]@Series[object]
| [[('a', {'b': 1})]]@struct<a:
struct<b: int64>> |
+| struct<list<int64>>:standard | [{'a': [1, 2]}]@Series[object]
| [[('a', [1, 2])]]@struct<a:
list<item: int64>> |
+| datetime64[ns]:standard | [Timestamp('2024-06-15
18:30:00')]@Series[datetime64[ns]] | [2024-06-15
18:30:00]@timestamp[ns] |
+| datetime64[ns]:nullable | [Timestamp('2024-06-15 18:30:00'),
NaT]@Series[datetime64[ns]] | [2024-06-15 18:30:00,
None]@timestamp[ns] |
+| datetime64[ns]:empty | []@Series[datetime64[ns]]
| []@timestamp[ns]
|
+| datetime64[ns,tz]:standard | [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC')]@Series[datetime64[ns, UTC]] | [2024-06-15
18:30:00+00:00]@timestamp[ns, tz=UTC] |
+| datetime64[ns,tz]:nullable | [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC'), NaT]@Series[datetime64[ns, UTC]] | [2024-06-15 18:30:00+00:00,
None]@timestamp[ns, tz=UTC] |
+| timedelta64[ns]:standard | [Timedelta('1 days 00:00:00'),
Timedelta('0 days 02:00:00')]@Series[timedelta64[ns]] | [1 days 00:00:00, 0
days 02:00:00]@duration[ns] |
+| timedelta64[ns]:nullable | [Timedelta('1 days 00:00:00'),
NaT]@Series[timedelta64[ns]] | [1 days 00:00:00,
None]@duration[ns] |
+| datetime64[us]:standard | [Timestamp('2024-06-15
18:30:00')]@Series[datetime64[us]] | [2024-06-15
18:30:00]@timestamp[us] |
+| datetime64[us]:nullable | [Timestamp('2024-06-15 18:30:00'),
NaT]@Series[datetime64[us]] | [2024-06-15 18:30:00,
None]@timestamp[us] |
+| datetime64[us]:empty | []@Series[datetime64[us]]
| []@timestamp[us]
|
+| datetime64[us,tz]:standard | [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC')]@Series[datetime64[us, UTC]] | [2024-06-15
18:30:00+00:00]@timestamp[us, tz=UTC] |
+| datetime64[us,tz]:nullable | [Timestamp('2024-06-15 18:30:00+0000',
tz='UTC'), NaT]@Series[datetime64[us, UTC]] | [2024-06-15 18:30:00+00:00,
None]@timestamp[us, tz=UTC] |
+| timedelta64[us]:standard | [Timedelta('1 days 00:00:00'),
Timedelta('0 days 02:00:00')]@Series[timedelta64[us]] | [1 day, 0:00:00,
2:00:00]@duration[us] |
+| timedelta64[us]:nullable | [Timedelta('1 days 00:00:00'),
NaT]@Series[timedelta64[us]] | [1 day, 0:00:00,
None]@duration[us] |
+| datetime64:inferred | [Timestamp('2024-06-15
18:30:00')]@Series[datetime64[ns]] | [2024-06-15
18:30:00]@timestamp[ns] |
+| timedelta64:inferred | [Timedelta('1 days 00:00:00'),
Timedelta('0 days 02:00:00')]@Series[timedelta64[ns]] | [1 days 00:00:00, 0
days 02:00:00]@duration[ns] |
+| datetime64[us]:out-of-ns-range | [Timestamp('1500-01-01
00:00:00')]@Series[datetime64[us]] | [1500-01-01
00:00:00]@timestamp[us] |
+| date:standard | [datetime.date(2024, 6,
15)]@Series[object] |
[2024-06-15]@date32[day] |
+| time:standard | [datetime.time(18, 30, 45)]@Series[object]
| [18:30:45]@time64[us]
|
+| object:datetime | [datetime.datetime(2024, 6, 15, 18,
30)]@Series[object] | [2024-06-15
18:30:00]@timestamp[us] |
+| object:datetime-sub-us | [Timestamp('2024-01-01
00:00:00.000000123')]@Series[object] | [2024-01-01
00:00:00]@timestamp[us] |
+| object:timedelta | [datetime.timedelta(days=1,
seconds=7200)]@Series[object] | [1 day,
2:00:00]@duration[us] |
+| category:standard | ['a', 'b', 'a']@Series[category]
| [a, b, a]@dictionary<values=string,
indices=int8, ordered=0> |
+| category:nullable | ['a', nan, 'b']@Series[category]
| [a, None,
b]@dictionary<values=string, indices=int8, ordered=0> |
+| Int8:standard | [0, 1, 127, -128]@Series[Int8]
| [0, 1, 127, -128]@int8
|
+| Int8:nullable | [0, 1, <NA>]@Series[Int8]
| [0, 1, None]@int8
|
+| Int16:standard | [0, 1, 32767, -32768]@Series[Int16]
| [0, 1, 32767, -32768]@int16
|
+| Int16:nullable | [0, 1, <NA>]@Series[Int16]
| [0, 1, None]@int16
|
+| Int32:standard | [0, 1, 2147483647,
-2147483648]@Series[Int32] | [0, 1,
2147483647, -2147483648]@int32 |
+| Int32:nullable | [0, 1, <NA>]@Series[Int32]
| [0, 1, None]@int32
|
+| Int64:standard | [0, 1, 9223372036854775807,
-9223372036854775808]@Series[Int64] | [0, 1,
9223372036854775807, -9223372036854775808]@int64 |
+| Int64:nullable | [0, 1, <NA>]@Series[Int64]
| [0, 1, None]@int64
|
+| UInt64:standard | [0, 1,
18446744073709551615]@Series[UInt64] |
[0, 1, 18446744073709551615]@uint64 |
+| Int64:empty | []@Series[Int64]
| []@int64
|
+| Int64:all-null | [<NA>, <NA>]@Series[Int64]
| [None, None]@int64
|
+| Float64:standard | [0.0, 1.5]@Series[Float64]
| [0.0, 1.5]@float64
|
+| Float64:nullable | [0.0, <NA>]@Series[Float64]
| [0.0, None]@float64
|
+| boolean:standard | [True, False]@Series[boolean]
| [True, False]@bool
|
+| boolean:nullable | [True, <NA>]@Series[boolean]
| [True, None]@bool
|
+| string[python]:standard | ['hello', 'world']@Series[string]
| [hello, world]@string
|
+| string[python]:nullable | ['hello', <NA>]@Series[string]
| [hello, None]@string
|
+| string[python]:empty | []@Series[string]
| []@string
|
+| int64[pyarrow]:standard | [0, 1, -1]@Series[int64[pyarrow]]
| [0, 1, -1]@int64
|
+| int64[pyarrow]:nullable | [0, 1, <NA>]@Series[int64[pyarrow]]
| [0, 1, None]@int64
|
+| int64[pyarrow]:empty | []@Series[int64[pyarrow]]
| []@int64
|
+| double[pyarrow]:nullable | [0.0, <NA>]@Series[double[pyarrow]]
| [0.0, None]@float64
|
+| bool[pyarrow]:nullable | [True, <NA>]@Series[bool[pyarrow]]
| [True, None]@bool
|
+| string[pyarrow]:standard | ['hello', 'world']@Series[string]
| [hello, world]@large_string
|
+| string[pyarrow]:nullable | ['hello', <NA>]@Series[string]
| [hello, None]@large_string
|
+| string[pyarrow]:empty | []@Series[string]
| []@large_string
|
+| timestamp[us][pyarrow]:standard | [Timestamp('2024-01-01
12:00:00')]@Series[timestamp[us][pyarrow]] | [2024-01-01
12:00:00]@timestamp[us] |
+| int64[pyarrow]:single-chunk | [1, 2]@Series[int64[pyarrow]]
| [1, 2]@int64
|
+| int64[pyarrow]:multi-chunk | [1, 2, 3]@Series[int64[pyarrow]]
| [1, 2, 3]@chunked<int64>
|
+| int64:overflow | [300, 1]@Series[int64]
| [300, 1]@int64
|
+| float64:fractional | [1.5, 2.5]@Series[float64]
| [1.5, 2.5]@float64
|
+| float64:infinity | [inf, 1.0]@Series[float64]
| [inf, 1.0]@float64
|
+| float64:precision | [1.1234567890123]@Series[float64]
| [1.1234567890123]@float64
|
+| datetime64[ns]:sub-us | [Timestamp('2024-01-01
00:00:00.000000123')]@Series[datetime64[ns]] | [2024-01-01
00:00:00.000000123]@timestamp[ns] |
+| object:date-then-datetime | [datetime.date(2024, 1, 1),
datetime.datetime(2024, 1, 1, 5, 30)]@Series[object] | [2024-01-01,
2024-01-01]@date32[day] |
+| object:datetime-then-date | [datetime.datetime(2024, 1, 1, 5, 30),
datetime.date(2024, 1, 1)]@Series[object] | ERR@ArrowTypeError
|
\ No newline at end of file
diff --git
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
new file mode 100644
index 000000000000..be0e518f416a
--- /dev/null
+++
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
@@ -0,0 +1,413 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Tests for PyArrow Array.from_pandas() with default arguments using golden file
comparison.
+
+This test monitors the pandas -> Arrow direction, which PySpark relies on for
+``createDataFrame(pandas_df)`` and for every pandas UDF's return value.
PySpark calls
+``pa.Array.from_pandas(series, mask=mask, type=arrow_type, safe=safecheck)`` in
+``pyspark/sql/conversion.py`` and ``pyspark/sql/pandas/conversion.py``; the
non-default
+arguments are covered by ``test_pyarrow_array_from_pandas_non_default.py``,
which reuses
+the source Series built here.
+
+Rows are grouped by how pandas stores the Series, because that decides which
branch
+PySpark takes when computing ``mask``:
+
+- numpy-backed dtypes, where ``mask=series.isnull()`` is passed;
+- dtypes implementing the ``__arrow_array__`` protocol, where PySpark passes
``mask=None``
+ because supplying a mask raises. The protocol means "can export Arrow", not
"is stored
+ as Arrow": ``Int64`` is numpy values plus a byte mask and ``string[python]``
is an object
+ ndarray, yet both implement it alongside the genuinely Arrow-backed
``[pyarrow]`` dtypes;
+- values chosen to be lossy or ambiguous when coerced, which the non-default
type tests
+ need and whose unconverted baseline is recorded here.
+
+## Golden File Cell Format
+
+Each cell uses the value@type format:
+- pandas Series: "python_list_repr@Series[dtype]"
+- PyArrow Array: "python_list_repr@arrow_type"
+- PyArrow ChunkedArray: "python_list_repr@chunked<arrow_type>"
+- Error: "ERR@ExceptionClassName"
+
+``from_pandas`` returns a ChunkedArray rather than an Array when the input
Series is backed
+by a chunked Arrow array, and the two are otherwise indistinguishable because
both report
+the element type. The distinction is a real contract:
``create_arrow_table_from_pandas``
+builds a ``pa.Table`` (which accepts either) specifically because of it -- see
SPARK-46776.
+``chunked<...>`` follows Arrow's angle-bracket spelling for parameterized
types, keeping it
+distinct from the square-bracket ``Series[...]`` / ``ndarray[...]`` used for
pandas and
+numpy containers. The chunk count is deliberately not recorded: it varies
with batching,
+whereas chunked-versus-not is the behavior under test.
+
+## Regenerating Golden Files
+
+Set SPARK_GENERATE_GOLDEN_FILES=1 before running:
+
+ SPARK_GENERATE_GOLDEN_FILES=1 python -m pytest \\
+
python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
+
+## PyArrow and pandas Version Compatibility
+
+The golden files capture behavior for specific PyArrow and pandas versions.
+Regenerate when upgrading either dependency, as from_pandas() behavior may
change.
+"""
+
+import datetime
+import decimal
+import unittest
+
+from pyspark.loose_version import LooseVersion
+from pyspark.testing.utils import (
+ have_pyarrow,
+ have_pandas,
+ have_numpy,
+ pyarrow_requirement_message,
+ pandas_requirement_message,
+ numpy_requirement_message,
+)
+from pyspark.testing.goldenutils import GoldenFileTestMixin
+
+if have_pandas:
+ import pandas as pd
+if have_pyarrow:
+ import pyarrow as pa
+if have_numpy:
+ import numpy as np
+
+
[email protected](
+ not have_pyarrow or not have_pandas or not have_numpy,
+ pyarrow_requirement_message or pandas_requirement_message or
numpy_requirement_message,
+)
+class PyArrowArrayFromPandasDefaultTests(GoldenFileTestMixin,
unittest.TestCase):
+ """
+ Tests pa.Array.from_pandas() with default arguments via golden file
comparison.
+
+ Three group methods build the source Series so the non-default tests can
reuse the whole
+ inventory or one group. Groups are disjoint and their order fixes the row
order.
+ """
+
+ @staticmethod
+ def repr_from_pandas_result(value):
+ """
+ Format a ``from_pandas`` return value, marking a ChunkedArray as
``chunked<type>``.
+
+ ``repr_value`` reports the element type, which is the same for an
Array and a
+ ChunkedArray, so without this the two are indistinguishable in a cell.
+ """
+ rendered = GoldenFileTestMixin.repr_value(value, max_len=0)
+ if isinstance(value, pa.ChunkedArray):
+ values, _, arrow_type = rendered.rpartition("@")
+ return f"{values}@chunked<{arrow_type}>"
+ return rendered
+
+ def _numpy_backed_sources(self):
+ """
+ Series whose storage is numpy, so ``hasattr(series.array,
"__arrow_array__")`` is
+ False and PySpark passes ``mask=series.isnull()``.
+ """
+ sources = {}
+
+ # =====================================================================
+ # Integer types
+ # =====================================================================
+ for dtype in ["int8", "int16", "int32", "int64"]:
+ info = np.iinfo(dtype)
+ sources[f"{dtype}:standard"] = pd.Series([0, 1, -1, info.max,
info.min], dtype=dtype)
+ sources[f"{dtype}:empty"] = pd.Series([], dtype=dtype)
+ # A numpy integer Series cannot hold a null, so pandas widens it to
float64.
+ sources["int64:nullable"] = pd.Series([0, 1, None])
+
+ for dtype in ["uint8", "uint16", "uint32", "uint64"]:
+ sources[f"{dtype}:standard"] = pd.Series([0, 1,
np.iinfo(dtype).max], dtype=dtype)
+
+ # =====================================================================
+ # Float and boolean types
+ # =====================================================================
+ for dtype in ["float32", "float64"]:
+ sources[f"{dtype}:standard"] = pd.Series([0.0, 1.5, -1.5],
dtype=dtype)
+ sources[f"{dtype}:nullable"] = pd.Series([0.0, np.nan, 1.5],
dtype=dtype)
+ sources[f"{dtype}:empty"] = pd.Series([], dtype=dtype)
+
+ sources["bool:standard"] = pd.Series([True, False, True])
+ sources["bool:empty"] = pd.Series([], dtype="bool")
+
+ # =====================================================================
+ # Object types (string, binary, decimal)
+ # =====================================================================
+ # Only the string rows need an explicit dtype; pandas 3 would infer
its ``str``
+ # dtype and they would stop being object rows.
+ sources["object:string"] = pd.Series(["hello", "world", ""],
dtype=object)
+ sources["object:string-nullable"] = pd.Series(["hello", None,
"world"], dtype=object)
+ # Unpinned, so this records the dtype pandas infers from Python
strings: object on
+ # pandas 2, its dedicated str dtype on pandas 3, which Arrow reads as
large_string.
+ sources["string:inferred"] = pd.Series(["hello", "world"])
+ sources["object:bytes"] = pd.Series([b"hello", b"world"])
+ sources["object:empty"] = pd.Series([], dtype=object)
+ sources["object:all-null"] = pd.Series([None, None], dtype=object)
+ sources["object:decimal"] = pd.Series([decimal.Decimal("1.50"),
decimal.Decimal("-2.25")])
+
+ # =====================================================================
+ # Nested types
+ # =====================================================================
+ # Inference builds these from Python containers. It cannot reach
``map`` (that
+ # needs an explicit type), nor ``large_list`` / ``fixed_size_list``,
which
+ # ``to_arrow_type`` never requests.
+ sources["list<int64>:standard"] = pd.Series([[1, 2], [3]])
+ sources["list<int64>:nullable"] = pd.Series([[1, 2], None])
+ sources["list<int64>:null-element"] = pd.Series([[1, None], [3]])
+ sources["list<string>:standard"] = pd.Series([["a", "b"], ["c"]])
+ sources["list<list<int64>>:standard"] = pd.Series([[[1, 2], [3]],
[[4]]])
+ sources["list<struct>:standard"] = pd.Series([[{"a": 1}], [{"a": 2}]])
+ sources["struct:standard"] = pd.Series([{"a": 1, "b": "x"}])
+ sources["struct:nullable"] = pd.Series([{"a": 1, "b": "x"}, None])
+ sources["struct<struct>:standard"] = pd.Series([{"a": {"b": 1}}])
+ sources["struct<list<int64>>:standard"] = pd.Series([{"a": [1, 2]}])
+
+ # =====================================================================
+ # Temporal types
+ # =====================================================================
+ # Each row pins its resolution so the row name stays accurate on both
pandas
+ # majors: pandas 2 infers "ns" from a datetime, pandas 3 infers "us".
+ dt = datetime.datetime(2024, 6, 15, 18, 30, 0)
+ for unit in ["ns", "us"]:
+ sources[f"datetime64[{unit}]:standard"] = pd.Series([dt],
dtype=f"datetime64[{unit}]")
+ sources[f"datetime64[{unit}]:nullable"] = pd.Series(
+ [dt, None], dtype=f"datetime64[{unit}]"
+ )
+ sources[f"datetime64[{unit}]:empty"] = pd.Series([],
dtype=f"datetime64[{unit}]")
+ sources[f"datetime64[{unit},tz]:standard"] = pd.Series(
+ [dt], dtype=f"datetime64[{unit}, UTC]"
+ )
+ sources[f"datetime64[{unit},tz]:nullable"] = pd.Series(
+ [dt, None], dtype=f"datetime64[{unit}, UTC]"
+ )
+ sources[f"timedelta64[{unit}]:standard"] = pd.Series(
+ pd.to_timedelta(["1 days", "2 hours"]),
dtype=f"timedelta64[{unit}]"
+ )
+ sources[f"timedelta64[{unit}]:nullable"] = pd.Series(
+ pd.to_timedelta(["1 days", None]), dtype=f"timedelta64[{unit}]"
+ )
+
+ # Unpinned, so the cell always duplicates one of the pinned rows
above. Which one
+ # it matches is the behavior recorded: "ns" on pandas 2, "us" on
pandas 3.
+ sources["datetime64:inferred"] = pd.Series([dt])
+ sources["timedelta64:inferred"] = pd.Series(pd.to_timedelta(["1 days",
"2 hours"]))
+
+ # datetime64[ns] spans only 1677-2262, so a far-past value needs
microseconds.
+ sources["datetime64[us]:out-of-ns-range"] = pd.Series(
+ [datetime.datetime(1500, 1, 1)], dtype="datetime64[us]"
+ )
+
+ # pandas has no native date or time dtype, so these stay object on
their own.
+ sources["date:standard"] = pd.Series([datetime.date(2024, 6, 15)])
+ sources["time:standard"] = pd.Series([datetime.time(18, 30, 45)])
+
+ # In object dtype pyarrow takes the unit from datetime's own
microsecond
+ # resolution, not from a numpy dtype, so these give timestamp[us]
where the
+ # datetime64[ns] rows above give timestamp[ns] -- and sub-microsecond
digits are
+ # dropped silently, while an explicit type=timestamp("us") raises
ArrowInvalid.
+ sources["object:datetime"] = pd.Series([dt], dtype=object)
+ sources["object:datetime-sub-us"] = pd.Series(
+ [pd.Timestamp("2024-01-01 00:00:00.000000123")], dtype=object
+ )
+ sources["object:timedelta"] = pd.Series([datetime.timedelta(days=1,
hours=2)], dtype=object)
+
+ # =====================================================================
+ # Categorical type
+ # =====================================================================
+ # PySpark casts a categorical to its categories' dtype first, but the
raw
+ # categorical is what a user hands to createDataFrame.
+ sources["category:standard"] = pd.Series(pd.Categorical(["a", "b",
"a"]))
+ sources["category:nullable"] = pd.Series(pd.Categorical(["a", None,
"b"]))
+
+ return sources
+
+ def _protocol_sources(self):
+ """
+ Series implementing ``__arrow_array__``, so PySpark passes
``mask=None`` --
+ supplying one raises, because the protocol returns an array whose
validity bitmap
+ is already built.
+
+ The protocol means "can export Arrow", not "is stored as Arrow", and
the rows
+ below deliberately cover both: ``Int64`` is numpy values plus a byte
mask and
+ ``string[python]`` is an object ndarray, while the ``[pyarrow]``
dtypes hold Arrow
+ buffers. PySpark does not distinguish them -- ``conversion.py:435``
branches only
+ on the protocol -- so they share one group.
+ """
+ sources = {}
+
+ # =====================================================================
+ # Nullable extension dtypes (numpy values plus a byte mask)
+ # =====================================================================
+ for dtype in ["Int8", "Int16", "Int32", "Int64"]:
+ info = np.iinfo(dtype.lower())
+ sources[f"{dtype}:standard"] = pd.Series([0, 1, info.max,
info.min], dtype=dtype)
+ sources[f"{dtype}:nullable"] = pd.Series([0, 1, None], dtype=dtype)
+ sources["UInt64:standard"] = pd.Series([0, 1, np.iinfo("uint64").max],
dtype="UInt64")
+ sources["Int64:empty"] = pd.Series([], dtype="Int64")
+ sources["Int64:all-null"] = pd.Series([None, None], dtype="Int64")
+
+ sources["Float64:standard"] = pd.Series([0.0, 1.5], dtype="Float64")
+ sources["Float64:nullable"] = pd.Series([0.0, None], dtype="Float64")
+
+ sources["boolean:standard"] = pd.Series([True, False], dtype="boolean")
+ sources["boolean:nullable"] = pd.Series([True, None], dtype="boolean")
+
+ # StringArray: an object ndarray of Python str, yet it exports Arrow.
+ sources["string[python]:standard"] = pd.Series(["hello", "world"],
dtype="string[python]")
+ sources["string[python]:nullable"] = pd.Series(["hello", None],
dtype="string[python]")
+ sources["string[python]:empty"] = pd.Series([], dtype="string[python]")
+
+ # =====================================================================
+ # PyArrow-backed dtypes (Arrow buffers, so the handoff is zero-copy)
+ # =====================================================================
+ # Since pandas 2.2 a pyarrow-backed string stores large_string, and
the protocol
+ # hands that back rather than the 32-bit string the name suggests.
PySpark works
+ # around pyarrow < 19 ignoring a narrower request (SPARK-46776).
+ sources["int64[pyarrow]:standard"] = pd.Series([0, 1, -1],
dtype="int64[pyarrow]")
+ sources["int64[pyarrow]:nullable"] = pd.Series([0, 1, None],
dtype="int64[pyarrow]")
+ sources["int64[pyarrow]:empty"] = pd.Series([], dtype="int64[pyarrow]")
+ sources["double[pyarrow]:nullable"] = pd.Series([0.0, None],
dtype="double[pyarrow]")
+ sources["bool[pyarrow]:nullable"] = pd.Series([True, None],
dtype="bool[pyarrow]")
+ sources["string[pyarrow]:standard"] = pd.Series(["hello", "world"],
dtype="string[pyarrow]")
+ sources["string[pyarrow]:nullable"] = pd.Series(["hello", None],
dtype="string[pyarrow]")
+ sources["string[pyarrow]:empty"] = pd.Series([],
dtype="string[pyarrow]")
+ sources["timestamp[us][pyarrow]:standard"] = pd.Series(
+ [datetime.datetime(2024, 1, 1, 12, 0, 0)],
dtype="timestamp[us][pyarrow]"
+ )
+
+ # A chunked backing array makes from_pandas return a ChunkedArray,
which
+ # pa.RecordBatch.from_arrays rejects -- hence PySpark builds a
pa.Table.
+ sources["int64[pyarrow]:single-chunk"] = pd.Series(
+ pd.arrays.ArrowExtensionArray(pa.chunked_array([pa.array([1, 2],
pa.int64())]))
+ )
+ sources["int64[pyarrow]:multi-chunk"] = pd.Series(
+ pd.arrays.ArrowExtensionArray(
+ pa.chunked_array([pa.array([1, 2], pa.int64()), pa.array([3],
pa.int64())])
+ )
+ )
+
+ return sources
+
+ def _coercion_sources(self):
+ """
+ Series whose values are lossy or ambiguous once a target type is
requested.
+
+ These exist for the non-default type tests, where ``safe=`` decides
whether the
+ conversion raises or silently changes the value. Recording their
unconverted
+ results here gives those cells a baseline to be read against.
+ """
+ sources = {}
+
+ # =====================================================================
+ # Values that narrow lossily
+ # =====================================================================
+ sources["int64:overflow"] = pd.Series([300, 1])
+ sources["float64:fractional"] = pd.Series([1.5, 2.5])
+ sources["float64:infinity"] = pd.Series([np.inf, 1.0])
+ sources["float64:precision"] = pd.Series([1.1234567890123])
+
+ # The only temporal case where safe= flips.
+ sources["datetime64[ns]:sub-us"] = pd.Series(
+ pd.to_datetime(["2024-01-01 00:00:00.000000123"])
+ )
+
+ # =====================================================================
+ # Values whose inferred type depends on element order
+ # =====================================================================
+ # Inference commits to the first element's type, so these same values
either lose
+ # the time component or raise, depending on their order.
+ sources["object:date-then-datetime"] = pd.Series(
+ [datetime.date(2024, 1, 1), datetime.datetime(2024, 1, 1, 5, 30)],
dtype=object
+ )
+ sources["object:datetime-then-date"] = pd.Series(
+ [datetime.datetime(2024, 1, 1, 5, 30), datetime.date(2024, 1, 1)],
dtype=object
+ )
+
+ return sources
+
+ def _build_source_arrays(self):
+ """Build an ordered dict of named source pandas Series for testing."""
+ sources = {}
+ for group in [
+ self._numpy_backed_sources(),
+ self._protocol_sources(),
+ self._coercion_sources(),
+ ]:
+ sources.update(group)
+ return sources
+
+ def test_from_pandas_default(self):
+ """Test pa.Array.from_pandas() with default arguments against golden
file."""
+ sources = self._build_source_arrays()
+ row_names = list(sources.keys())
+ col_names = ["pandas series", "arrow array"]
+
+ overrides = {}
+ if LooseVersion(pd.__version__) >= LooseVersion("3.0.0"):
+ # Only the deliberately unpinned rows move: pandas 3 infers
microseconds where
+ # pandas 2 inferred nanoseconds, and its dedicated str dtype is
backed by
+ # large_string -- which a categorical's values inherit too.
+ overrides.update(
+ {
+ ("string:inferred", "pandas series"): "['hello',
'world']@Series[str]",
+ ("string:inferred", "arrow array"): "[hello,
world]@large_string",
+ ("datetime64:inferred", "pandas series"): (
+ "[Timestamp('2024-06-15
18:30:00')]@Series[datetime64[us]]"
+ ),
+ ("datetime64:inferred", "arrow array"): "[2024-06-15
18:30:00]@timestamp[us]",
+ ("timedelta64:inferred", "pandas series"): (
+ "[Timedelta('1 days 00:00:00'), "
+ "Timedelta('0 days 02:00:00')]@Series[timedelta64[us]]"
+ ),
+ ("timedelta64:inferred", "arrow array"): (
+ "[1 day, 0:00:00, 2:00:00]@duration[us]"
+ ),
+ ("category:standard", "arrow array"): (
+ "[a, b, a]@dictionary<values=large_string,
indices=int8, ordered=0>"
+ ),
+ ("category:nullable", "arrow array"): (
+ "[a, None, b]@dictionary<values=large_string,
indices=int8, ordered=0>"
+ ),
+ }
+ )
+
+ def compute_cell(row_name, col_name):
+ series = sources[row_name]
+ if col_name == "pandas series":
+ return self.repr_value(series, max_len=0)
+ else:
+ try:
+ return
self.repr_from_pandas_result(pa.Array.from_pandas(series))
+ except Exception as e:
+ return f"ERR@{type(e).__name__}"
+
+ self.compare_or_generate_golden_matrix(
+ row_names=row_names,
+ col_names=col_names,
+ compute_cell=compute_cell,
+ golden_file_prefix="golden_pyarrow_array_from_pandas_default",
+ index_name="test case",
+ overrides=overrides,
+ )
+
+
+if __name__ == "__main__":
+ from pyspark.testing import main
+
+ main(globals()["__file__"])
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]