Skip to content

Commit b0b792a

Browse files
authored
GH-48808: [Python] Drop support for Pandas < 2.0.3 (#50444)
### Rationale for this change [Pandas 2.0.0](https://pypi.org/project/pandas/2.0.0/) was released on April 3rd 2023. The last version of Pandas 1 was [Pandas 1.5.3](https://pypi.org/project/pandas/1.5.3/) and it was released on January 19th 2023. That version only supports up to Python 3.11. We are currently supporting up to Pandas 1.5.2 (first version supporting 3.11). On the issue we discussed to bump our Pandas support to 2.0.0 but given 2.0.3 is the latest in the 2.0.x, was released on June 2023, only supports up to Python 3.11 and bumping to it allows use to improve even further some of the conditionals we are bumping to Pandas >= 2.0.3 ### What changes are included in this PR? Update minimal support check to Version(2.0.3) and remove all test skips, conditionals and scenarios where we are checking for Pandas < 2.0.3. We found a couple of minor things ### Are these changes tested? Yes via CI ### Are there any user-facing changes? Yes, Pandas < 2 won't be supported. * GitHub Issue: #48808 Authored-by: Raúl Cumplido <raulcumplido@gmail.com> Signed-off-by: Antoine Pitrou <antoine@python.org>
1 parent 3b515ed commit b0b792a

14 files changed

Lines changed: 47 additions & 216 deletions

File tree

.github/workflows/python.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
name:
6868
- conda-python-docs
6969
- conda-python-3.12-nopandas
70-
- conda-python-3.11-pandas-1.5.2
70+
- conda-python-3.11-pandas-2.0.3
7171
- conda-python-3.14-pandas-latest
7272
- conda-python-3.13-no-numpy
7373
include:
@@ -79,11 +79,11 @@ jobs:
7979
image: conda-python
8080
title: AMD64 Conda Python 3.12 Without Pandas
8181
python: "3.12"
82-
- name: conda-python-3.11-pandas-1.5.2
82+
- name: conda-python-3.11-pandas-2.0.3
8383
image: conda-python-pandas
84-
title: AMD64 Conda Python 3.11 Pandas 1.5.2
84+
title: AMD64 Conda Python 3.11 Pandas 2.0.3
8585
python: "3.11"
86-
pandas: "1.5.2"
86+
pandas: "2.0.3"
8787
numpy: "1.23.2"
8888
- name: conda-python-3.14-pandas-latest
8989
image: conda-python-pandas

dev/tasks/tasks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ tasks:
727727

728728
############################## Integration tests ############################
729729

730-
{% for python_version, pandas_version, numpy_version, cache_leaf in [("3.11", "1.5.2", "1.23.2", True),
730+
{% for python_version, pandas_version, numpy_version, cache_leaf in [("3.11", "2.0.3", "1.23.2", True),
731731
("3.12", "latest", "latest", False),
732732
("3.13", "latest", "1.26.2", False),
733733
("3.13", "latest", "latest", False),

docs/source/python/install.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Dependencies
7474
Optional dependencies
7575

7676
* **NumPy 1.23.2** or higher.
77-
* **pandas 1.5.2** or higher,
77+
* **pandas 2.0.3** or higher,
7878
* **cffi**.
7979

8080
Additional packages PyArrow is compatible with are :ref:`fsspec <filesystem-fsspec>`

python/pyarrow/array.pxi

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,10 +2383,6 @@ cdef _array_like_to_pandas(obj, options, types_mapper):
23832383
arr = dtype.__from_arrow__(obj)
23842384
return pandas_api.series(arr, name=name, copy=False)
23852385

2386-
if pandas_api.is_v1():
2387-
# ARROW-3789: Coerce date/timestamp types to datetime64[ns]
2388-
c_options.coerce_temporal_nanoseconds = True
2389-
23902386
if isinstance(obj, Array):
23912387
with nogil:
23922388
check_status(ConvertArrayToPandas(c_options,

python/pyarrow/pandas-shim.pxi

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ cdef class _PandasAPIShim(object):
3838
object _array_like_types, _is_extension_array_dtype, _lock
3939
bint has_sparse
4040
bint _pd024
41-
bint _is_v1, _is_ge_v21, _is_ge_v23, _is_ge_v3, _is_ge_v3_strict
41+
bint _is_ge_v21, _is_ge_v23, _is_ge_v3, _is_ge_v3_strict
4242

4343
def __init__(self):
4444
self._lock = Lock()
@@ -61,25 +61,23 @@ cdef class _PandasAPIShim(object):
6161
self._pd = pd
6262
self._version = pd.__version__
6363
self._loose_version = Version(pd.__version__)
64-
self._is_v1 = False
6564

66-
if self._loose_version < Version('1.0.0'):
65+
if self._loose_version < Version('2.0.3'):
6766
self._have_pandas = False
6867
if raise_:
6968
raise ImportError(
70-
f"pyarrow requires pandas 1.0.0 or above, pandas {self._version} is "
69+
f"pyarrow requires pandas 2.0.3 or above, pandas {self._version} is "
7170
"installed"
7271
)
7372
else:
7473
warnings.warn(
75-
f"pyarrow requires pandas 1.0.0 or above, pandas {self._version} is "
74+
f"pyarrow requires pandas 2.0.3 or above, pandas {self._version} is "
7675
"installed. Therefore, pandas-specific integration is not "
7776
"used.",
7877
stacklevel=2
7978
)
8079
return
8180

82-
self._is_v1 = self._loose_version < Version('2.0.0')
8381
self._is_ge_v21 = self._loose_version >= Version('2.1.0')
8482
self._is_ge_v23 = self._loose_version >= Version('2.3.0.dev0')
8583
self._is_ge_v3 = self._loose_version >= Version('3.0.0.dev0')
@@ -166,10 +164,6 @@ cdef class _PandasAPIShim(object):
166164
self._check_import()
167165
return self._version
168166

169-
def is_v1(self):
170-
self._check_import()
171-
return self._is_v1
172-
173167
def is_ge_v21(self):
174168
self._check_import()
175169
return self._is_ge_v21

python/pyarrow/pandas_compat.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,8 +790,6 @@ def _reconstruct_block(item, columns=None, extension_columns=None, return_block=
790790

791791

792792
def make_datetimetz(unit, tz):
793-
if _pandas_api.is_v1():
794-
unit = 'ns' # ARROW-3789: Coerce date/timestamp types to datetime64[ns]
795793
tz = pa.lib.string_to_tzinfo(tz, prefer_zoneinfo=_pandas_api.is_ge_v3())
796794
return _pandas_api.datetimetz_type(unit, tz=tz)
797795

python/pyarrow/src/arrow/python/arrow_to_pandas.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,7 @@ struct ObjectWriterVisitor {
12931293
PyObject** out) {
12941294
ARROW_DCHECK(internal::BorrowPandasDataOffsetType() != nullptr);
12951295
// DateOffset objects do not add nanoseconds component to pd.Timestamp.
1296-
// as of Pandas 1.3.3
1296+
// as of Pandas 1.3.3
12971297
// (https://github.com/pandas-dev/pandas/issues/43892).
12981298
// So convert microseconds and remainder to preserve data
12991299
// but give users more expected results.

python/pyarrow/table.pxi

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4070,10 +4070,6 @@ def table_to_blocks(options, Table table, categories, extension_columns):
40704070
c_options.extension_columns = make_shared[unordered_set[c_string]](
40714071
unordered_set[c_string]({tobytes(col) for col in extension_columns}))
40724072

4073-
if pandas_api.is_v1():
4074-
# ARROW-3789: Coerce date/timestamp types to datetime64[ns]
4075-
c_options.coerce_temporal_nanoseconds = True
4076-
40774073
if c_options.self_destruct:
40784074
# Move the shared_ptr, table is now unsafe to use further
40794075
c_table = move(table.sp_table)

python/pyarrow/tests/interchange/test_conversion.py

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
from datetime import datetime as dt
1919
import pyarrow as pa
20-
from pyarrow.vendored.version import Version
2120
import pytest
2221

2322
try:
@@ -122,9 +121,6 @@ def test_offset_of_sliced_array():
122121
]
123122
)
124123
def test_pandas_roundtrip(uint, int, float, np_float_str):
125-
if Version(pd.__version__) < Version("1.5.0"):
126-
pytest.skip("__dataframe__ added to pandas in 1.5.0")
127-
128124
arr = [1, 2, 3]
129125
table = pa.table(
130126
{
@@ -153,9 +149,6 @@ def test_pandas_roundtrip(uint, int, float, np_float_str):
153149
@pytest.mark.pandas
154150
def test_pandas_roundtrip_string():
155151
# See https://github.com/pandas-dev/pandas/issues/50554
156-
if Version(pd.__version__) < Version("1.6"):
157-
pytest.skip("Column.size() bug in pandas")
158-
159152
arr = ["a", "", "c"]
160153
table = pa.table({"a": pa.array(arr)})
161154

@@ -182,46 +175,32 @@ def test_pandas_roundtrip_string():
182175
@pytest.mark.pandas
183176
def test_pandas_roundtrip_large_string():
184177
# See https://github.com/pandas-dev/pandas/issues/50554
185-
if Version(pd.__version__) < Version("1.6"):
186-
pytest.skip("Column.size() bug in pandas")
187-
188178
arr = ["a", "", "c"]
189179
table = pa.table({"a_large": pa.array(arr, type=pa.large_string())})
190180

191181
from pandas.api.interchange import (
192182
from_dataframe as pandas_from_dataframe
193183
)
194184

195-
if Version(pd.__version__) >= Version("2.0.1"):
196-
pandas_df = pandas_from_dataframe(table)
197-
result = pi.from_dataframe(pandas_df)
198-
199-
assert result["a_large"].to_pylist() == table["a_large"].to_pylist()
200-
assert pa.types.is_large_string(table["a_large"].type)
201-
assert pa.types.is_large_string(result["a_large"].type)
185+
pandas_df = pandas_from_dataframe(table)
186+
result = pi.from_dataframe(pandas_df)
202187

203-
table_protocol = table.__dataframe__()
204-
result_protocol = result.__dataframe__()
188+
assert result["a_large"].to_pylist() == table["a_large"].to_pylist()
189+
assert pa.types.is_large_string(table["a_large"].type)
190+
assert pa.types.is_large_string(result["a_large"].type)
205191

206-
assert table_protocol.num_columns() == result_protocol.num_columns()
207-
assert table_protocol.num_rows() == result_protocol.num_rows()
208-
assert table_protocol.num_chunks() == result_protocol.num_chunks()
209-
assert table_protocol.column_names() == result_protocol.column_names()
192+
table_protocol = table.__dataframe__()
193+
result_protocol = result.__dataframe__()
210194

211-
else:
212-
# large string not supported by pandas implementation for
213-
# older versions of pandas
214-
# https://github.com/pandas-dev/pandas/issues/52795
215-
with pytest.raises(AssertionError):
216-
pandas_from_dataframe(table)
195+
assert table_protocol.num_columns() == result_protocol.num_columns()
196+
assert table_protocol.num_rows() == result_protocol.num_rows()
197+
assert table_protocol.num_chunks() == result_protocol.num_chunks()
198+
assert table_protocol.column_names() == result_protocol.column_names()
217199

218200

219201
@pytest.mark.pandas
220202
def test_pandas_roundtrip_string_with_missing():
221203
# See https://github.com/pandas-dev/pandas/issues/50554
222-
if Version(pd.__version__) < Version("1.6"):
223-
pytest.skip("Column.size() bug in pandas")
224-
225204
arr = ["a", "", "c", None]
226205
table = pa.table({"a": pa.array(arr),
227206
"a_large": pa.array(arr, type=pa.large_string())})
@@ -230,29 +209,20 @@ def test_pandas_roundtrip_string_with_missing():
230209
from_dataframe as pandas_from_dataframe
231210
)
232211

233-
if Version(pd.__version__) >= Version("2.0.2"):
234-
pandas_df = pandas_from_dataframe(table)
235-
result = pi.from_dataframe(pandas_df)
212+
pandas_df = pandas_from_dataframe(table)
213+
result = pi.from_dataframe(pandas_df)
236214

237-
assert result["a"].to_pylist() == table["a"].to_pylist()
238-
assert pa.types.is_string(table["a"].type)
239-
assert pa.types.is_large_string(result["a"].type)
215+
assert result["a"].to_pylist() == table["a"].to_pylist()
216+
assert pa.types.is_string(table["a"].type)
217+
assert pa.types.is_large_string(result["a"].type)
240218

241-
assert result["a_large"].to_pylist() == table["a_large"].to_pylist()
242-
assert pa.types.is_large_string(table["a_large"].type)
243-
assert pa.types.is_large_string(result["a_large"].type)
244-
else:
245-
# older versions of pandas do not have bitmask support
246-
# https://github.com/pandas-dev/pandas/issues/49888
247-
with pytest.raises(NotImplementedError):
248-
pandas_from_dataframe(table)
219+
assert result["a_large"].to_pylist() == table["a_large"].to_pylist()
220+
assert pa.types.is_large_string(table["a_large"].type)
221+
assert pa.types.is_large_string(result["a_large"].type)
249222

250223

251224
@pytest.mark.pandas
252225
def test_pandas_roundtrip_categorical():
253-
if Version(pd.__version__) < Version("2.0.2"):
254-
pytest.skip("Bitmasks not supported in pandas interchange implementation")
255-
256226
arr = ["Mon", "Tue", "Mon", "Wed", "Mon", "Thu", "Fri", "Sat", None]
257227
table = pa.table(
258228
{"weekday": pa.array(arr).dictionary_encode()}
@@ -299,21 +269,14 @@ def test_pandas_roundtrip_categorical():
299269
@pytest.mark.pandas
300270
@pytest.mark.parametrize("unit", ['s', 'ms', 'us', 'ns'])
301271
def test_pandas_roundtrip_datetime(unit):
302-
if Version(pd.__version__) < Version("1.5.0"):
303-
pytest.skip("__dataframe__ added to pandas in 1.5.0")
304272
from datetime import datetime as dt
305273

306274
# timezones not included as they are not yet supported in
307275
# the pandas implementation
308276
dt_arr = [dt(2007, 7, 13), dt(2007, 7, 14), dt(2007, 7, 15)]
309277
table = pa.table({"a": pa.array(dt_arr, type=pa.timestamp(unit))})
310278

311-
if Version(pd.__version__) < Version("1.6"):
312-
# pandas < 2.0 always creates datetime64 in "ns"
313-
# resolution
314-
expected = pa.table({"a": pa.array(dt_arr, type=pa.timestamp('ns'))})
315-
else:
316-
expected = table
279+
expected = table
317280

318281
from pandas.api.interchange import (
319282
from_dataframe as pandas_from_dataframe
@@ -337,9 +300,6 @@ def test_pandas_roundtrip_datetime(unit):
337300
"np_float_str", ["float32", "float64"]
338301
)
339302
def test_pandas_to_pyarrow_with_missing(np_float_str):
340-
if Version(pd.__version__) < Version("1.5.0"):
341-
pytest.skip("__dataframe__ added to pandas in 1.5.0")
342-
343303
np_array = np.array([0, np.nan, 2], dtype=np.dtype(np_float_str))
344304
datetime_array = [None, dt(2007, 7, 14), dt(2007, 7, 15)]
345305
df = pd.DataFrame({
@@ -359,9 +319,6 @@ def test_pandas_to_pyarrow_with_missing(np_float_str):
359319

360320
@pytest.mark.pandas
361321
def test_pandas_to_pyarrow_float16_with_missing():
362-
if Version(pd.__version__) < Version("1.5.0"):
363-
pytest.skip("__dataframe__ added to pandas in 1.5.0")
364-
365322
# np.float16 errors if ps.is_nan is used
366323
# pyarrow.lib.ArrowNotImplementedError: Function 'is_nan' has no kernel
367324
# matching input types (halffloat)
@@ -482,9 +439,6 @@ def test_nan_as_null():
482439

483440
@pytest.mark.pandas
484441
def test_allow_copy_false():
485-
if Version(pd.__version__) < Version("1.5.0"):
486-
pytest.skip("__dataframe__ added to pandas in 1.5.0")
487-
488442
# Test that an error is raised when a copy is needed
489443
# to create a bitmask
490444

@@ -501,9 +455,6 @@ def test_allow_copy_false():
501455

502456
@pytest.mark.pandas
503457
def test_allow_copy_false_bool_categorical():
504-
if Version(pd.__version__) < Version("1.5.0"):
505-
pytest.skip("__dataframe__ added to pandas in 1.5.0")
506-
507458
# Test that an error is raised for boolean
508459
# and categorical dtype (copy is always made)
509460

python/pyarrow/tests/parquet/test_pandas.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,6 @@ def test_pandas_categorical_roundtrip():
524524
@pytest.mark.pandas
525525
def test_categories_with_string_pyarrow_dtype(tempdir):
526526
# gh-33727: writing to parquet should not fail
527-
if Version(pd.__version__) < Version("1.3.0"):
528-
pytest.skip("PyArrow backed string data type introduced in pandas 1.3.0")
529-
530527
df1 = pd.DataFrame({"x": ["foo", "bar", "foo"]}, dtype="string[pyarrow]")
531528
df1 = df1.astype("category")
532529

0 commit comments

Comments
 (0)