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.
@@ -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 - (self.spec.confidence_interval_width / 100),
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 generate_report(self):
102
- """Generates the forecasting report."""
103
- import warnings
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
- start_time = time.time()
118
- result_df = self._build_model()
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 = os.path.join(unique_output_dir, self.spec.report_filename)
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=os.path.join(unique_output_dir, self.spec.forecast_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=os.path.join(
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=os.path.join(
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=os.path.join(
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=os.path.join(
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=os.path.join(unique_output_dir, "model_params.json"),
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
- self._save_model(unique_output_dir, storage_options)
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=os.path.join(
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
- s_id=series_id
854
- )[self.spec.datetime_column.name].reset_index(drop=True)
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 ..const import AUTO_SELECT, SpeedAccuracyMode, SupportedModels
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
- if model_type == AUTO_SELECT:
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