Skip to content
1 change: 1 addition & 0 deletions ads/model/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class Framework(ExtendedEnum):
PYOD = "pyod"
SPACY = "spacy"
PROPHET = "prophet"
THETA = "theta"
SKTIME = "sktime"
STATSMODELS = "statsmodels"
CUML = "cuml"
Expand Down
61 changes: 61 additions & 0 deletions ads/opctl/operator/lowcode/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
InvalidParameterError,
)
from ads.secrets import ADBSecretKeeper
from sktime.param_est.seasonality import SeasonalityACF


def call_pandas_fsspec(pd_fn, filename, storage_options, **kwargs):
Expand Down Expand Up @@ -385,3 +386,63 @@ def enable_print():
except Exception:
pass
sys.stdout = sys.__stdout__


def find_seasonal_period_from_dataset(data: pd.DataFrame) -> tuple[int, list]:
try:
sp_est = SeasonalityACF()
sp_est.fit(data)
sp = sp_est.get_fitted_params()["sp"]
probable_sps = sp_est.get_fitted_params()["sp_significant"]
return sp, probable_sps
except Exception as e:
logger.warning(f"Unable to find seasonal period: {e}")
return None, None


def normalize_frequency(freq: str) -> str:
"""
Normalize pandas frequency strings to sktime/period-compatible formats.
Args:
freq: Pandas frequency string
Returns:
Normalized frequency string compatible with PeriodIndex
"""
if freq is None:
return None

freq = freq.upper()

# Handle weekly frequencies with day anchors (W-SUN, W-MON, etc.)
if freq.startswith("W-"):
return "W"

# Handle month start/end frequencies
freq_mapping = {
"MS": "M", # Month Start -> Month End
"ME": "M", # Month End -> Month
"BMS": "M", # Business Month Start -> Month
"BME": "M", # Business Month End -> Month
"QS": "Q", # Quarter Start -> Quarter
"QE": "Q", # Quarter End -> Quarter
"BQS": "Q", # Business Quarter Start -> Quarter
"BQE": "Q", # Business Quarter End -> Quarter
"YS": "Y", # Year Start -> Year (Alias: A)
"AS": "Y", # Year Start -> Year (Alias: A)
"YE": "Y", # Year End -> Year
"AE": "Y", # Year End -> Year
"BYS": "Y", # Business Year Start -> Year
"BAS": "Y", # Business Year Start -> Year
"BYE": "Y", # Business Year End -> Year
"BAE": "Y", # Business Year End -> Year
}

# Handle frequencies with prefixes (e.g., "2W", "3M")
for old_freq, new_freq in freq_mapping.items():
if freq.endswith(old_freq):
prefix = freq[:-len(old_freq)]
return f"{prefix}{new_freq}" if prefix else new_freq

return freq
1 change: 1 addition & 0 deletions ads/opctl/operator/lowcode/forecast/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SupportedModels(ExtendedEnum):
NeuralProphet = "neuralprophet"
LGBForecast = "lgbforecast"
AutoMLX = "automlx"
Theta = "theta"
AutoTS = "autots"
# Auto = "auto"

Expand Down
35 changes: 35 additions & 0 deletions ads/opctl/operator/lowcode/forecast/model/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,3 +962,38 @@ def _get_unique_filename(self, base_path: str, storage_options: dict = None) ->
f"Error checking OCI path existence: {e}. Using original path."
)
return base_path

def generate_explanation_report_from_data(self) -> tuple[rc.Block, rc.Block]:
if not self.target_cat_col:
self.formatted_global_explanation = (
self.formatted_global_explanation.rename(
{"Series 1": self.original_target_column},
axis=1,
)
)
self.formatted_local_explanation.drop(
"Series", axis=1, inplace=True
)

# Create a markdown section for the global explainability
global_explanation_section = rc.Block(
rc.Heading("Global Explainability", level=2),
rc.Text(
"The following tables provide the feature attribution for the global explainability."
),
rc.DataTable(self.formatted_global_explanation, index=True),
)

blocks = [
rc.DataTable(
local_ex_df.drop("Series", axis=1),
label=s_id if self.target_cat_col else None,
index=True,
)
for s_id, local_ex_df in self.local_explanation.items()
]
local_explanation_section = rc.Block(
rc.Heading("Local Explanation of Models", level=2),
rc.Select(blocks=blocks) if len(blocks) > 1 else blocks[0],
)
return global_explanation_section, local_explanation_section
2 changes: 2 additions & 0 deletions ads/opctl/operator/lowcode/forecast/model/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .ml_forecast import MLForecastOperatorModel
from .neuralprophet import NeuralProphetOperatorModel
from .prophet import ProphetOperatorModel
from .theta import ThetaOperatorModel


