code: def run_ensemble(
      self, valid_frac, expected_precision, expected_recall
  ):
      logger.info(
          "Training model & detection threshold on training data..."
      )
  
      # build windowed statistics detector with window size = 30min
      # and data resampled once every 5min
      model0 = WindStats(
          WindStatsConfig(
              wind_sz=60, transform=TemporalResample("5min")
          )
      )
  
      # build windowed statistics detector with window size = 120min
      # and data resampled once every 30min
      model1 = WindStats(
          WindStatsConfig(
              wind_sz=120, transform=TemporalResample("30min")
          )
      )
  
      # build windowed statistics detector with window size = 360min
      # and no data resampling
      model2 = WindStats(config=WindStatsConfig(wind_sz=360))
  
      # Build ensemble model
      model = DetectorEnsemble(
          models=[model0, model1, model2],
          config=DetectorEnsembleConfig(
              combiner=ModelSelector(metric=TSADMetric.F1)
          ),
      )
      train_config = DetectorEnsembleTrainConfig(valid_frac=valid_frac)
      train_kwargs = {
          "anomaly_labels": self.train_labels,
          "train_config": train_config,
      }
  
      evaluator = TSADEvaluator(
          model=model,
          config=TSADEvaluatorConfig(
              train_window=None, retrain_freq="7d"
          ),
      )
      _, alarms = evaluator.get_predict(
          train_vals=self.train_data,
          test_vals=self.test_data,
          post_process=True,
          train_kwargs=train_kwargs,
      )
  
      # Determine the number of alarms raised
      n_alarms = np.sum(alarms.to_pd().values != 0)
      print()
      logger.info("# of alarms = " + str(n_alarms))
  
      # Evaluate ensemble anomaly detector's performance on test split
      f1 = evaluator.evaluate(
          ground_truth=self.test_labels,
          predict=alarms,
          metric=TSADMetric.F1,
      )
      p = evaluator.evaluate(
          ground_truth=self.test_labels,
          predict=alarms,
          metric=TSADMetric.Precision,
      )
      r = evaluator.evaluate(
          ground_truth=self.test_labels,
          predict=alarms,
          metric=TSADMetric.Recall,
      )
      logger.info(f"F1={f1:.4f}, precision={p:.4f}, recall={r:.4f}")
      self.assertAlmostEqual(p, expected_precision, places=4)
      self.assertAlmostEqual(r, expected_recall, places=4)
      return model
  
file_path: merlion/tests/evaluate/test_eval_anomaly.py
imports: import logging
  from os.path import abspath, dirname, join
  import sys
  import unittest
  import numpy
  import pandas
  from merlion.evaluate.anomaly import TSADEvaluator, TSADEvaluatorConfig, TSADMetric
  from merlion.models.anomaly.isolation_forest import IsolationForest, IsolationForestConfig
  from merlion.models.anomaly.windstats import WindStats, WindStatsConfig
  from merlion.models.ensemble.anomaly import DetectorEnsemble, DetectorEnsembleConfig, DetectorEnsembleTrainConfig
  from merlion.models.ensemble.combine import ModelSelector, Mean
  from merlion.transform.resample import TemporalResample
  from merlion.utils import TimeSeries
label_desc: Use image node_id 5 for calling the TestEvaluateAnomaly obj's underlying member method code with example usage: obj.run_ensemble(valid_frac, expected_precision, expected_recall) and returns: model