# -*- coding: utf-8 -*-
"""
fpl_analytics._fpl_model

learning model
"""
import pandas as pd
import numpy as np
from sklearn.ensemble import (RandomForestRegressor, AdaBoostRegressor)
from sklearn.linear_model import (LinearRegression, Lasso, Ridge, 
                                  ElasticNet, TweedieRegressor,
                                  HuberRegressor)
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_validate
from sklearn.preprocessing import StandardScaler
from . _fpl_nn import FPLDataset, MLP

from abc import ABCMeta, abstractmethod

mean_func = lambda x: np.mean(x)
min_func = lambda x: np.min(x)

def validate(model,
             candid,
             X,
             y,
             metric="neg_mean_squared_error",
             best_func=mean_func,
             pre_model_map=None):
    """ cross validate func using scipy metrics"""
    scores = {}   
    if pre_model_map is None:
        pre_model_map = {}
    no_change = lambda * X_y: X_y
    
    for c in candid:
        m = model(c)
        X1, y1 = pre_model_map.get(c, no_change)(X, y)
        scores[c]=cross_validate(m, X1, y1,
                scoring=[metric])[f"test_{metric}"]
    sort_score = sorted([(best_func(v), k) for k, v in scores.items()],
                        reverse=True)
    return {"scores": scores,
            "model": [model(x) for _, x in sort_score],
            "para": [x for _, x in sort_score]
            }


def _generate_submodel(fpl_ins, i):
    class MixinModel(fpl_ins.__class__):
        def __init__(self):
            self.model=[fpl_ins.model[i]]
    return MixinModel()

class SubModelMixin(metaclass=ABCMeta):
    """ sub model mixin """
    def submodel(self, i):
        return _generate_submodel(self, i)

class FplModel(metaclass=ABCMeta):
    """ fpl model class """
    def __init__(self, **kwargs) -> None:
        """ init """
        pass

    def pre_model(self, X, y):
        """ process X y before model fit """
        idx = X.index.intersection(y.index)
        X, y = X.reindex(idx), y.reindex(idx)
        return X, y   

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        self.model.fit(self.X, self.y)

    @abstractmethod
    def run(self, x1):
        """ run testing set """
        pass


class FplRondomForest(FplModel):
    """ random forest class """
    def __init__(self,
                 oob_score=True, n_estimators=1000, 
                 max_features=10,
                 **kwargs):
        self.oob_score = oob_score
        self.n_estimators = n_estimators
        self.max_features = max_features
        self.init_kwargs = kwargs
        self.model = RandomForestRegressor(
                        oob_score = self.oob_score,
                        n_estimators = self.n_estimators,
                        max_features = self.max_features,
                        **self.init_kwargs)

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        self.model.fit(self.X, self.y)
        self.importances = self.model.feature_importances_
        self.importances = pd.Series(
                self.importances, index=self.X.columns)
        self.columns = self.X.columns
    
    def run(self, x1):
        res = self.model.predict(x1)
        return pd.Series(res, 
                         index=x1.index).sort_values(
                                ascending=False)


class FplAdaBoost(FplModel, SubModelMixin):
    """ Ada boost class """
    def __init__(self,
                 X,
                 y,
                 base_estimator=None,
                 n_estimators=1000, 
                 learning_rates=None,
                 loss="linear",
                 top=None,
                 **kwargs):
        self.base_estimator = base_estimator
        self.n_estimators = n_estimators
        self.learning_rates = learning_rates
        self.loss = loss
        self.init_kwargs = kwargs
        self.top = top

        if self.learning_rates is None:
            self.learning_rates = [
                            0.01, 0.05, 0.1, 0.25,
                            0.5, 1.0, 1.5, 2.0, 2.5]
        if top is not None:
            assert len(self.learning_rates) >= top
        
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: AdaBoostRegressor(
                        base_estimator=self.base_estimator,
                        n_estimators=self.n_estimators, 
                        learning_rate=c,
                        loss=self.loss,
                        **self.init_kwargs)
        idx = X.index.intersection(y.index)
        X, y = X.reindex(idx), y.reindex(idx)
        self.valid = validate(
                    model_func,
                    self.learning_rates,
                    X,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)

        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        for i in range(top):
            self.model[i].fit(self.X, self.y)

        self.model = self.model[:top]

        self.importances = [None for i in range(top)]

        for i in range(top):
            importances = self.model[i].feature_importances_
            importances = pd.Series(
                importances, index=self.X.columns)
            self.importances[i] = importances
        self.columns = self.X.columns
    
    def run(self, x1):
        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        res = [pd.Series(model.predict(x1),
                         index=x1.index).sort_values(
                             ascending=False
                         ) for model in self.model[:top]]
        res = pd.concat(res, axis=1).apply(np.mean, axis=1)
        return res


