Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ready to review] ODSC-68841: API Returns Data Directly #1048

Merged
merged 13 commits into from
Feb 17, 2025
2 changes: 1 addition & 1 deletion .github/workflows/run-forecast-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ jobs:
$CONDA/bin/conda init
source /home/runner/.bashrc
pip install -r test-requirements-operators.txt
pip install "oracle-automlx[forecasting]>=24.4.1"
pip install "oracle-automlx[forecasting]>=25.1.1"
pip install pandas>=2.2.0
python -m pytest -v -p no:warnings --durations=5 tests/operators/forecast
11 changes: 11 additions & 0 deletions ads/opctl/anomaly_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python

# Copyright (c) 2025 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/

from ads.opctl.operator.lowcode.anomaly.__main__ import operate
from ads.opctl.operator.lowcode.anomaly.operator_config import AnomalyOperatorConfig

if __name__ == "__main__":
config = AnomalyOperatorConfig()
operate(config)
11 changes: 11 additions & 0 deletions ads/opctl/forecast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python

# Copyright (c) 2025 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/

from ads.opctl.operator.lowcode.forecast.__main__ import operate
from ads.opctl.operator.lowcode.forecast.operator_config import ForecastOperatorConfig

if __name__ == "__main__":
config = ForecastOperatorConfig()
operate(config)
10 changes: 5 additions & 5 deletions ads/opctl/operator/lowcode/forecast/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*--

# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
# Copyright (c) 2023, 2025 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/

import json
Expand All @@ -15,17 +14,17 @@
from ads.opctl.operator.common.const import ENV_OPERATOR_ARGS
from ads.opctl.operator.common.utils import _parse_input_args

from .model.forecast_datasets import ForecastDatasets, ForecastResults
from .operator_config import ForecastOperatorConfig
from .model.forecast_datasets import ForecastDatasets
from .whatifserve import ModelDeploymentManager


def operate(operator_config: ForecastOperatorConfig) -> None:
def operate(operator_config: ForecastOperatorConfig) -> ForecastResults:
"""Runs the forecasting operator."""
from .model.factory import ForecastOperatorModelFactory

