Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 79 additions & 40 deletions docs/examples/sktime.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,48 @@
"import pandas as pd"
]
},
{
"cell_type": "markdown",
"id": "d7bc0155",
"metadata": {},
"source": [
"## Setup the sktime model\n",
"\n",
"Sktime models can be passed in the `forecasters` argument when initializing the TimeCopilot agent where they will be wrapped in an adapter with an alias based on the type name. \n",
"\n",
"If multiple sktime forecasters of the same type are passed, each model after the first will have be wrapped in an adapter with an alias that has `'_n'` appended to it with `n` being incremented by 1 for each additional occurrence of the same model type. \n",
"\n",
"For example, if you pass two `TrendForecaster` sktime models, the first one will have an alias of `'sktime.TrendForecaster'` and the second one will have an alias of `'sktime.TrendForecaster_2'`.\n",
"\n",
"If you would rather specify the alias yourself, you will need to adapt the model manually with `SKTimeAdapter`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f4b60a78",
"metadata": {},
"outputs": [],
"source": [
"from sktime.forecasting.trend import TrendForecaster\n",
"\n",
"trend_forecaster = TrendForecaster()"
]
},
{
"cell_type": "markdown",
"id": "694b7854",
"metadata": {},
"source": [
"## Setup the sktime model and adapt it to TimeCopilot\n",
"### Manually adapt sktime model\n",
"\n",
"sktime models need to be adapted to work properly with TimeCopilot. This is done by creating your model with sktime and passing it through SKTimeAdapter. Some sktime models may require more configuration to function properly with the data you intend to use it on. For example, when using sktime's NaiveForecaster with yearly data you might want to initialize it with an `sp` argument of `12` like this `NaiveForecaster(sp=12)`.\n",
"If you would rather decide on the alias yourself, you will need to manually adapt the model with `SKTimeAdapter`.\n",
"\n",
"The `Alias` argument should also be provided, especially if you plan on adding multiple sktime forecasters. If you add multiple sktime models without specifying aliases, TimeCopilot will not be able to properly call all of them."
"The `model` argument should be an sktime `Forecaster` model. The `alias` argument should be a string that uniquely identifies the model.\n",
"\n",
"After adapting the model you would pass it in the `forecasters` argument when initializing the TimeCopilot agent.\n",
"\n",
"If you add multiple manually adapted sktime models of the same type without specifying aliases, TimeCopilot may not be able to properly call all of them."
]
},
{
Expand All @@ -57,9 +89,16 @@
"\n",
"trend_forecaster = TrendForecaster()\n",
"\n",
"adapted_model = SKTimeAdapter(\n",
"manually_adapted_model = SKTimeAdapter(\n",
" model=trend_forecaster,\n",
" alias=\"TrendForecaster\",\n",
")\n",
"\n",
"tc = timecopilot.TimeCopilot(\n",
" llm=\"openai:gpt-4o\",\n",
" forecasters=[\n",
" manually_adapted_model\n",
" ]\n",
")"
]
},
Expand All @@ -83,7 +122,7 @@
"tc = timecopilot.TimeCopilot(\n",
" llm=\"openai:gpt-4o\",\n",
" forecasters=[\n",
" adapted_model,\n",
" trend_forecaster,\n",
" ],\n",
")"
]
Expand All @@ -93,7 +132,7 @@
"id": "401d5b6f",
"metadata": {},
"source": [
"### Extending default model list with an sktime adapted model\n",
"### Extending default model list with an sktime model\n",
"\n",
"if you want to use the default list with the addition of your sktime model you could make a copy of the default list and append your model to it:"
]
Expand All @@ -106,7 +145,7 @@
"outputs": [],
"source": [
"model_list = timecopilot.agent.DEFAULT_MODELS.copy()\n",
"model_list.append(adapted_model)\n",
"model_list.append(trend_forecaster)\n",
"\n",
"tc = timecopilot.TimeCopilot(\n",
" llm=\"openai:gpt-4o\",\n",
Expand Down Expand Up @@ -143,9 +182,9 @@
"name": "stderr",
"output_type": "stream",
"text": [
"1it [00:00, 4.70it/s]\n",
"1it [00:00, 223.32it/s]\n",
"11it [00:00, 77.11it/s]\n"
"1it [00:00, 4.52it/s]\n",
"1it [00:00, 220.94it/s]\n",
"11it [00:00, 83.97it/s]\n"
]
}
],
Expand All @@ -157,15 +196,15 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 6,
"id": "7355c143",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The 'AirPassengers' time series has a series length of 144 with a clear seasonal pattern identified using key features. The high 'seasonal_strength' of 0.981 suggests strong seasonality, evident from the 12-month seasonal period. The time series also exhibits trends, shown by a 'trend' score of 0.997, and moderate curvature at 1.069. The high autocorrelation 'x_acf1' at 0.948 indicates the persistence of patterns over time. The Holt-Winters parameters suggest a stable level (alpha ~1) with no trend component (beta ~0) and significant seasonal smoothing (gamma ~0.75). These features suggest that both trend and seasonality are prominent and need to be captured by the model.\n"
"The 'AirPassengers' data exhibits a strong trend with a stability of 0.933, indicating a consistent pattern over time. It also has a very strong seasonal component (seasonal_strength 0.982) with a seasonal period of 12 months, typical for annual data. The high x_acf1 (0.948) indicates strong autocorrelation, implying that past values are highly predictive of future values, suggesting that a time series model with a trend and seasonal structure would be appropriate. The entropy of the series is relatively low (0.429), indicating a high level of predictability, which substantiates the use of deterministic trend models. These features warrant the use of models that incorporate seasonality and trends, such as Holt's linear trend method or even seasonal naive models.\n"
]
}
],
Expand All @@ -175,7 +214,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 7,
"id": "86acfa60",
"metadata": {},
"outputs": [
Expand All @@ -202,7 +241,7 @@
" <th></th>\n",
" <th>unique_id</th>\n",
" <th>ds</th>\n",
" <th>TrendForecaster</th>\n",
" <th>sktime.TrendForecaster</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
Expand Down Expand Up @@ -355,34 +394,34 @@
"</div>"
],
"text/plain": [
" unique_id ds TrendForecaster\n",
"0 AirPassengers 1961-01-01 473.023018\n",
"1 AirPassengers 1961-02-01 475.729097\n",
"2 AirPassengers 1961-03-01 478.173296\n",
"3 AirPassengers 1961-04-01 480.879374\n",
"4 AirPassengers 1961-05-01 483.498159\n",
"5 AirPassengers 1961-06-01 486.204237\n",
"6 AirPassengers 1961-07-01 488.823023\n",
"7 AirPassengers 1961-08-01 491.529101\n",
"8 AirPassengers 1961-09-01 494.235179\n",
"9 AirPassengers 1961-10-01 496.853964\n",
"10 AirPassengers 1961-11-01 499.560042\n",
"11 AirPassengers 1961-12-01 502.178827\n",
"12 AirPassengers 1962-01-01 504.884906\n",
"13 AirPassengers 1962-02-01 507.590984\n",
"14 AirPassengers 1962-03-01 510.035183\n",
"15 AirPassengers 1962-04-01 512.741261\n",
"16 AirPassengers 1962-05-01 515.360046\n",
"17 AirPassengers 1962-06-01 518.066125\n",
"18 AirPassengers 1962-07-01 520.684910\n",
"19 AirPassengers 1962-08-01 523.390988\n",
"20 AirPassengers 1962-09-01 526.097066\n",
"21 AirPassengers 1962-10-01 528.715851\n",
"22 AirPassengers 1962-11-01 531.421929\n",
"23 AirPassengers 1962-12-01 534.040714"
" unique_id ds sktime.TrendForecaster\n",
"0 AirPassengers 1961-01-01 473.023018\n",
"1 AirPassengers 1961-02-01 475.729097\n",
"2 AirPassengers 1961-03-01 478.173296\n",
"3 AirPassengers 1961-04-01 480.879374\n",
"4 AirPassengers 1961-05-01 483.498159\n",
"5 AirPassengers 1961-06-01 486.204237\n",
"6 AirPassengers 1961-07-01 488.823023\n",
"7 AirPassengers 1961-08-01 491.529101\n",
"8 AirPassengers 1961-09-01 494.235179\n",
"9 AirPassengers 1961-10-01 496.853964\n",
"10 AirPassengers 1961-11-01 499.560042\n",
"11 AirPassengers 1961-12-01 502.178827\n",
"12 AirPassengers 1962-01-01 504.884906\n",
"13 AirPassengers 1962-02-01 507.590984\n",
"14 AirPassengers 1962-03-01 510.035183\n",
"15 AirPassengers 1962-04-01 512.741261\n",
"16 AirPassengers 1962-05-01 515.360046\n",
"17 AirPassengers 1962-06-01 518.066125\n",
"18 AirPassengers 1962-07-01 520.684910\n",
"19 AirPassengers 1962-08-01 523.390988\n",
"20 AirPassengers 1962-09-01 526.097066\n",
"21 AirPassengers 1962-10-01 528.715851\n",
"22 AirPassengers 1962-11-01 531.421929\n",
"23 AirPassengers 1962-12-01 534.040714"
]
},
"execution_count": 8,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
Expand Down
31 changes: 31 additions & 0 deletions timecopilot/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from tsfeatures.tsfeatures import _get_feats