class FplLinearRegress(FplModel):
    """ linear regression class """
    def __init__(self, max_value_ignore=100,
                 **kwargs):
        self.model = LinearRegression()
        self.max_value_ignore = max_value_ignore
    
    def pre_model(self, X, y):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(
            X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)
        return X_norm.reindex(idx), y.reindex(idx)

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        self.model.fit(self.X, self.y)
        self.columns = self.X.columns
        
    def run(self, x1):
        x1_p = self.scaling.transform(x1)
        x1 = pd.DataFrame(x1_p, index=x1.index, columns=x1.columns)
        res = self.model.predict(x1)
        res = pd.Series(res, 
                         index=x1.index).sort_values(
                                ascending=False)
        return res[res<self.max_value_ignore]


class FplLassoRegress(FplModel, SubModelMixin):
    """ lasso regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: Lasso(alpha=c)
        self.valid = validate(
                    model_func,
                    alphas,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]

    def submodel(self, i):
        res = super().submodel(i)
        res.scaling = self.scaling
        return res

    def pre_model(self, X, y):
        X_norm = self.scaling.transform(X)
        X_norm = pd.DataFrame(
            X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)
        return X_norm.reindex(idx), y.reindex(idx)

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        for i in range(top):
            self.model[i].fit(self.X, self.y)

        self.model = self.model[:top]
        
        self.columns = self.X.columns
    
    def run(self, x1):
        x1_p = self.scaling.transform(x1)
        x1 = pd.DataFrame(x1_p, index=x1.index, columns=x1.columns)

        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        res = [pd.Series(model.predict(x1),
                         index=x1.index).sort_values(
                             ascending=False
                         ) for model in self.model[:top]]
        res = pd.concat(res, axis=1).apply(np.mean, axis=1)
        return res


class FplRidgeRegress(FplLassoRegress):
    """ Ridge regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: Ridge(alpha=c)
        self.valid = validate(
                    model_func,
                    alphas,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


class FplElasticNetRegress(FplLassoRegress):
    """ ElasticNet regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        l1_ratios = kwargs.get("l1_ratio",
                        [0, 0.1, 0.2, 0.3, 0.4, 0.5,
                        0.6, 0.7, 0.8, 0.9, 1.0])
        candids = [(a, l1) for a in alphas for l1 in l1_ratios]
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda cand: ElasticNet(alpha=cand[0],
                                            l1_ratio=cand[1])
        self.valid = validate(
                    model_func,
                    candids,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


class FplHuberRegress(FplLassoRegress):
    """ Huber regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.0001, 0.001, 0.01, 0.1])
        epsilons = kwargs.get("epsilons",
                        [1.01, 1.1, 1.35, 1.75, 2.0])
        candids = [(a, ep) for a in alphas for ep in epsilons]
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda cand: HuberRegressor(alpha=cand[0],
                                            epsilon=cand[1])
        self.valid = validate(
                    model_func,
                    candids,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


class FplTweedieRegress(FplLassoRegress):
    """ Tweedie regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        y = y.clip(lower=0.0)

        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        powers = kwargs.get("powers",
                        [1])
        max_iters = kwargs.get("max_iters",
                        [100, 1000])
        candids = [(a, p, m) 
                    for a in alphas 
                        for p in powers
                            for m in max_iters]
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda cand: TweedieRegressor(
                alpha=cand[0], 
                power=cand[1],
                max_iter=cand[2])

        self.valid = validate(
                    model_func,
                    candids,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]

    def pre_model(self, X, y):
        X_norm = self.scaling.transform(X)
        X_norm = pd.DataFrame(
            X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)
        return X_norm.reindex(idx), y.reindex(idx).clip(lower=0.0)


class Fpl3LayerNeural(FplModel, SubModelMixin):
    """ PyTorch 3 layers Neural Network class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        layers = kwargs.get("layers", 
                            [(20, 10), (20, 5)])
        top = kwargs.get("top", None)
        if top is not None:
            assert len(layers) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: Lasso(alpha=c)
        self.valid = validate(
                    model_func,
                    alphas,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]

    def submodel(self, i):
        res = super().submodel(i)
        res.scaling = self.scaling
        return res

    def fit(self, X, y) -> None:
        """ fit model """
        X_norm = self.scaling.transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        for i in range(top):
            self.model[i].fit(X_norm, y)

        self.model = self.model[:top]
        
        self.columns = X_norm.columns
    
    def run(self, x1):
        x1_p = self.scaling.transform(x1)
        x1 = pd.DataFrame(x1_p, index=x1.index, columns=x1.columns)

        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        res = [pd.Series(model.predict(x1),
                         index=x1.index).sort_values(
                             ascending=False
                         ) for model in self.model[:top]]
        res = pd.concat(res, axis=1).apply(np.mean, axis=1)
        return res

class FplCVBestModel(FplModel):
    """ fpl cross validate best model class """
    def __init__(self, X, y, choices, **kwargs):
        
        self.choices = choices
        self.models = {}
        self.pre_model_map = {}
        for k, v in choices.items():
            self.models[k] = v.model
        metric = kwargs.get("metric", "max_error")
        best_func = kwargs.get("best_func", min_func)
        self.subtop = kwargs.get("subtop", 1)
        self.top = kwargs.get("top", 1)
        self.top_func = kwargs.get("top_func", np.max)
        new_key = []

        for k in list(self.models.keys()):
            if isinstance(ml:= self.models[k], list):
                for i in range(min(len(ml), self.subtop)):
                    new_key.append((k, i))
            else:
                new_key.append((k, None))

        assert len(new_key)>=self.top
        def model_func(x):
            if x[1] is None:
                return self.models[x[0]]
            else:
                return self.models[x[0]][x[1]]

        for k1, k2 in new_key:
            
            self.pre_model_map[(k1, k2)] = choices[k1].pre_model    

        #idx = X.index.intersection(y.index)
        #X, y = X.reindex(idx), y.reindex(idx)
        self.valid = validate(
            model_func,
            new_key,
            X,
            y,
            metric=metric,
            best_func=best_func,
            pre_model_map=self.pre_model_map)
        self.model = self.valid["model"]

        for i in range(self.top):
            selected = (self.valid["para"][i]) 
            if selected[1] is None:
                print (selected[0], f" the {i+1}-model selected")
            else:
                print (selected[0], 
                       f"# {selected[1]}" f" the {i+1}-model selected")
        para = self.valid["para"]

        def model_choice(x):
            if x[1] is None:
                return self.choices[x[0]]
            else:
                return self.choices[x[0]].submodel(x[1])
        self.fpl_model = [model_choice(para[i]) for i in range(self.top)]
        print ("# of model for final "+ str(len(self.fpl_model)))

    def fit(self, X, y):
        for i in range(self.top):
            self.fpl_model[i].fit(X, y)

    def run(self, x1):
        res = [model.run(x1) for model in self.fpl_model]
        self.res = res
        res = pd.concat(res, axis=1).apply(self.top_func, axis=1)
        return res
# -*- coding: utf-8 -*-
"""
fpl_analytics._fpl_model

learning model
"""
import pandas as pd
import numpy as np
from sklearn.ensemble import (RandomForestRegressor, AdaBoostRegressor)
from sklearn.linear_model import (LinearRegression, Lasso, Ridge, 
                                  ElasticNet, TweedieRegressor,
                                  HuberRegressor)
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_validate
from sklearn.preprocessing import StandardScaler
from . _fpl_nn import FPLDataset, MLP

from abc import ABCMeta, abstractmethod

mean_func = lambda x: np.mean(x)
min_func = lambda x: np.min(x)

def validate(model,
             candid,
             X,
             y,
             metric="neg_mean_squared_error",
             best_func=mean_func,
             pre_model_map=None):
    """ cross validate func using scipy metrics"""
    scores = {}   
    if pre_model_map is None:
        pre_model_map = {}
    no_change = lambda * X_y: X_y
    
    for c in candid:
        m = model(c)
        X1, y1 = pre_model_map.get(c, no_change)(X, y)
        scores[c]=cross_validate(m, X1, y1,
                scoring=[metric])[f"test_{metric}"]
    sort_score = sorted([(best_func(v), k) for k, v in scores.items()],
                        reverse=True)
    return {"scores": scores,
            "model": [model(x) for _, x in sort_score],
            "para": [x for _, x in sort_score]
            }


def _generate_submodel(fpl_ins, i):
    class MixinModel(fpl_ins.__class__):
        def __init__(self):
            self.model=[fpl_ins.model[i]]
    return MixinModel()

class SubModelMixin(metaclass=ABCMeta):
    """ sub model mixin """
    def submodel(self, i):
        return _generate_submodel(self, i)

class FplModel(metaclass=ABCMeta):
    """ fpl model class """
    def __init__(self, **kwargs) -> None:
        """ init """
        pass

    def pre_model(self, X, y):
        """ process X y before model fit """
        idx = X.index.intersection(y.index)
        X, y = X.reindex(idx), y.reindex(idx)
        return X, y   

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        self.model.fit(self.X, self.y)

    @abstractmethod
    def run(self, x1):
        """ run testing set """
        pass


class FplRondomForest(FplModel):
    """ random forest class """
    def __init__(self,
                 oob_score=True, n_estimators=1000, 
                 max_features=10,
                 **kwargs):
        self.oob_score = oob_score
        self.n_estimators = n_estimators
        self.max_features = max_features
        self.init_kwargs = kwargs
        self.model = RandomForestRegressor(
                        oob_score = self.oob_score,
                        n_estimators = self.n_estimators,
                        max_features = self.max_features,
                        **self.init_kwargs)

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        self.model.fit(self.X, self.y)
        self.importances = self.model.feature_importances_
        self.importances = pd.Series(
                self.importances, index=self.X.columns)
        self.columns = self.X.columns
    
    def run(self, x1):
        res = self.model.predict(x1)
        return pd.Series(res, 
                         index=x1.index).sort_values(
                                ascending=False)


class FplAdaBoost(FplModel, SubModelMixin):
    """ Ada boost class """
    def __init__(self,
                 X,
                 y,
                 base_estimator=None,
                 n_estimators=1000, 
                 learning_rates=None,
                 loss="linear",
                 top=None,
                 **kwargs):
        self.base_estimator = base_estimator
        self.n_estimators = n_estimators
        self.learning_rates = learning_rates
        self.loss = loss
        self.init_kwargs = kwargs
        self.top = top

        if self.learning_rates is None:
            self.learning_rates = [
                            0.01, 0.05, 0.1, 0.25,
                            0.5, 1.0, 1.5, 2.0, 2.5]
        if top is not None:
            assert len(self.learning_rates) >= top
        
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: AdaBoostRegressor(
                        base_estimator=self.base_estimator,
                        n_estimators=self.n_estimators, 
                        learning_rate=c,
                        loss=self.loss,
                        **self.init_kwargs)
        idx = X.index.intersection(y.index)
        X, y = X.reindex(idx), y.reindex(idx)
        self.valid = validate(
                    model_func,
                    self.learning_rates,
                    X,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)

        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        for i in range(top):
            self.model[i].fit(self.X, self.y)

        self.model = self.model[:top]

        self.importances = [None for i in range(top)]

        for i in range(top):
            importances = self.model[i].feature_importances_
            importances = pd.Series(
                importances, index=self.X.columns)
            self.importances[i] = importances
        self.columns = self.X.columns
    
    def run(self, x1):
        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        res = [pd.Series(model.predict(x1),
                         index=x1.index).sort_values(
                             ascending=False
                         ) for model in self.model[:top]]
        res = pd.concat(res, axis=1).apply(np.mean, axis=1)
        return res


class FplLinearRegress(FplModel):
    """ linear regression class """
    def __init__(self, max_value_ignore=100,
                 **kwargs):
        self.model = LinearRegression()
        self.max_value_ignore = max_value_ignore
    
    def pre_model(self, X, y):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(
            X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)
        return X_norm.reindex(idx), y.reindex(idx)

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        self.model.fit(self.X, self.y)
        self.columns = self.X.columns
        
    def run(self, x1):
        x1_p = self.scaling.transform(x1)
        x1 = pd.DataFrame(x1_p, index=x1.index, columns=x1.columns)
        res = self.model.predict(x1)
        res = pd.Series(res, 
                         index=x1.index).sort_values(
                                ascending=False)
        return res[res<self.max_value_ignore]


class FplLassoRegress(FplModel, SubModelMixin):
    """ lasso regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: Lasso(alpha=c)
        self.valid = validate(
                    model_func,
                    alphas,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]

    def submodel(self, i):
        res = super().submodel(i)
        res.scaling = self.scaling
        return res

    def pre_model(self, X, y):
        X_norm = self.scaling.transform(X)
        X_norm = pd.DataFrame(
            X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)
        return X_norm.reindex(idx), y.reindex(idx)

    def fit(self, X, y) -> None:
        """ fit model """
        self.X, self.y = self.pre_model(X, y)
        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        for i in range(top):
            self.model[i].fit(self.X, self.y)

        self.model = self.model[:top]
        
        self.columns = self.X.columns
    
    def run(self, x1):
        x1_p = self.scaling.transform(x1)
        x1 = pd.DataFrame(x1_p, index=x1.index, columns=x1.columns)

        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        res = [pd.Series(model.predict(x1),
                         index=x1.index).sort_values(
                             ascending=False
                         ) for model in self.model[:top]]
        res = pd.concat(res, axis=1).apply(np.mean, axis=1)
        return res


class FplRidgeRegress(FplLassoRegress):
    """ Ridge regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: Ridge(alpha=c)
        self.valid = validate(
                    model_func,
                    alphas,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


class FplElasticNetRegress(FplLassoRegress):
    """ ElasticNet regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        l1_ratios = kwargs.get("l1_ratio",
                        [0, 0.1, 0.2, 0.3, 0.4, 0.5,
                        0.6, 0.7, 0.8, 0.9, 1.0])
        candids = [(a, l1) for a in alphas for l1 in l1_ratios]
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda cand: ElasticNet(alpha=cand[0],
                                            l1_ratio=cand[1])
        self.valid = validate(
                    model_func,
                    candids,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


class FplHuberRegress(FplLassoRegress):
    """ Huber regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        alphas = kwargs.get("alphas", 
                            [0, 0.0001, 0.001, 0.01, 0.1])
        epsilons = kwargs.get("epsilons",
                        [1.01, 1.1, 1.35, 1.75, 2.0])
        candids = [(a, ep) for a in alphas for ep in epsilons]
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda cand: HuberRegressor(alpha=cand[0],
                                            epsilon=cand[1])
        self.valid = validate(
                    model_func,
                    candids,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]


class FplTweedieRegress(FplLassoRegress):
    """ Tweedie regression class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        y = y.clip(lower=0.0)

        alphas = kwargs.get("alphas", 
                            [0, 0.1, 0.2, 0.3, 0.4, 
                             0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
        powers = kwargs.get("powers",
                        [1])
        max_iters = kwargs.get("max_iters",
                        [100, 1000])
        candids = [(a, p, m) 
                    for a in alphas 
                        for p in powers
                            for m in max_iters]
        top = kwargs.get("top", None)
        if top is not None:
            assert len(alphas) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda cand: TweedieRegressor(
                alpha=cand[0], 
                power=cand[1],
                max_iter=cand[2])

        self.valid = validate(
                    model_func,
                    candids,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]

    def pre_model(self, X, y):
        X_norm = self.scaling.transform(X)
        X_norm = pd.DataFrame(
            X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)
        return X_norm.reindex(idx), y.reindex(idx).clip(lower=0.0)


class Fpl3LayerNeural(FplModel, SubModelMixin):
    """ PyTorch 3 layers Neural Network class """
    def __init__(self, X, y,
                 **kwargs):
        self.scaling = StandardScaler(with_std=True)
        X_norm = self.scaling.fit_transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        layers = kwargs.get("layers", 
                            [(20, 10), (20, 5)])
        top = kwargs.get("top", None)
        if top is not None:
            assert len(layers) >= top
        self.top = top
        best_func = kwargs.get("best_func", min_func)
        model_func = lambda c: Lasso(alpha=c)
        self.valid = validate(
                    model_func,
                    alphas,
                    X_norm,
                    y,
                    metric="max_error",
                    best_func=best_func)

        self.model = self.valid["model"]

    def submodel(self, i):
        res = super().submodel(i)
        res.scaling = self.scaling
        return res

    def fit(self, X, y) -> None:
        """ fit model """
        X_norm = self.scaling.transform(X)
        X_norm = pd.DataFrame(X_norm, index=X.index, columns=X.columns)
        
        idx = X_norm.index.intersection(y.index)

        X_norm, y = X_norm.reindex(idx), y.reindex(idx)
        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        for i in range(top):
            self.model[i].fit(X_norm, y)

        self.model = self.model[:top]
        
        self.columns = X_norm.columns
    
    def run(self, x1):
        x1_p = self.scaling.transform(x1)
        x1 = pd.DataFrame(x1_p, index=x1.index, columns=x1.columns)

        top = len(self.model) if (not hasattr(self, "top") or self.top
                                  is None) else self.top

        res = [pd.Series(model.predict(x1),
                         index=x1.index).sort_values(
                             ascending=False
                         ) for model in self.model[:top]]
        res = pd.concat(res, axis=1).apply(np.mean, axis=1)
        return res

class FplCVBestModel(FplModel):
    """ fpl cross validate best model class """
    def __init__(self, X, y, choices, **kwargs):
        
        self.choices = choices
        self.models = {}
        self.pre_model_map = {}
        for k, v in choices.items():
            self.models[k] = v.model
        metric = kwargs.get("metric", "max_error")
        best_func = kwargs.get("best_func", min_func)
        self.subtop = kwargs.get("subtop", 1)
        self.top = kwargs.get("top", 1)
        self.top_func = kwargs.get("top_func", np.max)
        new_key = []

        for k in list(self.models.keys()):
            if isinstance(ml:= self.models[k], list):
                for i in range(min(len(ml), self.subtop)):
                    new_key.append((k, i))
            else:
                new_key.append((k, None))

        assert len(new_key)>=self.top
        def model_func(x):
            if x[1] is None:
                return self.models[x[0]]
            else:
                return self.models[x[0]][x[1]]

        for k1, k2 in new_key:
            
            self.pre_model_map[(k1, k2)] = choices[k1].pre_model    

        #idx = X.index.intersection(y.index)
        #X, y = X.reindex(idx), y.reindex(idx)
        self.valid = validate(
            model_func,
            new_key,
            X,
            y,
            metric=metric,
            best_func=best_func,
            pre_model_map=self.pre_model_map)
        self.model = self.valid["model"]

        for i in range(self.top):
            selected = (self.valid["para"][i]) 
            if selected[1] is None:
                print (selected[0], f" the {i+1}-model selected")
            else:
                print (selected[0], 
                       f"# {selected[1]}" f" the {i+1}-model selected")
        para = self.valid["para"]

        def model_choice(x):
            if x[1] is None:
                return self.choices[x[0]]
            else:
                return self.choices[x[0]].submodel(x[1])
        self.fpl_model = [model_choice(para[i]) for i in range(self.top)]
        print ("# of model for final "+ str(len(self.fpl_model)))

    def fit(self, X, y):
        for i in range(self.top):
            self.fpl_model[i].fit(X, y)

    def run(self, x1):
        res = [model.run(x1) for model in self.fpl_model]
        self.res = res
        res = pd.concat(res, axis=1).apply(self.top_func, axis=1)
        return res
# -*- coding: utf-8 -*- fpl_analytics._fpl_model  learning model cross validate func using scipy metrics sub model mixin fpl model class init process X y before model fit fit model run testing set random forest class fit model Ada boost class fit model linear regression class fit model lasso regression class fit model Ridge regression class ElasticNet regression class Huber regression class Tweedie regression class PyTorch 3 layers Neural Network class fit model fpl cross validate best model class #idx = X.index.intersection(y.index) #X, y = X.reindex(idx), y.reindex(idx)