Skip to content

Commit 026a524

Browse files
authored
Add support for pandas 3.X, removing the pandas version bound (#1431)
Signed-off-by: Avi Shinnar <shinnar@us.ibm.com>
1 parent b0a801d commit 026a524

11 files changed

Lines changed: 137 additions & 39 deletions

File tree

examples/demo_column_transformer.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,9 @@
268268
"outputs": [],
269269
"source": [
270270
"import numpy as np\n",
271+
"from lale.helpers import safe_issubdtype\n",
271272
"num_cols = [col for col in train_X.columns\n",
272-
" if np.issubdtype(train_X.dtypes[col], np.number)]\n",
273+
" if safe_issubdtype(train_X.dtypes[col], np.number)]\n",
273274
"cat_cols = [col for col in train_X.columns if col not in num_cols]"
274275
]
275276
},

examples/demo_fairness_datasets.ipynb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
"metadata": {},
117117
"outputs": [],
118118
"source": [
119+
"from lale.helpers import safe_issubdtype\n",
120+
"\n",
119121
"def format_protected_attribute(pattrs, index):\n",
120122
" return \" \" if len(pattrs) <= index else pattrs[index][\"feature\"]\n",
121123
"\n",
@@ -128,7 +130,7 @@
128130
" \"origin\": dataset_origins[dataset_name],\n",
129131
" \"n_rows\": len(X),\n",
130132
" \"n_cols\": X.shape[1],\n",
131-
" \"any_categorical\": any(not np.issubdtype(t, np.number) for t in X.dtypes),\n",
133+
" \"any_categorical\": any(not safe_issubdtype(t, np.number) for t in X.dtypes),\n",
132134
" \"any_missing\": X.isna().any().any(),\n",
133135
" \"n_labels\": len(y.unique()),\n",
134136
" \"target_name\": y.name,\n",
@@ -496,7 +498,7 @@
496498
"\n",
497499
"def make_prep(X):\n",
498500
" any_missing = X.isna().any().any()\n",
499-
" cols_num = [c for c, t in zip(X.columns, X.dtypes) if np.issubdtype(t, np.number)]\n",
501+
" cols_num = [c for c, t in zip(X.columns, X.dtypes) if safe_issubdtype(t, np.number)]\n",
500502
" cols_cat = [c for c in X.columns if c not in cols_num]\n",
501503
" if len(cols_num) > 0:\n",
502504
" prep_num = lale.lib.rasl.Project(columns=cols_num)\n",

lale/datasets/data_schemas.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from typing import Any, List, Literal, Optional, Tuple, Type, Union
1616

1717
import numpy as np
18-
from numpy import issubdtype, ndarray
18+
from numpy import ndarray
1919
from pandas import DataFrame, Series
2020
from pandas.core.groupby import DataFrameGroupBy, SeriesGroupBy
2121
from scipy.sparse import csr_matrix
@@ -365,24 +365,42 @@ def strip_schema(obj):
365365

366366

367367
def _dtype_to_schema(typ) -> JSON_TYPE:
368+
from lale.helpers import safe_issubdtype
369+
368370
result: JSON_TYPE
369-
if typ is bool or issubdtype(typ, np.bool_):
371+
# Handle pandas extension dtypes (e.g., StringDtype in pandas 3.x)
372+
# These are not np.dtype instances and have a 'name' attribute
373+
if hasattr(typ, "name") and not isinstance(typ, np.dtype):
374+
# Pandas extension dtype - check the name to determine the type
375+
dtype_name = str(typ.name).lower()
376+
if "string" in dtype_name or "str" in dtype_name:
377+
result = {"type": "string"}
378+
elif "int" in dtype_name:
379+
result = {"type": "integer"}
380+
elif "float" in dtype_name or "double" in dtype_name:
381+
result = {"type": "number"}
382+
elif "bool" in dtype_name:
383+
result = {"type": "boolean"}
384+
else:
385+
# Default to string for unknown extension dtypes
386+
result = {"type": "string"}
387+
elif typ is bool or safe_issubdtype(typ, np.bool_):
370388
result = {"type": "boolean"}
371-
elif issubdtype(typ, np.unsignedinteger):
389+
elif safe_issubdtype(typ, np.unsignedinteger):
372390
result = {"type": "integer", "minimum": 0}
373-
elif issubdtype(typ, np.integer):
391+
elif safe_issubdtype(typ, np.integer):
374392
result = {"type": "integer"}
375-
elif issubdtype(typ, np.number):
393+
elif safe_issubdtype(typ, np.number):
376394
result = {"type": "number"}
377-
elif issubdtype(typ, np.str_) or issubdtype(typ, np.bytes_):
395+
elif safe_issubdtype(typ, np.str_) or safe_issubdtype(typ, np.bytes_):
378396
result = {"type": "string"}
379397
elif isinstance(typ, np.dtype):
380398
if typ.fields:
381399
props = {k: _dtype_to_schema(t) for k, t in typ.fields.items()}
382400
result = {"type": "object", "properties": props}
383401
elif typ.shape:
384402
result = _shape_and_dtype_to_schema(typ.shape, typ.subdtype)
385-
elif issubdtype(typ, np.object_):
403+
elif safe_issubdtype(typ, np.object_):
386404
result = {"type": "string"}
387405
else:
388406
assert False, f"unexpected dtype {typ}"

lale/datasets/openml/openml_datasets.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,14 @@ def fetch(
684684
y: Optional[Any] = None
685685
if preprocess:
686686
arffData = pd.DataFrame(dataDictionary["data"])
687+
# Convert string columns to object dtype for backward compatibility with pandas 2.x
688+
# In pandas 3.x, string columns use StringDtype by default which causes issues with SimpleImputer
689+
for col in arffData.columns:
690+
if (
691+
hasattr(arffData[col].dtype, "name")
692+
and arffData[col].dtype.name == "string"
693+
):
694+
arffData[col] = arffData[col].astype("object")
687695
# arffData = arffData.fillna(0)
688696
attributes = dataDictionary["attributes"]
689697

@@ -729,7 +737,9 @@ def fetch(
729737
transformers1 = [
730738
(
731739
"imputer_str",
732-
SimpleImputer(missing_values=None, strategy="most_frequent"),
740+
# Use np.nan for missing_values to handle both None and np.nan
741+
# In pandas 3.x, pd.NA becomes np.nan when converted to object dtype
742+
SimpleImputer(missing_values=np.nan, strategy="most_frequent"),
733743
categorical_cols,
734744
),
735745
("imputer_num", SimpleImputer(strategy="mean"), numeric_cols),
@@ -780,6 +790,14 @@ def fetch(
780790
else:
781791
col_names = [attr[0].lower() for attr in dataDictionary["attributes"]]
782792
df_all = pd.DataFrame(dataDictionary["data"], columns=col_names)
793+
# Convert string columns to object dtype for backward compatibility with pandas 2.x
794+
# In pandas 3.x, string columns use StringDtype by default which causes issues with sklearn
795+
for col in df_all.columns:
796+
if (
797+
hasattr(df_all[col].dtype, "name")
798+
and df_all[col].dtype.name == "string"
799+
):
800+
df_all[col] = df_all[col].astype("object")
783801
assert target_col in col_names, (target_col, col_names)
784802
y = df_all[target_col]
785803
# the type stubs for pandas are not currently complete enough to type this correctly

lale/helpers.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,32 @@
5757
torch_cat = None # type: ignore[assignment]
5858
torch_from_numpy = None # type: ignore[assignment]
5959

60+
61+
def safe_issubdtype(typ: Any, dtype_class: Any) -> bool:
62+
"""
63+
Safely check if typ is a subdtype of dtype_class.
64+
Handles pandas extension dtypes (e.g., StringDtype in pandas 3.x) that
65+
np.issubdtype cannot handle.
66+
67+
Parameters
68+
----------
69+
typ
70+
The dtype to check
71+
dtype_class
72+
The dtype class to check against (e.g., np.number, np.integer)
73+
74+
Returns
75+
-------
76+
bool
77+
True if typ is a subtype of dtype_class, False otherwise
78+
"""
79+
try:
80+
return np.issubdtype(typ, dtype_class)
81+
except (TypeError, AttributeError):
82+
# pandas extension dtypes raise TypeError in np.issubdtype
83+
return False
84+
85+
6086
spark_loader = util.find_spec("pyspark")
6187
spark_installed = spark_loader is not None
6288
if spark_installed:
@@ -179,11 +205,11 @@ def subarray_to_json(indices: Tuple[int, ...]) -> Any:
179205
if len(indices) == len(arr.shape):
180206
if isinstance(arr[indices], (bool, int, float, str)):
181207
return arr[indices]
182-
elif np.issubdtype(arr.dtype, np.bool_):
208+
elif safe_issubdtype(arr.dtype, np.bool_):
183209
return bool(arr[indices])
184-
elif np.issubdtype(arr.dtype, np.integer):
210+
elif safe_issubdtype(arr.dtype, np.integer):
185211
return int(arr[indices])
186-
elif np.issubdtype(arr.dtype, np.number):
212+
elif safe_issubdtype(arr.dtype, np.number):
187213
return float(arr[indices])
188214
elif arr.dtype.kind in ["U", "S", "O"]:
189215
return str(arr[indices])

lale/lib/aif360/orbis.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ def _orbis_resample(X, y, diaeresis_y, osizes, nsizes, sampler_hparams):
135135
**{h: v for h, v in sampler_hparams.items() if h not in ["replacement"]},
136136
"sampling_strategy": over_sizes,
137137
}
138-
cats_mask = [not np.issubdtype(typ, np.number) for typ in Xyy.dtypes]
138+
from lale.helpers import safe_issubdtype
139+
140+
cats_mask = [not safe_issubdtype(typ, np.number) for typ in Xyy.dtypes]
139141
if all(cats_mask): # all nominal -> use SMOTEN
140142
over_op = imblearn.over_sampling.SMOTEN(**over_hparams)
141143
elif not any(cats_mask): # all continuous -> use vanilla SMOTE

lale/lib/category_encoders/target_encoder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ def __init__(self, **hyperparams):
180180
def fit(self, X, y):
181181
if catenc_version is None:
182182
raise ValueError("The package 'category_encoders' is not installed.")
183-
if np.issubdtype(y.dtype, np.number):
183+
from lale.helpers import safe_issubdtype
184+
185+
if safe_issubdtype(y.dtype, np.number):
184186
numeric_y = y
185187
else:
186188
from sklearn.preprocessing import LabelEncoder

lale/lib/imblearn/smotenc.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ def __init__(self, operator=None, **hyperparams):
5151
def fit(self, X, y=None):
5252
if self.resampler is None:
5353
if self._hyperparams["categorical_features"] is None:
54+
from lale.helpers import safe_issubdtype
55+
5456
self._hyperparams["categorical_features"] = [
55-
not np.issubdtype(typ, np.number) for typ in X.dtypes
57+
not safe_issubdtype(typ, np.number) for typ in X.dtypes
5658
]
5759
self.resampler = imblearn.over_sampling.SMOTENC(**self._hyperparams)
5860
return super().fit(X, y)

lale/lib/rasl/ordinal_encoder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ def from_monoid(self, monoid: _OrdinalEncoderMonoid):
9393
def _build_transformer(self):
9494
assert self._monoid is not None
9595

96+
from lale.helpers import safe_issubdtype
97+
9698
def simplify_val(v):
97-
if np.issubdtype(type(v), np.integer):
99+
if safe_issubdtype(type(v), np.integer):
98100
return int(v)
99-
if np.issubdtype(type(v), np.floating):
101+
if safe_issubdtype(type(v), np.floating):
100102
return float(v)
101103
return v
102104

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"jsonsubschema>=0.0.6",
5151
"scikit-learn>=1.0.0,<1.8.0",
5252
"scipy",
53-
"pandas<3.0.0",
53+
"pandas",
5454
"packaging",
5555
"decorator",
5656
"typing-extensions",

0 commit comments

Comments
 (0)