|
14 | 14 | logger = logging.getLogger(__name__) |
15 | 15 |
|
16 | 16 |
|
| 17 | +def _validate_evaluate_inputs(executor, estimator_handle: str, y: str) -> dict[str, Any] | None: |
| 18 | + """Reject non-forecasters and non-Series targets before any CV work. |
| 19 | +
|
| 20 | + Returns an error dict to short-circuit on, or None when inputs are valid. |
| 21 | + Cross-validation here uses an ExpandingWindowSplitter and forecasting |
| 22 | + metrics, which only make sense for a forecaster on a single Series. |
| 23 | + """ |
| 24 | + import pandas as pd |
| 25 | + |
| 26 | + try: |
| 27 | + instance = executor._handle_manager.get_instance(estimator_handle) |
| 28 | + except KeyError: |
| 29 | + return {"success": False, "error": executor._handle_manager.describe_missing(estimator_handle)} |
| 30 | + |
| 31 | + get_tag = getattr(instance, "get_class_tag", None) |
| 32 | + obj_type = get_tag("object_type", "") if callable(get_tag) else "" |
| 33 | + if obj_type != "forecaster": |
| 34 | + return { |
| 35 | + "success": False, |
| 36 | + "error": ( |
| 37 | + f"evaluate cross-validates forecasters, but this handle is a " |
| 38 | + f"{obj_type or 'non-forecaster object'}. Use call_method for " |
| 39 | + "non-forecaster estimators." |
| 40 | + ), |
| 41 | + } |
| 42 | + |
| 43 | + # Resolve y and confirm it is a numeric Series (not Panel/Hierarchical, and |
| 44 | + # not categorical label data from a classification dataset). |
| 45 | + y_res = executor._resolve_source(y) |
| 46 | + if not y_res["success"]: |
| 47 | + return y_res |
| 48 | + _y = y_res["data"] |
| 49 | + try: |
| 50 | + from sktime.datatypes import check_is_scitype |
| 51 | + |
| 52 | + is_series = check_is_scitype(_y, scitype="Series", return_metadata=[])[0] |
| 53 | + except Exception: |
| 54 | + is_series = True # let the downstream run surface an unusual case |
| 55 | + if not is_series: |
| 56 | + return { |
| 57 | + "success": False, |
| 58 | + "error": ( |
| 59 | + "evaluate expects a univariate/Series target, but the given y is Panel " |
| 60 | + "or Hierarchical scitype. Forecasting CV is not defined for panel data." |
| 61 | + ), |
| 62 | + } |
| 63 | + |
| 64 | + import numpy as np |
| 65 | + |
| 66 | + if isinstance(_y, pd.Series): |
| 67 | + numeric = pd.api.types.is_numeric_dtype(_y) |
| 68 | + elif isinstance(_y, pd.DataFrame): |
| 69 | + numeric = all(pd.api.types.is_numeric_dtype(_y[c]) for c in _y.columns) |
| 70 | + elif isinstance(_y, np.ndarray): |
| 71 | + numeric = np.issubdtype(_y.dtype, np.number) |
| 72 | + else: |
| 73 | + numeric = True |
| 74 | + if not numeric: |
| 75 | + return { |
| 76 | + "success": False, |
| 77 | + "error": ( |
| 78 | + f"evaluate needs a numeric forecasting target, but y '{y}' is non-numeric " |
| 79 | + "(categorical/label data — this looks like classification data, not a " |
| 80 | + "forecasting series)." |
| 81 | + ), |
| 82 | + } |
| 83 | + return None |
| 84 | + |
| 85 | + |
17 | 86 | def evaluate_tool( |
18 | 87 | estimator_handle: str, |
19 | 88 | y: str, |
@@ -42,6 +111,12 @@ def evaluate_tool( |
42 | 111 |
|
43 | 112 | executor = get_executor() |
44 | 113 |
|
| 114 | + # Scitype validation up front, so async calls reject synchronously instead |
| 115 | + # of burning a background job on invalid inputs (#535). |
| 116 | + invalid = _validate_evaluate_inputs(executor, estimator_handle, y) |
| 117 | + if invalid is not None: |
| 118 | + return invalid |
| 119 | + |
45 | 120 | if run_async: |
46 | 121 | job_manager = get_job_manager() |
47 | 122 | try: |
|
0 commit comments