class UnSupportedModelError(Exception):
Expand All @@ -46,6 +47,7 @@ class ForecastOperatorModelFactory:
SupportedModels.LGBForecast: MLForecastOperatorModel,
SupportedModels.AutoMLX: AutoMLXOperatorModel,
SupportedModels.AutoTS: AutoTSOperatorModel,
SupportedModels.Theta: ThetaOperatorModel,
}

@classmethod
Expand Down
19 changes: 9 additions & 10 deletions ads/opctl/operator/lowcode/forecast/model/forecast_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,41 +345,40 @@ def populate_series_output(
f"\nPlease refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE} for resolution steps."
) from e

start_idx = output_i.shape[0] - self.horizon - len(fit_val)
if (output_i.shape[0] - self.horizon) == len(fit_val):
output_i["fitted_value"].iloc[: -self.horizon] = (
fit_val # Note: may need to do len(output_i) - (len(fit_val) + horizon) : -horizon
)
output_i.loc[output_i.index[
: -self.horizon], "fitted_value"] = fit_val # Note: may need to do len(output_i) - (len(fit_val) + horizon) : -horizon
elif (output_i.shape[0] - self.horizon) > len(fit_val):
logger.debug(
f"Fitted Values were only generated on a subset ({len(fit_val)}/{(output_i.shape[0] - self.horizon)}) of the data for Series: {series_id}."
)
start_idx = output_i.shape[0] - self.horizon - len(fit_val)
output_i["fitted_value"].iloc[start_idx : -self.horizon] = fit_val
output_i.loc[output_i.index[start_idx: -self.horizon], "fitted_value"] = fit_val
else:
output_i["fitted_value"].iloc[start_idx : -self.horizon] = fit_val[
-(output_i.shape[0] - self.horizon) :
output_i.loc[output_i.index[start_idx: -self.horizon], "fitted_value"] = fit_val[
-(output_i.shape[0] - self.horizon):
]

if len(forecast_val) != self.horizon:
raise ValueError(
f"Attempting to set forecast along horizon ({self.horizon}) for series: {series_id}, however forecast is only length {len(forecast_val)}"
f"\nPlease refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE} for resolution steps."
)
output_i["forecast_value"].iloc[-self.horizon :] = forecast_val
output_i.loc[output_i.index[-self.horizon:], "forecast_value"] = forecast_val

if len(upper_bound) != self.horizon:
raise ValueError(
f"Attempting to set upper_bound along horizon ({self.horizon}) for series: {series_id}, however upper_bound is only length {len(upper_bound)}"
f"\nPlease refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE} for resolution steps."
)
output_i[self.upper_bound_name].iloc[-self.horizon :] = upper_bound
output_i.loc[output_i.index[-self.horizon:], self.upper_bound_name] = upper_bound

if len(lower_bound) != self.horizon:
raise ValueError(
f"Attempting to set lower_bound along horizon ({self.horizon}) for series: {series_id}, however lower_bound is only length {len(lower_bound)}"
f"\nPlease refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE} for resolution steps."
)
output_i[self.lower_bound_name].iloc[-self.horizon :] = lower_bound
output_i.loc[output_i.index[-self.horizon:], self.lower_bound_name] = lower_bound

self.series_id_map[series_id] = output_i
self.verify_series_output(series_id)
Expand Down
33 changes: 1 addition & 32 deletions ads/opctl/operator/lowcode/forecast/model/prophet.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,38 +412,7 @@ def _generate_report(self):
# If the key is present, call the "explain_model" method
self.explain_model()

if not self.target_cat_col:
self.formatted_global_explanation = (
self.formatted_global_explanation.rename(
{"Series 1": self.original_target_column},
axis=1,
)
)
self.formatted_local_explanation.drop(
"Series", axis=1, inplace=True
)

# Create a markdown section for the global explainability
global_explanation_section = rc.Block(
rc.Heading("Global Explainability", level=2),
rc.Text(
"The following tables provide the feature attribution for the global explainability."
),
rc.DataTable(self.formatted_global_explanation, index=True),
)

blocks = [
rc.DataTable(
local_ex_df.drop("Series", axis=1),
label=s_id if self.target_cat_col else None,
index=True,
)
for s_id, local_ex_df in self.local_explanation.items()
]
local_explanation_section = rc.Block(
rc.Heading("Local Explanation of Models", level=2),
rc.Select(blocks=blocks) if len(blocks) > 1 else blocks[0],
)
global_explanation_section, local_explanation_section = self.generate_explanation_report_from_data()

# Append the global explanation text and section to the "all_sections" list
all_sections = all_sections + [
Expand Down
Loading
Loading