Skip to content

Commit

Permalink
[ready to review] ODSC-68841: API Returns Data Directly (#1048)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahosler authored Feb 17, 2025
2 parents 1e99804 + 583b3db commit a030882
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 103 deletions.
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

0 comments on commit a030882

Please sign in to comment.