{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Compare the effect of different scalers on data with outliers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importing libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib import cm\n",
    "\n",
    "from sklearn.preprocessing import MinMaxScaler\n",
    "from sklearn.preprocessing import minmax_scale\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.preprocessing import RobustScaler\n",
    "from sklearn.preprocessing import MaxAbsScaler\n",
    "from sklearn.preprocessing import Normalizer\n",
    "from sklearn.preprocessing import QuantileTransformer\n",
    "from sklearn.preprocessing import PowerTransformer\n",
    "\n",
    "from sklearn.datasets import fetch_california_housing\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Data prepration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Automatically created module for IPython interactive environment\n"
     ]
    }
   ],
   "source": [
    "print(__doc__)\n",
    "\n",
    "dataset = fetch_california_housing()\n",
    "X_full , y_full = dataset.data , dataset.target\n",
    "\n",
    "# Take only 2 features to make visualization easier\n",
    "# Feature 0 has a long tail distribution.\n",
    "# Feature 5 has a few but very large outliers.\n",
    "\n",
    "X = X_full[: , [0,5]]\n",
    "\n",
    "distributions = [\n",
    "    ('Original Data' , X) ,\n",
    "    ('Data after StandardScaler' , StandardScaler().fit_transform(X)),\n",
    "    ('Data after MinMaxScaler' , MinMaxScaler().fit_transform(X)),\n",
    "    ('Data after MaxAbsScaler' , MaxAbsScaler().fit_transform(X)),\n",
    "    ('Data after RoburstScaler', RobustScaler(quantile_range=(25, 75)).fit_transform(X)),\n",
    "    ('Data after PowerTransformer (yeo-johnson)' , PowerTransformer(method='yeo-johnson').fit_transform(X)),\n",
    "    ('Data after PowerTransformer (box-cox)' , PowerTransformer(method='box-cox').fit_transform(X)),\n",
    "    ('Data after QuantileTransformer (gaussian pdf)' ,QuantileTransformer(output_distribution='normal').fit_transform(X)),\n",
    "    ('Data after QuatileTransformer (uniform pdf)' , QuantileTransformer(output_distribution='uniform').fit_transform(X)),\n",
    "    ('Data after Normalizer' , Normalizer().fit_transform(X)),\n",
    "]\n",
    "\n",
    "# Scale the output between 0 and 1 for colorbar\n",
    "y = minmax_scale(y_full)\n",
    "\n",
    "cmap = getattr(cm , 'plasma_r' , cm.afmhot_r)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Create a function for create_axes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_axes(title , figsize=(16 , 6)):\n",
    "    fig = plt.figure(figsize=figsize)\n",
    "    fig.suptitle(title)\n",
    "    \n",
    "    ## Define the axes for the first plot\n",
    "    left , width = 0.1 , 0.22\n",
    "    bottom , height = 0.1 , 0.7\n",
    "    bottom_h = height + 0.15\n",
    "    left_h = left + width + 0.02\n",
    "    \n",
    "    rect_scatter = [left , bottom , width , height]\n",
    "    rect_histx = [left , bottom_h , width , 0.1]\n",
    "    rect_histy = [left_h , bottom , 0.05 , height]\n",
    "    \n",
    "    ax_scatter = plt.axes(rect_scatter)\n",
    "    ax_histx = plt.axes(rect_histx)\n",
    "    ax_histy = plt.axes(rect_histy)\n",
    "    \n",
    "    ## Define the axes for the zoomed in plot\n",
    "    left = left + width + 0.2\n",
    "    left_h = left + width + 0.02\n",
    "    \n",
    "    rect_scatter = [left, bottom, width, height]\n",
    "    rect_histx = [left, bottom_h, width, 0.1]\n",
    "    rect_histy = [left_h, bottom, 0.05, height]\n",
    "\n",
    "    ax_scatter_zoom = plt.axes(rect_scatter)\n",
    "    ax_histx_zoom = plt.axes(rect_histx)\n",
    "    ax_histy_zoom = plt.axes(rect_histy)\n",
    "    \n",
    "    ## Define axes for the colorbar\n",
    "    left , width = width + left + 0.13 , 0.01\n",
    "    \n",
    "    rect_colorbar = [left , bottom , width , height]\n",
    "    ax_colorbar = plt.axes(rect_colorbar)\n",
    "    \n",
    "    return ((ax_scatter , ax_histx , ax_histy) ,\n",
    "           (ax_scatter_zoom , ax_histx_zoom , ax_histy_zoom) ,\n",
    "           ax_colorbar)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Define function to plot distribution of the plots"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_distribution(axes , X , y, hist_nbins=50 , title='' , x0_label='' , x1_label=''):\n",
    "    ax , hist_X1 , hist_X0 = axes\n",
    "    \n",
    "    ax.set_title(title)\n",
    "    ax.set_xlabel(x0_label)\n",
    "    ax.set_ylabel(x1_label)\n",
    "    \n",
    "    colors = cmap(y)\n",
    "    ax.scatter(X[:,0] , X[:,1] ,alpha=0.5, marker='o', s=5, lw=0, c=colors)\n",
    "    \n",
    "    # Removing the top and the right spine for aesthetics\n",
    "    # make nice axis layout\n",
    "    ax.spines['top'].set_visible(False)\n",
    "    ax.spines['right'].set_visible(False)\n",
    "    \n",
    "    ax.get_xaxis().tick_bottom()\n",
    "    ax.get_yaxis().tick_left()\n",
    "    \n",
    "    ax.spines['left'].set_position(('outward', 10))\n",
    "    ax.spines['bottom'].set_position(('outward', 10))\n",
    "    \n",
    "    # Histogram for axis X1 (feature 5)\n",
    "    hist_X1.set_ylim(ax.get_ylim())\n",
    "    hist_X1.hist(X[:, 1], bins=hist_nbins, orientation='horizontal',\n",
    "                 color='grey', ec='grey')\n",
    "    hist_X1.axis('off')\n",
    "\n",
    "    # Histogram for axis X0 (feature 0)\n",
    "    hist_X0.set_xlim(ax.get_xlim())\n",
    "    hist_X0.hist(X[:, 0], bins=hist_nbins, orientation='vertical',\n",
    "                 color='grey', ec='grey')\n",
    "    hist_X0.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## define make_plot function to plot both plots"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_plot(item_idx):\n",
    "    title , x = distributions[item_idx]\n",
    "    ax_zoom_out , ax_zoom_in , ax_colorbar = create_axes(title)\n",
    "    axarr = (ax_zoom_out , ax_zoom_in)\n",
    "    \n",
    "    plot_distribution(axarr[0] ,X , y ,\n",
    "                      title='Full Data' , \n",
    "                      hist_nbins=200 ,\n",
    "                      x0_label='Median Income',\n",
    "                      x1_label= 'Number of Households'\n",
    "                     )\n",
    "    \n",
    "    ## Zoom in \n",
    "    zoom_in_percentile_range = (0,99)\n",
    "    cutoffs_X0 = np.percentile(X[:, 0], zoom_in_percentile_range)\n",
    "    cutoffs_X1 = np.percentile(X[:, 1], zoom_in_percentile_range)\n",
    "\n",
    "    non_outliers_mask = (\n",
    "        np.all(X > [cutoffs_X0[0], cutoffs_X1[0]], axis=1) &\n",
    "        np.all(X < [cutoffs_X0[1], cutoffs_X1[1]], axis=1))\n",
    "    \n",
    "    plot_distribution(axarr[1], X[non_outliers_mask], y[non_outliers_mask],\n",
    "                      hist_nbins=50,\n",
    "                      x0_label=\"Median Income\",\n",
    "                      x1_label=\"Number of households\",\n",
    "                      title=\"Zoom-in\")\n",
    "    \n",
    "    \n",
    "    norm = mpl.colors.Normalize(y_full.min(), y_full.max())\n",
    "    mpl.colorbar.ColorbarBase(ax_colorbar, cmap=cmap,\n",
    "                              norm=norm, orientation='vertical',\n",
    "                              label='Color mapping for values of y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Original data\n",
    "Each transformation is plotted showing two transformed features, with the left plot showing the entire dataset, and the right zoomed-in to show the dataset without the marginal outliers. A large majority of the samples are compacted to a specific range, [0, 10] for the median income and [0, 6] for the number of households. Note that there are some marginal outliers (some blocks have more than 1200 households). Therefore, a specific pre-processing can be very beneficial depending of the application. In the following, we present some insights and behaviors of those pre-processing methods in the presence of marginal outliers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 166192 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### StandardScaler\n",
    "StandardScaler removes the mean and scales the data to unit variance. However, the outliers have an influence when computing the empirical mean and standard deviation which shrink the range of the feature values as shown in the left figure below. Note in particular that because the outliers on each feature have different magnitudes, the spread of the transformed data on each feature is very different: most of the data lie in the [-2, 4] range for the transformed median income feature while the same data is squeezed in the smaller [-0.2, 0.2] range for the transformed number of households.\n",
    "\n",
    "StandardScaler therefore cannot guarantee balanced feature scales in the presence of outliers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 167332 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## MinMaxScaler\n",
    "MinMaxScaler rescales the data set such that all feature values are in the range [0, 1] as shown in the right panel below. However, this scaling compress all inliers in the narrow range [0, 0.005] for the transformed number of households.\n",
    "\n",
    "As StandardScaler, MinMaxScaler is very sensitive to the presence of outliers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 167220 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## MaxAbsScaler\n",
    "MaxAbsScaler differs from the previous scaler such that the absolute values are mapped in the range [0, 1]. On positive only data, this scaler behaves similarly to MinMaxScaler and therefore also suffers from the presence of large outliers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 167248 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## RobustScaler¶\n",
    "Unlike the previous scalers, the centering and scaling statistics of this scaler are based on percentiles and are therefore not influenced by a few number of very large marginal outliers. Consequently, the resulting range of the transformed feature values is larger than for the previous scalers and, more importantly, are approximately similar: for both features most of the transformed values lie in a [-2, 3] range as seen in the zoomed-in figure. Note that the outliers themselves are still present in the transformed data. If a separate outlier clipping is desirable, a non-linear transformation is required"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 166984 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## PowerTransformer¶\n",
    "PowerTransformer applies a power transformation to each feature to make the data more Gaussian-like. Currently, PowerTransformer implements the Yeo-Johnson and Box-Cox transforms. The power transform finds the optimal scaling factor to stabilize variance and mimimize skewness through maximum likelihood estimation. By default, PowerTransformer also applies zero-mean, unit variance normalization to the transformed output. Note that Box-Cox can only be applied to strictly positive data. Income and number of households happen to be strictly positive, but if negative values are present the Yeo-Johnson transformed is to be preferred."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 168500 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 168364 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## QuantileTransformer (Gaussian output)¶\n",
    "QuantileTransformer has an additional output_distribution parameter allowing to match a Gaussian distribution instead of a uniform distribution. Note that this non-parametetric transformer introduces saturation artifacts for extreme values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 168852 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## QuantileTransformer (uniform output)¶\n",
    "QuantileTransformer applies a non-linear transformation such that the probability density function of each feature will be mapped to a uniform distribution. In this case, all the data will be mapped in the range [0, 1], even the outliers which cannot be distinguished anymore from the inliers.\n",
    "\n",
    "As RobustScaler, QuantileTransformer is robust to outliers in the sense that adding or removing outliers in the training set will yield approximately the same transformation on held out data. But contrary to RobustScaler, QuantileTransformer will also automatically collapse any outlier by setting them to the a priori defined range boundaries (0 and 1)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 168472 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Normalizer¶\n",
    "The Normalizer rescales the vector for each sample to have unit norm, independently of the distribution of the samples. It can be seen on both figures below where all samples are mapped onto the unit circle. In our example the two selected features have only positive values; therefore the transformed data only lie in the positive quadrant. This would not be the case if some original features had a mix of positive and negative values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 166744 characters]\n",
      "text/plain": [
       "<Figure size 1152x432 with 7 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "make_plot(9)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