datasets = ForecastDatasets(operator_config)
ForecastOperatorModelFactory.get_model(
results = ForecastOperatorModelFactory.get_model(
operator_config, datasets
).generate_report()
# saving to model catalog
Expand All @@ -36,6 +35,7 @@ def operate(operator_config: ForecastOperatorConfig) -> None:
if spec.what_if_analysis.model_deployment:
mdm.create_deployment()
mdm.save_deployment_info()
return results


def verify(spec: Dict, **kwargs: Dict) -> bool:
Expand Down
31 changes: 20 additions & 11 deletions ads/opctl/operator/lowcode/forecast/model/automlx.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
# Copyright (c) 2023, 2025 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
import logging
import os
Expand Down Expand Up @@ -66,8 +66,7 @@ def preprocess(self, data, series_id): # TODO: re-use self.le for explanations
@runtime_dependency(
module="automlx",
err_msg=(
"Please run `pip3 install oracle-automlx>=23.4.1` and "
"`pip3 install oracle-automlx[forecasting]>=23.4.1` "
"Please run `pip3 install oracle-automlx[forecasting]>=25.1.1` "
"to install the required dependencies for automlx."
),
)
Expand Down Expand Up @@ -105,7 +104,7 @@ def _build_model(self) -> pd.DataFrame:
engine_opts = (
None
if engine_type == "local"
else ({"ray_setup": {"_temp_dir": "/tmp/ray-temp"}},)
else {"ray_setup": {"_temp_dir": "/tmp/ray-temp"}}
)
init(
engine=engine_type,
Expand Down Expand Up @@ -272,11 +271,15 @@ def _generate_report(self):
self.formatted_local_explanation = aggregate_local_explanations

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_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
)
self.formatted_local_explanation.drop("Series", axis=1, inplace=True)

# Create a markdown section for the global explainability
global_explanation_section = rc.Block(
Expand Down Expand Up @@ -436,7 +439,9 @@ def explain_model(self):

# Generate explanations for the forecast
explanations = explainer.explain_prediction(
X=self.datasets.additional_data.get_data_for_series(series_id=s_id)
X=self.datasets.additional_data.get_data_for_series(
series_id=s_id
)
.drop(self.spec.datetime_column.name, axis=1)
.tail(self.spec.horizon)
if self.spec.additional_data
Expand All @@ -448,7 +453,9 @@ def explain_model(self):
explanations_df = pd.concat(
[exp.to_dataframe() for exp in explanations]
)
explanations_df["row"] = explanations_df.groupby("Feature").cumcount()
explanations_df["row"] = explanations_df.groupby(
"Feature"
).cumcount()
explanations_df = explanations_df.pivot(
index="row", columns="Feature", values="Attribution"
)
Expand All @@ -460,5 +467,7 @@ def explain_model(self):
# Fall back to the default explanation generation method
super().explain_model()
except Exception as e:
logger.warning(f"Failed to generate explanations for series {s_id} with error: {e}.")
logger.warning(
f"Failed to generate explanations for series {s_id} with error: {e}."
)
logger.debug(f"Full Traceback: {traceback.format_exc()}")
76 changes: 55 additions & 21 deletions ads/opctl/operator/lowcode/forecast/model/base_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python

# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
# Copyright (c) 2023, 2025 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/

import logging
Expand All @@ -19,6 +19,7 @@
from ads.common.decorator.runtime_dependency import runtime_dependency
from ads.common.object_storage_details import ObjectStorageDetails
from ads.opctl import logger
from ads.opctl.operator.lowcode.common.const import DataColumns
from ads.opctl.operator.lowcode.common.utils import (
datetime_to_seconds,
disable_print,
Expand All @@ -28,7 +29,6 @@
seconds_to_datetime,
write_data,
)
from ads.opctl.operator.lowcode.common.const import DataColumns
from ads.opctl.operator.lowcode.forecast.model.forecast_datasets import TestData
from ads.opctl.operator.lowcode.forecast.utils import (
_build_metrics_df,
Expand All @@ -49,10 +49,9 @@
SpeedAccuracyMode,
SupportedMetrics,
SupportedModels,
BACKTEST_REPORT_NAME,
)
from ..operator_config import ForecastOperatorConfig, ForecastOperatorSpec
from .forecast_datasets import ForecastDatasets
from .forecast_datasets import ForecastDatasets, ForecastResults

logging.getLogger("report_creator").setLevel(logging.WARNING)

Expand Down Expand Up @@ -127,8 +126,9 @@ def generate_report(self):
if self.spec.generate_report or self.spec.generate_metrics:
self.eval_metrics = self.generate_train_metrics()
if not self.target_cat_col:
self.eval_metrics.rename({"Series 1": self.original_target_column},
axis=1, inplace=True)
self.eval_metrics.rename(
{"Series 1": self.original_target_column}, axis=1, inplace=True
)

if self.spec.test_data:
try:
Expand All @@ -140,8 +140,11 @@ def generate_report(self):
elapsed_time=elapsed_time,
)
if not self.target_cat_col:
self.test_eval_metrics.rename({"Series 1": self.original_target_column},
axis=1, inplace=True)
self.test_eval_metrics.rename(
{"Series 1": self.original_target_column},
axis=1,
inplace=True,
)
except Exception:
logger.warn("Unable to generate Test Metrics.")
logger.debug(f"Full Traceback: {traceback.format_exc()}")
Expand Down Expand Up @@ -223,17 +226,23 @@ def generate_report(self):
rc.Block(
first_10_title,
# series_subtext,
rc.Select(blocks=first_5_rows_blocks) if self.target_cat_col else first_5_rows_blocks[0],
rc.Select(blocks=first_5_rows_blocks)
if self.target_cat_col
else first_5_rows_blocks[0],
),
rc.Block(
last_10_title,
# series_subtext,
rc.Select(blocks=last_5_rows_blocks) if self.target_cat_col else last_5_rows_blocks[0],
rc.Select(blocks=last_5_rows_blocks)
if self.target_cat_col
else last_5_rows_blocks[0],
),
rc.Block(
summary_title,
# series_subtext,
rc.Select(blocks=data_summary_blocks) if self.target_cat_col else data_summary_blocks[0],
rc.Select(blocks=data_summary_blocks)
if self.target_cat_col
else data_summary_blocks[0],
),
rc.Separator(),
)
Expand Down Expand Up @@ -308,7 +317,7 @@ def generate_report(self):
horizon=self.spec.horizon,
test_data=test_data,
ci_interval_width=self.spec.confidence_interval_width,
target_category_column=self.target_cat_col
target_category_column=self.target_cat_col,
)
if (
series_name is not None
Expand Down Expand Up @@ -341,11 +350,12 @@ def generate_report(self):
)

