@@ -384,7 +384,20 @@ def _yield_array_api_checks(estimator, only_numpy=False):
384384 other_ns_and_device = other_ns_and_device ,
385385 X_ns_and_device = X_ns_and_device ,
386386 )
387- # 3. Namespace/device consistency between fit and predict/transform
387+ # 3. String `y` and numeric `X` from all supported namespace/devices
388+ if is_classifier (estimator ):
389+ for (
390+ array_namespace ,
391+ device_name ,
392+ dtype_name ,
393+ ) in yield_namespace_device_dtype_combinations ():
394+ yield partial (
395+ check_array_api_string_and_numeric_inputs ,
396+ array_namespace = array_namespace ,
397+ device_name = device_name ,
398+ dtype_name = dtype_name ,
399+ )
400+ # 4. Namespace/device consistency between fit and predict/transform
388401 # Only test with one namespace to keep costs down
389402 # There should be no dependency on the exact namespace used.
390403 yield partial (
@@ -1098,16 +1111,12 @@ def _check_array_api_core(
10981111 xp_X , device_X = _array_api_for_tests (
10991112 X_ns_and_device .xp , X_ns_and_device .device , dtype_name
11001113 )
1101- xp_other , device_other = _array_api_for_tests (
1102- other_ns_and_device .xp , other_ns_and_device .device
1103- )
11041114
11051115 X , y = make_classification (n_samples = 30 , n_features = 10 , random_state = 42 )
11061116 if dtype_name is None :
11071117 max_float_dtype = _max_precision_float_dtype (xp_X , device_X )
11081118 # Convert to string, so it is accepted by NumPy (`X` is NumPy array)
11091119 dtype_name = "float32" if max_float_dtype == xp_X .float32 else "float64"
1110-
11111120 X = X .astype (dtype_name , copy = False )
11121121
11131122 X = _enforce_estimator_tags_X (estimator_orig , X )
@@ -1117,7 +1126,17 @@ def _check_array_api_core(
11171126 set_random_state (est )
11181127
11191128 X_xp = xp_X .asarray (X , device = device_X )
1120- y_xp = xp_other .asarray (y , device = device_other )
1129+
1130+ if other_ns_and_device == "string" :
1131+ xp_other , device_other = _array_api_for_tests ("numpy" , "cpu" )
1132+ # Convert binary `y` to string
1133+ y_xp = np .array (["a" , "b" ])[y ]
1134+ y = y_xp
1135+ else :
1136+ xp_other , device_other = _array_api_for_tests (
1137+ other_ns_and_device .xp , other_ns_and_device .device
1138+ )
1139+ y_xp = xp_other .asarray (y , device = device_other )
11211140
11221141 fit_kwargs = {}
11231142 fit_kwargs_xp = {}
@@ -1136,48 +1155,50 @@ def _check_array_api_core(
11361155 est_xp .fit (X_xp , y_xp , ** fit_kwargs_xp )
11371156
11381157 X_ns = xp_X .__name__
1158+ y_ns = xp_other .__name__
11391159
11401160 array_attributes = {
11411161 key : value for key , value in vars (est ).items () if isinstance (value , np .ndarray )
11421162 }
11431163
11441164 # Fitted attributes which are arrays must have the same namespace as `X`,
11451165 # except `classes_`, to allow it to be string when `y` is string.
1146- for key , attribute in array_attributes .items ():
1147- est_xp_param = getattr (est_xp , key )
1148- with config_context (array_api_dispatch = True ):
1149- attribute_ns = get_namespace (est_xp_param )[0 ].__name__
1150- if key != "classes_" :
1151- assert attribute_ns == X_ns , (
1152- f"'{ key } ' attribute is in wrong namespace, expected { X_ns } "
1153- f"got { attribute_ns } "
1154- )
1155-
1166+ for attribute_name , attribute_value in array_attributes .items ():
1167+ est_xp_attr = getattr (est_xp , attribute_name )
1168+ # `classes_` should be in same ns and device as `y`
1169+ expected_xp , expected_ns = (
1170+ (y_xp , y_ns ) if attribute_name == "classes_" else (X_xp , X_ns )
1171+ )
11561172 with config_context (array_api_dispatch = True ):
1157- if key != "classes_" :
1158- assert array_device (est_xp_param ) == array_device (X_xp )
1173+ attribute_ns = get_namespace (est_xp_attr )[0 ].__name__
1174+ assert array_device (est_xp_attr ) == array_device (expected_xp )
1175+ assert attribute_ns == expected_ns , (
1176+ f"'{ attribute_name } ' attribute is in wrong namespace, expected "
1177+ f"{ expected_ns } got { attribute_ns } "
1178+ )
11591179
1160- est_xp_param_np = move_to (est_xp_param , xp = np , device = "cpu" )
1180+ est_xp_attr_np = move_to (est_xp_attr , xp = np , device = "cpu" )
11611181 if check_values :
11621182 assert_allclose (
1163- attribute ,
1164- est_xp_param_np ,
1165- err_msg = f"{ key } not the same" ,
1183+ attribute_value ,
1184+ est_xp_attr_np ,
1185+ err_msg = f"{ attribute_name } not the same" ,
11661186 atol = _atol_for_type (X .dtype ),
11671187 )
11681188 else :
1169- assert attribute .shape == est_xp_param_np .shape
1170- expected_dtype = attribute .dtype
1171- if np .issubdtype (attribute .dtype , np .floating ):
1172- max_float_dtype = _max_precision_float_dtype (
1173- xp_X , device = X_ns_and_device .device
1174- )
1175- # for some devices the maximum supported floating dtype is float32
1176- if max_float_dtype == xp_X .float32 :
1177- expected_dtype = np .float32
1178- assert est_xp_param_np .dtype == expected_dtype
1189+ assert attribute_value .shape == est_xp_attr_np .shape
1190+ if attribute_name != "classes_" :
1191+ expected_dtype = attribute_value .dtype
1192+ if np .issubdtype (attribute_value .dtype , np .floating ):
1193+ max_float_dtype = _max_precision_float_dtype (
1194+ xp_X , device = X_ns_and_device .device
1195+ )
1196+ # for some devices the maximum supported floating dtype is float32
1197+ if max_float_dtype == xp_X .float32 :
1198+ expected_dtype = np .float32
1199+ assert est_xp_attr_np .dtype == expected_dtype
11791200
1180- # Check estimator methods, if supported, give the same results
1201+ # Check supported estimator methods give the same results
11811202 methods = (
11821203 "score" ,
11831204 "score_samples" ,
@@ -1245,14 +1266,22 @@ def _check_array_api_core(
12451266
12461267 with config_context (array_api_dispatch = True ):
12471268 result_ns = get_namespace (result_xp )[0 ].__name__
1248- assert result_ns == X_ns , (
1249- f"'{ method } ' output is in wrong namespace, expected { X_ns } , "
1269+ # `predict` would be string when `y` is string
1270+ expected_ns = X_ns
1271+ if other_ns_and_device == "string" and method_name == "predict" :
1272+ expected_ns = y_ns
1273+ assert result_ns == expected_ns , (
1274+ f"'{ method } ' output is in wrong namespace, expected { expected_ns } , "
12501275 f"got { result_ns } ."
12511276 )
12521277
12531278 if expect_only_array_outputs :
1279+ # `predict` would be string and on same device as `y`
1280+ expected_xp = X_xp
1281+ if other_ns_and_device == "string" and method_name == "predict" :
1282+ expected_xp = y_xp
12541283 with config_context (array_api_dispatch = True ):
1255- assert array_device (result_xp ) == array_device (X_xp )
1284+ assert array_device (result_xp ) == array_device (expected_xp )
12561285
12571286 result_xp_np = move_to (result_xp , xp = np , device = "cpu" )
12581287 if check_values :
@@ -1441,6 +1470,68 @@ def check_array_api_mixed_inputs(
14411470 )
14421471
14431472
1473+ def check_array_api_string_and_numeric_inputs (
1474+ name ,
1475+ estimator_orig ,
1476+ array_namespace ,
1477+ device_name = None ,
1478+ dtype_name = None ,
1479+ check_values = False ,
1480+ expect_only_array_outputs = True ,
1481+ ):
1482+ """Check `estimator_orig` works with string `y` and array API `X`.
1483+
1484+ For this check `y` is always a NumPy array of strings and `X` is an array
1485+ with namespace and device combinations generated by
1486+ `yield_namespace_device_dtype_combinations`.
1487+
1488+ Note `sample_weight` is never checked.
1489+
1490+ See `check_array_api_input` and `check_array_api_mixed_inputs` for similar
1491+ tests.
1492+
1493+ Parameters
1494+ ----------
1495+ name : str
1496+ The name of the estimator. Used in error messages but ignored here.
1497+
1498+ estimator_orig : estimator
1499+ Original (uncloned) estimator instance.
1500+
1501+ array_namespace : str
1502+ The name of the Array API namespace of all estimator inputs.
1503+
1504+ device_name : str, default=None
1505+ The name of the device on which to allocate the estimator input arrays.
1506+
1507+ dtype_name : str, default=None
1508+ The name of the data type to use for arrays. If `None`,
1509+ `_max_precision_float_dtype` of namespace and device of
1510+ `X_ns_and_device` is used.
1511+
1512+ check_values : bool, default=False
1513+ Whether to check the values of attributes, method outputs (including
1514+ `inverse_transform`) obtained with array API inputs match that of all-NumPy
1515+ inputs. If `False` only the namespace, device, shape and dtype of attributes
1516+ and method outputs are checked.
1517+
1518+ expect_only_array_outputs : bool, default=True
1519+ Whether to expect non-array outputs such as sparse data structures and lists.
1520+ If `False` the checks are looser; device, shape and dtype checks for method
1521+ outputs are skipped and only a smoke test is performed for `inverse_transform`.
1522+ """
1523+ X_ns_and_device = NamespaceAndDevice (array_namespace , device_name )
1524+ _check_array_api_core (
1525+ estimator_orig ,
1526+ X_ns_and_device = X_ns_and_device ,
1527+ other_ns_and_device = "string" ,
1528+ dtype_name = dtype_name ,
1529+ check_values = check_values ,
1530+ check_sample_weight = False ,
1531+ expect_only_array_outputs = expect_only_array_outputs ,
1532+ )
1533+
1534+
14441535def check_array_api_same_namespace (
14451536 name , estimator_orig , array_namespace , device_name = None
14461537):
0 commit comments