This is an automated email from the ASF dual-hosted git repository.
AlenkaF pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 6f161d65250 GH-51302: [Python] Avoid deprecated .values call in
pandas->pyarrow conversion (#51484)
6f161d65250 is described below
commit 6f161d6525047a648f89a6320c2fb71debe2d184
Author: Joris Van den Bossche <[email protected]>
AuthorDate: Thu Oct 1 13:16:29 2026 +0200
GH-51302: [Python] Avoid deprecated .values call in pandas->pyarrow
conversion (#51484)
### Rationale for this change
Pandas has deprecated certain aspects of `.values` (when it looses
information), which our pandas->pyarrow conversion currently relies upon. So
updating that code path to avoid this usage.
### What changes are included in this PR?
Removed usage of `.values` for the deprecated cases (period, interval,
datetimetz). In the custom `get_values`, we already avoided this for period and
interval, added a special case for converting datetimetz to a numpy array. And
ensured that other cases were we did a direct `.values` goes through this
`get_values` helper.
### Are these changes tested?
Yes, I added more tests first to ensure full coverage of all cases that
might run into the deprecation (e.g. the `Schema.from_pandas(df)` or
`pa.array(EA)` with period/datetimetz dtypes were not currently covered)
### Are there any user-facing changes?
No
### Was AI used for this PR?
No
* GitHub Issue: #51302
Authored-by: Joris Van den Bossche <[email protected]>
Signed-off-by: AlenkaF <[email protected]>
---
python/pyarrow/array.pxi | 10 +++----
python/pyarrow/pandas-shim.pxi | 3 ++
python/pyarrow/pandas_compat.py | 15 ++++++----
python/pyarrow/tests/test_pandas.py | 57 ++++++++++++++++++++++++++++++-------
python/pyarrow/tests/test_schema.py | 9 ++++++
5 files changed, 72 insertions(+), 22 deletions(-)
diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi
index 223b77dd4fa..bf9a6d67228 100644
--- a/python/pyarrow/array.pxi
+++ b/python/pyarrow/array.pxi
@@ -5411,16 +5411,16 @@ cdef object get_array_class_from_type(
return _array_classes[data_type.id()]
-cdef object get_values(object obj, bint* is_series):
+cdef object get_values(object obj, bint* is_pandas_object):
if pandas_api.is_series(obj) or pandas_api.is_index(obj):
result = pandas_api.get_values(obj)
- is_series[0] = True
+ is_pandas_object[0] = True
elif isinstance(obj, np.ndarray):
result = obj
- is_series[0] = False
+ is_pandas_object[0] = False
else:
- result = pandas_api.series(obj, copy=False).values
- is_series[0] = False
+ result = pandas_api.get_values(pandas_api.series(obj, copy=False))
+ is_pandas_object[0] = False
return result
diff --git a/python/pyarrow/pandas-shim.pxi b/python/pyarrow/pandas-shim.pxi
index 603b2b09d40..bd29eb03222 100644
--- a/python/pyarrow/pandas-shim.pxi
+++ b/python/pyarrow/pandas-shim.pxi
@@ -239,6 +239,9 @@ cdef class _PandasAPIShim(object):
if isinstance(obj.dtype, (self.pd.api.types.IntervalDtype,
self.pd.api.types.PeriodDtype)):
return obj.array
+ elif isinstance(obj.dtype, self.pd.api.types.DatetimeTZDtype):
+ # get the underlying to numpy datetime64 UTC values
+ return obj.to_numpy("datetime64[{}]".format(obj.dtype.unit))
return obj.values
cdef _PandasAPIShim pandas_api = _PandasAPIShim()
diff --git a/python/pyarrow/pandas_compat.py b/python/pyarrow/pandas_compat.py
index 36d336d5135..495df5645f3 100644
--- a/python/pyarrow/pandas_compat.py
+++ b/python/pyarrow/pandas_compat.py
@@ -578,7 +578,7 @@ def dataframe_to_types(df, preserve_index, columns=None):
types = []
# If pandas knows type, skip conversion
for c in columns_to_convert:
- values = c.values
+ values = _pandas_api.get_values(c)
if _pandas_api.is_categorical(values):
type_ = pa.array(c, from_pandas=True).type
elif _pandas_api.is_extension_array_dtype(values):
@@ -646,10 +646,13 @@ def dataframe_to_arrays(df, schema, preserve_index,
nthreads=1, columns=None,
f"had {result.null_count} null values")
return result
- def _can_definitely_zero_copy(arr):
- return (isinstance(arr, np.ndarray) and
- arr.flags.contiguous and
- issubclass(arr.dtype.type, np.integer))
+ def _can_definitely_zero_copy(ser):
+ if isinstance(ser.dtype, np.dtype):
+ arr = ser.values
+ return (isinstance(arr, np.ndarray) and
+ arr.flags.contiguous and
+ issubclass(arr.dtype.type, np.integer))
+ return False
if nthreads == 1:
arrays = [convert_column(c, f)
@@ -658,7 +661,7 @@ def dataframe_to_arrays(df, schema, preserve_index,
nthreads=1, columns=None,
arrays = []
with futures.ThreadPoolExecutor(nthreads) as executor:
for c, f in zip(columns_to_convert, convert_fields):
- if _can_definitely_zero_copy(c.values):
+ if _can_definitely_zero_copy(c):
arrays.append(convert_column(c, f))
else:
arrays.append(executor.submit(convert_column, c, f))
diff --git a/python/pyarrow/tests/test_pandas.py
b/python/pyarrow/tests/test_pandas.py
index dd20a0aa977..6fc9925eb01 100644
--- a/python/pyarrow/tests/test_pandas.py
+++ b/python/pyarrow/tests/test_pandas.py
@@ -3334,21 +3334,31 @@ def _fully_loaded_dataframe_example():
c1 = pd.date_range('2000-01-01', periods=10)
data = {
- 0: c1,
- 1: c1.tz_localize('utc'),
- 2: c1.tz_localize('US/Eastern'),
- 3: c1[::2].tz_localize('utc').repeat(2).astype('category'),
- 4: ['foo', 'bar'] * 5,
- 5: pd.Series(['foo', 'bar'] * 5).astype('category').values,
- 6: [True, False] * 5,
- 7: np.random.randn(10),
- 8: np.random.randint(0, 100, size=10),
- 9: pd.period_range('2013', periods=10, freq='M'),
- 10: pd.interval_range(start=1, freq=1, periods=10),
+ "col0": c1,
+ "col1": c1.tz_localize('utc'),
+ "col2": c1.tz_localize('US/Eastern'),
+ "col3": c1[::2].tz_localize('utc').repeat(2).astype('category'),
+ "col4": ['foo', 'bar'] * 5,
+ "col5": pd.Series(['foo', 'bar'] * 5).astype('category').values,
+ "col6": [True, False] * 5,
+ "col7": np.random.randn(10),
+ "col8": np.random.randint(0, 100, size=10),
+ "col9": pd.period_range('2013', periods=10, freq='M'),
+ "col10": pd.interval_range(start=1, freq=1, periods=10),
}
return pd.DataFrame(data, index=index)
+def test_roundtrip_fully_loaded_dataframe_example():
+ df = _fully_loaded_dataframe_example()
+ expected = df.copy()
+ expected["col3"] = df["col3"].cat.rename_categories(
+ df["col3"].cat.categories.tz_convert(None))
+ _check_pandas_roundtrip(df, preserve_index=None, expected=expected)
+ _check_pandas_roundtrip(df, preserve_index=None,
+ expected=expected, use_threads=True)
+
+
@pytest.mark.parametrize('columns', ([b'foo'], ['foo']))
def test_roundtrip_with_bytes_unicode(columns):
df = pd.DataFrame(columns=columns)
@@ -5327,3 +5337,28 @@ def test_json_unserializable_pd_df_attrs():
pd_metadata = json.loads(df_table.schema.metadata[b"pandas"])
assert not pd_metadata["attributes"]
+
+
+def test_pandas_array_likes_with_extension_arrays():
+ # https://github.com/apache/arrow/issues/51302
+ dtidx = pd.date_range("2025-01-01", periods=10)
+
+ arrays = [
+ (dtidx.array.tz_localize("UTC"), None),
+ (dtidx.array.tz_localize("Europe/Paris"), None),
+ (dtidx.array.tz_localize("Europe/Paris").as_unit("s"), None),
+ (pd.period_range('2013', periods=10, freq='M'), None),
+ (pd.interval_range(start=1, freq=1, periods=10), None),
+ (pd.array([1, 2, 3], dtype="Int32"), np.array([1, 2, 3],
dtype="int32")),
+ (pd.array([1.1, 2.2, None], dtype="Float64"),
+ np.array([1.1, 2.2, np.nan], dtype="float64")),
+ (pd.array([True, False, True], dtype="boolean"),
+ np.array([True, False, True], dtype="bool")),
+ (pd.array(["a", "b", None], dtype="string"),
+ np.array(["a", "b", None], dtype="object")),
+ ]
+
+ for arr in arrays:
+ for box in [pd.array, pd.Index, pd.Series]:
+ _check_array_roundtrip(box(arr[0]), expected=pd.Series(
+ arr[1]) if arr[1] is not None else None)
diff --git a/python/pyarrow/tests/test_schema.py
b/python/pyarrow/tests/test_schema.py
index 27b6419f9ad..93519fc0173 100644
--- a/python/pyarrow/tests/test_schema.py
+++ b/python/pyarrow/tests/test_schema.py
@@ -27,6 +27,7 @@ except ImportError:
import pyarrow as pa
import pyarrow.tests.util as test_util
+from pyarrow.tests.test_pandas import _fully_loaded_dataframe_example
def test_schema_constructor_errors():
@@ -719,6 +720,14 @@ def test_schema_from_pandas():
assert schema == expected
[email protected]
+def test_schema_from_pandas_all_types():
+ df = _fully_loaded_dataframe_example()
+ schema = pa.Schema.from_pandas(df)
+ expected = pa.Table.from_pandas(df).schema
+ assert schema == expected
+
+
def test_schema_sizeof():
schema = pa.schema([
pa.field('foo', pa.int32()),