# save the report and result CSV
self._save_report(
return self._save_report(
report_sections=report_sections,
result_df=result_df,
metrics_df=self.eval_metrics,
test_metrics_df=self.test_eval_metrics,
test_data=test_data,
)

def _test_evaluate_metrics(self, elapsed_time=0):
Expand Down Expand Up @@ -462,10 +472,12 @@ def _save_report(
result_df: pd.DataFrame,
metrics_df: pd.DataFrame,
test_metrics_df: pd.DataFrame,
test_data: pd.DataFrame,
):
"""Saves resulting reports to the given folder."""

unique_output_dir = self.spec.output_directory.url
results = ForecastResults()

if ObjectStorageDetails.is_oci_path(unique_output_dir):
storage_options = default_signer()
Expand All @@ -491,13 +503,22 @@ def _save_report(
f2.write(f1.read())

# forecast csv report
result_df = result_df if self.target_cat_col else result_df.drop(DataColumns.Series, axis=1)
# if self.spec.test_data is not None:
# test_data_dict = test_data.get_dict_by_series()
# for series_id, test_data_values in test_data_dict.items():
# result_df[DataColumns.Series] = test_data_values[]
result_df = (
result_df
if self.target_cat_col
else result_df.drop(DataColumns.Series, axis=1)
)
write_data(
data=result_df,
filename=os.path.join(unique_output_dir, self.spec.forecast_filename),
format="csv",
storage_options=storage_options,
)
results.set_forecast(result_df)

# metrics csv report
if self.spec.generate_metrics:
Expand All @@ -507,17 +528,19 @@ def _save_report(
else "Series 1"
)
if metrics_df is not None:
metrics_df_formatted = metrics_df.reset_index().rename(
{"index": "metrics", "Series 1": metrics_col_name}, axis=1
)
write_data(
data=metrics_df.reset_index().rename(
{"index": "metrics", "Series 1": metrics_col_name}, axis=1
),
data=metrics_df_formatted,
filename=os.path.join(
unique_output_dir, self.spec.metrics_filename
),
format="csv",
storage_options=storage_options,
index=False,
)
results.set_metrics(metrics_df_formatted)
else:
logger.warn(
f"Attempted to generate the {self.spec.metrics_filename} file with the training metrics, however the training metrics could not be properly generated."
Expand All @@ -526,17 +549,19 @@ def _save_report(
# test_metrics csv report
if self.spec.test_data is not None:
if test_metrics_df is not None:
test_metrics_df_formatted = test_metrics_df.reset_index().rename(
{"index": "metrics", "Series 1": metrics_col_name}, axis=1
)
write_data(
data=test_metrics_df.reset_index().rename(
{"index": "metrics", "Series 1": metrics_col_name}, axis=1
),
data=test_metrics_df_formatted,
filename=os.path.join(
unique_output_dir, self.spec.test_metrics_filename
),
format="csv",
storage_options=storage_options,
index=False,
)
results.set_test_metrics(test_metrics_df_formatted)
else:
logger.warn(
f"Attempted to generate the {self.spec.test_metrics_filename} file with the test metrics, however the test metrics could not be properly generated."
Expand All @@ -554,6 +579,7 @@ def _save_report(
storage_options=storage_options,
index=True,
)
results.set_global_explanations(self.formatted_global_explanation)
else:
logger.warn(
f"Attempted to generate global explanations for the {self.spec.global_explanation_filename} file, but an issue occured in formatting the explanations."
Expand All @@ -569,6 +595,7 @@ def _save_report(
storage_options=storage_options,
index=True,
)
results.set_local_explanations(self.formatted_local_explanation)
else:
logger.warn(
f"Attempted to generate local explanations for the {self.spec.local_explanation_filename} file, but an issue occured in formatting the explanations."
Expand All @@ -589,10 +616,12 @@ def _save_report(
index=True,
indent=4,
)
results.set_model_parameters(self.model_parameters)

# model pickle
if self.spec.generate_model_pickle:
self._save_model(unique_output_dir, storage_options)
results.set_models(self.models)

logger.info(
f"The outputs have been successfully "
Expand All @@ -612,8 +641,10 @@ def _save_report(
index=True,
indent=4,
)
results.set_errors_dict(self.errors_dict)
else:
logger.info("All modeling completed successfully.")
return results

def preprocess(self, df, series_id):
"""The method that needs to be implemented on the particular model level."""
Expand Down Expand Up @@ -667,7 +698,10 @@ def _save_model(self, output_dir, storage_options):
)

def _validate_automlx_explanation_mode(self):
if self.spec.model != SupportedModels.AutoMLX and self.spec.explanations_accuracy_mode == SpeedAccuracyMode.AUTOMLX:
if (
self.spec.model != SupportedModels.AutoMLX
and self.spec.explanations_accuracy_mode == SpeedAccuracyMode.AUTOMLX
):
raise ValueError(
"AUTOMLX explanation accuracy mode is only supported for AutoMLX models. "
"Please select mode other than AUTOMLX from the available explanations_accuracy_mode options"
Expand Down
Loading
Loading