Skip to content

Commit

Permalink
Merge branch 'main' into feature/odsc68406/amlx_global_explainer
Browse files Browse the repository at this point in the history
  • Loading branch information
ahosler authored Feb 17, 2025
2 parents c33c9a2 + a030882 commit 1619dee
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 84 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
7 changes: 3 additions & 4 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
37 changes: 28 additions & 9 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 Down Expand Up @@ -51,7 +51,7 @@
SupportedModels,
)
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 @@ -350,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 @@ -471,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 @@ -500,6 +503,11 @@ def _save_report(
f2.write(f1.read())

# forecast csv report
# todo: add test data into forecast.csv
# 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
Expand All @@ -511,6 +519,7 @@ def _save_report(
format="csv",
storage_options=storage_options,
)
results.set_forecast(result_df)

# metrics csv report
if self.spec.generate_metrics:
Expand All @@ -520,17 +529,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 @@ -539,17 +550,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 @@ -567,6 +580,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 @@ -582,6 +596,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 @@ -602,10 +617,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 @@ -625,8 +642,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
62 changes: 60 additions & 2 deletions ads/opctl/operator/lowcode/forecast/model/forecast_datasets.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/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/

from typing import Dict, List

import pandas as pd

from ads.opctl import logger
Expand Down Expand Up @@ -167,7 +169,7 @@ def get_data_multi_indexed(self):
self.historical_data.data,
self.additional_data.data,
],
axis=1
axis=1,
)

def get_data_by_series(self, include_horizon=True):
Expand Down Expand Up @@ -416,3 +418,59 @@ def get_forecast_long(self):
for df in self.series_id_map.values():
output = pd.concat([output, df])
return output.reset_index(drop=True)


class ForecastResults:
"""
Forecast Results contains all outputs from the forecast run.
This class is returned to users who use the Forecast's `operate` method.
"""

def set_forecast(self, df: pd.DataFrame):
self.forecast = df

def get_forecast(self):
return getattr(self, "forecast", None)

def set_metrics(self, df: pd.DataFrame):
self.metrics = df

def get_metrics(self):
return getattr(self, "metrics", None)

def set_test_metrics(self, df: pd.DataFrame):
self.test_metrics = df

def get_test_metrics(self):
return getattr(self, "test_metrics", None)

def set_local_explanations(self, df: pd.DataFrame):
self.local_explanations = df

def get_local_explanations(self):
return getattr(self, "local_explanations", None)

def set_global_explanations(self, df: pd.DataFrame):
self.global_explanations = df

def get_global_explanations(self):
return getattr(self, "global_explanations", None)

def set_model_parameters(self, df: pd.DataFrame):
self.model_parameters = df

def get_model_parameters(self):
return getattr(self, "model_parameters", None)

def set_models(self, models: List):
self.models = models

def get_models(self):
return getattr(self, "models", None)

def set_errors_dict(self, errors_dict: Dict):
self.errors_dict = errors_dict

def get_errors_dict(self):
return getattr(self, "errors_dict", None)
Loading

0 comments on commit 1619dee

Please sign in to comment.