Repository: jeongyoonlee/Kaggler (language: python)

Issue:
AutoLGB tune is example is not working
Hello, I tried to run the AutoLGB on Kaggle notebook,  I downloaded with pip install:

`!pip install kaggler`

Kaggle Version: '0.9.13'

After that I used the following code snippet and got an error, the same error happened with another dataset.

```
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from kaggler.metrics import auc
from kaggler.model import AutoLGB


RANDOM_SEED = 42
N_OBS = 10000
N_FEATURE = 100
N_IMP_FEATURE = 20

X, y = make_classification(n_samples=N_OBS,
                            n_features=N_FEATURE,
                            n_informative=N_IMP_FEATURE,
                            random_state=RANDOM_SEED)
X = pd.DataFrame(X, columns=['x{}'.format(i) for i in range(X.shape[1])])
y = pd.Series(y)

X_trn, X_tst, y_trn, y_tst = train_test_split(X, y,
                                                test_size=.2,
                                                random_state=RANDOM_SEED)

model = AutoLGB(objective='binary', metric='auc')
model.tune(X_trn, y_trn)
model.fit(X_trn, y_trn)
p = model.predict(X_tst)
print('AUC: {:.4f}'.format(auc(y_tst, p)))
```

![image](https://user-images.githubusercontent.com/14017613/129496123-8b38cd35-1d58-4b84-b08a-f942e361599a.png)

AutoLGB tune is example is not working
Hello, I tried to run the AutoLGB on Kaggle notebook,  I downloaded with pip install:

`!pip install kaggler`

Kaggle Version: '0.9.13'

After that I used the following code snippet and got an error, the same error happened with another dataset.

```
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from kaggler.metrics import auc
from kaggler.model import AutoLGB


RANDOM_SEED = 42
N_OBS = 10000
N_FEATURE = 100
N_IMP_FEATURE = 20

X, y = make_classification(n_samples=N_OBS,
                            n_features=N_FEATURE,
                            n_informative=N_IMP_FEATURE,
                            random_state=RANDOM_SEED)
X = pd.DataFrame(X, columns=['x{}'.format(i) for i in range(X.shape[1])])
y = pd.Series(y)

X_trn, X_tst, y_trn, y_tst = train_test_split(X, y,
                                                test_size=.2,
                                                random_state=RANDOM_SEED)

model = AutoLGB(objective='binary', metric='auc')
model.tune(X_trn, y_trn)
model.fit(X_trn, y_trn)
p = model.predict(X_tst)
print('AUC: {:.4f}'.format(auc(y_tst, p)))
```

![image](https://user-images.githubusercontent.com/14017613/129496123-8b38cd35-1d58-4b84-b08a-f942e361599a.png)



Discussion / hints:

Fix patch:
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 10f0adc..670ee0e 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -18,6 +18,6 @@ formats: all
 
 # Optionally set the version of Python and requirements required to build your docs
 python:
-    version: 3.7
+    version: 3.9
     install:
        - requirements: docs/requirements.txt
diff --git a/examples/automl_example.ipynb b/examples/automl_example.ipynb
new file mode 100644
index 0000000..5b29324
--- /dev/null
+++ b/examples/automl_example.ipynb
@@ -0,0 +1,530 @@
+{
+ "cells": [
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "14bb155c",
+   "metadata": {
+    "nterop": {
+     "id": "14"
+    }
+   },
+   "source": [
+    "# AutoML Example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "910dca6c",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:47.431921Z",
+     "start_time": "2022-03-06T05:32:47.395390Z"
+    },
+    "nterop": {
+     "id": "1"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "%reload_ext autoreload\n",
+    "%autoreload 2\n",
+    "%matplotlib inline"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "a67cbbc9",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:48.645109Z",
+     "start_time": "2022-03-06T05:32:47.433620Z"
+    },
+    "nterop": {
+     "id": "2"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib\n",
+    "from matplotlib import pyplot as plt\n",
+    "import numpy as np\n",
+    "import pandas as pd\n",
+    "import sklearn\n",
+    "from sklearn.datasets import make_classification, make_regression\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from warnings import simplefilter\n",
+    "\n",
+    "import kaggler\n",
+    "from kaggler.metrics import auc, mae, rmse, plot_curve\n",
+    "from kaggler.model import AutoLGB, AutoXGB"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "afc0012a",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:48.696082Z",
+     "start_time": "2022-03-06T05:32:48.647183Z"
+    },
+    "nterop": {
+     "id": "3"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "matplotlib: 3.5.1\n",
+      "     numpy: 1.21.5\n",
+      "    pandas: 1.2.4\n",
+      "   sklearn: 0.24.2\n",
+      "   kaggler: 0.9.13\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f\"matplotlib: {matplotlib.__version__}\")\n",
+    "print(f\"     numpy: {np.__version__}\")\n",
+    "print(f\"    pandas: {pd.__version__}\")\n",
+    "print(f\"   sklearn: {sklearn.__version__}\")\n",
+    "print(f\"   kaggler: {kaggler.__version__}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "d2f5f4e8",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:48.858487Z",
+     "start_time": "2022-03-06T05:32:48.697869Z"
+    },
+    "nterop": {
+     "id": "4"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "plt.style.use(\"fivethirtyeight\")\n",
+    "simplefilter(\"ignore\")\n",
+    "\n",
+    "RANDOM_SEED = 42\n",
+    "N_OBS = 1000\n",
+    "N_FEATURE = 10\n",
+    "N_IMP_FEATURE = 2"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "fcd3dfe2",
+   "metadata": {
+    "nterop": {
+     "id": "15"
+    }
+   },
+   "source": [
+    "## Classification with AutoLGB"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "4f8d4a80",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:49.009941Z",
+     "start_time": "2022-03-06T05:32:48.860131Z"
+    },
+    "nterop": {
+     "id": "5"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "X dim: (1000, 10), y dim: (1000,)\n"
+     ]
+    }
+   ],
+   "source": [
+    "X, y = make_classification(\n",
+    "    n_samples=N_OBS,\n",
+    "    n_features=N_FEATURE,\n",
+    "    n_informative=N_IMP_FEATURE,\n",
+    "    random_state=RANDOM_SEED,\n",
+    ")\n",
+    "X = pd.DataFrame(X, columns=[\"x{}\".format(i) for i in range(X.shape[1])])\n",
+    "y = pd.Series(y)\n",
+    "print(f\"X dim: {X.shape}, y dim: {y.shape}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "3b3374d9",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:49.226779Z",
+     "start_time": "2022-03-06T05:32:49.011516Z"
+    },
+    "nterop": {
+     "id": "6"
+    }
+   },
+   "outputs": [],
+   "source": [
+    "X_trn, X_tst, y_trn, y_tst = train_test_split(\n",
+    "    X, y, test_size=0.2, random_state=RANDOM_SEED\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "de9b786a",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:55.041113Z",
+     "start_time": "2022-03-06T05:32:49.228411Z"
+    },
+    "nterop": {
+     "id": "7"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "100%|██████████| 10/10 [00:00<00:00, 10.97trial/s, best loss: -0.9498742928975488]\n",
+      "100%|██████████| 100/100 [00:04<00:00, 21.28trial/s, best loss: -0.95953125]\n",
+      "AUC (LGB): 0.9437\n"
+     ]
+    }
+   ],
+   "source": [
+    "model = AutoLGB(objective=\"binary\", metric=\"auc\")\n",
+    "model.tune(X_trn, y_trn)\n",
+    "model.fit(X_trn, y_trn)\n",
+    "p = model.predict(X_tst)\n",
+    "print(f\"AUC (LGB): {auc(y_tst, p):.4f}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "90842dd7",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:55.106533Z",
+     "start_time": "2022-03-06T05:32:55.043396Z"
+    },
+    "nterop": {
+     "id": "8"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "10 features out of 10 features selected: ['x6', 'x8', 'x0', 'x2', 'x9', 'x5', 'x3', 'x7', 'x4', 'x1']\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f\"{len(model.features)} features out of {N_FEATURE} features selected: {model.features}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "95a76fb5",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:55.405977Z",
+     "start_time": "2022-03-06T05:32:55.109183Z"
+    },
+    "nterop": {
+     "id": "9"
+    }
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'bagging_freq': 1,\n",
+       " 'verbosity': -1,\n",
+       " 'seed': 42,\n",
+       " 'num_threads': -1,\n",
+       " 'feature_pre_filter': False,\n",
+       " 'objective': 'binary',\n",
+       " 'metric': 'auc',\n",
+       " 'boosting': 'gbdt',\n",
+       " 'bagging_fraction': 0.9,\n",
+       " 'feature_fraction': 0.9,\n",
+       " 'lambda_l1': 0.1,\n",
+       " 'lambda_l2': 1,\n",
+       " 'learning_rate': 0.11371927176320622,\n",
+       " 'max_depth': -1,\n",
+       " 'min_child_samples': 25,\n",
+       " 'num_leaves': 255}"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {
+      "nterop": {
+       "id": "16"
+      }
+     },
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model.params"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "640fa2f7",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:55.831369Z",
+     "start_time": "2022-03-06T05:32:55.407892Z"
+    },
+    "nterop": {
+     "id": "11"
+    }
+   },
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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3nlH77zzjtFRAACAAW6rzDh9+rRWrFih5cuX68cff5T096Vax44dq/bt26tIkSJ2CQkA/3b48AVFRCzVqVNJmWNz5vyqBx4oqp497zMwGQAAAAB7y3GZ0apVK+3cuVMWi0W1atXS66+/rg4dOig0NNSe+QAgi4MHExQRsURnz161Ge/YsZK6datiUCoA9jJ27FiZTCYNHz5cZrNZY8eOveVzTCaTRowY4YB0AADACDkuMxITE/XCCy+oY8eOKlu2rD0zAcAN/fbbObVrt1TnztleQalr1yqaMqWF3N3veCsgAHnUu+++K5PJpGHDhsnT01PvvvvuLZ9DmQEAQP6W4zIju8ufAYAj7dsXp8jIZUpISLYZf/zxapo0qfkNr2YCwLlduHDhprcBAIDrYeUPwCns2XNGERFLsxQZfftW14cfPkyRAQAAALiQGx6ZUaNGDZnNZv3www/y8PBQjRo1ZDLd/AoBJpNJe/fuze2MAFzcrl2n1KnTCl26lGozHhVVS+++2/iWP5sA5C8pKSm6evWqChUqlDl2/vx5ff7557p48aLatWunOnXqGJgQAADY2w3LjAYNGshkMslsNtvcBgBH2rr1hLp2jVZSUprN+FNP3a833mjIzyXABT399NM6cOCANm7cKEm6evWqHn74YR09elSSNHXqVK1cuVJ169Y1MCUAALCnG5YZ06ZNu+ltALC3jRuPq1u3aF29mm4zPnx4mF56qR5FBuCitm3bph49emTeXrJkiY4ePaolS5aoevXq6tChg9577z0tWbLEwJQAAMCecnyS+YIFCxQbG3vD+48dO6YFCxbkSigAkKTLl1OUmmqxGXvhhboaPbo+RQbgwuLj420uDb969WqFhYWpefPmCg4OVo8ePfTzzz8bmBAAANhbjsuMwYMHa9euXTe8/8cff9TgwYNzJRQASFKbNhU0c2ZLmc1/FxevvtpAI0dy2Djg6vz8/JSYmChJSk9P17Zt29SkSZPM+318fHT58mVjwgEAAIfI8aVZrVbrTe+/du2a3Nzc7joQAPxT+/aVlJZm0dmzVzVkCBv6AZBq166tL774Qo0aNdLXX3+tpKQktWzZMvP+I0eOKDg42MCEAADA3m5aZhw/flzHjh3LvP3HH39o69atWR6XmJioTz/9VKVLl879hABcXufO9xodAUAeMnr0aEVGRqpp06ayWq1q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\n",
+      "text/plain": [
+       "<Figure size 1152x576 with 2 Axes>"
+      ]
+     },
+     "metadata": {
+      "nterop": {
+       "id": "17"
+      }
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "plt.figure(figsize=(16, 8))\n",
+    "plt.subplot(1, 2, 1)\n",
+    "plot_curve(y_tst, p, kind=\"roc\")\n",
+    "plt.subplot(1, 2, 2)\n",
+    "plot_curve(y_tst, p, kind=\"pr\")"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "ea371d24",
+   "metadata": {
+    "nterop": {
+     "id": "13"
+    }
+   },
+   "source": [
+    "## Regression with AutoXGB"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "7f0fa543",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:32:55.920829Z",
+     "start_time": "2022-03-06T05:32:55.833069Z"
+    },
+    "nterop": {
+     "id": "18"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "X dim: (1000, 10), y dim: (1000,)\n"
+     ]
+    }
+   ],
+   "source": [
+    "X, y = make_regression(\n",
+    "    n_samples=N_OBS,\n",
+    "    n_features=N_FEATURE,\n",
+    "    n_informative=N_IMP_FEATURE,\n",
+    "    random_state=RANDOM_SEED,\n",
+    ")\n",
+    "X = pd.DataFrame(X, columns=[\"x{}\".format(i) for i in range(X.shape[1])])\n",
+    "y = pd.Series(y)\n",
+    "print(f\"X dim: {X.shape}, y dim: {y.shape}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "ad91525f",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:34:57.457330Z",
+     "start_time": "2022-03-06T05:32:55.922529Z"
+    },
+    "nterop": {
+     "id": "19"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "100%|██████████| 10/10 [00:17<00:00,  1.70s/trial, best loss: 0.265587]\n",
+      "100%|██████████| 100/100 [01:44<00:00,  1.04s/trial, best loss: 0.249275]\n",
+      "RMSE (XGB): 0.2955\n"
+     ]
+    }
+   ],
+   "source": [
+    "model = AutoXGB(objective=\"reg:squarederror\", metric=\"rmse\")\n",
+    "model.tune(X_trn, y_trn)\n",
+    "model.fit(X_trn, y_trn)\n",
+    "p = model.predict(X_tst)\n",
+    "print(f\"RMSE (XGB): {rmse(y_tst, p):.4f}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "1037a6d0",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:34:57.538205Z",
+     "start_time": "2022-03-06T05:34:57.459097Z"
+    },
+    "nterop": {
+     "id": "20"
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "10 features out of 10 features selected: ['x6', 'x2', 'x8', 'x1', 'x9', 'x5', 'x0', 'x4', 'x7', 'x3']\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f\"{len(model.features)} features out of {N_FEATURE} features selected: {model.features}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "0dc2e44d",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2022-03-06T05:34:57.772323Z",
+     "start_time": "2022-03-06T05:34:57.539772Z"
+    },
+    "nterop": {
+     "id": "21"
+    }
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'random_state': 42,\n",
+       " 'n_jobs': -1,\n",
+       " 'objective': 'reg:squarederror',\n",
+       " 'booster': 'gbtree',\n",
+       " 'colsample_bytree': 0.7000000000000001,\n",
+       " 'learning_rate': 0.2140971222961297,\n",
+       " 'max_depth': 6,\n",
+       " 'min_child_weight': 10,\n",
+       " 'subsample': 0.7000000000000001}"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {
+      "nterop": {
+       "id": "22"
+      }
+     },
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model.params"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e8c8be3c",
+   "metadata": {
+    "nterop": {
+     "id": "23"
+    }
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "hide_input": false,
+  "kernelspec": {
+   "display_name": "Python 3.7 (Kaggler)",
+   "language": "python",
+   "name": "kaggler"
+  },
+  "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.7.7"
+  },
+  "nterop": {
+   "seedId": "23"
+  },
+  "toc": {
+   "base_numbering": 1,
+   "nav_menu": {},
+   "number_sections": true,
+   "sideBar": true,
+   "skip_h1_title": false,
+   "title_cell": "Table of Contents",
+   "title_sidebar": "Contents",
+   "toc_cell": false,
+   "toc_position": {},
+   "toc_section_display": true,
+   "toc_window_display": false
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/kaggler/__init__.py b/kaggler/__init__.py
index d9cbdce..9dd4710 100644
--- a/kaggler/__init__.py
+++ b/kaggler/__init__.py
@@ -1,10 +1,12 @@
-__version__ = '0.9.14'
-__all__ = ['const',
-           'data_io',
-           'ensemble',
-           'feature_selection',
-           'metrics',
-           'model',
-           'online_model',
-           'preprocessing',
-           'util']
+__version__ = "0.9.14"
+__all__ = [
+    "const",
+    "data_io",
+    "ensemble",
+    "feature_selection",
+    "metrics",
+    "model",
+    "online_model",
+    "preprocessing",
+    "util",
+]
diff --git a/kaggler/data_io.py b/kaggler/data_io.py
index cff7b6a..9d80f42 100644
--- a/kaggler/data_io.py
+++ b/kaggler/data_io.py
@@ -34,13 +34,13 @@ def save_data(X, y, path):
         y (numpy array): Target vector. If None, all zero vector will be saved.
         path (str): Path to the CSV, LibSVM or HDF5 file to save data.
     """
-    catalog = {'.csv': save_csv, '.sps': save_libsvm, '.h5': save_hdf5}
+    catalog = {".csv": save_csv, ".sps": save_libsvm, ".h5": save_hdf5}
 
     ext = os.path.splitext(path)[1]
     func = catalog[ext]
 
     if y is None:
-        y = np.zeros((X.shape[0], ))
+        y = np.zeros((X.shape[0],))
 
     func(X, y, path)
 
@@ -57,7 +57,7 @@ def save_csv(X, y, path):
     if sparse.issparse(X):
         X = X.todense()
 
-    np.savetxt(path, np.hstack((y.reshape((-1, 1)), X)), delimiter=',')
+    np.savetxt(path, np.hstack((y.reshape((-1, 1)), X)), delimiter=",")
 
 
 def save_libsvm(X, y, path):
@@ -81,21 +81,21 @@ def save_hdf5(X, y, path):
         path (str): Path to the HDF5 file to save data.
     """
 
-    with h5py.File(path, 'w') as f:
+    with h5py.File(path, "w") as f:
         is_sparse = 1 if sparse.issparse(X) else 0
-        f['issparse'] = is_sparse
-        f['target'] = y
+        f["issparse"] = is_sparse
+        f["target"] = y
 
         if is_sparse:
             if not sparse.isspmatrix_csr(X):
                 X = X.tocsr()
 
-            f['shape'] = np.array(X.shape)
-            f['data'] = X.data
-            f['indices'] = X.indices
-            f['indptr'] = X.indptr
+            f["shape"] = np.array(X.shape)
+            f["data"] = X.data
+            f["indices"] = X.indices
+            f["indptr"] = X.indptr
         else:
-            f['data'] = X
+            f["data"] = X
 
 
 def load_data(path, dense=False):
@@ -110,7 +110,7 @@ def load_data(path, dense=False):
         Data matrix X and target vector y
     """
 
-    catalog = {'.csv': load_csv, '.sps': load_svmlight_file, '.h5': load_hdf5}
+    catalog = {".csv": load_csv, ".sps": load_svmlight_file, ".h5": load_hdf5}
 
     ext = os.path.splitext(path)[1]
     func = catalog[ext]
@@ -137,8 +137,9 @@ def load_csv(path):
     with open(path) as f:
         line = f.readline().strip()
 
-    X = np.loadtxt(path, delimiter=',',
-                   skiprows=0 if is_number(line.split(',')[0]) else 1)
+    X = np.loadtxt(
+        path, delimiter=",", skiprows=0 if is_number(line.split(",")[0]) else 1
+    )
 
     y = np.array(X[:, 0]).flatten()
     X = X[:, 1:]
@@ -158,18 +159,18 @@ def load_hdf5(path):
         Data matrix X and target vector y
     """
 
-    with h5py.File(path, 'r') as f:
-        is_sparse = f['issparse'][...]
+    with h5py.File(path, "r") as f:
+        is_sparse = f["issparse"][...]
         if is_sparse:
-            shape = tuple(f['shape'][...])
-            data = f['data'][...]
-            indices = f['indices'][...]
-            indptr = f['indptr'][...]
+            shape = tuple(f["shape"][...])
+            data = f["data"][...]
+            indices = f["indices"][...]
+            indptr = f["indptr"][...]
             X = sparse.csr_matrix((data, indices, indptr), shape=shape)
         else:
-            X = f['data'][...]
+            X = f["data"][...]
 
-        y = f['target'][...]
+        y = f["target"][...]
 
     return X, y
 
@@ -186,7 +187,7 @@ def read_sps(path):
 
     for line in open(path):
         # parse x
-        xs = line.rstrip().split(' ')
+        xs = line.rstrip().split(" ")
 
         yield xs[1:], int(xs[0])
 
@@ -218,7 +219,7 @@ class PathJoiner:
         y = load(PATH.data('targets.array'))
     """
 
-    def __init__(self, filename='SETTINGS.json'):
+    def __init__(self, filename="SETTINGS.json"):
         with open(filename) as file:
             self.subdirs = json.load(file)
 
@@ -227,14 +228,14 @@ def __getattr__(self, attr):
         return lambda *dirs: os.path.join(subdir, *dirs)
 
 
-def stream_lines(filename, encoding='utf-8', ignore_errors=False):
-    errors = 'ignore' if ignore_errors else 'strict'
+def stream_lines(filename, encoding="utf-8", ignore_errors=False):
+    errors = "ignore" if ignore_errors else "strict"
     with open(filename, encoding=encoding, errors=errors) as file:
         for line in file:
             yield line
 
 
-def stream_csv(filename, encoding='utf-8', ignore_errors=False):
+def stream_csv(filename, encoding="utf-8", ignore_errors=False):
     stream = stream_lines(filename, encoding, ignore_errors)
     return csv.reader(stream)
 
@@ -247,68 +248,67 @@ def limit_stream(stream, count=1, skip=0):
 
 
 def save_obj(filename, obj):
-    with open(filename, 'wb') as file:
+    with open(filename, "wb") as file:
         pickle.dump(obj, file, protocol=pickle.HIGHEST_PROTOCOL)
-    logger.info('saved : {}\t{}'.format(filename, type(obj)))
+    logger.info("saved : {}\t{}".format(filename, type(obj)))
 
 
 def load_obj(filename):
-    with open(filename, 'rb') as file:
+    with open(filename, "rb") as file:
         obj = pickle.load(file)
-    logger.info('loaded : {}\t{}'.format(filename, type(obj)))
+    logger.info("loaded : {}\t{}".format(filename, type(obj)))
     return obj
 
 
 def save_array(filename, X):
-    with h5py.File(filename, 'w') as file:
-        file['data'] = X
-    logger.info('saved : {}\t{}\t{}'.format(filename, X.dtype, X.shape))
+    with h5py.File(filename, "w") as file:
+        file["data"] = X
+    logger.info("saved : {}\t{}\t{}".format(filename, X.dtype, X.shape))
 
 
 def load_array(filename):
-    with h5py.File(filename, 'r') as file:
-        X = file['data'][...]
-    logger.info('loaded : {}\t{}\t{}'.format(filename, X.dtype, X.shape))
+    with h5py.File(filename, "r") as file:
+        X = file["data"][...]
+    logger.info("loaded : {}\t{}\t{}".format(filename, X.dtype, X.shape))
     return X
 
 
 def save_sparse(filename, X):
-    with h5py.File(filename, 'w') as file:
-        file['shape'] = np.array(X.shape)
-        file['data'] = X.data
-        file['indices'] = X.indices
-        file['indptr'] = X.indptr
-    logger.info('saved : {}\t{}\t{}'.format(filename, X.dtype, X.shape))
+    with h5py.File(filename, "w") as file:
+        file["shape"] = np.array(X.shape)
+        file["data"] = X.data
+        file["indices"] = X.indices
+        file["indptr"] = X.indptr
+    logger.info("saved : {}\t{}\t{}".format(filename, X.dtype, X.shape))
 
 
 def load_sparse(filename):
-    with h5py.File(filename, 'r') as file:
-        shape = tuple(file['shape'][...])
-        data = file['data'][...]
-        indices = file['indices'][...]
-        indptr = file['indptr'][...]
+    with h5py.File(filename, "r") as file:
+        shape = tuple(file["shape"][...])
+        data = file["data"][...]
+        indices = file["indices"][...]
+        indptr = file["indptr"][...]
     X = sparse.csr_matrix((data, indices, indptr), shape=shape)
-    logger.info('loaded : {}\t{}\t{}'.format(filename, X.dtype, X.shape))
+    logger.info("loaded : {}\t{}\t{}".format(filename, X.dtype, X.shape))
     return X
 
 
 def save(filename, X):
-    catalog = {'obj': save_obj, 'array': save_array, 'sparse': save_sparse}
-    extension = filename.split('.')[-1]
+    catalog = {"obj": save_obj, "array": save_array, "sparse": save_sparse}
+    extension = filename.split(".")[-1]
     func = catalog[extension]
     func(filename, X)
 
 
 def load(filename):
-    catalog = {'obj': load_obj, 'array': load_array, 'sparse': load_sparse}
-    extension = filename.split('.')[-1]
+    catalog = {"obj": load_obj, "array": load_array, "sparse": load_sparse}
+    extension = filename.split(".")[-1]
     func = catalog[extension]
     X = func(filename)
     return X
 
 
 class Clock(object):
-
     def __init__(self):
         self.start = time.time()
         self.last = self.start
@@ -321,8 +321,8 @@ def check(self):
         self.last = self.now
 
     def report(self):
-        txt = '\n[CLOCK]  [ {} ]    '
-        txt += 'since start: [ {} ]    since last: [ {} ]\n'
+        txt = "\n[CLOCK]  [ {} ]    "
+        txt += "since start: [ {} ]    since last: [ {} ]\n"
         current = time.asctime().split()[3]
         since_start = datetime.timedelta(seconds=round(self.now - self.start))
         since_last = datetime.timedelta(seconds=round(self.now - self.last))
@@ -331,7 +331,7 @@ def report(self):
 
 def beep(n=1):
     for _ in range(n):
-        os.system('beep')
+        os.system("beep")
 
 
 def print_shape_type(*objs):
diff --git a/kaggler/ensemble/__init__.py b/kaggler/ensemble/__init__.py
index eed38b7..7e404cb 100644
--- a/kaggler/ensemble/__init__.py
+++ b/kaggler/ensemble/__init__.py
@@ -1,4 +1,4 @@
 from .linear import netflix
 
 
-__all__ = ['netflix']
+__all__ = ["netflix"]
diff --git a/kaggler/ensemble/linear.py b/kaggler/ensemble/linear.py
index 80553a5..6ed7e08 100755
--- a/kaggler/ensemble/linear.py
+++ b/kaggler/ensemble/linear.py
@@ -1,7 +1,7 @@
 import numpy as np
 
 
-def netflix(es, ps, e0, l=.0001):
+def netflix(es, ps, e0, l=0.0001):
     """Combine predictions with the optimal weights to minimize RMSE.
 
     Ref: Töscher, A., Jahrer, M., & Bell, R. M. (2009). The bigchaos solution to the netflix grand prize.
@@ -22,7 +22,7 @@ def netflix(es, ps, e0, l=.0001):
     n = len(ps[0])
 
     X = np.stack(ps).T
-    pTy = .5 * (n * e0**2 + (X**2).sum(axis=0) - n * np.array(es)**2)
+    pTy = 0.5 * (n * e0**2 + (X**2).sum(axis=0) - n * np.array(es) ** 2)
 
     w = np.linalg.pinv(X.T.dot(X) + l * n * np.eye(m)).dot(pTy)
 
diff --git a/kaggler/feature_selection/__init__.py b/kaggler/feature_selection/__init__.py
index 07e9a37..cd0d5ea 100644
--- a/kaggler/feature_selection/__init__.py
+++ b/kaggler/feature_selection/__init__.py
@@ -2,4 +2,4 @@
 from .feature_selection import DropLowInfo
 
 
-__all__ = ['DropInactive', 'DropLowInfo']
+__all__ = ["DropInactive", "DropLowInfo"]
diff --git a/kaggler/feature_selection/feature_selection.py b/kaggler/feature_selection/feature_selection.py
index b5d61c7..8f47a04 100644
--- a/kaggler/feature_selection/feature_selection.py
+++ b/kaggler/feature_selection/feature_selection.py
@@ -15,7 +15,7 @@ def __init__(self, lowest=25):
     def fit(self, X, y=None):
         x = (X > 0.0).astype(bool)
         s = np.array(x.sum(axis=0)).flatten()
-        self.mask = (s >= self.lowest)
+        self.mask = s >= self.lowest
         return self
 
     def transform(self, X):
@@ -42,12 +42,12 @@ def fit(self, X, y=None):
             x = X.multiply(ys).sum(axis=0)
             x = x / X.sum(axis=0)
         else:
-            x = (X > 0)
+            x = X > 0
             s = x.sum(axis=0)
             x = x.multiply(ys).sum(axis=0) / s
-        x = np.array(x).flatten().astype('f4')
-        mask1 = (x < lower)
-        mask2 = (x > upper)
+        x = np.array(x).flatten().astype("f4")
+        mask1 = x < lower
+        mask2 = x > upper
         self.mask = (mask1 + mask2).astype(bool)
         return self
 
diff --git a/kaggler/metrics/classification.py b/kaggler/metrics/classification.py
index 0ee7a1d..f5cecaf 100644
--- a/kaggler/metrics/classification.py
+++ b/kaggler/metrics/classification.py
@@ -1,6 +1,6 @@
 from sklearn.metrics import log_loss
-from sklearn.metrics import roc_auc_score as auc            # noqa 
-from sklearn.metrics import average_precision_score as ap   # noqa
+from sklearn.metrics import roc_auc_score as auc  # noqa
+from sklearn.metrics import average_precision_score as ap  # noqa
 from ..const import EPS
 
 
diff --git a/kaggler/model/__init__.py b/kaggler/model/__init__.py
index 15aef98..38e72e4 100644
--- a/kaggler/model/__init__.py
+++ b/kaggler/model/__init__.py
@@ -1,4 +1,4 @@
 from .nn import NN
 from .automl import BaseAutoML, AutoLGB, AutoXGB
 
-__all__ = ['NN', 'BaseAutoML', 'AutoLGB', 'AutoXGB']
+__all__ = ["NN", "BaseAutoML", "AutoLGB", "AutoXGB"]
diff --git a/kaggler/model/automl.py b/kaggler/model/automl.py
index ed90dd5..9979bcf 100644
--- a/kaggler/model/automl.py
+++ b/kaggler/model/automl.py
@@ -6,6 +6,7 @@
 the competition website (https://www.4paradigm.com/competition/kddcup2019).
 """
 
+from abc import ABCMeta, abstractmethod, abstractstaticmethod
 import hyperopt
 from hyperopt import STATUS_OK, Trials, hp, space_eval, tpe
 import lightgbm as lgb
@@ -20,7 +21,7 @@
 
 logger = getLogger(__name__)
 SAMPLE_SIZE = 10000
-VALID_SIZE = .2
+VALID_SIZE = 0.2
 
 
 def sample_data(X, y, nrows, shuffle=True, random_state=None):
@@ -53,12 +54,28 @@ def sample_data(X, y, nrows, shuffle=True, random_state=None):
     return X_s, y_s
 
 
-class BaseAutoML(object):
+class BaseAutoML(meta=ABCMeta):
     """Base optimized regressor class."""
 
-    def __init__(self, params, space, n_est=500, n_stop=10, sample_size=SAMPLE_SIZE, valid_size=VALID_SIZE,
-                 shuffle=True, feature_selection=True, n_fs=10, fs_th=0., fs_pct=.0, hyperparam_opt=True,
-                 n_hpopt=100, minimize=True, n_random_col=10, random_state=RANDOM_SEED):
+    def __init__(
+        self,
+        params,
+        space,
+        n_est=500,
+        n_stop=10,
+        sample_size=SAMPLE_SIZE,
+        valid_size=VALID_SIZE,
+        shuffle=True,
+        feature_selection=True,
+        n_fs=10,
+        fs_th=0.0,
+        fs_pct=0.0,
+        hyperparam_opt=True,
+        n_hpopt=100,
+        minimize=True,
+        n_random_col=10,
+        random_state=RANDOM_SEED,
+    ):
         """Initialize an optimized regressor class object.
 
         Args:
@@ -93,7 +110,7 @@ def __init__(self, params, space, n_est=500, n_stop=10, sample_size=SAMPLE_SIZE,
         self.n_hpopt = n_hpopt
         self.sample_size = sample_size
         self.valid_size = valid_size
-        self.shuffle = True
+        self.shuffle = shuffle
         self.feature_selection = feature_selection
         self.fs_th = fs_th
         self.fs_pct = fs_pct
@@ -109,7 +126,7 @@ def __init__(self, params, space, n_est=500, n_stop=10, sample_size=SAMPLE_SIZE,
         elif isinstance(random_state, np.random.RandomState):
             self.random_state = random_state
         else:
-            raise ValueError('Invalid input for random_state: {}'.format(random_state))
+            raise ValueError("Invalid input for random_state: {}".format(random_state))
 
         self.n_best = -1
         self.model = None
@@ -126,31 +143,40 @@ def tune(self, X, y):
             self
         """
         if self.feature_selection or self.hyperparam_opt:
-            X_s, y_s = sample_data(X, y, self.sample_size, shuffle=self.shuffle, random_state=self.random_state)
+            X_s, y_s = sample_data(
+                X,
+                y,
+                self.sample_size,
+                shuffle=self.shuffle,
+                random_state=self.random_state,
+            )
 
         if self.feature_selection:
             self.features = self.select_features(X_s, y_s)
-            logger.info(f'selecting top {len(self.features)} out of {X.shape[1]} features')
+            logger.info(
+                f"selecting top {len(self.features)} out of {X.shape[1]} features"
+            )
         else:
             self.features = X.columns.tolist()
 
         if self.hyperparam_opt:
-            logger.info('hyper-parameter tuning')
-            hyperparams, trials = self.optimize_hyperparam(X_s[self.features].values,
-                                                           y_s.values,
-                                                           n_eval=self.n_hpopt)
+            logger.info("hyper-parameter tuning")
+            hyperparams, trials = self.optimize_hyperparam(
+                X_s[self.features].values, y_s.values, n_eval=self.n_hpopt
+            )
 
             self.params.update(hyperparams)
-            self.n_best = trials.best_trial['result']['model'].best_iteration
-            logger.info(f'best parameters: {self.params}')
-            logger.info(f'best iterations: {self.n_best}')
+            self.n_best = trials.best_trial["result"]["model"].best_iteration
+            logger.info(f"best parameters: {self.params}")
+            logger.info(f"best iterations: {self.n_best}")
 
         return self
 
-    @staticmethod
+    @abstractstaticmethod
     def get_feature_importance(model):
         raise NotImplementedError
 
+    @abstractmethod
     def feature_importance(self):
         raise NotImplementedError
 
@@ -172,58 +198,97 @@ def select_features(self, X, y):
 
         # trying for all features
         for i in range(1, self.n_random_col + 1):
-            random_col = '__random_{}__'.format(i)
+            random_col = "__random_{}__".format(i)
             X[random_col] = self.random_state.rand(X.shape[0])
             random_cols.append(random_col)
 
         _, trials = self.optimize_hyperparam(X.values, y.values, n_eval=self.n_fs)
 
-        feature_importances = self.get_feature_importance(trials.best_trial['result']['model'])
-        imp = pd.DataFrame({'feature_importances': feature_importances, 'feature_names': X.columns.tolist()})
-        imp = imp.sort_values('feature_importances', ascending=False).drop_duplicates()
+        feature_importances = self.get_feature_importance(
+            trials.best_trial["result"]["model"]
+        )
+        imp = pd.DataFrame(
+            {
+                "feature_importances": feature_importances,
+                "feature_names": X.columns.tolist(),
+            }
+        )
+        imp = imp.sort_values("feature_importances", ascending=False).drop_duplicates()
 
         if len(random_cols) == 0:
-            imp = imp[imp['feature_importances'] > self.fs_th]
+            imp = imp[imp["feature_importances"] > self.fs_th]
         else:
-            imp_random = imp.loc[imp.feature_names.isin(random_cols), 'feature_importances'].values
+            imp_random = imp.loc[
+                imp.feature_names.isin(random_cols), "feature_importances"
+            ].values
             th = max(np.percentile(imp_random, self.fs_pct * 100), self.fs_th)
-            logger.debug(f'feature importance (th={th:.2f}):\n{imp}')
-            imp = imp[(imp.feature_importances > th) & ~(imp.feature_names.isin(random_cols))]
+            logger.debug(f"feature importance (th={th:.2f}):\n{imp}")
+            imp = imp[
+                (imp.feature_importances > th) & ~(imp.feature_names.isin(random_cols))
+            ]
 
-        return imp['feature_names'].tolist()
+        return imp["feature_names"].tolist()
 
-    def optimize_hyperparam(self, X, y, test_size=.2, n_eval=100):
+    @abstractmethod
+    def optimize_hyperparam(self, X, y, test_size=0.2, n_eval=100):
         raise NotImplementedError
 
 
 class AutoXGB(BaseAutoML):
 
-    params = {'random_state': RANDOM_SEED,
-              'n_jobs': -1}
+    params = {"random_state": RANDOM_SEED, "n_jobs": -1}
 
-    space = {
+    default_xgb_space = {
         "learning_rate": hp.loguniform("learning_rate", np.log(0.01), np.log(0.3)),
         "max_depth": hp.choice("num_leaves", [6, 8, 10]),
-        "colsample_bytree": hp.quniform("colsample_bytree", .5, .9, 0.1),
-        "subsample": hp.quniform("subsample", .5, .9, 0.1),
-        "min_child_weight": hp.choice('min_child_weight', [10, 25, 100]),
+        "colsample_bytree": hp.quniform("colsample_bytree", 0.5, 0.9, 0.1),
+        "subsample": hp.quniform("subsample", 0.5, 0.9, 0.1),
+        "min_child_weight": hp.choice("min_child_weight", [10, 25, 100]),
     }
 
-    def __init__(self, objective='reg:linear', metric='rmse', boosting='gbtree', params=params, space=space,
-                 n_est=500, n_stop=10, sample_size=SAMPLE_SIZE, feature_selection=True, n_fs=10, fs_th=1e-5, fs_pct=.1,
-                 hyperparam_opt=True, n_hpopt=100, n_random_col=10, random_state=RANDOM_SEED, shuffle=True):
+    def __init__(
+        self,
+        objective="reg:squarederror",
+        metric="rmse",
+        boosting="gbtree",
+        params=params,
+        space=default_xgb_space,
+        n_est=500,
+        n_stop=10,
+        sample_size=SAMPLE_SIZE,
+        feature_selection=True,
+        n_fs=10,
+        fs_th=1e-5,
+        fs_pct=0.1,
+        hyperparam_opt=True,
+        n_hpopt=100,
+        n_random_col=10,
+        random_state=RANDOM_SEED,
+        shuffle=True,
+    ):
 
         self.metric, minimize = self._get_metric_alias_minimize(metric)
 
         self.params.update(params)
-        self.params.update({'objective': objective,
-                            'booster': boosting})
-
-        super(AutoXGB, self).__init__(params=self.params, space=space, n_est=n_est, n_stop=n_stop,
-                                      sample_size=sample_size, feature_selection=feature_selection, n_fs=n_fs,
-                                      fs_th=fs_th, fs_pct=fs_pct, hyperparam_opt=hyperparam_opt, n_hpopt=n_hpopt,
-                                      minimize=minimize, n_random_col=n_random_col, random_state=random_state,
-                                      shuffle=shuffle)
+        self.params.update({"objective": objective, "booster": boosting})
+
+        super(AutoXGB, self).__init__(
+            params=self.params,
+            space=space,
+            n_est=n_est,
+            n_stop=n_stop,
+            sample_size=sample_size,
+            feature_selection=feature_selection,
+            n_fs=n_fs,
+            fs_th=fs_th,
+            fs_pct=fs_pct,
+            hyperparam_opt=hyperparam_opt,
+            n_hpopt=n_hpopt,
+            minimize=minimize,
+            n_random_col=n_random_col,
+            random_state=random_state,
+            shuffle=shuffle,
+        )
 
     @staticmethod
     def _get_metric_alias_minimize(metric):
@@ -241,11 +306,26 @@ def _get_metric_alias_minimize(metric):
                 - (bool): a flag whether to minimize or maximize the metric
         """
 
-        assert metric in ['rmse', 'rmsle', 'mae', 'logloss', 'error', 'merror', 'mlogloss', 'auc', 'aucpr',
-                          'ndcg', 'map', 'poisson-nloglik', 'gamma-nloglik', 'cox-nloglik', 'gamma-deviance',
-                          'tweedie-nloglik'], 'Invalid metric: {}'.format(metric)
-
-        if metric in ['auc', 'aucpr', 'ndcg', 'map']:
+        assert metric in [
+            "rmse",
+            "rmsle",
+            "mae",
+            "logloss",
+            "error",
+            "merror",
+            "mlogloss",
+            "auc",
+            "aucpr",
+            "ndcg",
+            "map",
+            "poisson-nloglik",
+            "gamma-nloglik",
+            "cox-nloglik",
+            "gamma-deviance",
+            "tweedie-nloglik",
+        ], "Invalid metric: {}".format(metric)
+
+        if metric in ["auc", "aucpr", "ndcg", "map"]:
             minimize = False
         else:
             minimize = True
@@ -259,31 +339,44 @@ def get_feature_importance(model):
     def feature_importance(self):
         return self.model.feature_importances_
 
-    def optimize_hyperparam(self, X, y, test_size=.2, n_eval=100):
-        X_trn, X_val, y_trn, y_val = train_test_split(X, y, test_size=test_size, shuffle=self.shuffle)
+    def optimize_hyperparam(self, X, y, test_size=0.2, n_eval=100):
+        X_trn, X_val, y_trn, y_val = train_test_split(
+            X, y, test_size=test_size, shuffle=self.shuffle
+        )
 
         def objective(hyperparams):
             model = XGBModel(n_estimators=self.n_est, **self.params, **hyperparams)
-            model.fit(X=X_trn, y=y_trn,
-                      eval_set=[(X_val, y_val)],
-                      eval_metric=self.metric,
-                      early_stopping_rounds=self.n_stop,
-                      verbose=False)
-            score = model.evals_result()['validation_0'][self.metric][model.best_iteration] * self.loss_sign
-
-            return {'loss': score, 'status': STATUS_OK, 'model': model}
+            model.fit(
+                X=X_trn,
+                y=y_trn,
+                eval_set=[(X_val, y_val)],
+                eval_metric=self.metric,
+                early_stopping_rounds=self.n_stop,
+                verbose=False,
+            )
+            score = (
+                model.evals_result()["validation_0"][self.metric][model.best_iteration]
+                * self.loss_sign
+            )
+
+            return {"loss": score, "status": STATUS_OK, "model": model}
 
         trials = Trials()
-        best = hyperopt.fmin(fn=objective, space=self.space, trials=trials,
-                             algo=tpe.suggest, max_evals=n_eval, verbose=1,
-                             rstate=self.random_state)
+        best = hyperopt.fmin(
+            fn=objective,
+            space=self.space,
+            trials=trials,
+            algo=tpe.suggest,
+            max_evals=n_eval,
+            verbose=1,
+        )
 
         hyperparams = space_eval(self.space, best)
         return hyperparams, trials
 
     def fit(self, X, y):
         self.model = XGBModel(n_estimators=self.n_best, **self.params)
-        self.model.fit(X=X[self.features], y=y, eval_metric='mae', verbose=False)
+        self.model.fit(X=X[self.features], y=y, eval_metric="mae", verbose=False)
         return self
 
     def predict(self, X):
@@ -300,32 +393,61 @@ class AutoLGB(BaseAutoML):
         "feature_pre_filter": False,
     }
 
-    space = {
+    default_lgb_space = {
         "learning_rate": hp.loguniform("learning_rate", np.log(0.01), np.log(0.3)),
         "num_leaves": hp.choice("num_leaves", [15, 31, 63, 127, 255]),
         "max_depth": hp.choice("max_depth", [-1, 4, 6, 8, 10]),
-        "feature_fraction": hp.quniform("feature_fraction", .5, .9, 0.1),
-        "bagging_fraction": hp.quniform("bagging_fraction", .5, .9, 0.1),
-        "min_child_samples": hp.choice('min_child_samples', [10, 25, 100]),
-        "lambda_l1": hp.choice('lambda_l1', [0, .1, 1, 10]),
-        "lambda_l2": hp.choice('lambda_l2', [0, .1, 1, 10]),
+        "feature_fraction": hp.quniform("feature_fraction", 0.5, 0.9, 0.1),
+        "bagging_fraction": hp.quniform("bagging_fraction", 0.5, 0.9, 0.1),
+        "min_child_samples": hp.choice("min_child_samples", [10, 25, 100]),
+        "lambda_l1": hp.choice("lambda_l1", [0, 0.1, 1, 10]),
+        "lambda_l2": hp.choice("lambda_l2", [0, 0.1, 1, 10]),
     }
 
-    def __init__(self, objective='regression', metric='mae', boosting='gbdt', params=params, space=space,
-                 n_est=500, n_stop=10, sample_size=SAMPLE_SIZE, feature_selection=True, n_fs=10, fs_th=1e-5, fs_pct=.1,
-                 hyperparam_opt=True, n_hpopt=100, n_random_col=10, random_state=RANDOM_SEED, shuffle=True):
+    def __init__(
+        self,
+        objective="regression",
+        metric="mae",
+        boosting="gbdt",
+        params=params,
+        space=default_lgb_space,
+        n_est=500,
+        n_stop=10,
+        sample_size=SAMPLE_SIZE,
+        feature_selection=True,
+        n_fs=10,
+        fs_th=1e-5,
+        fs_pct=0.1,
+        hyperparam_opt=True,
+        n_hpopt=100,
+        n_random_col=10,
+        random_state=RANDOM_SEED,
+        shuffle=True,
+    ):
 
         self.metric, minimize = self._get_metric_alias_minimize(metric)
 
         self.params.update(params)
-        self.params.update({'objective': objective,
-                            'metric': self.metric,
-                            'boosting': boosting})
-        super(AutoLGB, self).__init__(params=self.params, space=space, n_est=n_est, n_stop=n_stop,
-                                      sample_size=sample_size, feature_selection=feature_selection, n_fs=n_fs,
-                                      fs_th=fs_th, fs_pct=fs_pct, hyperparam_opt=hyperparam_opt, n_hpopt=n_hpopt,
-                                      minimize=minimize, n_random_col=n_random_col, random_state=random_state,
-                                      shuffle=shuffle)
+        self.params.update(
+            {"objective": objective, "metric": self.metric, "boosting": boosting}
+        )
+        super(AutoLGB, self).__init__(
+            params=self.params,
+            space=space,
+            n_est=n_est,
+            n_stop=n_stop,
+            sample_size=sample_size,
+            feature_selection=feature_selection,
+            n_fs=n_fs,
+            fs_th=fs_th,
+            fs_pct=fs_pct,
+            hyperparam_opt=hyperparam_opt,
+            n_hpopt=n_hpopt,
+            minimize=minimize,
+            n_random_col=n_random_col,
+            random_state=random_state,
+            shuffle=shuffle,
+        )
 
     @staticmethod
     def _get_metric_alias_minimize(metric):
@@ -343,37 +465,66 @@ def _get_metric_alias_minimize(metric):
                 - (bool): a flag whether to minimize or maximize the metric
         """
 
-        if metric in ['l1', 'l2', 'rmse', 'quantile', 'mape', 'huber', 'fair', 'poisson', 'gamma', 'gamma_deviance',
-                      'tweedie', 'ndcg', 'map', 'auc', 'binary_logloss', 'binary_error', 'multi_logloss',
-                      'multi_error', 'cross_entropy', 'cross_entropy_lambda', 'kullerback_leibler']:
+        if metric in [
+            "l1",
+            "l2",
+            "rmse",
+            "quantile",
+            "mape",
+            "huber",
+            "fair",
+            "poisson",
+            "gamma",
+            "gamma_deviance",
+            "tweedie",
+            "ndcg",
+            "map",
+            "auc",
+            "binary_logloss",
+            "binary_error",
+            "multi_logloss",
+            "multi_error",
+            "cross_entropy",
+            "cross_entropy_lambda",
+            "kullerback_leibler",
+        ]:
             pass
-        elif metric in ['mae', 'mean_absolute_error', 'regression_l1']:
-            metric = 'l1'
-        elif metric in ['mean_squared_error', 'mse', 'regression_l2', 'regression']:
-            metric = 'l2'
-        elif metric in ['root_mean_squared_error', 'l2_root']:
-            metric = 'rmse'
-        elif metric in ['mean_absolute_percentage_error']:
-            metric = 'mape'
-        elif metric in ['lamdarank']:
-            metric = 'ndcg'
-        elif metric in ['mean_average_precision']:
-            metric = 'map'
-        elif metric in ['binary']:
-            metric = 'binary_logloss'
-        elif metric in ['multiclass', 'softmax', 'multiclassova', 'multiclass_ova', 'ova', 'ovr']:
-            metric = 'multi_logloss'
-        elif metric in ['xentropy']:
-            metric = 'cross_entropy'
-        elif metric in ['xentlambda']:
-            metric = 'cross_entropy_lambda'
-        elif metric in ['kldiv']:
-            metric = 'kullback_leibler'
+        elif metric in ["mae", "mean_absolute_error", "regression_l1"]:
+            metric = "l1"
+        elif metric in ["mean_squared_error", "mse", "regression_l2", "regression"]:
+            metric = "l2"
+        elif metric in ["root_mean_squared_error", "l2_root"]:
+            metric = "rmse"
+        elif metric in ["mean_absolute_percentage_error"]:
+            metric = "mape"
+        elif metric in ["lamdarank"]:
+            metric = "ndcg"
+        elif metric in ["mean_average_precision"]:
+            metric = "map"
+        elif metric in ["binary"]:
+            metric = "binary_logloss"
+        elif metric in [
+            "multiclass",
+            "softmax",
+            "multiclassova",
+            "multiclass_ova",
+            "ova",
+            "ovr",
+        ]:
+            metric = "multi_logloss"
+        elif metric in ["xentropy"]:
+            metric = "cross_entropy"
+        elif metric in ["xentlambda"]:
+            metric = "cross_entropy_lambda"
+        elif metric in ["kldiv"]:
+            metric = "kullback_leibler"
         else:
-            raise ValueError('{} is not a valid metric. See https://lightgbm.readthedocs.io/en/latest/Parameters.html '
-                             'for the full list of metrics available.'.format(metric))
+            raise ValueError(
+                "{} is not a valid metric. See https://lightgbm.readthedocs.io/en/latest/Parameters.html "
+                "for the full list of metrics available.".format(metric)
+            )
 
-        if metric in ['auc', 'ndcg', 'map']:
+        if metric in ["auc", "ndcg", "map"]:
             minimize = False
         else:
             minimize = True
@@ -382,29 +533,42 @@ def _get_metric_alias_minimize(metric):
 
     @staticmethod
     def get_feature_importance(model):
-        return model.feature_importance(importance_type='gain')
+        return model.feature_importance(importance_type="gain")
 
     def feature_importance(self):
-        return self.model.feature_importance(importance_type='gain')
+        return self.model.feature_importance(importance_type="gain")
 
-    def optimize_hyperparam(self, X, y, test_size=.2, n_eval=100):
-        X_trn, X_val, y_trn, y_val = train_test_split(X, y, test_size=test_size, shuffle=self.shuffle)
+    def optimize_hyperparam(self, X, y, test_size=0.2, n_eval=100):
+        X_trn, X_val, y_trn, y_val = train_test_split(
+            X, y, test_size=test_size, shuffle=self.shuffle
+        )
 
         train_data = lgb.Dataset(X_trn, label=y_trn)
         valid_data = lgb.Dataset(X_val, label=y_val)
 
         def objective(hyperparams):
-            model = lgb.train({**self.params, **hyperparams}, train_data, self.n_est,
-                              valid_data, early_stopping_rounds=self.n_stop, verbose_eval=0)
+            model = lgb.train(
+                {**self.params, **hyperparams},
+                train_data,
+                self.n_est,
+                valid_data,
+                early_stopping_rounds=self.n_stop,
+                verbose_eval=0,
+            )
 
             score = model.best_score["valid_0"][self.metric] * self.loss_sign
 
-            return {'loss': score, 'status': STATUS_OK, 'model': model}
+            return {"loss": score, "status": STATUS_OK, "model": model}
 
         trials = Trials()
-        best = hyperopt.fmin(fn=objective, space=self.space, trials=trials,
-                             algo=tpe.suggest, max_evals=n_eval, verbose=1,
-                             rstate=self.random_state)
+        best = hyperopt.fmin(
+            fn=objective,
+            space=self.space,
+            trials=trials,
+            algo=tpe.suggest,
+            max_evals=n_eval,
+            verbose=1,
+        )
 
         hyperparams = space_eval(self.space, best)
         return hyperparams, trials
diff --git a/kaggler/model/nn.py b/kaggler/model/nn.py
index aa2db0f..43e1dc8 100644
--- a/kaggler/model/nn.py
+++ b/kaggler/model/nn.py
@@ -15,7 +15,7 @@
 class NN(object):
     """Implement a neural network with a single h layer."""
 
-    def __init__(self, n=5, h=10, b=100000, l1=.0, l2=.0, random_state=None):
+    def __init__(self, n=5, h=10, b=100000, l1=0.0, l2=0.0, random_state=None):
         """Initialize the NN class object.
 
         Args:
@@ -58,20 +58,20 @@ def fit(self, X, y, X_val=None, y_val=None):
         # Set initial weights randomly.
         self.i = X.shape[1]
         self.l1 = self.l1 / self.i
-        self.w = (np.random.rand((self.i + 2) * self.h + 1) - .5) * 1e-6
+        self.w = (np.random.rand((self.i + 2) * self.h + 1) - 0.5) * 1e-6
         self.w_opt = self.w
         self.n_opt = 0
 
-        logger.info('training ...')
+        logger.info("training ...")
         n_obs = X.shape[0]
         batch = self.b
         n_epoch = self.n
         idx = range(n_obs)
-        self.auc_opt = .5
+        self.auc_opt = 0.5
 
         start = time.time()
-        print('\tEPOCH TRAIN     VALID     BEST      TIME (m)')
-        print('\t--------------------------------------------')
+        print("\tEPOCH TRAIN     VALID     BEST      TIME (m)")
+        print("\t--------------------------------------------")
 
         # Before training
         p = self.predict_raw(X)
@@ -81,9 +81,11 @@ def fit(self, X, y, X_val=None, y_val=None):
             p_val = self.predict_raw(X_val)
             auc_val = roc_auc_score(y_val, p_val)
 
-        print('\t{:3d}:  {:.6f}  {:.6f}  {:.6f}  {:.2f}'.format(
-              0, auc, auc_val, self.auc_opt,
-              (time.time() - start) / SEC_PER_MIN))
+        print(
+            "\t{:3d}:  {:.6f}  {:.6f}  {:.6f}  {:.2f}".format(
+                0, auc, auc_val, self.auc_opt, (time.time() - start) / SEC_PER_MIN
+            )
+        )
 
         # Use 'while' instead of 'for' to increase n_epoch if the validation
         # error keeps improving at the end of n_epoch
@@ -101,15 +103,13 @@ def fit(self, X, y, X_val=None, y_val=None):
             # speed and space trade-offs.
             for i in range(int(n_obs / batch) + 1):
                 if (i + 1) * batch > n_obs:
-                    sub_idx = idx[batch * i:n_obs]
+                    sub_idx = idx[batch * i : n_obs]
                 else:
-                    sub_idx = idx[batch * i:batch * (i + 1)]
+                    sub_idx = idx[batch * i : batch * (i + 1)]
 
                 x = X[sub_idx]
-                neg_idx = [n_idx for n_idx, n_y in enumerate(y[sub_idx])
-                           if n_y == 0.]
-                pos_idx = [p_idx for p_idx, p_y in enumerate(y[sub_idx])
-                           if p_y == 1.]
+                neg_idx = [n_idx for n_idx, n_y in enumerate(y[sub_idx]) if n_y == 0.0]
+                pos_idx = [p_idx for p_idx, p_y in enumerate(y[sub_idx]) if p_y == 1.0]
                 x0 = x[neg_idx]
                 x1 = x[pos_idx]
                 # Update weights to minimize the cost function using the
@@ -117,12 +117,14 @@ def fit(self, X, y, X_val=None, y_val=None):
                 #   func -- cost function
                 #   jac -- jacobian (derivative of the cost function)
                 #   maxiter -- number of iterations for L-BFGS-B
-                ret = minimize(self.func,
-                               self.w,
-                               args=(x0, x1),
-                               method='L-BFGS-B',
-                               jac=self.fprime,
-                               options={'maxiter': 5})
+                ret = minimize(
+                    self.func,
+                    self.w,
+                    args=(x0, x1),
+                    method="L-BFGS-B",
+                    jac=self.fprime,
+                    options={"maxiter": 5},
+                )
                 self.w = ret.x
 
             p = self.predict_raw(X)
@@ -143,18 +145,23 @@ def fit(self, X, y, X_val=None, y_val=None):
                     if epoch == n_epoch:
                         n_epoch += 5
 
-            print('\t{:3d}:  {:.6f}  {:.6f}  {:.6f}  {:.2f}'.format(
-                  epoch, auc, auc_val, self.auc_opt,
-                  (time.time() - start) / SEC_PER_MIN))
+            print(
+                "\t{:3d}:  {:.6f}  {:.6f}  {:.6f}  {:.2f}".format(
+                    epoch,
+                    auc,
+                    auc_val,
+                    self.auc_opt,
+                    (time.time() - start) / SEC_PER_MIN,
+                )
+            )
 
             epoch += 1
 
         if X_val is not None:
-            print('Optimal epoch is {0} ({1:.6f})'.format(self.n_opt,
-                                                          self.auc_opt))
+            print("Optimal epoch is {0} ({1:.6f})".format(self.n_opt, self.auc_opt))
             self.w = self.w_opt
 
-        logger.info('done training')
+        logger.info("done training")
 
     def predict(self, X):
         """Predict targets for a feature matrix.
@@ -165,7 +172,7 @@ def predict(self, X):
         Returns:
             prediction (np.array)
         """
-        logger.info('predicting ...')
+        logger.info("predicting ...")
         ps = self.predict_raw(X)
 
         return sigm(ps[:, 0])
@@ -178,21 +185,21 @@ def predict_raw(self, X):
         """
         # b -- bias for the input and h layers
         b = np.ones((X.shape[0], 1))
-        w2 = self.w[-(self.h + 1):].reshape(self.h + 1, 1)
-        w1 = self.w[:-(self.h + 1)].reshape(self.i + 1, self.h)
+        w2 = self.w[-(self.h + 1) :].reshape(self.h + 1, 1)
+        w1 = self.w[: -(self.h + 1)].reshape(self.i + 1, self.h)
 
         # Make X to have the same number of columns as self.i.
         # Because of the sparse matrix representation, X for prediction can
         # have a different number of columns.
         if X.shape[1] > self.i:
             # If X has more columns, cut extra columns.
-            X = X[:, :self.i]
+            X = X[:, : self.i]
         elif X.shape[1] < self.i:
             # If X has less columns, cut the rows of the weight matrix between
             # the input and h layers instead of X itself because the SciPy
             # sparse matrix does not support .set_shape() yet.
             idx = range(X.shape[1])
-            idx.append(self.i)        # Include the last row for the bias
+            idx.append(self.i)  # Include the last row for the bias
             w1 = w1[idx, :]
 
         if sparse.issparse(X):
@@ -233,15 +240,19 @@ def func(self, w, *args):
         # Predict for features -- cannot use predict_raw() because here
         # different weights can be used.
         if sparse.issparse(x0):
-            p0 = np.hstack((sigm(sparse.hstack((x0, b0)).dot(w[:-h1].reshape(
-                               i1, h))), b0)).dot(w[-h1:].reshape(h1, 1))
-            p1 = np.hstack((sigm(sparse.hstack((x1, b1)).dot(w[:-h1].reshape(
-                               i1, h))), b1)).dot(w[-h1:].reshape(h1, 1))
+            p0 = np.hstack(
+                (sigm(sparse.hstack((x0, b0)).dot(w[:-h1].reshape(i1, h))), b0)
+            ).dot(w[-h1:].reshape(h1, 1))
+            p1 = np.hstack(
+                (sigm(sparse.hstack((x1, b1)).dot(w[:-h1].reshape(i1, h))), b1)
+            ).dot(w[-h1:].reshape(h1, 1))
         else:
-            p0 = np.hstack((sigm(np.hstack((x0, b0)).dot(w[:-h1].reshape(
-                               i1, h))), b0)).dot(w[-h1:].reshape(h1, 1))
-            p1 = np.hstack((sigm(np.hstack((x1, b1)).dot(w[:-h1].reshape(
-                               i1, h))), b1)).dot(w[-h1:].reshape(h1, 1))
+            p0 = np.hstack(
+                (sigm(np.hstack((x0, b0)).dot(w[:-h1].reshape(i1, h))), b0)
+            ).dot(w[-h1:].reshape(h1, 1))
+            p1 = np.hstack(
+                (sigm(np.hstack((x1, b1)).dot(w[:-h1].reshape(i1, h))), b1)
+            ).dot(w[-h1:].reshape(h1, 1))
 
         p0 = p0[idx0]
         p1 = p1[idx1]
@@ -249,9 +260,11 @@ def func(self, w, *args):
         # Return the cost that consists of the sum of squared error +
         # L2-regularization for weights between the input and h layers +
         # L2-regularization for weights between the h and output layers.
-        return .5 * (sum((1 - p1 + p0) ** 2) / n +
-                     self.l1 * sum(w[:-h1] ** 2) / (i1 * h) +
-                     self.l2 * sum(w[-h1:] ** 2) / h1)
+        return 0.5 * (
+            sum((1 - p1 + p0) ** 2) / n
+            + self.l1 * sum(w[:-h1] ** 2) / (i1 * h)
+            + self.l2 * sum(w[-h1:] ** 2) / h1
+        )
 
     def fprime(self, w, *args):
         """Return the derivatives of the cost function for predictions.
@@ -308,9 +321,10 @@ def fprime(self, w, *args):
         # Calculate the derivative of the cost function w.r.t. F and w2 where:
         # F -- weights between the input and h layers
         # w2 -- weights between the h and output layers
-        dw1 = -(xb1.T.dot(dy.dot(w2[:-1].reshape(1, h)) * dsigm(xb1.dot(w1))) -
-                xb0.T.dot(dy.dot(w2[:-1].reshape(1, h)) * dsigm(xb0.dot(w1)))
-                ).reshape(i1 * h) + self.l1 * w[:-h1] / (i1 * h)
+        dw1 = -(
+            xb1.T.dot(dy.dot(w2[:-1].reshape(1, h)) * dsigm(xb1.dot(w1)))
+            - xb0.T.dot(dy.dot(w2[:-1].reshape(1, h)) * dsigm(xb0.dot(w1)))
+        ).reshape(i1 * h) + self.l1 * w[:-h1] / (i1 * h)
         dw2 = -(z1 - z0).T.dot(dy).reshape(h1) + self.l2 * w[-h1:] / h1
 
         return np.append(dw1, dw2)
diff --git a/kaggler/online_model/__init__.py b/kaggler/online_model/__init__.py
index db002df..cf3439a 100644
--- a/kaggler/online_model/__init__.py
+++ b/kaggler/online_model/__init__.py
@@ -6,5 +6,4 @@
 from .classification_tree import ClassificationTree
 
 
-__all__ = ['FTRL', 'FM', 'NN', 'NN_H2', 'SGD',
-           'ClassificationTree']
+__all__ = ["FTRL", "FM", "NN", "NN_H2", "SGD", "ClassificationTree"]
diff --git a/kaggler/online_model/classification_tree.py b/kaggler/online_model/classification_tree.py
index f76b8aa..f082f1c 100644
--- a/kaggler/online_model/classification_tree.py
+++ b/kaggler/online_model/classification_tree.py
@@ -3,12 +3,13 @@
 
 
 class ClassificationTree(Tree):
-
-    def __init__(self,
-                 number_of_features,
-                 number_of_functions=10,
-                 min_sample_split=200,
-                 predict_initialize={'count_dict': {}}):
+    def __init__(
+        self,
+        number_of_features,
+        number_of_functions=10,
+        min_sample_split=200,
+        predict_initialize={"count_dict": {}},
+    ):
 
         # Constant values
         self.number_of_features = number_of_features
@@ -29,28 +30,28 @@ def _calculate_split_score(self, split):
 
         score = current_error - after_split_error
         """
-        left_error = gini(split['left'])
-        right_error = gini(split['right'])
+        left_error = gini(split["left"])
+        right_error = gini(split["right"])
         error = gini(self.Y)
         # if the split is any good, the score should be greater than 0
         total = float(len(self.Y))
-        score = (error - 1 / total * (len(split['left']) * left_error +
-                                      len(split['right']) * right_error))
+        score = error - 1 / total * (
+            len(split["left"]) * left_error + len(split["right"]) * right_error
+        )
         return score
 
     def _apply_best_split(self):
         best_split, best_split_score = self._find_best_split()
         if best_split_score > 0:
-            self.criterion = lambda x: x[best_split['feature']] \
-                             > best_split['value']
+            self.criterion = lambda x: x[best_split["feature"]] > best_split["value"]
             # create the left child
             self.left = ClassificationTree(
                 number_of_features=self.number_of_features,
                 number_of_functions=self.number_of_functions,
                 min_sample_split=self.min_sample_split,
                 predict_initialize={
-                    'count_dict': count_dict(best_split['left']),
-                }
+                    "count_dict": count_dict(best_split["left"]),
+                },
             )
             # create the right child
             self.right = ClassificationTree(
@@ -58,8 +59,8 @@ def _apply_best_split(self):
                 number_of_functions=self.number_of_functions,
                 min_sample_split=self.min_sample_split,
                 predict_initialize={
-                    'count_dict': count_dict(best_split['right']),
-                }
+                    "count_dict": count_dict(best_split["right"]),
+                },
             )
             # Collect garbage
             self.samples = {}
@@ -71,7 +72,7 @@ def predict(self, x):
         node and the statistics inherited from parent.
         """
         if self._is_leaf():
-            d1 = self.predict_initialize['count_dict']
+            d1 = self.predict_initialize["count_dict"]
             d2 = count_dict(self.Y)
             for key, value in d1.items():
                 if key in d2:
diff --git a/kaggler/preprocessing/__init__.py b/kaggler/preprocessing/__init__.py
index f0fde21..04cb150 100644
--- a/kaggler/preprocessing/__init__.py
+++ b/kaggler/preprocessing/__init__.py
@@ -1,6 +1,21 @@
 from .autoencoder import DAE, SDAE
-from .categorical import OneHotEncoder, LabelEncoder, TargetEncoder, EmbeddingEncoder, FrequencyEncoder
+from .categorical import (
+    OneHotEncoder,
+    LabelEncoder,
+    TargetEncoder,
+    EmbeddingEncoder,
+    FrequencyEncoder,
+)
 from .numerical import Normalizer, QuantileEncoder
 
-__all__ = ['DAE', 'SDAE', 'OneHotEncoder', 'LabelEncoder', 'TargetEncoder', 'EmbeddingEncoder',
-           'Normalizer', 'QuantileEncoder', 'FrequencyEncoder']
+__all__ = [
+    "DAE",
+    "SDAE",
+    "OneHotEncoder",
+    "LabelEncoder",
+    "TargetEncoder",
+    "EmbeddingEncoder",
+    "Normalizer",
+    "QuantileEncoder",
+    "FrequencyEncoder",
+]
diff --git a/kaggler/preprocessing/autoencoder.py b/kaggler/preprocessing/autoencoder.py
index c0f508c..c1f49a9 100644
--- a/kaggler/preprocessing/autoencoder.py
+++ b/kaggler/preprocessing/autoencoder.py
@@ -6,7 +6,15 @@
 import tensorflow as tf
 from tensorflow.keras import Input, Model, backend as K
 from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
-from tensorflow.keras.layers import Concatenate, Dense, Dropout, Embedding, Layer, Reshape, GaussianNoise
+from tensorflow.keras.layers import (
+    Concatenate,
+    Dense,
+    Dropout,
+    Embedding,
+    Layer,
+    Reshape,
+    GaussianNoise,
+)
 from tensorflow.keras.losses import mean_squared_error
 from tensorflow.keras.optimizers import Adam
 from tensorflow.python.keras.utils import control_flow_util
@@ -39,8 +47,7 @@ def call(self, inputs, training=True):
 
     def get_config(self):
         config = super().get_config().copy()
-        config.update({'probs': self.probs,
-                       'seed': self.seed})
+        config.update({"probs": self.probs, "seed": self.seed})
 
 
 @tf.keras.utils.register_keras_serializable()
@@ -52,16 +59,16 @@ def call(self, inputs, training=True):
             training = K.learning_phase()
 
         def mask_inputs():
-            mask = tf.random.stateless_binomial(shape=tf.shape(inputs),
-                                                seed=self.seed,
-                                                counts=tf.ones((tf.shape(inputs)[1],)),
-                                                probs=self.probs)
+            mask = tf.random.stateless_binomial(
+                shape=tf.shape(inputs),
+                seed=self.seed,
+                counts=tf.ones((tf.shape(inputs)[1],)),
+                probs=self.probs,
+            )
 
             return tf.where(mask == 1, tf.zeros_like(inputs), inputs)
 
-        outputs = control_flow_util.smart_cond(training,
-                                               mask_inputs,
-                                               lambda: inputs)
+        outputs = control_flow_util.smart_cond(training, mask_inputs, lambda: inputs)
 
         return outputs
 
@@ -75,20 +82,25 @@ def call(self, inputs, training=True):
             training = K.learning_phase()
 
         def mask_inputs():
-            mask = tf.random.stateless_binomial(shape=tf.shape(inputs),
-                                                seed=self.seed,
-                                                counts=tf.ones((tf.shape(inputs)[1],)),
-                                                probs=self.probs)
+            mask = tf.random.stateless_binomial(
+                shape=tf.shape(inputs),
+                seed=self.seed,
+                counts=tf.ones((tf.shape(inputs)[1],)),
+                probs=self.probs,
+            )
 
             # tf.random.shuffle() without tf.gather() doesn't work in a custom layer
             # ref: https://github.com/tensorflow/tensorflow/issues/6269#issuecomment-465850464
-            return tf.where(mask == 1,
-                            tf.gather(inputs, tf.random.shuffle(tf.range(tf.shape(inputs)[0]), seed=self.seed[0])),
-                            inputs)
+            return tf.where(
+                mask == 1,
+                tf.gather(
+                    inputs,
+                    tf.random.shuffle(tf.range(tf.shape(inputs)[0]), seed=self.seed[0]),
+                ),
+                inputs,
+            )
 
-        outputs = control_flow_util.smart_cond(training,
-                                               mask_inputs,
-                                               lambda: inputs)
+        outputs = control_flow_util.smart_cond(training, mask_inputs, lambda: inputs)
 
         return outputs
 
@@ -97,8 +109,16 @@ def mask_inputs():
 class DAELayer(Layer):
     """A DAE layer with one pair of the encoder and decoder."""
 
-    def __init__(self, encoding_dim=128, n_encoder=1, noise_std=.0, swap_prob=.2, mask_prob=.0, seed=42,
-                 **kwargs):
+    def __init__(
+        self,
+        encoding_dim=128,
+        n_encoder=1,
+        noise_std=0.0,
+        swap_prob=0.2,
+        mask_prob=0.0,
+        seed=42,
+        **kwargs,
+    ):
         """Initialize a DAE (Denoising AutoEncoder) layer.
 
         Args:
@@ -119,12 +139,16 @@ def __init__(self, encoding_dim=128, n_encoder=1, noise_std=.0, swap_prob=.2, ma
         self.mask_prob = mask_prob
         self.seed = seed
 
-        self.encoders = [Dense(encoding_dim, activation='relu', name=f'{self.name}_encoder_{i}')
-                         for i in range(self.n_encoder)]
+        self.encoders = [
+            Dense(encoding_dim, activation="relu", name=f"{self.name}_encoder_{i}")
+            for i in range(self.n_encoder)
+        ]
 
     def build(self, input_shape):
         self.input_dim = input_shape[-1]
-        self.decoder = Dense(self.input_dim, activation='linear', name=f'{self.name}_decoder')
+        self.decoder = Dense(
+            self.input_dim, activation="linear", name=f"{self.name}_decoder"
+        )
 
     def call(self, inputs, training):
         if training is None:
@@ -136,12 +160,14 @@ def call(self, inputs, training):
                 masked_inputs = GaussianNoise(self.noise_std)(masked_inputs)
 
             if self.swap_prob > 0:
-                masked_inputs = SwapNoiseMasker(probs=[self.swap_prob] * self.input_dim,
-                                                seed=[self.seed] * 2)(masked_inputs)
+                masked_inputs = SwapNoiseMasker(
+                    probs=[self.swap_prob] * self.input_dim, seed=[self.seed] * 2
+                )(masked_inputs)
 
             if self.mask_prob > 0:
-                masked_inputs = ZeroNoiseMasker(probs=[self.mask_prob] * self.input_dim,
-                                                seed=[self.seed] * 2)(masked_inputs)
+                masked_inputs = ZeroNoiseMasker(
+                    probs=[self.mask_prob] * self.input_dim, seed=[self.seed] * 2
+                )(masked_inputs)
 
         x = masked_inputs
         encoded_list = []
@@ -149,7 +175,9 @@ def call(self, inputs, training):
             x = encoder(x)
             encoded_list.append(x)
 
-        encoded = Concatenate()(encoded_list) if len(encoded_list) > 1 else encoded_list[0]
+        encoded = (
+            Concatenate()(encoded_list) if len(encoded_list) > 1 else encoded_list[0]
+        )
         decoded = self.decoder(encoded)
 
         rec_loss = K.mean(mean_squared_error(inputs, decoded))
@@ -159,21 +187,42 @@ def call(self, inputs, training):
 
     def get_config(self):
         config = super().get_config().copy()
-        config.update({'encoding_dim': self.encoding_dim,
-                       'n_encoder': self.n_encoder,
-                       'noise_std': self.noise_std,
-                       'swap_prob': self.swap_prob,
-                       'mask_prob': self.mask_prob,
-                       'random_state': self.seed})
+        config.update(
+            {
+                "encoding_dim": self.encoding_dim,
+                "n_encoder": self.n_encoder,
+                "noise_std": self.noise_std,
+                "swap_prob": self.swap_prob,
+                "mask_prob": self.mask_prob,
+                "random_state": self.seed,
+            }
+        )
 
 
 class DAE(base.BaseEstimator):
     """Denoising AutoEncoder feature transformer."""
 
-    def __init__(self, cat_cols=[], num_cols=[], embedding_dims=[], encoding_dim=128, n_layer=1, n_encoder=1,
-                 noise_std=.0, swap_prob=.2, mask_prob=.0, dropout=.2, min_obs=1, n_epoch=10, batch_size=1024,
-                 learning_rate=.004, random_state=42, label_encoding=True, pretrained_model=None,
-                 freeze_embedding=True):
+    def __init__(
+        self,
+        cat_cols=[],
+        num_cols=[],
+        embedding_dims=[],
+        encoding_dim=128,
+        n_layer=1,
+        n_encoder=1,
+        noise_std=0.0,
+        swap_prob=0.2,
+        mask_prob=0.0,
+        dropout=0.2,
+        min_obs=1,
+        n_epoch=10,
+        batch_size=1024,
+        learning_rate=0.004,
+        random_state=42,
+        label_encoding=True,
+        pretrained_model=None,
+        freeze_embedding=True,
+    ):
         """Initialize a DAE (Denoising AutoEncoder) class object.
 
         Args:
@@ -209,7 +258,7 @@ def __init__(self, cat_cols=[], num_cols=[], embedding_dims=[], encoding_dim=128
                 assert len(cat_cols) == len(embedding_dims)
                 self.embedding_dims = embedding_dims
         else:
-            raise ValueError('embedding_dims should be int or list')
+            raise ValueError("embedding_dims should be int or list")
         self.input_dims = [None] * len(self.embedding_dims)
 
         assert (encoding_dim > 0) and (n_layer > 0) and (n_encoder > 0)
@@ -217,7 +266,12 @@ def __init__(self, cat_cols=[], num_cols=[], embedding_dims=[], encoding_dim=128
         self.n_layer = n_layer
         self.n_encoder = n_encoder
 
-        assert (0. <= noise_std) and (0. <= swap_prob < 1.) and (0. <= mask_prob < 1.) and (0. <= dropout < 1.)
+        assert (
+            (0.0 <= noise_std)
+            and (0.0 <= swap_prob < 1.0)
+            and (0.0 <= mask_prob < 1.0)
+            and (0.0 <= dropout < 1.0)
+        )
         self.noise_std = noise_std
         self.swap_prob = swap_prob
         self.mask_prob = mask_prob
@@ -288,16 +342,23 @@ def build_dae(self, input_dims: List[int]):
 
         if self.cat_cols:
             for i, col in enumerate(self.cat_cols):
-                assert self.input_dims[i] or input_dims[i], f"{i}th input_dim is neither set nor passed."
+                assert (
+                    self.input_dims[i] or input_dims[i]
+                ), f"{i}th input_dim is neither set nor passed."
                 if not self.input_dims[i]:
                     self.input_dims[i] = input_dims[i]
 
                 if not self.embedding_dims[i]:
-                    self.embedding_dims[i] = max(MIN_EMBEDDING, 2 * int(np.log2(self.input_dims[i])))
+                    self.embedding_dims[i] = max(
+                        MIN_EMBEDDING, 2 * int(np.log2(self.input_dims[i]))
+                    )
 
                 inp = Input(shape=(1,), name=col)
-                emb = Embedding(input_dim=self.input_dims[i], output_dim=self.embedding_dims[i],
-                                name=col + EMBEDDING_SUFFIX)(inp)
+                emb = Embedding(
+                    input_dim=self.input_dims[i],
+                    output_dim=self.embedding_dims[i],
+                    name=col + EMBEDDING_SUFFIX,
+                )(inp)
                 emb = Dropout(self.dropout)(emb)
                 emb = Reshape((self.embedding_dims[i],))(emb)
 
@@ -305,24 +366,36 @@ def build_dae(self, input_dims: List[int]):
                 embeddings.append(emb)
 
         if self.num_cols:
-            num_inputs = Input(shape=(len(self.num_cols),), name='num_inputs')
-            merged_inputs = Concatenate()(embeddings + [num_inputs]) if embeddings else num_inputs
+            num_inputs = Input(shape=(len(self.num_cols),), name="num_inputs")
+            merged_inputs = (
+                Concatenate()(embeddings + [num_inputs]) if embeddings else num_inputs
+            )
 
             inputs = inputs + [num_inputs]
         else:
-            merged_inputs = Concatenate()(embeddings) if len(embeddings) > 1 else embeddings[0]
+            merged_inputs = (
+                Concatenate()(embeddings) if len(embeddings) > 1 else embeddings[0]
+            )
 
         dae_layers = []
         for i in range(self.n_layer):
-            dae_layers.append(DAELayer(encoding_dim=self.encoding_dim, n_encoder=self.n_encoder,
-                                       noise_std=self.noise_std, swap_prob=self.swap_prob, mask_prob=self.mask_prob,
-                                       seed=self.seed, name=f'dae_layer_{i}'))
+            dae_layers.append(
+                DAELayer(
+                    encoding_dim=self.encoding_dim,
+                    n_encoder=self.n_encoder,
+                    noise_std=self.noise_std,
+                    swap_prob=self.swap_prob,
+                    mask_prob=self.mask_prob,
+                    seed=self.seed,
+                    name=f"dae_layer_{i}",
+                )
+            )
 
             encoded, decoded = dae_layers[i](merged_inputs)
             _, merged_inputs = dae_layers[i](merged_inputs, training=False)
 
-        self.encoder = Model(inputs=inputs, outputs=encoded, name='encoder_model')
-        self.dae = Model(inputs=inputs, outputs=decoded, name='decoder_model')
+        self.encoder = Model(inputs=inputs, outputs=encoded, name="encoder_model")
+        self.dae = Model(inputs=inputs, outputs=decoded, name="decoder_model")
         return encoded, inputs
 
     def fit(self, X, y=None, validation_data=None):
@@ -366,21 +439,36 @@ def fit(self, X, y=None, validation_data=None):
             if self.num_cols:
                 features_val += [X_val[self.num_cols].values]
 
-        es = EarlyStopping(monitor='val_loss', min_delta=.0, patience=5, verbose=1, mode='min',
-                           baseline=None, restore_best_weights=True)
-        rlr = ReduceLROnPlateau(monitor='val_loss', factor=.5, patience=3, min_lr=1e-6, mode='min')
+        es = EarlyStopping(
+            monitor="val_loss",
+            min_delta=0.0,
+            patience=5,
+            verbose=1,
+            mode="min",
+            baseline=None,
+            restore_best_weights=True,
+        )
+        rlr = ReduceLROnPlateau(
+            monitor="val_loss", factor=0.5, patience=3, min_lr=1e-6, mode="min"
+        )
         if validation_data is None:
-            self.model.fit(x=features, y=y,
-                           epochs=self.n_epoch,
-                           validation_split=.2,
-                           batch_size=self.batch_size,
-                           callbacks=[es, rlr])
+            self.model.fit(
+                x=features,
+                y=y,
+                epochs=self.n_epoch,
+                validation_split=0.2,
+                batch_size=self.batch_size,
+                callbacks=[es, rlr],
+            )
         else:
-            self.model.fit(x=features, y=y,
-                           epochs=self.n_epoch,
-                           validation_data=(features_val, y_val),
-                           batch_size=self.batch_size,
-                           callbacks=[es, rlr])
+            self.model.fit(
+                x=features,
+                y=y,
+                epochs=self.n_epoch,
+                validation_data=(features_val, y_val),
+                batch_size=self.batch_size,
+                callbacks=[es, rlr],
+            )
 
     def transform(self, X):
         """Encode features using the DAE trained
@@ -419,11 +507,31 @@ def fit_transform(self, X, y=None, validation_data=None):
 class SDAE(DAE):
     """Supervised Denoising AutoEncoder feature transformer."""
 
-    def __init__(self, cat_cols=[], num_cols=[], embedding_dims=[], encoding_dim=128, n_layer=1, n_encoder=1,
-                 noise_std=.0, swap_prob=.2, mask_prob=.0, dropout=.2, min_obs=1, n_epoch=10, batch_size=1024,
-                 learning_rate=.004, random_state=42, label_encoding=True, pretrained_model=None, freeze_embedding=True,
-                 n_class=None, output_layer_size=1024, output_activation='sigmoid',
-                 output_loss='binary_classification'):
+    def __init__(
+        self,
+        cat_cols=[],
+        num_cols=[],
+        embedding_dims=[],
+        encoding_dim=128,
+        n_layer=1,
+        n_encoder=1,
+        noise_std=0.0,
+        swap_prob=0.2,
+        mask_prob=0.0,
+        dropout=0.2,
+        min_obs=1,
+        n_epoch=10,
+        batch_size=1024,
+        learning_rate=0.004,
+        random_state=42,
+        label_encoding=True,
+        pretrained_model=None,
+        freeze_embedding=True,
+        n_class=None,
+        output_layer_size=1024,
+        output_activation="sigmoid",
+        output_loss="binary_classification",
+    ):
 
         # If None, it will be set in fit()
         self.n_class = n_class
@@ -431,24 +539,43 @@ def __init__(self, cat_cols=[], num_cols=[], embedding_dims=[], encoding_dim=128
         self.output_activation = output_activation
         self.output_loss = output_loss
 
-        super().__init__(cat_cols=cat_cols, num_cols=num_cols, embedding_dims=embedding_dims, encoding_dim=encoding_dim,
-                         n_layer=n_layer, n_encoder=n_encoder, noise_std=noise_std, swap_prob=swap_prob,
-                         mask_prob=mask_prob, dropout=dropout, min_obs=min_obs, n_epoch=n_epoch, batch_size=batch_size,
-                         learning_rate=learning_rate, random_state=random_state, label_encoding=label_encoding,
-                         pretrained_model=pretrained_model, freeze_embedding=freeze_embedding)
+        super().__init__(
+            cat_cols=cat_cols,
+            num_cols=num_cols,
+            embedding_dims=embedding_dims,
+            encoding_dim=encoding_dim,
+            n_layer=n_layer,
+            n_encoder=n_encoder,
+            noise_std=noise_std,
+            swap_prob=swap_prob,
+            mask_prob=mask_prob,
+            dropout=dropout,
+            min_obs=min_obs,
+            n_epoch=n_epoch,
+            batch_size=batch_size,
+            learning_rate=learning_rate,
+            random_state=random_state,
+            label_encoding=label_encoding,
+            pretrained_model=pretrained_model,
+            freeze_embedding=freeze_embedding,
+        )
 
     def build_model(self, X, y=None):
         if self.pretrained_model is None:
             encoded, inputs = self.build_dae(X.nunique().tolist())
         else:
-            encoded, inputs = self.load_dae(self.pretrained_model, self.freeze_embedding)
+            encoded, inputs = self.load_dae(
+                self.pretrained_model, self.freeze_embedding
+            )
 
         # supervised head
-        x = Dense(self.output_layer_size, 'relu')(encoded)
+        x = Dense(self.output_layer_size, "relu")(encoded)
         x = Dropout(self.dropout)(x)
         supervised_outputs = Dense(self.n_class, activation=self.output_activation)(x)
 
-        self.model = Model(inputs=inputs, outputs=supervised_outputs, name='supervised_model')
+        self.model = Model(
+            inputs=inputs, outputs=supervised_outputs, name="supervised_model"
+        )
         self.model.compile(optimizer=Adam(self.learning_rate), loss=self.output_loss)
 
     def fit(self, X, y, validation_data=None):
@@ -462,22 +589,22 @@ def fit(self, X, y, validation_data=None):
         Returns:
             None
         """
-        assert y is not None, 'SDAE needs y (target variable) for fit()'
+        assert y is not None, "SDAE needs y (target variable) for fit()"
 
         if self.n_class is None:
             if y.dtype in [np.int32, np.int64]:
                 n_class = len(np.unique(y))
                 if n_class == 2:
                     self.n_class = 1
-                    self.output_activation = 'sigmoid'
-                    self.output_loss = 'binary_crossentropy'
+                    self.output_activation = "sigmoid"
+                    self.output_loss = "binary_crossentropy"
                 elif n_class > 2:
                     self.n_class = n_class
-                    self.output_activation = 'softmax'
-                    self.output_loss = 'sparse_categorical_crossentropy'
+                    self.output_activation = "softmax"
+                    self.output_loss = "sparse_categorical_crossentropy"
             else:
                 self.n_class = 1
-                self.output_activation = 'linear'
-                self.output_loss = 'mean_squared_error'
+                self.output_activation = "linear"
+                self.output_loss = "mean_squared_error"
 
         super().fit(X, y, validation_data)
diff --git a/kaggler/preprocessing/categorical.py b/kaggler/preprocessing/categorical.py
index bb5cb10..739f22f 100644
--- a/kaggler/preprocessing/categorical.py
+++ b/kaggler/preprocessing/categorical.py
@@ -5,7 +5,15 @@
 from sklearn.model_selection import KFold
 import pandas as pd
 from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
-from tensorflow.keras.layers import Embedding, Dense, Dropout, Input, Reshape, Concatenate, BatchNormalization
+from tensorflow.keras.layers import (
+    Embedding,
+    Dense,
+    Dropout,
+    Input,
+    Reshape,
+    Concatenate,
+    BatchNormalization,
+)
 from tensorflow.keras.metrics import AUC
 from tensorflow.keras.models import Model
 from tensorflow.keras.optimizers import Adam
@@ -37,7 +45,7 @@ def __init__(self, min_obs=10):
         self.is_fitted = False
 
     def __repr__(self):
-        return ('LabelEncoder(min_obs={})').format(self.min_obs)
+        return ("LabelEncoder(min_obs={})").format(self.min_obs)
 
     def _get_label_encoder_and_max(self, x):
         """Return a mapping from values and its maximum of a column to integer labels.
@@ -64,8 +72,9 @@ def _get_label_encoder_and_max(self, x):
         # that appear less than min_obs.
         offset = 0 if n_uniq == n_uniq_new else 1
 
-        label_encoder = pd.Series(np.arange(n_uniq_new) + offset,
-                                  index=label_count.index)
+        label_encoder = pd.Series(
+            np.arange(n_uniq_new) + offset, index=label_count.index
+        )
         max_label = label_encoder.max()
         label_encoder = label_encoder.to_dict()
 
@@ -88,8 +97,10 @@ def fit(self, X, y=None):
         self.label_maxes = [None] * X.shape[1]
 
         for i, col in enumerate(X.columns):
-            self.label_encoders[i], self.label_maxes[i] = \
-                self._get_label_encoder_and_max(X[col])
+            (
+                self.label_encoders[i],
+                self.label_maxes[i],
+            ) = self._get_label_encoder_and_max(X[col])
 
         self.is_fitted = True
         return self
@@ -104,7 +115,9 @@ def transform(self, X):
             (pandas.DataFrame): label encoded columns
         """
 
-        assert self.is_fitted, "fit() or fit_transform() must be called before transform()."
+        assert (
+            self.is_fitted
+        ), "fit() or fit_transform() must be called before transform()."
 
         X = X.copy()
         for i, col in enumerate(X.columns):
@@ -127,12 +140,14 @@ def fit_transform(self, X, y=None):
 
         X = X.copy()
         for i, col in enumerate(X.columns):
-            self.label_encoders[i], self.label_maxes[i] = \
-                self._get_label_encoder_and_max(X[col])
+            (
+                self.label_encoders[i],
+                self.label_maxes[i],
+            ) = self._get_label_encoder_and_max(X[col])
 
-            X.loc[:, col] = (X[col].fillna(NAN_INT)
-                             .map(self.label_encoders[i])
-                             .fillna(0).astype(int))
+            X.loc[:, col] = (
+                X[col].fillna(NAN_INT).map(self.label_encoders[i]).fillna(0).astype(int)
+            )
 
         self.is_fitted = True
         return X
@@ -161,7 +176,7 @@ def __init__(self, min_obs=10):
         self.is_fitted = False
 
     def __repr__(self):
-        return ('OneHotEncoder(min_obs={})').format(self.min_obs)
+        return ("OneHotEncoder(min_obs={})").format(self.min_obs)
 
     def _transform_col(self, x, i):
         """Encode one categorical column into sparse matrix with one-hot-encoding.
@@ -184,8 +199,9 @@ def _transform_col(self, x, i):
         j = labels[labels > 0] - 1  # column index starts from 0
 
         if len(i) > 0:
-            return sparse.coo_matrix((np.ones_like(i), (i, j)),
-                                     shape=(x.shape[0], label_max))
+            return sparse.coo_matrix(
+                (np.ones_like(i), (i, j)), shape=(x.shape[0], label_max)
+            )
         else:
             # if there is no non-zero value, return no matrix
             return None
@@ -206,7 +222,9 @@ def transform(self, X):
             (scipy.sparse.coo_matrix): sparse matrix encoding categorical
                                        variables into dummy variables
         """
-        assert self.is_fitted, "fit() or fit_transform() must be called before transform()."
+        assert (
+            self.is_fitted
+        ), "fit() or fit_transform() must be called before transform()."
 
         for i, col in enumerate(X.columns):
             X_col = self._transform_col(X[col], i)
@@ -216,8 +234,8 @@ def transform(self, X):
                 else:
                     X_new = sparse.hstack((X_new, X_col))
 
-            logger.debug('{} --> {} features'.format(
-                col, self.label_encoder.label_maxes[i])
+            logger.debug(
+                "{} --> {} features".format(col, self.label_encoder.label_maxes[i])
             )
 
         return X_new
@@ -258,14 +276,18 @@ def __init__(self, smoothing=1, min_samples=10, cv=kfold):
             min_samples (int): minimum samples to take category average into account
             cv (sklearn.model_selection._BaseKFold, optional): sklearn CV object. default=KFold(5, True, 42)
         """
-        assert (min_samples >= 0) and (smoothing >= 0), 'min_samples and smoothing should be positive'
+        assert (min_samples >= 0) and (
+            smoothing >= 0
+        ), "min_samples and smoothing should be positive"
         self.smoothing = smoothing
         self.min_samples = min_samples
         self.cv = cv
         self.is_fitted = False
 
     def __repr__(self):
-        return('TargetEncoder(smoothing={}, min_samples={}, cv={})'.format(self.smoothing, self.min_samples, self.cv))
+        return "TargetEncoder(smoothing={}, min_samples={}, cv={})".format(
+            self.smoothing, self.min_samples, self.cv
+        )
 
     def _get_target_encoder(self, x, y):
         """Return a mapping from categories to average target values.
@@ -282,10 +304,18 @@ def _get_target_encoder(self, x, y):
 
         # NaN cannot be used as a key for dict. So replace it with a random
         # integer
-        mean_count = pd.DataFrame({y.name: y, x.name: x.fillna(NAN_INT)}).groupby(x.name)[y.name].agg(['mean', 'count'])
-        smoothing = 1 / (1 + np.exp(-(mean_count['count'] - self.min_samples) / self.smoothing))
-
-        mean_count[y.name] = self.target_mean * (1 - smoothing) + mean_count['mean'] * smoothing
+        mean_count = (
+            pd.DataFrame({y.name: y, x.name: x.fillna(NAN_INT)})
+            .groupby(x.name)[y.name]
+            .agg(["mean", "count"])
+        )
+        smoothing = 1 / (
+            1 + np.exp(-(mean_count["count"] - self.min_samples) / self.smoothing)
+        )
+
+        mean_count[y.name] = (
+            self.target_mean * (1 - smoothing) + mean_count["mean"] * smoothing
+        )
         return mean_count[y.name].to_dict()
 
     def fit(self, X, y):
@@ -308,7 +338,9 @@ def fit(self, X, y):
                 self.target_encoders[i] = []
                 for i_cv, (i_trn, i_val) in enumerate(self.cv.split(X[col], y), 1):
                     i_trn, i_val = X.index[i_trn], X.index[i_val]
-                    self.target_encoders[i].append(self._get_target_encoder(X.loc[i_trn, col], y.loc[i_trn]))
+                    self.target_encoders[i].append(
+                        self._get_target_encoder(X.loc[i_trn, col], y.loc[i_trn])
+                    )
 
         self.is_fitted = True
         return self
@@ -322,20 +354,35 @@ def transform(self, X):
         Returns:
             (pandas.DataFrame): encoded columns
         """
-        assert self.is_fitted, "fit() or fit_transform() must be called before transform()."
+        assert (
+            self.is_fitted
+        ), "fit() or fit_transform() must be called before transform()."
 
         X = X.copy()
         for i, col in enumerate(X.columns):
             if self.cv is None:
-                X.loc[:, col] = (X[col].fillna(NAN_INT)
-                                       .map(self.target_encoders[i])
-                                       .fillna(self.target_mean))
+                X.loc[:, col] = (
+                    X[col]
+                    .fillna(NAN_INT)
+                    .map(self.target_encoders[i])
+                    .fillna(self.target_mean)
+                )
             else:
                 for i_enc, target_encoder in enumerate(self.target_encoders[i], 1):
                     if i_enc == 1:
-                        x = X[col].fillna(NAN_INT).map(target_encoder).fillna(self.target_mean)
+                        x = (
+                            X[col]
+                            .fillna(NAN_INT)
+                            .map(target_encoder)
+                            .fillna(self.target_mean)
+                        )
                     else:
-                        x += X[col].fillna(NAN_INT).map(target_encoder).fillna(self.target_mean)
+                        x += (
+                            X[col]
+                            .fillna(NAN_INT)
+                            .map(target_encoder)
+                            .fillna(self.target_mean)
+                        )
 
                 X.loc[:, col] = x / i_enc
 
@@ -359,18 +406,26 @@ def fit_transform(self, X, y):
             if self.cv is None:
                 self.target_encoders[i] = self._get_target_encoder(X[col], y)
 
-                X.loc[:, col] = (X[col].fillna(NAN_INT)
-                                       .map(self.target_encoders[i])
-                                       .fillna(self.target_mean))
+                X.loc[:, col] = (
+                    X[col]
+                    .fillna(NAN_INT)
+                    .map(self.target_encoders[i])
+                    .fillna(self.target_mean)
+                )
             else:
                 self.target_encoders[i] = []
                 for i_cv, (i_trn, i_val) in enumerate(self.cv.split(X[col], y), 1):
                     i_trn, i_val = X.index[i_trn], X.index[i_val]
-                    target_encoder = self._get_target_encoder(X.loc[i_trn, col], y[i_trn])
+                    target_encoder = self._get_target_encoder(
+                        X.loc[i_trn, col], y[i_trn]
+                    )
 
-                    X.loc[i_val, col] = (X.loc[i_val, col].fillna(NAN_INT)
-                                                          .map(target_encoder)
-                                                          .fillna(y[i_trn].mean()))
+                    X.loc[i_val, col] = (
+                        X.loc[i_val, col]
+                        .fillna(NAN_INT)
+                        .map(target_encoder)
+                        .fillna(y[i_trn].mean())
+                    )
 
                     self.target_encoders[i].append(target_encoder)
 
@@ -385,8 +440,17 @@ class EmbeddingEncoder(base.BaseEstimator):
     at https://www.kaggle.com/abhishek/entity-embeddings-to-handle-categories
     """
 
-    def __init__(self, cat_cols, num_cols=[], n_emb=[], min_obs=10, n_epoch=10, batch_size=1024, cv=None,
-                 random_state=42):
+    def __init__(
+        self,
+        cat_cols,
+        num_cols=[],
+        n_emb=[],
+        min_obs=10,
+        n_epoch=10,
+        batch_size=1024,
+        cv=None,
+        random_state=42,
+    ):
         """Initialize an EmbeddingEncoder class object.
 
         Args:
@@ -411,7 +475,7 @@ def __init__(self, cat_cols, num_cols=[], n_emb=[], min_obs=10, n_epoch=10, batc
                 assert len(cat_cols) == len(n_emb)
                 self.n_emb = n_emb
         else:
-            raise ValueError('n_emb should be int or list')
+            raise ValueError("n_emb should be int or list")
 
         self.min_obs = min_obs
         self.n_epoch = n_epoch
@@ -435,15 +499,17 @@ def _get_model(X, cat_cols, num_cols, n_uniq, n_emb, output_activation):
                 n_emb[i] = max(MIN_EMBEDDING, 2 * int(np.log2(n_uniq[i])))
 
             _input = Input(shape=(1,), name=col)
-            _embed = Embedding(input_dim=n_uniq[i], output_dim=n_emb[i], name=col + EMBEDDING_SUFFIX)(_input)
-            _embed = Dropout(.2)(_embed)
+            _embed = Embedding(
+                input_dim=n_uniq[i], output_dim=n_emb[i], name=col + EMBEDDING_SUFFIX
+            )(_input)
+            _embed = Dropout(0.2)(_embed)
             _embed = Reshape((n_emb[i],))(_embed)
 
             inputs.append(_input)
             embeddings.append(_embed)
 
         if num_cols:
-            num_inputs = Input(shape=(len(num_cols),), name='num_inputs')
+            num_inputs = Input(shape=(len(num_cols),), name="num_inputs")
             merged_input = Concatenate(axis=1)(embeddings + [num_inputs])
 
             inputs = inputs + [num_inputs]
@@ -451,11 +517,11 @@ def _get_model(X, cat_cols, num_cols, n_uniq, n_emb, output_activation):
             merged_input = Concatenate(axis=1)(embeddings)
 
         x = BatchNormalization()(merged_input)
-        x = Dense(128, activation='relu')(x)
-        x = Dropout(.5)(x)
+        x = Dense(128, activation="relu")(x)
+        x = Dropout(0.5)(x)
         x = BatchNormalization()(x)
-        x = Dense(64, activation='relu')(x)
-        x = Dropout(.5)(x)
+        x = Dense(64, activation="relu")(x)
+        x = Dropout(0.5)(x)
         x = BatchNormalization()(x)
         output = Dense(1, activation=output_activation)(x)
 
@@ -477,18 +543,20 @@ def fit(self, X, y):
 
         X_cat = self.lbe.fit_transform(X[self.cat_cols])
         if is_classification:
-            assert np.isin(y, [0, 1]).all(), 'Target values should be 0 or 1 for classification.'
-            output_activation = 'sigmoid'
-            loss = 'binary_crossentropy'
+            assert np.isin(
+                y, [0, 1]
+            ).all(), "Target values should be 0 or 1 for classification."
+            output_activation = "sigmoid"
+            loss = "binary_crossentropy"
             metrics = [AUC()]
-            monitor = 'val_auc'
-            mode = 'max'
+            monitor = "val_auc"
+            mode = "max"
         else:
-            output_activation = 'linear'
-            loss = 'mse'
-            metrics = ['mse']
-            monitor = 'val_mse'
-            mode = 'min'
+            output_activation = "linear"
+            loss = "mse"
+            metrics = ["mse"]
+            monitor = "val_mse"
+            mode = "min"
 
         n_uniq = [X_cat[col].nunique() for col in self.cat_cols]
         if self.cv:
@@ -496,8 +564,14 @@ def fit(self, X, y):
             n_fold = self.cv.get_n_splits(X)
             for i_cv, (i_trn, i_val) in enumerate(self.cv.split(X, y), 1):
                 i_trn, i_val = X.index[i_trn], X.index[i_val]
-                model, self.n_emb, _ = self._get_model(X_cat, self.cat_cols, self.num_cols, n_uniq, self.n_emb,
-                                                       output_activation)
+                model, self.n_emb, _ = self._get_model(
+                    X_cat,
+                    self.cat_cols,
+                    self.num_cols,
+                    n_uniq,
+                    self.n_emb,
+                    output_activation,
+                )
                 model.compile(optimizer=Adam(lr=0.01), loss=loss, metrics=metrics)
 
                 features_trn = [X_cat.loc[i_trn, col] for col in self.cat_cols]
@@ -506,15 +580,26 @@ def fit(self, X, y):
                     features_trn += [X.loc[i_trn, self.num_cols]]
                     features_val += [X.loc[i_val, self.num_cols]]
 
-                es = EarlyStopping(monitor=monitor, min_delta=.001, patience=5, verbose=1, mode=mode,
-                                   baseline=None, restore_best_weights=True)
-                rlr = ReduceLROnPlateau(monitor=monitor, factor=.5, patience=3, min_lr=1e-6, mode=mode)
-                model.fit(x=features_trn,
-                          y=y[i_trn],
-                          validation_data=(features_val, y[i_val]),
-                          epochs=self.n_epoch,
-                          batch_size=self.batch_size,
-                          callbacks=[es, rlr])
+                es = EarlyStopping(
+                    monitor=monitor,
+                    min_delta=0.001,
+                    patience=5,
+                    verbose=1,
+                    mode=mode,
+                    baseline=None,
+                    restore_best_weights=True,
+                )
+                rlr = ReduceLROnPlateau(
+                    monitor=monitor, factor=0.5, patience=3, min_lr=1e-6, mode=mode
+                )
+                model.fit(
+                    x=features_trn,
+                    y=y[i_trn],
+                    validation_data=(features_val, y[i_val]),
+                    epochs=self.n_epoch,
+                    batch_size=self.batch_size,
+                    callbacks=[es, rlr],
+                )
 
                 for i_col, col in enumerate(self.cat_cols):
                     emb = model.get_layer(col + EMBEDDING_SUFFIX).get_weights()[0]
@@ -524,34 +609,55 @@ def fit(self, X, y):
                         self.embs[i_col] += emb / n_fold
 
         else:
-            model, self.n_emb, _ = self._get_model(X_cat, self.cat_cols, self.num_cols, n_uniq, self.n_emb,
-                                                   output_activation)
+            model, self.n_emb, _ = self._get_model(
+                X_cat,
+                self.cat_cols,
+                self.num_cols,
+                n_uniq,
+                self.n_emb,
+                output_activation,
+            )
             model.compile(optimizer=Adam(lr=0.01), loss=loss, metrics=metrics)
 
             features = [X_cat[col] for col in self.cat_cols]
             if self.num_cols:
                 features += [X[self.num_cols].values]
 
-            es = EarlyStopping(monitor=monitor, min_delta=.001, patience=5, verbose=1, mode=mode,
-                               baseline=None, restore_best_weights=True)
-            rlr = ReduceLROnPlateau(monitor=monitor, factor=.5, patience=3, min_lr=1e-6, mode=mode)
-            model.fit(x=features,
-                      y=y,
-                      epochs=self.n_epoch,
-                      validation_split=.2,
-                      batch_size=self.batch_size,
-                      callbacks=[es, rlr])
+            es = EarlyStopping(
+                monitor=monitor,
+                min_delta=0.001,
+                patience=5,
+                verbose=1,
+                mode=mode,
+                baseline=None,
+                restore_best_weights=True,
+            )
+            rlr = ReduceLROnPlateau(
+                monitor=monitor, factor=0.5, patience=3, min_lr=1e-6, mode=mode
+            )
+            model.fit(
+                x=features,
+                y=y,
+                epochs=self.n_epoch,
+                validation_split=0.2,
+                batch_size=self.batch_size,
+                callbacks=[es, rlr],
+            )
 
             self.embs = []
             for i, col in enumerate(self.cat_cols):
-                self.embs.append(model.get_layer(col + EMBEDDING_SUFFIX).get_weights()[0])
-                logger.debug('{}: {}'.format(col, self.embs[i].shape))
+                self.embs.append(
+                    model.get_layer(col + EMBEDDING_SUFFIX).get_weights()[0]
+                )
+                logger.debug("{}: {}".format(col, self.embs[i].shape))
 
         self.is_fitted = True
         return self
 
     def transform(self, X):
-        assert self.is_fitted, "fit() or fit_transform() must be called before transform()."
+        assert (
+            self.is_fitted
+        ), "fit() or fit_transform() must be called before transform()."
 
         X_cat = self.lbe.transform(X[self.cat_cols])
         X_emb = []
@@ -582,7 +688,7 @@ def __init__(self, cv=None):
         self.is_fitted = False
 
     def __repr__(self):
-        return('FrequencyEncoder(cv={})'.format(self.cv))
+        return "FrequencyEncoder(cv={})".format(self.cv)
 
     def _get_frequency_encoder(self, x):
         """Return a mapping from categories to frequency.
@@ -596,9 +702,9 @@ def _get_frequency_encoder(self, x):
 
         # NaN cannot be used as a key for dict. So replace it with a random
         # integer
-        df = pd.DataFrame({x.name: x.fillna('NaN')})
-        df[x.name + '_freq'] = df[x.name].map(df[x.name].value_counts())
-        return df.groupby(x.name)[x.name + '_freq'].size().to_dict()
+        df = pd.DataFrame({x.name: x.fillna("NaN")})
+        df[x.name + "_freq"] = df[x.name].map(df[x.name].value_counts())
+        return df.groupby(x.name)[x.name + "_freq"].size().to_dict()
 
     def fit(self, X, y=None):
         """Encode categorical columns into frequency.
@@ -618,7 +724,9 @@ def fit(self, X, y=None):
             else:
                 self.frequency_encoders[i] = []
                 for i_cv, (i_trn, i_val) in enumerate(self.cv.split(X[col]), 1):
-                    self.frequency_encoders[i].append(self._get_frequency_encoder(X.iloc[i_trn][col]))
+                    self.frequency_encoders[i].append(
+                        self._get_frequency_encoder(X.iloc[i_trn][col])
+                    )
 
         self.is_fitted = True
         return self
@@ -632,18 +740,24 @@ def transform(self, X):
         Returns:
             (pandas.DataFrame): encoded columns
         """
-        assert self.is_fitted, "fit() or fit_transform() must be called before transform()."
+        assert (
+            self.is_fitted
+        ), "fit() or fit_transform() must be called before transform()."
 
         X = X.copy()
         for i, col in enumerate(X.columns):
             if self.cv is None:
-                X.loc[:, col] = X[col].fillna('NaN').map(self.frequency_encoders[i]).fillna(0)
+                X.loc[:, col] = (
+                    X[col].fillna("NaN").map(self.frequency_encoders[i]).fillna(0)
+                )
             else:
-                for i_enc, frequency_encoder in enumerate(self.frequency_encoders[i], 1):
+                for i_enc, frequency_encoder in enumerate(
+                    self.frequency_encoders[i], 1
+                ):
                     if i_enc == 1:
-                        x = X[col].fillna('NaN').map(frequency_encoder).fillna(0)
+                        x = X[col].fillna("NaN").map(frequency_encoder).fillna(0)
                     else:
-                        x += X[col].fillna('NaN').map(frequency_encoder).fillna(0)
+                        x += X[col].fillna("NaN").map(frequency_encoder).fillna(0)
 
                 X.loc[:, col] = x / i_enc
 
@@ -663,14 +777,18 @@ def fit_transform(self, X, y=None):
             if self.cv is None:
                 self.frequency_encoders[i] = self._get_frequency_encoder(X[col])
 
-                X.loc[:, col] = X[col].fillna('NaN').map(self.frequency_encoders[i]).fillna(0)
+                X.loc[:, col] = (
+                    X[col].fillna("NaN").map(self.frequency_encoders[i]).fillna(0)
+                )
             else:
                 self.frequency_encoders[i] = []
                 for i_cv, (i_trn, i_val) in enumerate(self.cv.split(X[col]), 1):
                     i_trn, i_val = X.index[i_trn], X.index[i_val]
                     frequency_encoder = self._get_frequency_encoder(X.loc[i_trn, col])
 
-                    X.loc[i_val, col] = X.loc[i_val, col].fillna('NaN').map(frequency_encoder).fillna(0)
+                    X.loc[i_val, col] = (
+                        X.loc[i_val, col].fillna("NaN").map(frequency_encoder).fillna(0)
+                    )
                     self.frequency_encoders[i].append(frequency_encoder)
 
         self.is_fitted = True
diff --git a/kaggler/preprocessing/const.py b/kaggler/preprocessing/const.py
index 84fc564..435a3a9 100644
--- a/kaggler/preprocessing/const.py
+++ b/kaggler/preprocessing/const.py
@@ -1,3 +1,3 @@
 NAN_INT = 7535805
 MIN_EMBEDDING = 4
-EMBEDDING_SUFFIX = '_emb'
+EMBEDDING_SUFFIX = "_emb"
diff --git a/kaggler/preprocessing/numerical.py b/kaggler/preprocessing/numerical.py
index 25c4ff6..54a4c15 100644
--- a/kaggler/preprocessing/numerical.py
+++ b/kaggler/preprocessing/numerical.py
@@ -38,21 +38,20 @@ def fit(self, X, y=None):
         Returns:
             A trained QuantileEncoder object.
         """
+
         def _calculate_ecdf(x):
             return ECDF(x[~np.isnan(x)])
 
         if self.sample >= X.shape[0]:
             self.ecdfs = X.apply(_calculate_ecdf, axis=0)
         elif self.sample > 1:
-            self.ecdfs = X.sample(n=self.sample,
-                                  random_state=self.random_state).apply(
-                                      _calculate_ecdf, axis=0
-                                  )
+            self.ecdfs = X.sample(n=self.sample, random_state=self.random_state).apply(
+                _calculate_ecdf, axis=0
+            )
         else:
-            self.ecdfs = X.sample(frac=self.sample,
-                                  random_state=self.random_state).apply(
-                                      _calculate_ecdf, axis=0
-                                  )
+            self.ecdfs = X.sample(
+                frac=self.sample, random_state=self.random_state
+            ).apply(_calculate_ecdf, axis=0)
 
         self.is_fitted = True
         return self
@@ -79,7 +78,9 @@ def transform(self, X):
         Returns:
             Encoded features (pandas.DataFrame).
         """
-        assert self.is_fitted, "fit() or fit_transform() must be called before transform()."
+        assert (
+            self.is_fitted
+        ), "fit() or fit_transform() must be called before transform()."
 
         X = X.copy()
         for i, col in enumerate(X.columns):
@@ -101,8 +102,7 @@ def _transform_col(self, x, i):
         rv = np.ones_like(x) * -1
 
         filt = ~np.isnan(x)
-        rv[filt] = np.floor((self.ecdfs[i](x[filt]) * 0.998 + .001) *
-                            self.n_label)
+        rv[filt] = np.floor((self.ecdfs[i](x[filt]) * 0.998 + 0.001) * self.n_label)
 
         return rv
 
@@ -167,23 +167,22 @@ def _transform_col(self, x, col):
             A normalized feature vector.
         """
 
-        return norm.ppf(self.ecdfs[col](x.values) * .998 + .001)
+        return norm.ppf(self.ecdfs[col](x.values) * 0.998 + 0.001)
 
 
 class BandpassFilter(base.BaseEstimator):
-
-    def __init__(self, fs=10., lowcut=.5, highcut=3., order=3):
-        self.fs = 10.
-        self.lowcut = .5
-        self.highcut = 3.
+    def __init__(self, fs=10.0, lowcut=0.5, highcut=3.0, order=3):
+        self.fs = 10.0
+        self.lowcut = 0.5
+        self.highcut = 3.0
         self.order = 3
         self.b, self.a = self._butter_bandpass()
 
     def _butter_bandpass(self):
-        nyq = .5 * self.fs
+        nyq = 0.5 * self.fs
         low = self.lowcut / nyq
         high = self.highcut / nyq
-        b, a = butter(self.order, [low, high], btype='band')
+        b, a = butter(self.order, [low, high], btype="band")
 
         return b, a
 
diff --git a/setup.py b/setup.py
index 355eda7..609cc59 100644
--- a/setup.py
+++ b/setup.py
@@ -7,13 +7,14 @@
     from Cython.Build import build_ext
 except ImportError:
     from setuptools.command.build_ext import build_ext
-    ext = '.c'
+
+    ext = ".c"
 else:
-    ext = '.pyx'
+    ext = ".pyx"
 
 
 # from https://stackoverflow.com/a/52466939/3216742
-extra_compile_args = ['-O3']
+extra_compile_args = ["-O3"]
 extra_link_args = []
 
 if platform.system() == "Darwin":
@@ -26,7 +27,7 @@
 
 
 def read_md(path):
-    with open(path, 'r') as f:
+    with open(path, "r") as f:
         f.read()
 
 
@@ -43,75 +44,95 @@ def finalize_options(self):
         build_ext.finalize_options(self)
 
         # prevent numpy from thinking it is still in its setup process:
-        set_builtin('__NUMPY_SETUP__', False)
+        set_builtin("__NUMPY_SETUP__", False)
         import numpy as np
+
         self.include_dirs.append(np.get_include())
 
 
 setup(
-    name='Kaggler',
+    name="Kaggler",
     version=kaggler.__version__,
-    author='Jeong-Yoon Lee',
-    author_email='jeongyoon.lee1@gmail.com',
-    python_requires=">=3.6",
-    packages=['kaggler',
-              'kaggler.feature_selection',
-              'kaggler.ensemble',
-              'kaggler.model',
-              'kaggler.metrics',
-              'kaggler.online_model',
-              'kaggler.preprocessing'],
-    url='https://github.com/jeongyoonlee/Kaggler',
-    license='LICENSE',
-    description='Code for Kaggle Data Science Competitions.',
-    long_description=read_md('README.md'),
-    long_description_content_type='text/markdown',
+    author="Jeong-Yoon Lee",
+    author_email="jeongyoon.lee1@gmail.com",
+    python_requires=">=3.7",
+    packages=[
+        "kaggler",
+        "kaggler.feature_selection",
+        "kaggler.ensemble",
+        "kaggler.model",
+        "kaggler.metrics",
+        "kaggler.online_model",
+        "kaggler.preprocessing",
+    ],
+    url="https://github.com/jeongyoonlee/Kaggler",
+    license="LICENSE",
+    description="Code for Kaggle Data Science Competitions.",
+    long_description=read_md("README.md"),
+    long_description_content_type="text/markdown",
     install_requires=requirements,
-    setup_requires=['setuptools>=18.0', 'cython>=0.29.0', 'numpy', 'setupmeta'],
-    cmdclass={'build_ext': my_build_ext},
-    ext_modules=[Extension('kaggler.online_model.ftrl',
-                           ['kaggler/online_model/ftrl' + ext,
-                            'kaggler/online_model/murmurhash/MurmurHash3.cpp'],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 Extension('kaggler.online_model.sgd',
-                           ['kaggler/online_model/sgd' + ext],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 Extension('kaggler.online_model.fm',
-                           ['kaggler/online_model/fm' + ext],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 Extension('kaggler.online_model.nn',
-                           ['kaggler/online_model/nn' + ext],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 Extension('kaggler.online_model.nn_h2',
-                           ['kaggler/online_model/nn_h2' + ext],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 Extension('kaggler.online_model._tree',
-                           ['kaggler/online_model/_tree' + ext],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 Extension('kaggler.util',
-                           ['kaggler/util' + ext, 'kaggler/util.pxd'],
-                           libraries=[],
-                           include_dirs=['.'],
-                           extra_compile_args=extra_compile_args,
-                           extra_link_args=extra_link_args),
-                 ],
+    setup_requires=["setuptools>=18.0", "cython>=0.29.0", "numpy", "setupmeta"],
+    cmdclass={"build_ext": my_build_ext},
+    ext_modules=[
+        Extension(
+            "kaggler.online_model.ftrl",
+            [
+                "kaggler/online_model/ftrl" + ext,
+                "kaggler/online_model/murmurhash/MurmurHash3.cpp",
+            ],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+        Extension(
+            "kaggler.online_model.sgd",
+            ["kaggler/online_model/sgd" + ext],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+        Extension(
+            "kaggler.online_model.fm",
+            ["kaggler/online_model/fm" + ext],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+        Extension(
+            "kaggler.online_model.nn",
+            ["kaggler/online_model/nn" + ext],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+        Extension(
+            "kaggler.online_model.nn_h2",
+            ["kaggler/online_model/nn_h2" + ext],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+        Extension(
+            "kaggler.online_model._tree",
+            ["kaggler/online_model/_tree" + ext],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+        Extension(
+            "kaggler.util",
+            ["kaggler/util" + ext, "kaggler/util.pxd"],
+            libraries=[],
+            include_dirs=["."],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_link_args,
+        ),
+    ],
     zip_safe=False,
 )
diff --git a/tox.ini b/tox.ini
index c9ca73c..140f6b0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py37
+envlist = py39
 
 [testenv]
 setenv =


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index a0c6978..e061625 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,7 +11,7 @@
 N_NUM_FEATURE = 5
 
 
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
 def generate_data():
 
     generated = False
@@ -27,12 +27,16 @@ def _generate_data():
             X_cat = np.random.randint(0, N_CATEGORY, size=(N_OBS, N_CAT_FEATURE))
             df = pd.DataFrame(
                 np.hstack((X_num, X_cat)),
-                columns=['num_{}'.format(x) for x in range(N_NUM_FEATURE)]
-                + ['cat_{}'.format(x) for x in range(N_CAT_FEATURE)]
+                columns=["num_{}".format(x) for x in range(N_NUM_FEATURE)]
+                + ["cat_{}".format(x) for x in range(N_CAT_FEATURE)],
+            )
+            df[TARGET_COL] = (
+                1
+                + X_num[:, 0] * X_num[:, 1]
+                - np.log1p(np.exp(X_num[:, 1] + X_num[:, 2]))
+                + 10 * (X_cat[:, 0] == 0).astype(int)
+                + np.random.normal(scale=0.01, size=N_OBS)
             )
-            df[TARGET_COL] = (1 + X_num[:, 0] * X_num[:, 1] - np.log1p(np.exp(X_num[:, 1] + X_num[:, 2]))
-                              + 10 * (X_cat[:, 0] == 0).astype(int)
-                              + np.random.normal(scale=.01, size=N_OBS))
 
         return df
 
diff --git a/tests/const.py b/tests/const.py
index 8603c82..d5b8d60 100644
--- a/tests/const.py
+++ b/tests/const.py
@@ -1,2 +1,2 @@
 RANDOM_SEED = 42
-TARGET_COL = 'target'
+TARGET_COL = "target"
diff --git a/tests/model/test_automl.py b/tests/model/test_automl.py
index 3941082..8525c77 100644
--- a/tests/model/test_automl.py
+++ b/tests/model/test_automl.py
@@ -1,21 +1,21 @@
 import logging
 import numpy as np
 import pandas as pd
-from sklearn.datasets import make_regression
+from sklearn.datasets import make_classification, make_regression
 from sklearn.model_selection import train_test_split
-from sklearn.metrics import mean_absolute_error as mae
+from kaggler.metrics import auc, mae, rmse
 from kaggler.model import AutoLGB, AutoXGB
 
 
 RANDOM_SEED = 42
 N_OBS = 1000
-N_FEATURE = 20
-N_IMP_FEATURE = 10
+N_FEATURE = 10
+N_IMP_FEATURE = 2
 
 logging.basicConfig(level=logging.DEBUG)
 
 
-def test_automl():
+def test_automl_regression():
     X, y = make_regression(
         n_samples=N_OBS,
         n_features=N_FEATURE,
@@ -38,10 +38,40 @@ def test_automl():
     logging.info(f"MAE (LGB): {mae(y_tst, p):.4f}")
     assert mae(y_tst, p) < mae(y_tst, r)
 
-    model = AutoXGB(objective="reg:linear", metric="rmse")
+    model = AutoXGB(objective="reg:squarederror", metric="rmse")
     model.tune(X_trn, y_trn)
     model.fit(X_trn, y_trn)
     p = model.predict(X_tst)
     r = np.random.rand(X_tst.shape[0]) * (y_trn.max() - y_trn.min()) + y_trn.min()
-    logging.info(f"MAE (XGB): {mae(y_tst, p):.4f}")
+    logging.info(f"MAE (RMSE): {rmse(y_tst, p):.4f}")
     assert mae(y_tst, p) < mae(y_tst, r)
+
+
+def test_automl_classification():
+    X, y = make_classification(
+        n_samples=N_OBS,
+        n_features=N_FEATURE,
+        n_informative=N_IMP_FEATURE,
+        random_state=RANDOM_SEED,
+    )
+    X = pd.DataFrame(X, columns=["x{}".format(i) for i in range(X.shape[1])])
+    y = pd.Series(y)
+    logging.info(f"X dim: {X.shape}, y dim: {y.shape}")
+
+    X_trn, X_tst, y_trn, y_tst = train_test_split(
+        X, y, test_size=0.2, random_state=RANDOM_SEED
+    )
+
+    model = AutoLGB(objective="binary", metric="auc")
+    model.tune(X_trn, y_trn)
+    model.fit(X_trn, y_trn)
+    p = model.predict(X_tst)
+    logging.info(f"AUC (LGB): {auc(y_tst, p):.4f}")
+    assert auc(y_tst, p) > 0.5
+
+    model = AutoXGB(objective="binary:logistic", metric="auc")
+    model.tune(X_trn, y_trn)
+    model.fit(X_trn, y_trn)
+    p = model.predict(X_tst)
+    logging.info(f"AUC (XGB): {auc(y_tst, p):.4f}")
+    assert auc(y_tst, p) > 0.5
diff --git a/tests/model/test_classification_tree.py b/tests/model/test_classification_tree.py
index 9abde9a..eb935f1 100644
--- a/tests/model/test_classification_tree.py
+++ b/tests/model/test_classification_tree.py
@@ -12,10 +12,10 @@ def test():
     data = np.random.randint(0, 1000, size=(N_OBS, N_FEATURE))
     y = np.random.randint(2, size=N_OBS)
 
-    train = data[0:N_OBS // 2]
-    ytrain = y[0:N_OBS // 2]
-    test = data[N_OBS // 2:]
-    ytest = y[N_OBS // 2:]
+    train = data[0 : N_OBS // 2]
+    ytrain = y[0 : N_OBS // 2]
+    test = data[N_OBS // 2 :]
+    ytest = y[N_OBS // 2 :]
 
     learner = ClassificationTree(number_of_features=N_FEATURE)
 
@@ -31,5 +31,5 @@ def test():
     print(correct_num)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     profile.run("test()")
diff --git a/tests/model/test_ftrl.py b/tests/model/test_ftrl.py
index 943d987..66c2bf0 100644
--- a/tests/model/test_ftrl.py
+++ b/tests/model/test_ftrl.py
@@ -12,16 +12,20 @@
 
 
 def main():
-    print('create y...')
+    print("create y...")
     y = np.random.randint(2, size=N_OBS)
-    print('create X...')
+    print("create X...")
     row = np.random.randint(N_OBS, size=N_VALUE)
     col = np.random.randint(N_FEATURE, size=N_VALUE)
     data = np.ones(N_VALUE)
     X = sparse.csr_matrix((data, (row, col)), dtype=np.int8)
 
-    print('train...')
-    profiler = cProfile.Profile(subcalls=True, builtins=True, timeunit=0.001,)
+    print("train...")
+    profiler = cProfile.Profile(
+        subcalls=True,
+        builtins=True,
+        timeunit=0.001,
+    )
     clf = FTRL(interaction=False)
     profiler.enable()
     clf.fit(X, y)
@@ -29,10 +33,10 @@ def main():
     profiler.print_stats()
 
     p = clf.predict(X)
-    print('AUC: {:.4f}'.format(auc(y, p)))
+    print("AUC: {:.4f}".format(auc(y, p)))
 
-    assert auc(y, p) > .5
+    assert auc(y, p) > 0.5
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/tests/preprocessing/test_lbe.py b/tests/preprocessing/test_lbe.py
index 8223310..20b412f 100644
--- a/tests/preprocessing/test_lbe.py
+++ b/tests/preprocessing/test_lbe.py
@@ -10,9 +10,11 @@
 
 
 def test():
-    df = pd.DataFrame(np.random.randint(0, N_CATEGORY, size=(N_OBS, N_FEATURE)),
-                      columns=['c{}'.format(x) for x in range(N_FEATURE)])
-    profiler = cProfile.Profile(subcalls=True, builtins=True, timeunit=.001)
+    df = pd.DataFrame(
+        np.random.randint(0, N_CATEGORY, size=(N_OBS, N_FEATURE)),
+        columns=["c{}".format(x) for x in range(N_FEATURE)],
+    )
+    profiler = cProfile.Profile(subcalls=True, builtins=True, timeunit=0.001)
     lbe = LabelEncoder(min_obs=100)
     profiler.enable()
     lbe.fit(df)
@@ -21,5 +23,5 @@ def test():
     profiler.print_stats()
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     test()
diff --git a/tests/preprocessing/test_ohe.py b/tests/preprocessing/test_ohe.py
index dafa134..9763055 100644
--- a/tests/preprocessing/test_ohe.py
+++ b/tests/preprocessing/test_ohe.py
@@ -10,17 +10,19 @@
 
 
 def test():
-    df = pd.DataFrame(np.random.randint(0, N_CATEGORY, size=(N_OBS, N_FEATURE)),
-                      columns=['c{}'.format(x) for x in range(N_FEATURE)])
-    profiler = cProfile.Profile(subcalls=True, builtins=True, timeunit=.001)
+    df = pd.DataFrame(
+        np.random.randint(0, N_CATEGORY, size=(N_OBS, N_FEATURE)),
+        columns=["c{}".format(x) for x in range(N_FEATURE)],
+    )
+    profiler = cProfile.Profile(subcalls=True, builtins=True, timeunit=0.001)
     ohe = OneHotEncoder(min_obs=100)
     profiler.enable()
     ohe.fit(df)
     X_new = ohe.transform(df)
     profiler.disable()
     profiler.print_stats()
-    print('{} --> {}'.format(df.shape, X_new.shape))
+    print("{} --> {}".format(df.shape, X_new.shape))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     test()