# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3.8.8 ('base')
#     language: python
#     name: python3
# ---

# # Python Learning Sessions: Feature Engineering

# ## Basics

# ### Pipelines
# Piplines are a way to combine multiple steps of preprocessing into a single step.
# Pandas has a built-in function for creating pipelines. The method can be called by a DataFrame.pipe() method. You can pass a function to the pipe() method and chain multiple transformations together by calling the pipe() method after another.
#
# SKLearn has a built-in pipeline class that sequentially applies each step of the pipeline. SKLearn also has ColumnTransformer class that can be used to combine multiple steps of preprocessing into a single step. These two classes can be used together to provide a more flexible way to combine multiple steps of preprocessing into a single step.

# #### Pandas Pipeline
# ---

# When applying a series of transformations to a dataframe, we usually call it like this:
#
# ```python
# df = h(df)
# df = g(df, arg1=a)
# df = func(df, arg2=b, arg3=c)
# ```
#
# Or we can nest the transformations using functions:
# ```python
# df = func(g(h(df), arg1=a), arg2=b, arg3=c)  
# ```
#
# Or we can use the pipe() method to apply a series of transformations to a dataframe:
#
# ```python
# (df.pipe(h)
#    .pipe(g, arg1=a)
#    .pipe(func, arg2=b, arg3=c)
# )
# ```

# You can define a pipeline operator as a function that takes a dataframe and returns a dataframe.

# +
import pandas as pd
import numpy as np

link = 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/taxis.csv'
df = pd.read_csv(link)
df


# +
def get_unique_routes(df):
    """
    Get the number of unique routes
    
    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame with the taxi data

    Returns
    -------
    df : pandas.DataFrame
        DataFrame with the taxi data with the number of unique routes

    """
    df = df.groupby(by=['pickup_borough', 'dropoff_borough']).agg({
        'distance': 'mean',
        'fare': 'mean',
        'tip': 'mean',
        'tolls': 'mean',
        'payment': pd.Series.mode,
        'color': pd.Series.mode,
    })
    df.reset_index(inplace=True)
    df['route'] = df.pickup_borough + '-' + df.dropoff_borough
    df.drop(['pickup_borough', 'dropoff_borough'], axis=1, inplace=True)
    return df


def bin_distance(df, bins):
    """
    Bin the distance column

    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame with the taxi data
    bins : list
        List of bins to use

    Returns
    -------
    df : pandas.DataFrame
        DataFrame with the taxi data with the distance column binned

    """
    df['distance_bin'] = pd.cut(df.distance, bins)
    df.drop(['distance'], axis=1, inplace=True)
    return df


def get_tip_toll_percentage(df):
    """
    Get the percentage of tip and tolls
    
    Parameters
    ----------
    df : pandas.DataFrame
        DataFrame with the taxi data
        
    Returns
    -------
    df : pandas.DataFrame
        DataFrame with the taxi data with the percentage of tip and tolls

    """
    df['ave_tip_percentage'] = df.tip / (df.fare)
    df['ave_toll_percentage'] = df.tolls / (df.fare)
    df.drop(['tip', 'tolls'], axis=1, inplace=True)
    return df


# -

df.pipe(get_unique_routes)\
  .pipe(bin_distance, bins=np.arange(0, 100, 5))\
  .pipe(get_tip_toll_percentage)

# #### SKLearn Pipeline, FeatureUnion, ColumnTransformer
# * `Pipeline`: chaining estimators
# * `FeatureUnion`: composite feature spaces
# * `ColumnTransformer` for heterogeneous data
# * `TransformedTargetRegressor`: transforming target in regression

# +
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer, TransformedTargetRegressor
from sklearn.preprocessing import (
    OneHotEncoder, StandardScaler, QuantileTransformer, MinMaxScaler
)
from sklearn.linear_model import LinearRegression
from sklearn.feature_selection import SelectKBest, VarianceThreshold
from sklearn.feature_extraction.text import CountVectorizer
from sklearn import set_config

numeric_preprocessor = Pipeline(
    steps=[
        ('quasi_constant_remover', VarianceThreshold(threshold=0.0)),
        (
            "imputation_mean",
            SimpleImputer(missing_values=np.nan, strategy="mean")
        ),
        ("scaler", StandardScaler()),
    ]
)

categorical_preprocessor = Pipeline(
    steps=[
        (
            "imputation_constant", 
            SimpleImputer(fill_value="missing", strategy="constant"),
        ),
        ("onehot", OneHotEncoder(handle_unknown="ignore")),
    ]
)

# Text preprocessing
#------------------------------------------------------------------------------
from sklearn.base import BaseEstimator, TransformerMixin

class AverageWordLengthExtractor(BaseEstimator, TransformerMixin):
    """Takes in dataframe, outputs average word length"""

    def __init__(self):
        pass

    def average_word_length(self, name):
        """Helper code to compute average word length of a name"""
        return np.mean([len(word) for word in name.split()])

    def transform(self, df, y=None):
        """The workhorse of this feature extractor"""
        return df['road_name'].apply(self.average_word_length)

    def fit(self, df, y=None):
        """Returns `self` unless something different happens in train, test"""
        return self

best_word_selector = Pipeline(
    steps=[
        ('bow', CountVectorizer()),
        ('select_one', SelectKBest(k=1))
    ]
)

text_preprocessor = FeatureUnion(
    transformer_list=[
        ('best_word', best_word_selector),
        ('ave', AverageWordLengthExtractor())
    ]
)

preprocessor = ColumnTransformer(
    [
        ('keys', 'drop', ['cif', 'mastercif', 'acct_num']),
        ("categorical", categorical_preprocessor, ["state", "gender"]),
        ("numerical", numeric_preprocessor, ["age", "weight"]),
        ('text', text_preprocessor, ['feedback']),
    ]
)

qt_transformer = QuantileTransformer(output_distribution='normal')
linreg = LinearRegression(n_jobs=-1)
regr = TransformedTargetRegressor(
    regressor=linreg,
    transformer=qt_transformer)

clf = GBMClassifier()

pipe = make_pipeline(preprocessor, clf)
# -

set_config(display="diagram")
pipe  # click on the diagram below to see the details of each step

pipe.get_params()

# ### Advantages of Pipelines

# #### Avoid target leakage

df.loc[lambda x: x.isna().any(axis=1), :]

df.fillna(df.mode().iloc[0])\
  .loc[df.isna().any(axis=1), :]

df.fillna(df.iloc[:4000].mode().iloc[0])\
  .loc[df.isna().any(axis=1), :]

# #### Grid Search and cross-validation with pipelines is more efficient

from sklearn.model_selection import GridSearchCV
gs = GridSearchCV(
    estimator=pipe,
    param_grid={
        'text__ngram__ngram__ngram_range': [(1, 1), (1, 2), (1, 3)],
        'text__ave__ave_word_length': [1, 2, 3],
        'regr__regressor__alpha': [0.1, 0.01, 0.001],
        'regr__regressor__max_iter': [100, 1000, 10000],
        'regr__regressor__normalize': [True, False],
        'regr__regressor__fit_intercept': [True, False],
        'columntransformer__numerical__scaler':[
            MinMaxScaler(), StandardScaler(), None, 
        ]
    },
)
gs

# # References
# * [6.1. Pipelines and composite estimators](https://scikit-learn.org/stable/modules/compose.html)

StarcoderdataJupyterScriptsDedupFiltered