oracle-ads 2.13.17rc0__py3-none-any.whl → 2.13.18rc0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ads/aqua/common/enums.py +1 -0
- ads/aqua/common/utils.py +38 -0
- ads/aqua/modeldeployment/config_loader.py +10 -0
- ads/aqua/modeldeployment/deployment.py +16 -10
- ads/aqua/modeldeployment/entities.py +1 -0
- ads/opctl/operator/lowcode/common/data.py +7 -2
- ads/opctl/operator/lowcode/common/transformations.py +207 -0
- ads/opctl/operator/lowcode/common/utils.py +8 -0
- ads/opctl/operator/lowcode/forecast/__init__.py +3 -0
- ads/opctl/operator/lowcode/forecast/__main__.py +53 -3
- ads/opctl/operator/lowcode/forecast/const.py +2 -0
- ads/opctl/operator/lowcode/forecast/errors.py +5 -0
- ads/opctl/operator/lowcode/forecast/meta_selector.py +310 -0
- ads/opctl/operator/lowcode/forecast/model/automlx.py +1 -1
- ads/opctl/operator/lowcode/forecast/model/base_model.py +119 -30
- ads/opctl/operator/lowcode/forecast/model/factory.py +33 -2
- ads/opctl/operator/lowcode/forecast/model/forecast_datasets.py +50 -14
- ads/opctl/operator/lowcode/forecast/model_evaluator.py +6 -1
- ads/opctl/operator/lowcode/forecast/schema.yaml +1 -0
- {oracle_ads-2.13.17rc0.dist-info → oracle_ads-2.13.18rc0.dist-info}/METADATA +1 -1
- {oracle_ads-2.13.17rc0.dist-info → oracle_ads-2.13.18rc0.dist-info}/RECORD +24 -23
- {oracle_ads-2.13.17rc0.dist-info → oracle_ads-2.13.18rc0.dist-info}/WHEEL +0 -0
- {oracle_ads-2.13.17rc0.dist-info → oracle_ads-2.13.18rc0.dist-info}/entry_points.txt +0 -0
- {oracle_ads-2.13.17rc0.dist-info → oracle_ads-2.13.18rc0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,310 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
# Copyright (c) 2023, 2025 Oracle and/or its affiliates.
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
import pandas as pd
|
8
|
+
|
9
|
+
|
10
|
+
class MetaSelector:
|
11
|
+
"""
|
12
|
+
A class to select the best forecasting model for each series based on pre-learned meta-rules.
|
13
|
+
The rules are based on the meta-features calculated by the FFORMS approach.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self):
|
17
|
+
"""Initialize the MetaSelector with pre-learned meta rules"""
|
18
|
+
# Pre-learned rules based on meta-features
|
19
|
+
self._meta_rules = {
|
20
|
+
# Rule 1: Strong trend, weak seasonality → ARIMA
|
21
|
+
"arima_0": {
|
22
|
+
"conditions": [
|
23
|
+
("ts_trend", "abs>=", 0.65), # Strong trend strength
|
24
|
+
("ts_seasonal_strength", "<", 0.20), # Weak seasonality
|
25
|
+
],
|
26
|
+
"model": "arima",
|
27
|
+
"priority": 1,
|
28
|
+
},
|
29
|
+
# Rule 2: Strong seasonality, long series → Prophet
|
30
|
+
"prophet_0": {
|
31
|
+
"conditions": [
|
32
|
+
("ts_seasonal_strength", ">=", 0.50), # Strong seasonality
|
33
|
+
("ts_n_obs", ">=", 200), # Long series
|
34
|
+
],
|
35
|
+
"model": "prophet",
|
36
|
+
"priority": 2,
|
37
|
+
},
|
38
|
+
# Rule 3: High entropy, low autocorrelation → AutoMLX
|
39
|
+
"automlx_0": {
|
40
|
+
"conditions": [
|
41
|
+
("ts_entropy", ">=", 4.0), # High entropy
|
42
|
+
("ts_acf1", "<=", 0.30), # Low autocorrelation
|
43
|
+
],
|
44
|
+
"model": "automlx",
|
45
|
+
"priority": 3,
|
46
|
+
},
|
47
|
+
# Rule 4: Strong seasonality with trend and changing patterns → Prophet
|
48
|
+
"prophet_1": {
|
49
|
+
"conditions": [
|
50
|
+
("ts_seasonal_strength", ">=", 0.3), # Strong seasonality
|
51
|
+
("ts_trend", "abs>=", 0.1), # Clear trend
|
52
|
+
("ts_turning_points_rate", ">=", 0.2), # Multiple change points
|
53
|
+
("ts_n_obs", ">=", 50), # Sufficient data
|
54
|
+
("ts_step_max", ">=", 100), # Significant steps
|
55
|
+
("ts_diff1_variance", ">=", 10), # Variable differences
|
56
|
+
],
|
57
|
+
"model": "prophet",
|
58
|
+
"priority": 4,
|
59
|
+
},
|
60
|
+
# Rule 5: Multiple seasonality with nonlinear patterns → Prophet
|
61
|
+
"prophet_2": {
|
62
|
+
"conditions": [
|
63
|
+
("ts_seasonal_peak_strength", ">=", 0.4), # Strong peak seasonality
|
64
|
+
("ts_seasonal_strength", ">=", 0.2), # Overall seasonality
|
65
|
+
("ts_acf10", ">=", 0.2), # Long-term correlation
|
66
|
+
("ts_entropy", ">=", 0.5), # Complex patterns
|
67
|
+
("ts_crossing_rate", ">=", 0.3), # Frequent mean crossings
|
68
|
+
],
|
69
|
+
"model": "prophet",
|
70
|
+
"priority": 5,
|
71
|
+
},
|
72
|
+
# Rule 6: Strong autocorrelation with stationary behavior → ARIMA
|
73
|
+
"arima_1": {
|
74
|
+
"conditions": [
|
75
|
+
("ts_acf1", ">=", 0.7), # Strong lag-1 correlation
|
76
|
+
("ts_acf2", ">=", 0.5), # Strong lag-2 correlation
|
77
|
+
("ts_seasonal_strength", "<", 0.3), # Weak seasonality
|
78
|
+
("ts_std_residuals", "<", 500), # Stable residuals
|
79
|
+
("ts_diff1_variance", "<", 100), # Stable first differences
|
80
|
+
("ts_hurst", ">", -0.1), # Some persistence
|
81
|
+
],
|
82
|
+
"model": "arima",
|
83
|
+
"priority": 6,
|
84
|
+
},
|
85
|
+
# Rule 7: Linear trend with moderate noise → ARIMA
|
86
|
+
"arima_2": {
|
87
|
+
"conditions": [
|
88
|
+
("ts_trend", "abs>=", 0.15), # Clear trend
|
89
|
+
("ts_trend_change", "<", 100), # Stable trend
|
90
|
+
("ts_cv", "<", 0.4), # Low variation
|
91
|
+
("ts_kurtosis", "<", 5), # Normal-like distribution
|
92
|
+
("ts_nonlinearity", "<", 1e5), # Linear relationships
|
93
|
+
],
|
94
|
+
"model": "arima",
|
95
|
+
"priority": 7,
|
96
|
+
},
|
97
|
+
# Rule 8: Complex seasonality with high nonlinearity → NeuralProphet
|
98
|
+
"neuralprophet_1": {
|
99
|
+
"conditions": [
|
100
|
+
("ts_seasonal_peak_strength", ">=", 0.5), # Strong seasonal peaks
|
101
|
+
("ts_nonlinearity", ">=", 1e6), # Nonlinear patterns
|
102
|
+
("ts_n_obs", ">=", 200), # Long series
|
103
|
+
("ts_entropy", ">=", 0.6), # Complex patterns
|
104
|
+
("ts_diff2_variance", ">=", 50), # Variable acceleration
|
105
|
+
],
|
106
|
+
"model": "neuralprophet",
|
107
|
+
"priority": 8,
|
108
|
+
},
|
109
|
+
# Rule 9: Multiple seasonal patterns with changing behavior → NeuralProphet
|
110
|
+
"neuralprophet_2": {
|
111
|
+
"conditions": [
|
112
|
+
("ts_seasonal_strength", ">=", 0.4), # Strong seasonality
|
113
|
+
("ts_turning_points_rate", ">=", 0.3), # Many turning points
|
114
|
+
("ts_skewness", "abs>=", 1), # Skewed distribution
|
115
|
+
("ts_diff1_mean", ">=", 10), # Large changes
|
116
|
+
("ts_crossing_rate", ">=", 0.4), # Frequent crossings
|
117
|
+
],
|
118
|
+
"model": "neuralprophet",
|
119
|
+
"priority": 9,
|
120
|
+
},
|
121
|
+
# Rule 10: High volatility with complex patterns → AutoMLX
|
122
|
+
"automlx_1": {
|
123
|
+
"conditions": [
|
124
|
+
("ts_cv", ">=", 0.6), # High variation
|
125
|
+
("ts_nonlinearity", ">=", 1e7), # Strong nonlinearity
|
126
|
+
("ts_spikes_rate", ">=", 0.1), # Frequent spikes
|
127
|
+
("ts_entropy", ">=", 0.7), # Very complex
|
128
|
+
("ts_std_residuals", ">=", 1000), # Large residuals
|
129
|
+
],
|
130
|
+
"model": "automlx",
|
131
|
+
"priority": 10,
|
132
|
+
},
|
133
|
+
# Rule 11: Unstable patterns with regime changes → AutoMLX
|
134
|
+
"automlx_2": {
|
135
|
+
"conditions": [
|
136
|
+
("ts_trend_change", ">=", 200), # Changing trend
|
137
|
+
("ts_turning_points_rate", ">=", 0.4), # Many turning points
|
138
|
+
("ts_diff2_variance", ">=", 100), # Variable acceleration
|
139
|
+
("ts_hurst", "<", -0.2), # Anti-persistent
|
140
|
+
("ts_step_max", ">=", 1000), # Large steps
|
141
|
+
],
|
142
|
+
"model": "automlx",
|
143
|
+
"priority": 11,
|
144
|
+
},
|
145
|
+
# Rule 12: Long series with stable seasonality → AutoTS
|
146
|
+
"autots_1": {
|
147
|
+
"conditions": [
|
148
|
+
("ts_n_obs", ">=", 150), # Long series
|
149
|
+
("ts_seasonal_strength", ">=", 0.2), # Moderate seasonality
|
150
|
+
("ts_cv", "<", 0.5), # Moderate variation
|
151
|
+
("ts_entropy", "<", 0.5), # Not too complex
|
152
|
+
("ts_acf1", ">=", 0.3), # Some autocorrelation
|
153
|
+
],
|
154
|
+
"model": "autots",
|
155
|
+
"priority": 12,
|
156
|
+
},
|
157
|
+
# Rule 13: Stable patterns with low noise → Prophet
|
158
|
+
"prophet_3": {
|
159
|
+
"conditions": [
|
160
|
+
("ts_cv", "<", 0.3), # Low variation
|
161
|
+
("ts_kurtosis", "<", 4), # Normal-like
|
162
|
+
("ts_turning_points_rate", "<", 0.25), # Few turning points
|
163
|
+
("ts_diff1_variance", "<", 50), # Stable changes
|
164
|
+
("ts_seasonal_strength", ">=", 0.1), # Some seasonality
|
165
|
+
],
|
166
|
+
"model": "prophet",
|
167
|
+
"priority": 13,
|
168
|
+
},
|
169
|
+
# Rule 14: Short series with strong linear patterns → ARIMA
|
170
|
+
"arima_3": {
|
171
|
+
"conditions": [
|
172
|
+
("ts_n_obs", "<", 100), # Short series
|
173
|
+
("ts_trend", "abs>=", 0.2), # Strong trend
|
174
|
+
("ts_entropy", "<", 0.4), # Simple patterns
|
175
|
+
("ts_nonlinearity", "<", 1e5), # Linear
|
176
|
+
("ts_seasonal_strength", "<", 0.2), # Weak seasonality
|
177
|
+
],
|
178
|
+
"model": "arima",
|
179
|
+
"priority": 14,
|
180
|
+
},
|
181
|
+
# Rule 15: Complex seasonal patterns with long memory → NeuralProphet
|
182
|
+
"neuralprophet_3": {
|
183
|
+
"conditions": [
|
184
|
+
("ts_n_obs", ">=", 300), # Very long series
|
185
|
+
("ts_seasonal_strength", ">=", 0.3), # Clear seasonality
|
186
|
+
("ts_acf10", ">=", 0.3), # Long memory
|
187
|
+
("ts_hurst", ">", 0), # Persistent
|
188
|
+
("ts_nonlinearity", ">=", 5e5), # Some nonlinearity
|
189
|
+
],
|
190
|
+
"model": "neuralprophet",
|
191
|
+
"priority": 15,
|
192
|
+
},
|
193
|
+
# Rule 16: High complexity with non-normal distribution → AutoMLX
|
194
|
+
"automlx_3": {
|
195
|
+
"conditions": [
|
196
|
+
("ts_kurtosis", ">=", 5), # Heavy tails
|
197
|
+
("ts_skewness", "abs>=", 2), # Highly skewed
|
198
|
+
("ts_entropy", ">=", 0.6), # Complex
|
199
|
+
("ts_spikes_rate", ">=", 0.05), # Some spikes
|
200
|
+
("ts_diff2_mean", ">=", 5), # Changing acceleration
|
201
|
+
],
|
202
|
+
"model": "automlx",
|
203
|
+
"priority": 16,
|
204
|
+
},
|
205
|
+
# Rule 17: Simple patterns with weak seasonality → AutoTS
|
206
|
+
"autots_2": {
|
207
|
+
"conditions": [
|
208
|
+
("ts_entropy", "<", 0.3), # Simple patterns
|
209
|
+
("ts_seasonal_strength", "<", 0.3), # Weak seasonality
|
210
|
+
("ts_cv", "<", 0.4), # Low variation
|
211
|
+
("ts_nonlinearity", "<", 1e5), # Nearly linear
|
212
|
+
("ts_diff1_mean", "<", 10), # Small changes
|
213
|
+
],
|
214
|
+
"model": "autots",
|
215
|
+
"priority": 17,
|
216
|
+
},
|
217
|
+
}
|
218
|
+
|
219
|
+
def _evaluate_condition(self, value, operator, threshold):
|
220
|
+
"""Evaluate a single condition based on pre-defined operators"""
|
221
|
+
if pd.isna(value):
|
222
|
+
return False
|
223
|
+
|
224
|
+
if operator == ">=":
|
225
|
+
return value >= threshold
|
226
|
+
elif operator == ">":
|
227
|
+
return value > threshold
|
228
|
+
elif operator == "<":
|
229
|
+
return value < threshold
|
230
|
+
elif operator == "<=":
|
231
|
+
return value <= threshold
|
232
|
+
elif operator == "abs>=":
|
233
|
+
return abs(value) >= threshold
|
234
|
+
elif operator == "abs<":
|
235
|
+
return abs(value) < threshold
|
236
|
+
return False
|
237
|
+
|
238
|
+
def _check_model_conditions(self, features, model_rules):
|
239
|
+
"""Check if a series meets all conditions for a model"""
|
240
|
+
for feature, operator, threshold in model_rules["conditions"]:
|
241
|
+
if feature not in features:
|
242
|
+
return False
|
243
|
+
if not self._evaluate_condition(features[feature], operator, threshold):
|
244
|
+
return False
|
245
|
+
return True
|
246
|
+
|
247
|
+
def select_best_model(self, meta_features_df):
|
248
|
+
"""
|
249
|
+
Select the best model for each series based on pre-learned rules.
|
250
|
+
|
251
|
+
Parameters
|
252
|
+
----------
|
253
|
+
meta_features_df : pandas.DataFrame
|
254
|
+
DataFrame containing meta-features for each series, as returned by
|
255
|
+
build_fforms_meta_features
|
256
|
+
|
257
|
+
Returns
|
258
|
+
-------
|
259
|
+
pandas.DataFrame
|
260
|
+
DataFrame with series identifiers, selected model names, and matching rule info
|
261
|
+
"""
|
262
|
+
results = []
|
263
|
+
|
264
|
+
# Process each series
|
265
|
+
for _, row in meta_features_df.iterrows():
|
266
|
+
series_info = {}
|
267
|
+
|
268
|
+
# Preserve group columns if they exist
|
269
|
+
group_cols = [col for col in row.index if not col.startswith("ts_")]
|
270
|
+
for col in group_cols:
|
271
|
+
series_info[col] = row[col]
|
272
|
+
|
273
|
+
# Find matching models
|
274
|
+
matching_models = []
|
275
|
+
matched_features = {}
|
276
|
+
for rule_name, rules in self._meta_rules.items():
|
277
|
+
if self._check_model_conditions(row, rules):
|
278
|
+
matching_models.append((rule_name, rules["priority"]))
|
279
|
+
# Store which features triggered this rule
|
280
|
+
matched_features[rule_name] = [
|
281
|
+
(feature, row[feature]) for feature, _, _ in rules["conditions"]
|
282
|
+
]
|
283
|
+
|
284
|
+
# Select best model based on priority
|
285
|
+
if matching_models:
|
286
|
+
best_rule = min(matching_models, key=lambda x: x[1])[0]
|
287
|
+
best_model = self._meta_rules[best_rule]["model"]
|
288
|
+
series_info["matched_features"] = matched_features[best_rule]
|
289
|
+
else:
|
290
|
+
best_rule = "default"
|
291
|
+
best_model = "prophet" # Default to prophet if no rules match
|
292
|
+
series_info["matched_features"] = []
|
293
|
+
|
294
|
+
series_info["selected_model"] = best_model
|
295
|
+
series_info["rule_matched"] = best_rule
|
296
|
+
results.append(series_info)
|
297
|
+
|
298
|
+
return pd.DataFrame(results)
|
299
|
+
|
300
|
+
def get_model_conditions(self):
|
301
|
+
"""
|
302
|
+
Get the pre-learned conditions for each model.
|
303
|
+
This is read-only and cannot be modified at runtime.
|
304
|
+
|
305
|
+
Returns
|
306
|
+
-------
|
307
|
+
dict
|
308
|
+
Dictionary containing the conditions for each model
|
309
|
+
"""
|
310
|
+
return self._meta_rules.copy()
|
@@ -158,7 +158,7 @@ class AutoMLXOperatorModel(ForecastOperatorBaseModel):
|
|
158
158
|
summary_frame = model.forecast(
|
159
159
|
X=X_pred,
|
160
160
|
periods=horizon,
|
161
|
-
alpha=1 -
|
161
|
+
alpha=1 - self.spec.confidence_interval_width,
|
162
162
|
)
|
163
163
|
|
164
164
|
fitted_values = model.predict(data_i.drop(target, axis=1))[
|
@@ -8,6 +8,7 @@ import os
|
|
8
8
|
import tempfile
|
9
9
|
import time
|
10
10
|
import traceback
|
11
|
+
import warnings
|
11
12
|
from abc import ABC, abstractmethod
|
12
13
|
from typing import Tuple
|
13
14
|
|
@@ -50,6 +51,7 @@ from ..const import (
|
|
50
51
|
SpeedAccuracyMode,
|
51
52
|
SupportedMetrics,
|
52
53
|
SupportedModels,
|
54
|
+
TROUBLESHOOTING_GUIDE,
|
53
55
|
)
|
54
56
|
from ..operator_config import ForecastOperatorConfig, ForecastOperatorSpec
|
55
57
|
from .forecast_datasets import ForecastDatasets, ForecastResults
|
@@ -98,10 +100,21 @@ class ForecastOperatorBaseModel(ABC):
|
|
98
100
|
self.spec.tuning.n_trials is not None
|
99
101
|
)
|
100
102
|
|
101
|
-
def
|
102
|
-
"""
|
103
|
-
import
|
103
|
+
def build_model(self):
|
104
|
+
"""Builds the model and returns the result DataFrame and elapsed time."""
|
105
|
+
import time
|
104
106
|
|
107
|
+
start_time = time.time()
|
108
|
+
result_df = self._build_model()
|
109
|
+
elapsed_time = time.time() - start_time
|
110
|
+
logger.info("Building the models completed in %s seconds", elapsed_time)
|
111
|
+
return result_df, elapsed_time
|
112
|
+
|
113
|
+
def generate_report(
|
114
|
+
self, result_df=None, elapsed_time=None, save_sub_reports=False
|
115
|
+
):
|
116
|
+
"""Generates the forecasting report. Optionally accepts a precomputed result_df and elapsed_time.
|
117
|
+
If save_sub_reports is True, unique filenames are generated for all outputs."""
|
105
118
|
from sklearn.exceptions import ConvergenceWarning
|
106
119
|
|
107
120
|
with warnings.catch_warnings():
|
@@ -114,10 +127,8 @@ class ForecastOperatorBaseModel(ABC):
|
|
114
127
|
if self.spec.previous_output_dir is not None:
|
115
128
|
self._load_model()
|
116
129
|
|
117
|
-
|
118
|
-
|
119
|
-
elapsed_time = time.time() - start_time
|
120
|
-
logger.info("Building the models completed in %s seconds", elapsed_time)
|
130
|
+
if result_df is None or elapsed_time is None:
|
131
|
+
result_df, elapsed_time = self.build_model()
|
121
132
|
|
122
133
|
# Generate metrics
|
123
134
|
summary_metrics = None
|
@@ -354,6 +365,7 @@ class ForecastOperatorBaseModel(ABC):
|
|
354
365
|
metrics_df=self.eval_metrics,
|
355
366
|
test_metrics_df=self.test_eval_metrics,
|
356
367
|
test_data=test_data,
|
368
|
+
save_sub_reports=save_sub_reports,
|
357
369
|
)
|
358
370
|
|
359
371
|
def _test_evaluate_metrics(self, elapsed_time=0):
|
@@ -471,8 +483,9 @@ class ForecastOperatorBaseModel(ABC):
|
|
471
483
|
metrics_df: pd.DataFrame,
|
472
484
|
test_metrics_df: pd.DataFrame,
|
473
485
|
test_data: pd.DataFrame,
|
486
|
+
save_sub_reports: bool = False,
|
474
487
|
):
|
475
|
-
"""Saves resulting reports to the given folder."""
|
488
|
+
"""Saves resulting reports to the given folder. If save_sub_reports is True, use unique filenames."""
|
476
489
|
|
477
490
|
unique_output_dir = self.spec.output_directory.url
|
478
491
|
results = ForecastResults()
|
@@ -483,6 +496,12 @@ class ForecastOperatorBaseModel(ABC):
|
|
483
496
|
else {}
|
484
497
|
)
|
485
498
|
|
499
|
+
def get_path(filename):
|
500
|
+
path = os.path.join(unique_output_dir, filename)
|
501
|
+
if save_sub_reports:
|
502
|
+
return self._get_unique_filename(path, storage_options)
|
503
|
+
return path
|
504
|
+
|
486
505
|
# report-creator html report
|
487
506
|
if self.spec.generate_report:
|
488
507
|
with tempfile.TemporaryDirectory() as temp_dir:
|
@@ -492,7 +511,7 @@ class ForecastOperatorBaseModel(ABC):
|
|
492
511
|
report.save(rc.Block(*report_sections), report_local_path)
|
493
512
|
enable_print()
|
494
513
|
|
495
|
-
report_path =
|
514
|
+
report_path = get_path(self.spec.report_filename)
|
496
515
|
write_file(
|
497
516
|
local_filename=report_local_path,
|
498
517
|
remote_filename=report_path,
|
@@ -511,9 +530,10 @@ class ForecastOperatorBaseModel(ABC):
|
|
511
530
|
else result_df.drop(DataColumns.Series, axis=1)
|
512
531
|
)
|
513
532
|
if self.spec.generate_forecast_file:
|
533
|
+
forecast_path = get_path(self.spec.forecast_filename)
|
514
534
|
write_data(
|
515
535
|
data=result_df,
|
516
|
-
filename=
|
536
|
+
filename=forecast_path,
|
517
537
|
format="csv",
|
518
538
|
storage_options=storage_options,
|
519
539
|
)
|
@@ -531,11 +551,10 @@ class ForecastOperatorBaseModel(ABC):
|
|
531
551
|
{"index": "metrics", "Series 1": metrics_col_name}, axis=1
|
532
552
|
)
|
533
553
|
if self.spec.generate_metrics_file:
|
554
|
+
metrics_path = get_path(self.spec.metrics_filename)
|
534
555
|
write_data(
|
535
556
|
data=metrics_df_formatted,
|
536
|
-
filename=
|
537
|
-
unique_output_dir, self.spec.metrics_filename
|
538
|
-
),
|
557
|
+
filename=metrics_path,
|
539
558
|
format="csv",
|
540
559
|
storage_options=storage_options,
|
541
560
|
index=False,
|
@@ -553,11 +572,10 @@ class ForecastOperatorBaseModel(ABC):
|
|
553
572
|
{"index": "metrics", "Series 1": metrics_col_name}, axis=1
|
554
573
|
)
|
555
574
|
if self.spec.generate_metrics_file:
|
575
|
+
test_metrics_path = get_path(self.spec.test_metrics_filename)
|
556
576
|
write_data(
|
557
577
|
data=test_metrics_df_formatted,
|
558
|
-
filename=
|
559
|
-
unique_output_dir, self.spec.test_metrics_filename
|
560
|
-
),
|
578
|
+
filename=test_metrics_path,
|
561
579
|
format="csv",
|
562
580
|
storage_options=storage_options,
|
563
581
|
index=False,
|
@@ -567,6 +585,7 @@ class ForecastOperatorBaseModel(ABC):
|
|
567
585
|
logger.warning(
|
568
586
|
f"Attempted to generate the {self.spec.test_metrics_filename} file with the test metrics, however the test metrics could not be properly generated."
|
569
587
|
)
|
588
|
+
|
570
589
|
# explanations csv reports
|
571
590
|
if self.spec.generate_explanations:
|
572
591
|
try:
|
@@ -579,11 +598,12 @@ class ForecastOperatorBaseModel(ABC):
|
|
579
598
|
else col
|
580
599
|
)
|
581
600
|
if self.spec.generate_explanation_files:
|
601
|
+
global_exp_path = get_path(
|
602
|
+
self.spec.global_explanation_filename
|
603
|
+
)
|
582
604
|
write_data(
|
583
605
|
data=global_expl_rounded,
|
584
|
-
filename=
|
585
|
-
unique_output_dir, self.spec.global_explanation_filename
|
586
|
-
),
|
606
|
+
filename=global_exp_path,
|
587
607
|
format="csv",
|
588
608
|
storage_options=storage_options,
|
589
609
|
index=True,
|
@@ -603,11 +623,10 @@ class ForecastOperatorBaseModel(ABC):
|
|
603
623
|
else col
|
604
624
|
)
|
605
625
|
if self.spec.generate_explanation_files:
|
626
|
+
local_exp_path = get_path(self.spec.local_explanation_filename)
|
606
627
|
write_data(
|
607
628
|
data=local_expl_rounded,
|
608
|
-
filename=
|
609
|
-
unique_output_dir, self.spec.local_explanation_filename
|
610
|
-
),
|
629
|
+
filename=local_exp_path,
|
611
630
|
format="csv",
|
612
631
|
storage_options=storage_options,
|
613
632
|
index=True,
|
@@ -625,9 +644,10 @@ class ForecastOperatorBaseModel(ABC):
|
|
625
644
|
|
626
645
|
if self.spec.generate_model_parameters:
|
627
646
|
# model params
|
647
|
+
model_params_path = get_path("model_params.json")
|
628
648
|
write_data(
|
629
649
|
data=pd.DataFrame.from_dict(self.model_parameters),
|
630
|
-
filename=
|
650
|
+
filename=model_params_path,
|
631
651
|
format="json",
|
632
652
|
storage_options=storage_options,
|
633
653
|
index=True,
|
@@ -637,7 +657,13 @@ class ForecastOperatorBaseModel(ABC):
|
|
637
657
|
|
638
658
|
# model pickle
|
639
659
|
if self.spec.generate_model_pickle:
|
640
|
-
|
660
|
+
pickle_path = get_path("model.pkl")
|
661
|
+
write_pkl(
|
662
|
+
obj=self.models,
|
663
|
+
filename=os.path.basename(pickle_path),
|
664
|
+
output_dir=os.path.dirname(pickle_path),
|
665
|
+
storage_options=storage_options,
|
666
|
+
)
|
641
667
|
results.set_models(self.models)
|
642
668
|
|
643
669
|
logger.info(
|
@@ -648,11 +674,10 @@ class ForecastOperatorBaseModel(ABC):
|
|
648
674
|
f"The outputs have been successfully generated and placed into the directory: {unique_output_dir}."
|
649
675
|
)
|
650
676
|
if self.errors_dict:
|
677
|
+
errors_path = get_path(self.spec.errors_dict_filename)
|
651
678
|
write_json(
|
652
679
|
json_dict=self.errors_dict,
|
653
|
-
filename=
|
654
|
-
unique_output_dir, self.spec.errors_dict_filename
|
655
|
-
),
|
680
|
+
filename=errors_path,
|
656
681
|
storage_options=storage_options,
|
657
682
|
)
|
658
683
|
results.set_errors_dict(self.errors_dict)
|
@@ -719,6 +744,7 @@ class ForecastOperatorBaseModel(ABC):
|
|
719
744
|
raise ValueError(
|
720
745
|
"AUTOMLX explanation accuracy mode is only supported for AutoMLX models. "
|
721
746
|
"Please select mode other than AUTOMLX from the available explanations_accuracy_mode options"
|
747
|
+
f"\nPlease refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE} for resolution steps."
|
722
748
|
)
|
723
749
|
|
724
750
|
@runtime_dependency(
|
@@ -849,9 +875,9 @@ class ForecastOperatorBaseModel(ABC):
|
|
849
875
|
|
850
876
|
# Add date column to local explanation DataFrame
|
851
877
|
local_kernel_explnr_df[ForecastOutputColumns.DATE] = (
|
852
|
-
self.datasets.get_horizon_at_series(
|
853
|
-
|
854
|
-
|
878
|
+
self.datasets.get_horizon_at_series(s_id=series_id)[
|
879
|
+
self.spec.datetime_column.name
|
880
|
+
].reset_index(drop=True)
|
855
881
|
)
|
856
882
|
self.local_explanation[series_id] = local_kernel_explnr_df
|
857
883
|
|
@@ -873,3 +899,66 @@ class ForecastOperatorBaseModel(ABC):
|
|
873
899
|
return fcst
|
874
900
|
|
875
901
|
return _custom_predict
|
902
|
+
|
903
|
+
def _get_unique_filename(self, base_path: str, storage_options: dict = None) -> str:
|
904
|
+
"""Generate a unique filename by appending a sequential number if file exists.
|
905
|
+
|
906
|
+
Args:
|
907
|
+
base_path: The original file path to check
|
908
|
+
storage_options: Optional storage options for OCI paths
|
909
|
+
|
910
|
+
Returns:
|
911
|
+
A unique file path that doesn't exist
|
912
|
+
"""
|
913
|
+
if not ObjectStorageDetails.is_oci_path(base_path):
|
914
|
+
# For local files
|
915
|
+
directory = os.path.dirname(base_path)
|
916
|
+
basename = os.path.basename(base_path)
|
917
|
+
name, ext = os.path.splitext(basename)
|
918
|
+
|
919
|
+
model_suffix = "_" + self.spec.model
|
920
|
+
new_name = f"{name}{model_suffix}"
|
921
|
+
new_path = os.path.join(directory, f"{new_name}{ext}")
|
922
|
+
counter = 1
|
923
|
+
while os.path.exists(new_path):
|
924
|
+
new_path = os.path.join(directory, f"{new_name}_{counter}{ext}")
|
925
|
+
counter += 1
|
926
|
+
return new_path
|
927
|
+
else:
|
928
|
+
# For OCI paths, we need to list objects and check
|
929
|
+
try:
|
930
|
+
from oci.object_storage import ObjectStorageClient
|
931
|
+
|
932
|
+
client = ObjectStorageClient(config=storage_options)
|
933
|
+
|
934
|
+
# Parse OCI path components
|
935
|
+
bucket_name = ObjectStorageDetails.get_bucket_name(base_path)
|
936
|
+
namespace = ObjectStorageDetails.get_namespace(base_path)
|
937
|
+
object_name = ObjectStorageDetails.get_object_name(base_path)
|
938
|
+
|
939
|
+
name, ext = os.path.splitext(object_name)
|
940
|
+
|
941
|
+
model_suffix = "_" + self.spec.model
|
942
|
+
new_name = f"{name}{model_suffix}"
|
943
|
+
new_object_name = f"{new_name}{ext}"
|
944
|
+
counter = 1
|
945
|
+
while True:
|
946
|
+
try:
|
947
|
+
# Try to head the object to see if it exists
|
948
|
+
client.head_object(namespace, bucket_name, new_object_name)
|
949
|
+
# If we get here, the object exists
|
950
|
+
new_object_name = f"{new_name}_{counter}{ext}"
|
951
|
+
counter += 1
|
952
|
+
except:
|
953
|
+
# Object doesn't exist, we can use this name
|
954
|
+
break
|
955
|
+
|
956
|
+
# Reconstruct the full path
|
957
|
+
return ObjectStorageDetails.get_path(
|
958
|
+
namespace, bucket_name, new_object_name
|
959
|
+
)
|
960
|
+
except Exception as e:
|
961
|
+
logger.warning(
|
962
|
+
f"Error checking OCI path existence: {e}. Using original path."
|
963
|
+
)
|
964
|
+
return base_path
|
@@ -3,7 +3,16 @@
|
|
3
3
|
# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
|
4
4
|
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
5
|
|
6
|
-
from
|
6
|
+
from ads.opctl.operator.lowcode.common.transformations import Transformations
|
7
|
+
|
8
|
+
from ..const import (
|
9
|
+
AUTO_SELECT,
|
10
|
+
AUTO_SELECT_SERIES,
|
11
|
+
TROUBLESHOOTING_GUIDE,
|
12
|
+
SpeedAccuracyMode,
|
13
|
+
SupportedModels,
|
14
|
+
)
|
15
|
+
from ..meta_selector import MetaSelector
|
7
16
|
from ..model_evaluator import ModelEvaluator
|
8
17
|
from ..operator_config import ForecastOperatorConfig
|
9
18
|
from .arima import ArimaOperatorModel
|
@@ -21,6 +30,7 @@ class UnSupportedModelError(Exception):
|
|
21
30
|
super().__init__(
|
22
31
|
f"Model: `{model_type}` "
|
23
32
|
f"is not supported. Supported models: {SupportedModels.values()}"
|
33
|
+
f"\nPlease refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE} for resolution steps."
|
24
34
|
)
|
25
35
|
|
26
36
|
|
@@ -63,7 +73,28 @@ class ForecastOperatorModelFactory:
|
|
63
73
|
In case of not supported model.
|
64
74
|
"""
|
65
75
|
model_type = operator_config.spec.model
|
66
|
-
|
76
|
+
|
77
|
+
if model_type == AUTO_SELECT_SERIES:
|
78
|
+
# Initialize MetaSelector for series-specific model selection
|
79
|
+
selector = MetaSelector()
|
80
|
+
# Create a Transformations instance
|
81
|
+
transformer = Transformations(dataset_info=datasets.historical_data.spec)
|
82
|
+
|
83
|
+
# Calculate meta-features
|
84
|
+
meta_features = selector.select_best_model(
|
85
|
+
meta_features_df=transformer.build_fforms_meta_features(
|
86
|
+
data=datasets.historical_data.raw_data,
|
87
|
+
target_col=datasets.historical_data.spec.target_column,
|
88
|
+
group_cols=datasets.historical_data.spec.target_category_columns
|
89
|
+
)
|
90
|
+
)
|
91
|
+
# Get the most common model as default
|
92
|
+
model_type = meta_features['selected_model'].mode().iloc[0]
|
93
|
+
# Store the series-specific model selections in the config for later use
|
94
|
+
operator_config.spec.meta_features = meta_features
|
95
|
+
operator_config.spec.model_kwargs = {}
|
96
|
+
|
97
|
+
elif model_type == AUTO_SELECT:
|
67
98
|
model_type = cls.auto_select_model(datasets, operator_config)
|
68
99
|
operator_config.spec.model_kwargs = {}
|
69
100
|
# set the explanations accuracy mode to AUTOMLX if the selected model is automlx
|