Repository: mindsdb/lightwood

Issue:
:wrench: Implement a `MeanEnsemble` for numerical tasks
## Introduction

Lightwood is an AutoML library that aims at abstracting common procedures in the Machine Learning workflow (e.g. data preparation, feature engineering, model building, model analysis) with an approach that can be fully automated but easily customizable and extended thanks to its flexibility. 

One of the key components in model building are "mixers", models that intake features and learn to predict the target column. For any given predictive task, there might be a number of valid mixers that will get trained.

After all mixers are trained, the [`BaseEnsemble`](https://github.com/mindsdb/lightwood/blob/staging/lightwood/ensemble/base.py) abstraction enables ML practitioners to aggregate the predictions of all mixers as they deem convenient.

For example [`BestOf`](https://github.com/mindsdb/lightwood/blob/staging/lightwood/ensemble/best_of.py), the only currently available ensemble, is a simple procedure that evaluates all models and selects the best one based on validation accuracy. 

## Task

In this task, we seek to implement a `MeanEnsemble` for numerical targets. Simply put, it should obtain the predictions of all internal mixers, then output the mean of all predicted values.

## Steps :male_detective: :female_detective: 

- Fork the Lightwood repository, checkout the `staging` branch and from it create a new one.
- Implement the `MeanEnsemble` class. Any files should be added inside the `lightwood/ensemble/` directory.
- Implement a test where your ensemble is successfully used with a sample regression task, like [concrete strength](https://github.com/mindsdb/lightwood/blob/staging/tests/data/concrete_strength.csv). For guidance on how to trigger your custom ensemble, you can check out `Step 3` in the [custom mixer tutorial](https://lightwood.io/tutorials/custom_mixer/custom_mixer.html) (same idea, but you would modify the `.ensemble` key instead).
- Make the PR and address any comments that reviewers might make.

## Additional rewards :1st_place_medal: 

Accepted PRs for ensemble challenges bring :five: points for entry into the draw for a :computer:  Deep Learning Laptop powered by the NVIDIA RTX 3080 Max-Q GPU or other swag :shirt: :bear: .  For more info check out https://mindsdb.com/hacktoberfest/


Discussion:
hi @paxcema  I would like to do this. Please assign this task to me.

Resolving patch:
diff --git a/lightwood/ensemble/__init__.py b/lightwood/ensemble/__init__.py
--- a/lightwood/ensemble/__init__.py
+++ b/lightwood/ensemble/__init__.py
@@ -1,5 +1,6 @@
 from lightwood.ensemble.base import BaseEnsemble
 from lightwood.ensemble.best_of import BestOf
+from lightwood.ensemble.mean_ensemble import MeanEnsemble
 
 
-__all__ = ['BaseEnsemble', 'BestOf']
+__all__ = ['BaseEnsemble', 'BestOf', 'MeanEnsemble']
diff --git a/lightwood/ensemble/mean_ensemble.py b/lightwood/ensemble/mean_ensemble.py
new file mode 100644
--- /dev/null
+++ b/lightwood/ensemble/mean_ensemble.py
@@ -0,0 +1,25 @@
+from typing import List
+
+import pandas as pd
+
+from lightwood.mixer.base import BaseMixer
+from lightwood.ensemble.base import BaseEnsemble
+from lightwood.api.types import PredictionArguments
+from lightwood.data.encoded_ds import EncodedDs
+from lightwood import dtype
+
+
+class MeanEnsemble(BaseEnsemble):
+    def __init__(self, target, mixers: List[BaseMixer], data: EncodedDs, dtype_dict: dict) -> None:
+        super().__init__(target, mixers, data)
+        if dtype_dict[target] not in (dtype.float, dtype.integer, dtype.quantity):
+            raise Exception(
+                f'This ensemble can only be used regression problems! Got target dtype {dtype_dict[target]} instead!')
+
+    def __call__(self, ds: EncodedDs, args: PredictionArguments) -> pd.DataFrame:
+        predictions_df = pd.DataFrame()
+        for mixer in self.mixers:
+            predictions_df[f'__mdb_mixer_{type(mixer).__name__}'] = mixer(ds, args=args)['prediction']
+
+        return pd.DataFrame(predictions_df.mean(axis='columns'), columns=['prediction'])
+


Test patch:
diff --git a/tests/integration/basic/test_ensembles.py b/tests/integration/basic/test_ensembles.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/basic/test_ensembles.py
@@ -0,0 +1,33 @@
+import unittest
+import pandas as pd
+from sklearn.metrics import r2_score
+
+from lightwood.api.high_level import code_from_json_ai, json_ai_from_problem, predictor_from_code
+from lightwood.api.types import ProblemDefinition
+
+
+class TestBasic(unittest.TestCase):
+    def test_0_mean_ensemble(self):
+
+        df = pd.read_csv('tests/data/concrete_strength.csv')
+
+        target = 'concrete_strength'
+
+        json_ai = json_ai_from_problem(df, ProblemDefinition.from_dict({
+            'target': target,
+            'time_aim': 80
+        }))
+
+        json_ai.outputs[target].ensemble = {
+            'module': 'MeanEnsemble',
+            "args": {
+                'dtype_dict': '$dtype_dict'
+            }
+        }
+
+        code = code_from_json_ai(json_ai)
+        predictor = predictor_from_code(code)
+        predictor.learn(df)
+        predictions = predictor.predict(df)
+
+        self.assertTrue(r2_score(df[target], predictions['prediction']) > 0.5)