from .forecaster import Forecaster, TimeCopilotForecaster
from .models.adapters.sktime import SKTimeAdapter
from .models.prophet import Prophet
from .models.stats import (
ADIDA,
Expand Down Expand Up @@ -388,6 +389,18 @@ def _transform_anomalies_to_text(anomalies_df: pd.DataFrame) -> str:
return output


def _is_sktime_forecaster(obj: object) -> bool:
"""
Helper function for checking if an object is an sktime model by checking if
sktime's BaseForecaster class is in its inheritance tree.
"""
mro_types = type(obj).__mro__
for t in mro_types:
if t.__name__ == "BaseForecaster" and "sktime" in t.__module__:
return True
return False


class TimeCopilot:
"""
TimeCopilot: An AI agent for comprehensive time series analysis.
Expand Down Expand Up @@ -421,6 +434,24 @@ def __init__(

if forecasters is None:
forecasters = DEFAULT_MODELS
combined_forecasters = []
sktime_forecasters = []
for f in forecasters:
if _is_sktime_forecaster(f):
sktime_forecasters.append(f)
else:
combined_forecasters.append(f)
type_counts: dict[str, int] = {}
for f in sktime_forecasters:
alias = "sktime." + type(f).__name__
if type(f).__name__ in type_counts:
type_counts[type(f).__name__] += 1
alias += f"_{type_counts[type(f).__name__]}"
else:
type_counts[type(f).__name__] = 1
adapted = SKTimeAdapter(f, alias=alias)
combined_forecasters.append(adapted)
forecasters = combined_forecasters
self.forecasters = {forecaster.alias: forecaster for forecaster in forecasters}
if "SeasonalNaive" not in self.forecasters:
self.forecasters["SeasonalNaive"] = SeasonalNaive()
Expand Down