Source code for sktime_mcp.tools.split_data

"""
Data splitting tool for sktime MCP.

Provides temporal train/test splitting for time series data,
registering both halves as new data handles.
"""

import logging
import uuid
from typing import Any

from sktime_mcp.runtime.executor import get_executor

logger = logging.getLogger(__name__)


[docs] def split_data_tool( data_handle: str, test_size: float | None = None, fh: list[int] | int | None = None, ) -> dict[str, Any]: """Split a time series data handle into train and test sets. Uses sktime's temporal_train_test_split() when available, falling back to a pandas-based implementation. Exactly one of `test_size` or `fh` must be provided. Parameters ---------- data_handle : str Handle ID of the loaded data to split (from load_data_source). test_size : float or None, default=None Fraction of the data to hold out for testing (0.0–1.0). Mutually exclusive with `fh`. fh : int, list of int, or None, default=None Forecast horizon — the number of final time steps to reserve as the test set. Can be a single int or a list of relative step indices. Mutually exclusive with `test_size`. Returns ------- dict Dictionary containing the split train/test handles and metadata: - ``"success"`` (bool) -- True if the split completed successfully, False otherwise. - ``"train_handle"`` (str) -- The new unique data handle ID representing the training set. - ``"test_handle"`` (str) -- The new unique data handle ID representing the test set. - ``"cutoff"`` (str) -- The cutoff timestamp indicating the last training timestamp. - ``"train_size"`` (int) -- Number of observations in the training set. - ``"n_test"`` (int) -- Number of observations in the test set. - ``"error"`` (str, optional) -- Error message if "success" is False. """ executor = get_executor() # --- validation -------------------------------------------------------- if data_handle not in executor._data_handles: return { "success": False, "error": f"Data handle '{data_handle}' not found", "available_handles": list(executor._data_handles.keys()), } if test_size is not None and fh is not None: return { "success": False, "error": "Provide exactly one of 'test_size' or 'fh', not both.", } if test_size is None and fh is None: return { "success": False, "error": "Provide at least one of 'test_size' or 'fh'.", } if test_size is not None and (test_size <= 0.0 or test_size >= 1.0): return { "success": False, "error": f"test_size must be between 0.0 and 1.0 (exclusive), got {test_size}", } if fh is not None: if isinstance(fh, int): if fh < 1: return { "success": False, "error": f"fh must be a positive integer, got {fh}", } elif isinstance(fh, list): if not fh or not all(isinstance(step, int) and step > 0 for step in fh): return { "success": False, "error": "fh must be a non-empty list of positive integers.", } else: return { "success": False, "error": f"fh must be an integer or list of integers, got {type(fh).__name__}", } data_info = executor._data_handles[data_handle] y = data_info["y"] X = data_info.get("X") try: from sktime.datatypes import get_cutoff from sktime.split import temporal_train_test_split n = len(y) if test_size is not None: n_test = max(1, int(n * test_size)) elif isinstance(fh, int): n_test = fh else: n_test = max(fh) if n_test >= n: return { "success": False, "error": ( f"Test set would be {n_test} samples but the series only has {n} observations." ), } if X is not None: y_train, y_test, X_train, X_test = temporal_train_test_split(y, X, test_size=n_test) else: y_train, y_test = temporal_train_test_split(y, test_size=n_test) X_train, X_test = None, None # --- cutoff ------------------------------------------------------- cutoff = str(get_cutoff(y_train)) # --- register handles -------------------------------------------- train_handle = f"data_{uuid.uuid4().hex[:8]}" test_handle = f"data_{uuid.uuid4().hex[:8]}" base_meta = data_info.get("metadata", {}).copy() train_meta = { **base_meta, "split": "train", "rows": len(y_train), "start_date": str(y_train.index[0]), "end_date": str(y_train.index[-1]), "parent_handle": data_handle, } test_meta = { **base_meta, "split": "test", "rows": len(y_test), "start_date": str(y_test.index[0]), "end_date": str(y_test.index[-1]), "parent_handle": data_handle, } executor._register_data_handle( train_handle, { "y": y_train, "X": X_train, "metadata": train_meta, "validation": data_info.get("validation", {}), "config": data_info.get("config", {}), }, ) executor._register_data_handle( test_handle, { "y": y_test, "X": X_test, "metadata": test_meta, "validation": data_info.get("validation", {}), "config": data_info.get("config", {}), }, ) return { "success": True, "train_handle": train_handle, "test_handle": test_handle, "cutoff": cutoff, "train_size": len(y_train), "n_test": len(y_test), } except Exception as e: logger.exception("Error splitting data") return { "success": False, "error": str(e), "error_type": type(e).__name__, }