Repository: MindSetLib/Insolver (language: python)

Issue:
Fixed deviances. Adding joblib loading and saving.



Discussion / hints:

Fix patch:
diff --git a/InsolverDemo.gif b/InsolverDemo.gif
new file mode 100644
index 00000000..4e47e659
Binary files /dev/null and b/InsolverDemo.gif differ
diff --git a/InsolverElyraPipeline.gif b/InsolverElyraPipeline.gif
new file mode 100644
index 00000000..f05069b0
Binary files /dev/null and b/InsolverElyraPipeline.gif differ
diff --git a/InsolverImplementation.gif b/InsolverImplementation.gif
new file mode 100644
index 00000000..58c0e582
Binary files /dev/null and b/InsolverImplementation.gif differ
diff --git a/README.md b/README.md
index da9ed13f..467c7df0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
 [![PyPI](https://img.shields.io/pypi/v/insolver?style=flat)](https://pypi.org/project/insolver/)
 [![Documentation Status](https://readthedocs.org/projects/insolver/badge/?version=latest)](https://insolver.readthedocs.io/en/latest/?badge=latest)
 [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/MindSetLib/Insolver/Python%20application?logo=github&label=tests)](https://github.com/MindSetLib/Insolver/actions)
+[![Downloads](https://pepy.tech/badge/insolver/week)](https://pepy.tech/project/insolver)
 
 # Insolver
 
@@ -34,6 +35,7 @@ sudo apt install unixodbc-dev
 
 - [Private motor French insurer dataset](https://github.com/MindSetLib/Insolver/blob/master/examples/Insolver_FreMPL.ipynb)
 - [US  traffic accident dataset](https://github.com/MindSetLib/Insolver/blob/master/examples/Insolver_US_Accidents.ipynb)
+- [Landing club dataset](https://github.com/MindSetLib/Insolver/blob/master/examples/Insolver_LendingClub.ipynb)
 
 ## Documentation:
 
@@ -72,3 +74,13 @@ python -m pytest --cov=insolver; coverage html; xdg-open htmlcov/index.html
 ## Contributing to Insolver:
 
 Please, feel free to open an issue or/and suggest PR, if you find any bugs or any enhancements.
+
+## Demo
+### Example of models creation using insolver
+![](InsolverDemo.gif)
+
+### Example of model production service
+![](InsolverImplementation.gif)
+
+### Example of elyra pipeline built with insolver inside
+![](InsolverElyraPipeline.gif)
diff --git a/examples/insolver_pipeline/README.md b/examples/insolver_pipeline/README.md
new file mode 100644
index 00000000..55cabadb
--- /dev/null
+++ b/examples/insolver_pipeline/README.md
@@ -0,0 +1,5 @@
+# Example of elyra pipeline built with insolver inside:
+
+![](../../InsolverElyraPipeline.gif)
+
+## You can try it on [https://mset.space](https://mset.space).
diff --git a/examples/insolver_pipeline/insolver.pipeline b/examples/insolver_pipeline/insolver.pipeline
new file mode 100644
index 00000000..8c2eeedc
--- /dev/null
+++ b/examples/insolver_pipeline/insolver.pipeline
@@ -0,0 +1,302 @@
+{
+  "doc_type": "pipeline",
+  "version": "3.0",
+  "json_schema": "http://api.dataplatform.ibm.com/schemas/common-pipeline/pipeline-flow/pipeline-flow-v3-schema.json",
+  "id": "31b058e0-6a1d-4052-a838-e0c12a7a3df3",
+  "primary_pipeline": "031a9bb9-f5ed-45f5-b89b-6a668ef1fdca",
+  "pipelines": [
+    {
+      "id": "031a9bb9-f5ed-45f5-b89b-6a668ef1fdca",
+      "nodes": [
+        {
+          "id": "88919e68-6e07-45c7-af97-32f06493c56a",
+          "type": "execution_node",
+          "op": "execute-notebook-node",
+          "app_data": {
+            "filename": "load-insolver-df.ipynb",
+            "runtime_image": "amancevice/pandas:1.1.1",
+            "env_vars": [
+              "DATASET=freMPL-R.csv"
+            ],
+            "include_subdirectories": false,
+            "outputs": [
+              "data/df.csv"
+            ],
+            "dependencies": [],
+            "invalidNodeError": null,
+            "ui_data": {
+              "label": "load-insolver-df.ipynb",
+              "image": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20viewBox%3D%220%200%2022%2022%22%3E%0A%20%20%3Cg%20class%3D%22jp-icon-warn0%20jp-icon-selectable%22%20fill%3D%22%23EF6C00%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M18.7%203.3v15.4H3.3V3.3h15.4m1.5-1.5H1.8v18.3h18.3l.1-18.3z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M16.5%2016.5l-5.4-4.3-5.6%204.3v-11h11z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A",
+              "x_pos": 35,
+              "y_pos": 154,
+              "description": "Notebook file"
+            }
+          },
+          "inputs": [
+            {
+              "id": "inPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Input Port"
+                }
+              }
+            }
+          ],
+          "outputs": [
+            {
+              "id": "outPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Output Port"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "34d4374b-20a3-41b6-9e89-3b4a7e6144d9",
+          "type": "execution_node",
+          "op": "execute-notebook-node",
+          "app_data": {
+            "filename": "transforms.ipynb",
+            "runtime_image": "amancevice/pandas:1.1.1",
+            "env_vars": [],
+            "include_subdirectories": false,
+            "invalidNodeError": null,
+            "outputs": [],
+            "dependencies": [],
+            "ui_data": {
+              "label": "transforms.ipynb",
+              "image": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20viewBox%3D%220%200%2022%2022%22%3E%0A%20%20%3Cg%20class%3D%22jp-icon-warn0%20jp-icon-selectable%22%20fill%3D%22%23EF6C00%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M18.7%203.3v15.4H3.3V3.3h15.4m1.5-1.5H1.8v18.3h18.3l.1-18.3z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M16.5%2016.5l-5.4-4.3-5.6%204.3v-11h11z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A",
+              "x_pos": 246,
+              "y_pos": 153,
+              "description": "Notebook file"
+            }
+          },
+          "inputs": [
+            {
+              "id": "inPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Input Port"
+                }
+              },
+              "links": [
+                {
+                  "id": "7fd5b3e9-3770-4714-9ec6-30d90ee53df4",
+                  "node_id_ref": "88919e68-6e07-45c7-af97-32f06493c56a",
+                  "port_id_ref": "outPort"
+                }
+              ]
+            }
+          ],
+          "outputs": [
+            {
+              "id": "outPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Output Port"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "2f185f6c-3669-4d5c-bc5f-7c16c6b6cc4d",
+          "type": "execution_node",
+          "op": "execute-notebook-node",
+          "app_data": {
+            "filename": "split-df.ipynb",
+            "runtime_image": "amancevice/pandas:1.1.1",
+            "env_vars": [],
+            "include_subdirectories": false,
+            "invalidNodeError": null,
+            "outputs": [],
+            "dependencies": [],
+            "ui_data": {
+              "label": "split-df.ipynb",
+              "image": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20viewBox%3D%220%200%2022%2022%22%3E%0A%20%20%3Cg%20class%3D%22jp-icon-warn0%20jp-icon-selectable%22%20fill%3D%22%23EF6C00%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M18.7%203.3v15.4H3.3V3.3h15.4m1.5-1.5H1.8v18.3h18.3l.1-18.3z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M16.5%2016.5l-5.4-4.3-5.6%204.3v-11h11z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A",
+              "x_pos": 472,
+              "y_pos": 151,
+              "description": "Notebook file"
+            }
+          },
+          "inputs": [
+            {
+              "id": "inPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Input Port"
+                }
+              },
+              "links": [
+                {
+                  "id": "7cfd5059-022b-49aa-84e5-3a0c2904762f",
+                  "node_id_ref": "34d4374b-20a3-41b6-9e89-3b4a7e6144d9",
+                  "port_id_ref": "outPort"
+                }
+              ]
+            }
+          ],
+          "outputs": [
+            {
+              "id": "outPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Output Port"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "9ba30ca7-0807-4f56-861c-1b490d285a6a",
+          "type": "execution_node",
+          "op": "execute-notebook-node",
+          "app_data": {
+            "filename": "train-model.ipynb",
+            "runtime_image": "amancevice/pandas:1.1.1",
+            "env_vars": [
+              "TARGET=ClaimAmount",
+              "FEATURES=LicAge,Gender,MariStat,DrivAge,HasKmLimit,BonusMalus,RiskArea,Age_m,Age_f,Age_m_2,Age_f_2"
+            ],
+            "include_subdirectories": false,
+            "invalidNodeError": null,
+            "outputs": [],
+            "dependencies": [],
+            "ui_data": {
+              "label": "train-model.ipynb",
+              "image": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20viewBox%3D%220%200%2022%2022%22%3E%0A%20%20%3Cg%20class%3D%22jp-icon-warn0%20jp-icon-selectable%22%20fill%3D%22%23EF6C00%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M18.7%203.3v15.4H3.3V3.3h15.4m1.5-1.5H1.8v18.3h18.3l.1-18.3z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M16.5%2016.5l-5.4-4.3-5.6%204.3v-11h11z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A",
+              "x_pos": 695,
+              "y_pos": 151,
+              "description": "Notebook file"
+            }
+          },
+          "inputs": [
+            {
+              "id": "inPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Input Port"
+                }
+              },
+              "links": [
+                {
+                  "id": "27915b6a-24f3-4179-9823-1229788121d2",
+                  "node_id_ref": "2f185f6c-3669-4d5c-bc5f-7c16c6b6cc4d",
+                  "port_id_ref": "outPort"
+                }
+              ]
+            }
+          ],
+          "outputs": [
+            {
+              "id": "outPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Output Port"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "070efb63-e8d6-4a9b-a17b-97c28dcdf418",
+          "type": "execution_node",
+          "op": "execute-notebook-node",
+          "app_data": {
+            "filename": "predict.ipynb",
+            "runtime_image": "amancevice/pandas:1.1.1",
+            "env_vars": [],
+            "include_subdirectories": false,
+            "invalidNodeError": null,
+            "outputs": [],
+            "dependencies": [],
+            "ui_data": {
+              "label": "predict.ipynb",
+              "image": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20viewBox%3D%220%200%2022%2022%22%3E%0A%20%20%3Cg%20class%3D%22jp-icon-warn0%20jp-icon-selectable%22%20fill%3D%22%23EF6C00%22%3E%0A%20%20%20%20%3Cpath%20d%3D%22M18.7%203.3v15.4H3.3V3.3h15.4m1.5-1.5H1.8v18.3h18.3l.1-18.3z%22%2F%3E%0A%20%20%20%20%3Cpath%20d%3D%22M16.5%2016.5l-5.4-4.3-5.6%204.3v-11h11z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E%0A",
+              "x_pos": 921,
+              "y_pos": 149,
+              "description": "Notebook file"
+            }
+          },
+          "inputs": [
+            {
+              "id": "inPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Input Port"
+                }
+              },
+              "links": [
+                {
+                  "id": "7dd3bd2d-f9d6-403d-aaa5-09071760fb4e",
+                  "node_id_ref": "9ba30ca7-0807-4f56-861c-1b490d285a6a",
+                  "port_id_ref": "outPort"
+                }
+              ]
+            }
+          ],
+          "outputs": [
+            {
+              "id": "outPort",
+              "app_data": {
+                "ui_data": {
+                  "cardinality": {
+                    "min": 0,
+                    "max": -1
+                  },
+                  "label": "Output Port"
+                }
+              }
+            }
+          ]
+        }
+      ],
+      "app_data": {
+        "ui_data": {
+          "comments": []
+        },
+        "version": 3
+      },
+      "runtime_ref": ""
+    }
+  ],
+  "schemas": []
+}
\ No newline at end of file
diff --git a/examples/insolver_pipeline/load-insolver-df.ipynb b/examples/insolver_pipeline/load-insolver-df.ipynb
new file mode 100644
index 00000000..145941de
--- /dev/null
+++ b/examples/insolver_pipeline/load-insolver-df.ipynb
@@ -0,0 +1,171 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:34:59.959746Z",
+     "iopub.status.busy": "2021-02-04T14:34:59.958899Z",
+     "iopub.status.idle": "2021-02-04T14:35:00.427535Z",
+     "shell.execute_reply": "2021-02-04T14:35:00.426618Z"
+    },
+    "papermill": {
+     "duration": 0.491038,
+     "end_time": "2021-02-04T14:35:00.427829",
+     "exception": false,
+     "start_time": "2021-02-04T14:34:59.936791",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "import json\n",
+    "from pathlib import Path\n",
+    "import pandas as pd"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:00.449937Z",
+     "iopub.status.busy": "2021-02-04T14:35:00.449027Z",
+     "iopub.status.idle": "2021-02-04T14:35:00.452294Z",
+     "shell.execute_reply": "2021-02-04T14:35:00.452968Z"
+    },
+    "papermill": {
+     "duration": 0.017596,
+     "end_time": "2021-02-04T14:35:00.453170",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:00.435574",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "if os.environ.get('DATASET') is None:\n",
+    "    raise RuntimeError('Cannot run notebook. Required parameter is missing.')\n",
+    "    \n",
+    "data_file = os.environ['DATASET']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:00.472658Z",
+     "iopub.status.busy": "2021-02-04T14:35:00.471830Z",
+     "iopub.status.idle": "2021-02-04T14:35:03.507962Z",
+     "shell.execute_reply": "2021-02-04T14:35:03.508717Z"
+    },
+    "papermill": {
+     "duration": 3.049364,
+     "end_time": "2021-02-04T14:35:03.509067",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:00.459703",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "df = pd.read_csv(data_file, low_memory=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:03.535817Z",
+     "iopub.status.busy": "2021-02-04T14:35:03.533119Z",
+     "iopub.status.idle": "2021-02-04T14:35:09.742054Z",
+     "shell.execute_reply": "2021-02-04T14:35:09.742778Z"
+    },
+    "papermill": {
+     "duration": 6.225068,
+     "end_time": "2021-02-04T14:35:09.743087",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:03.518019",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "df.to_csv('data/df.csv', index=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:09.763082Z",
+     "iopub.status.busy": "2021-02-04T14:35:09.762238Z",
+     "iopub.status.idle": "2021-02-04T14:35:09.765972Z",
+     "shell.execute_reply": "2021-02-04T14:35:09.766657Z"
+    },
+    "papermill": {
+     "duration": 0.01597,
+     "end_time": "2021-02-04T14:35:09.766862",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:09.750892",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Dataset freMPL-R.csv loaded\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f'Dataset {data_file} loaded')"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.6"
+  },
+  "papermill": {
+   "default_parameters": {},
+   "duration": 11.401683,
+   "end_time": "2021-02-04T14:35:10.184043",
+   "environment_variables": {},
+   "exception": null,
+   "input_path": "/home/jovyan/work/insolver_pipeline/load-insolver-df.ipynb",
+   "output_path": "/home/jovyan/work/insolver_pipeline/load-insolver-df.ipynb",
+   "parameters": {},
+   "start_time": "2021-02-04T14:34:58.782360",
+   "version": "2.2.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/examples/insolver_pipeline/predict.ipynb b/examples/insolver_pipeline/predict.ipynb
new file mode 100644
index 00000000..d0811257
--- /dev/null
+++ b/examples/insolver_pipeline/predict.ipynb
@@ -0,0 +1,168 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:33.388577Z",
+     "iopub.status.busy": "2021-02-04T14:35:33.387752Z",
+     "iopub.status.idle": "2021-02-04T14:35:36.161257Z",
+     "shell.execute_reply": "2021-02-04T14:35:36.159459Z"
+    },
+    "papermill": {
+     "duration": 2.79361,
+     "end_time": "2021-02-04T14:35:36.161915",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:33.368305",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from insolver.wrappers import InsolverGLMWrapper"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:36.187561Z",
+     "iopub.status.busy": "2021-02-04T14:35:36.186747Z",
+     "iopub.status.idle": "2021-02-04T14:35:36.213910Z",
+     "shell.execute_reply": "2021-02-04T14:35:36.212430Z"
+    },
+    "papermill": {
+     "duration": 0.041194,
+     "end_time": "2021-02-04T14:35:36.214308",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:36.173114",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "test = pd.read_csv('data/test.csv', low_memory=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:36.236931Z",
+     "iopub.status.busy": "2021-02-04T14:35:36.236114Z",
+     "iopub.status.idle": "2021-02-04T14:35:36.239843Z",
+     "shell.execute_reply": "2021-02-04T14:35:36.239171Z"
+    },
+    "papermill": {
+     "duration": 0.016645,
+     "end_time": "2021-02-04T14:35:36.240018",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:36.223373",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "iglm = InsolverGLMWrapper(backend='sklearn', load_path='data/GLM.model')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:36.287565Z",
+     "iopub.status.busy": "2021-02-04T14:35:36.262199Z",
+     "iopub.status.idle": "2021-02-04T14:35:36.296434Z",
+     "shell.execute_reply": "2021-02-04T14:35:36.297070Z"
+    },
+    "papermill": {
+     "duration": 0.050506,
+     "end_time": "2021-02-04T14:35:36.297287",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:36.246781",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[2436.79705752 2383.88496771 1960.93471514 ... 1888.28571723 2231.56034088\n",
+      " 2193.73967469]"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "predict_glm = iglm.predict(test)\n",
+    "\n",
+    "print(predict_glm)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "papermill": {
+     "duration": 0.006298,
+     "end_time": "2021-02-04T14:35:36.310774",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:36.304476",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.6"
+  },
+  "papermill": {
+   "default_parameters": {},
+   "duration": 4.946241,
+   "end_time": "2021-02-04T14:35:37.329255",
+   "environment_variables": {},
+   "exception": null,
+   "input_path": "/home/jovyan/work/insolver_pipeline/predict.ipynb",
+   "output_path": "/home/jovyan/work/insolver_pipeline/predict.ipynb",
+   "parameters": {},
+   "start_time": "2021-02-04T14:35:32.383014",
+   "version": "2.2.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/examples/insolver_pipeline/split-df.ipynb b/examples/insolver_pipeline/split-df.ipynb
new file mode 100644
index 00000000..22954bf4
--- /dev/null
+++ b/examples/insolver_pipeline/split-df.ipynb
@@ -0,0 +1,217 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:21.181875Z",
+     "iopub.status.busy": "2021-02-04T14:35:21.180995Z",
+     "iopub.status.idle": "2021-02-04T14:35:24.201142Z",
+     "shell.execute_reply": "2021-02-04T14:35:24.201901Z"
+    },
+    "papermill": {
+     "duration": 3.044222,
+     "end_time": "2021-02-04T14:35:24.202405",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:21.158183",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from insolver.transforms import InsolverTransform"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:24.227693Z",
+     "iopub.status.busy": "2021-02-04T14:35:24.226865Z",
+     "iopub.status.idle": "2021-02-04T14:35:24.296983Z",
+     "shell.execute_reply": "2021-02-04T14:35:24.297698Z"
+    },
+    "papermill": {
+     "duration": 0.085606,
+     "end_time": "2021-02-04T14:35:24.297990",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:24.212384",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "df = pd.read_csv('data/df_transformed.csv', low_memory=False)\n",
+    "InsTransforms = InsolverTransform(df, [])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:24.320474Z",
+     "iopub.status.busy": "2021-02-04T14:35:24.319644Z",
+     "iopub.status.idle": "2021-02-04T14:35:24.322167Z",
+     "shell.execute_reply": "2021-02-04T14:35:24.322822Z"
+    },
+    "papermill": {
+     "duration": 0.016048,
+     "end_time": "2021-02-04T14:35:24.323023",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:24.306975",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# val_size, test_size, shuffle = config['VAL_SIZE'], config['TEST_SIZE'], config['SHUFFLE']\n",
+    "val_size, test_size, shuffle = 0.15, 0.15, True"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:24.347278Z",
+     "iopub.status.busy": "2021-02-04T14:35:24.346453Z",
+     "iopub.status.idle": "2021-02-04T14:35:24.359694Z",
+     "shell.execute_reply": "2021-02-04T14:35:24.360353Z"
+    },
+    "papermill": {
+     "duration": 0.028867,
+     "end_time": "2021-02-04T14:35:24.360581",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:24.331714",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "train, valid, test = InsTransforms.split_frame(val_size=0.15, test_size=0.15, random_state=0, shuffle=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:24.383646Z",
+     "iopub.status.busy": "2021-02-04T14:35:24.382819Z",
+     "iopub.status.idle": "2021-02-04T14:35:24.682839Z",
+     "shell.execute_reply": "2021-02-04T14:35:24.683532Z"
+    },
+    "papermill": {
+     "duration": 0.314484,
+     "end_time": "2021-02-04T14:35:24.683856",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:24.369372",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "train.to_csv('data/train.csv', index=False)\n",
+    "valid.to_csv('data/valid.csv', index=False)\n",
+    "test.to_csv('data/test.csv', index=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:24.707065Z",
+     "iopub.status.busy": "2021-02-04T14:35:24.706229Z",
+     "iopub.status.idle": "2021-02-04T14:35:24.711051Z",
+     "shell.execute_reply": "2021-02-04T14:35:24.711696Z"
+    },
+    "papermill": {
+     "duration": 0.01853,
+     "end_time": "2021-02-04T14:35:24.711897",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:24.693367",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Split frame done"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "print('Split frame done')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "papermill": {
+     "duration": 0.009243,
+     "end_time": "2021-02-04T14:35:24.730728",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:24.721485",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.6"
+  },
+  "papermill": {
+   "default_parameters": {},
+   "duration": 6.048805,
+   "end_time": "2021-02-04T14:35:26.260770",
+   "environment_variables": {},
+   "exception": null,
+   "input_path": "/home/jovyan/work/insolver_pipeline/split-df.ipynb",
+   "output_path": "/home/jovyan/work/insolver_pipeline/split-df.ipynb",
+   "parameters": {},
+   "start_time": "2021-02-04T14:35:20.211965",
+   "version": "2.2.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/examples/insolver_pipeline/train-model.ipynb b/examples/insolver_pipeline/train-model.ipynb
new file mode 100644
index 00000000..1a1a27d3
--- /dev/null
+++ b/examples/insolver_pipeline/train-model.ipynb
@@ -0,0 +1,301 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:27.519709Z",
+     "iopub.status.busy": "2021-02-04T14:35:27.518835Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.434947Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.435700Z"
+    },
+    "papermill": {
+     "duration": 2.942515,
+     "end_time": "2021-02-04T14:35:30.436156",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:27.493641",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "from pathlib import Path\n",
+    "import pandas as pd\n",
+    "\n",
+    "from insolver.wrappers import InsolverGLMWrapper"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.462776Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.461938Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.464459Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.465111Z"
+    },
+    "papermill": {
+     "duration": 0.018685,
+     "end_time": "2021-02-04T14:35:30.465313",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.446628",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "if os.environ.get('FEATURES') is None or os.environ.get('TARGET') is None:\n",
+    "    raise RuntimeError('Cannot run notebook. Required parameter is missing.')\n",
+    "    \n",
+    "features = os.environ['FEATURES']\n",
+    "target = os.environ['TARGET']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.494935Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.492977Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.497755Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.497007Z"
+    },
+    "papermill": {
+     "duration": 0.023293,
+     "end_time": "2021-02-04T14:35:30.497929",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.474636",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['LicAge',\n",
+       " 'Gender',\n",
+       " 'MariStat',\n",
+       " 'DrivAge',\n",
+       " 'HasKmLimit',\n",
+       " 'BonusMalus',\n",
+       " 'RiskArea',\n",
+       " 'Age_m',\n",
+       " 'Age_f',\n",
+       " 'Age_m_2',\n",
+       " 'Age_f_2']"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "features = features.strip().split(',')\n",
+    "features"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.524448Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.523629Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.605965Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.605188Z"
+    },
+    "papermill": {
+     "duration": 0.097908,
+     "end_time": "2021-02-04T14:35:30.606170",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.508262",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "train = pd.read_csv('data/train.csv', low_memory=False)\n",
+    "valid = pd.read_csv('data/valid.csv', low_memory=False)\n",
+    "test = pd.read_csv('data/test.csv', low_memory=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.647088Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.646146Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.648369Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.649091Z"
+    },
+    "papermill": {
+     "duration": 0.028098,
+     "end_time": "2021-02-04T14:35:30.649333",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.621235",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "x_train, y_train = train[features], train[target]\n",
+    "x_valid, y_valid = valid[features], valid[target]\n",
+    "x_test, y_test = test[features], test[target]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.680003Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.678967Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.774130Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.774988Z"
+    },
+    "papermill": {
+     "duration": 0.115016,
+     "end_time": "2021-02-04T14:35:30.775259",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.660243",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# train our model with sklearn\n",
+    "iglm = InsolverGLMWrapper(backend='sklearn', family='gamma', link='log', standardize=True)\n",
+    "iglm.fit(x_train, y_train)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.806020Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.805149Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.809175Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.810026Z"
+    },
+    "papermill": {
+     "duration": 0.02322,
+     "end_time": "2021-02-04T14:35:30.810233",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.787013",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "iglm.save_model('data/')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:30.842602Z",
+     "iopub.status.busy": "2021-02-04T14:35:30.841211Z",
+     "iopub.status.idle": "2021-02-04T14:35:30.850191Z",
+     "shell.execute_reply": "2021-02-04T14:35:30.851296Z"
+    },
+    "papermill": {
+     "duration": 0.030315,
+     "end_time": "2021-02-04T14:35:30.851598",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.821283",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "The GLM model have been trained"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "# rename saved model\n",
+    "models_path = Path('data')\n",
+    "model = list(models_path.glob('insolver_glm_*'))[0]\n",
+    "model.rename(models_path.joinpath('GLM.model'))\n",
+    "\n",
+    "print('The GLM model have been trained')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "papermill": {
+     "duration": 0.011661,
+     "end_time": "2021-02-04T14:35:30.881754",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:30.870093",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.6"
+  },
+  "papermill": {
+   "default_parameters": {},
+   "duration": 6.050596,
+   "end_time": "2021-02-04T14:35:32.345052",
+   "environment_variables": {},
+   "exception": null,
+   "input_path": "/home/jovyan/work/insolver_pipeline/train-model.ipynb",
+   "output_path": "/home/jovyan/work/insolver_pipeline/train-model.ipynb",
+   "parameters": {},
+   "start_time": "2021-02-04T14:35:26.294456",
+   "version": "2.2.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/examples/insolver_pipeline/transforms.ipynb b/examples/insolver_pipeline/transforms.ipynb
new file mode 100644
index 00000000..22d14320
--- /dev/null
+++ b/examples/insolver_pipeline/transforms.ipynb
@@ -0,0 +1,405 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:11.385752Z",
+     "iopub.status.busy": "2021-02-04T14:35:11.384847Z",
+     "iopub.status.idle": "2021-02-04T14:35:14.410852Z",
+     "shell.execute_reply": "2021-02-04T14:35:14.411545Z"
+    },
+    "papermill": {
+     "duration": 3.051096,
+     "end_time": "2021-02-04T14:35:14.412004",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:11.360908",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "\n",
+    "from insolver import InsolverDataFrame\n",
+    "from insolver.transforms import (\n",
+    "    InsolverTransform,\n",
+    "    TransformExp,\n",
+    "    TransformAge,\n",
+    "    TransformMapValues,\n",
+    "    TransformPolynomizer,\n",
+    "    TransformAgeGender,\n",
+    ")\n",
+    "\n",
+    "pd.options.display.float_format = '{:.2f}'.format"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:14.438412Z",
+     "iopub.status.busy": "2021-02-04T14:35:14.437586Z",
+     "iopub.status.idle": "2021-02-04T14:35:17.557139Z",
+     "shell.execute_reply": "2021-02-04T14:35:17.556363Z"
+    },
+    "papermill": {
+     "duration": 3.134792,
+     "end_time": "2021-02-04T14:35:17.557375",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:14.422583",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "df = pd.read_csv('data/df.csv', low_memory=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:17.585957Z",
+     "iopub.status.busy": "2021-02-04T14:35:17.585085Z",
+     "iopub.status.idle": "2021-02-04T14:35:17.983096Z",
+     "shell.execute_reply": "2021-02-04T14:35:17.983828Z"
+    },
+    "papermill": {
+     "duration": 0.415524,
+     "end_time": "2021-02-04T14:35:17.984152",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:17.568628",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "df = df[df.Dataset.isin([5, 6, 7, 8, 9])]\n",
+    "df.dropna(how='all', axis=1, inplace=True)\n",
+    "df = df[df.ClaimAmount > 0]\n",
+    "\n",
+    "\n",
+    "InsDataFrame = InsolverDataFrame(df)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:18.012084Z",
+     "iopub.status.busy": "2021-02-04T14:35:18.011248Z",
+     "iopub.status.idle": "2021-02-04T14:35:18.013885Z",
+     "shell.execute_reply": "2021-02-04T14:35:18.014536Z"
+    },
+    "papermill": {
+     "duration": 0.019522,
+     "end_time": "2021-02-04T14:35:18.014737",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:17.995215",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "@staticmethod\n",
+    "def new_exp(exp, exp_max):\n",
+    "    if pd.isnull(exp):\n",
+    "        exp = None\n",
+    "    elif exp < 0:\n",
+    "        exp = None\n",
+    "    else:\n",
+    "        exp = exp * 7 // 365\n",
+    "    if exp > exp_max:\n",
+    "        exp = exp_max\n",
+    "    return exp\n",
+    "\n",
+    "TransformExp._exp = new_exp"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:18.040878Z",
+     "iopub.status.busy": "2021-02-04T14:35:18.039900Z",
+     "iopub.status.idle": "2021-02-04T14:35:18.043164Z",
+     "shell.execute_reply": "2021-02-04T14:35:18.042459Z"
+    },
+    "papermill": {
+     "duration": 0.018854,
+     "end_time": "2021-02-04T14:35:18.043337",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:18.024483",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# We can set up user transformations\n",
+    "class TransformSocioCateg:\n",
+    "    def __init__(self, column_socio_categ):\n",
+    "        self.priority = 0\n",
+    "        self.column_socio_categ = column_socio_categ\n",
+    "\n",
+    "    def __call__(self, df):\n",
+    "        df[self.column_socio_categ] = df[self.column_socio_categ].str.slice(0,4)\n",
+    "        return df"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:18.070303Z",
+     "iopub.status.busy": "2021-02-04T14:35:18.069462Z",
+     "iopub.status.idle": "2021-02-04T14:35:18.072087Z",
+     "shell.execute_reply": "2021-02-04T14:35:18.072738Z"
+    },
+    "papermill": {
+     "duration": 0.019565,
+     "end_time": "2021-02-04T14:35:18.072938",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:18.053373",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# After that we can combine all transformations into one one object\n",
+    "InsTransforms = InsolverTransform(InsDataFrame, [\n",
+    "    TransformAge('DrivAge', 18, 75),\n",
+    "    TransformExp('LicAge', 57),\n",
+    "    TransformMapValues('Gender', {'Male':0, 'Female':1}),\n",
+    "    TransformMapValues('MariStat', {'Other':0, 'Alone':1}),\n",
+    "    TransformSocioCateg('SocioCateg'),\n",
+    "    TransformAgeGender('DrivAge', 'Gender', 'Age_m', 'Age_f', age_default=18, gender_male=0, gender_female=1),\n",
+    "    TransformPolynomizer('Age_m'),\n",
+    "    TransformPolynomizer('Age_f'),\n",
+    "    ])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:18.102093Z",
+     "iopub.status.busy": "2021-02-04T14:35:18.101241Z",
+     "iopub.status.idle": "2021-02-04T14:35:18.104963Z",
+     "shell.execute_reply": "2021-02-04T14:35:18.105636Z"
+    },
+    "papermill": {
+     "duration": 0.022664,
+     "end_time": "2021-02-04T14:35:18.105840",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:18.083176",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<insolver.transforms.InsolverTransforms.TransformAge at 0x7f968816f7c0>,\n",
+       " <insolver.transforms.InsolverTransforms.TransformExp at 0x7f968816fac0>,\n",
+       " <insolver.transforms.InsolverTransforms.TransformMapValues at 0x7f968816f6a0>,\n",
+       " <insolver.transforms.InsolverTransforms.TransformMapValues at 0x7f968816f610>,\n",
+       " <__main__.TransformSocioCateg at 0x7f968816f550>,\n",
+       " <insolver.transforms.InsolverTransforms.TransformAgeGender at 0x7f968816f8e0>,\n",
+       " <insolver.transforms.InsolverTransforms.TransformPolynomizer at 0x7f968816fb20>,\n",
+       " <insolver.transforms.InsolverTransforms.TransformPolynomizer at 0x7f968816fc10>]"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "InsTransforms.transforms"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:18.132474Z",
+     "iopub.status.busy": "2021-02-04T14:35:18.131632Z",
+     "iopub.status.idle": "2021-02-04T14:35:18.818517Z",
+     "shell.execute_reply": "2021-02-04T14:35:18.817760Z"
+    },
+    "papermill": {
+     "duration": 0.701693,
+     "end_time": "2021-02-04T14:35:18.818729",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:18.117036",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "!!! WARNING !!!"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Check the order of transforms. Transforms with higher priority should be done first."
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "{0: {'name': 'TransformAge',\n",
+       "  'attributes': {'age_max': 75,\n",
+       "   'age_min': 18,\n",
+       "   'column_driver_minage': 'DrivAge',\n",
+       "   'priority': 1}},\n",
+       " 1: {'name': 'TransformExp',\n",
+       "  'attributes': {'column_driver_minexp': 'LicAge',\n",
+       "   'exp_max': 57,\n",
+       "   'priority': 1}},\n",
+       " 2: {'name': 'TransformMapValues',\n",
+       "  'attributes': {'column_param': 'Gender',\n",
+       "   'dictionary': {'Male': 0, 'Female': 1},\n",
+       "   'priority': 1}},\n",
+       " 3: {'name': 'TransformMapValues',\n",
+       "  'attributes': {'column_param': 'MariStat',\n",
+       "   'dictionary': {'Other': 0, 'Alone': 1},\n",
+       "   'priority': 1}},\n",
+       " 4: {'name': 'TransformSocioCateg',\n",
+       "  'attributes': {'column_socio_categ': 'SocioCateg', 'priority': 0}},\n",
+       " 5: {'name': 'TransformAgeGender',\n",
+       "  'attributes': {'age_default': 18,\n",
+       "   'column_age': 'DrivAge',\n",
+       "   'column_age_f': 'Age_f',\n",
+       "   'column_age_m': 'Age_m',\n",
+       "   'column_gender': 'Gender',\n",
+       "   'gender_female': 1,\n",
+       "   'gender_male': 0,\n",
+       "   'priority': 2}},\n",
+       " 6: {'name': 'TransformPolynomizer',\n",
+       "  'attributes': {'column_param': 'Age_m', 'n': 2, 'priority': 3}},\n",
+       " 7: {'name': 'TransformPolynomizer',\n",
+       "  'attributes': {'column_param': 'Age_f', 'n': 2, 'priority': 3}}}"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "InsTransforms.ins_transform()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {
+    "execution": {
+     "iopub.execute_input": "2021-02-04T14:35:18.852692Z",
+     "iopub.status.busy": "2021-02-04T14:35:18.851841Z",
+     "iopub.status.idle": "2021-02-04T14:35:19.136876Z",
+     "shell.execute_reply": "2021-02-04T14:35:19.136136Z"
+    },
+    "papermill": {
+     "duration": 0.303801,
+     "end_time": "2021-02-04T14:35:19.137089",
+     "exception": false,
+     "start_time": "2021-02-04T14:35:18.833288",
+     "status": "completed"
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Transforms done"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "InsTransforms.to_csv('data/df_transformed.csv', index=False)\n",
+    "\n",
+    "print('Transforms done')"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.6"
+  },
+  "papermill": {
+   "default_parameters": {},
+   "duration": 9.953797,
+   "end_time": "2021-02-04T14:35:20.166757",
+   "environment_variables": {},
+   "exception": null,
+   "input_path": "/home/jovyan/work/insolver_pipeline/transforms.ipynb",
+   "output_path": "/home/jovyan/work/insolver_pipeline/transforms.ipynb",
+   "parameters": {},
+   "start_time": "2021-02-04T14:35:10.212960",
+   "version": "2.2.2"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/insolver/model_tools/model_comparison.py b/insolver/model_tools/model_comparison.py
index 3946f36f..4aba42e9 100644
--- a/insolver/model_tools/model_comparison.py
+++ b/insolver/model_tools/model_comparison.py
@@ -5,7 +5,7 @@
 from numpy import min, max, mean, var, std, quantile, median
 from pandas import DataFrame
 
-from insolver.wrappers import InsolverGLMWrapper, InsolverGBMWrapper, InsolverTrivialWrapper
+from insolver.wrappers import InsolverGLMWrapper, InsolverGBMWrapper, InsolverRFWrapper, InsolverTrivialWrapper
 
 
 class ModelMetricsCompare:
@@ -19,9 +19,13 @@ class ModelMetricsCompare:
         stats (:obj:`list`, :obj:`tuple`, :obj:`callable`, optional): Statistics or list of statistics to compute.
         folder with models. If `None`, taking current working directory as source.
         h2o_init_params (:obj:`dict`, optional): Parameters passed to `h2o.init()`, when `backend` == 'h2o'.
+        predict_params (:obj:`list`, optional): List of dictionaries containing parameters passed to predict methods
+         for each model.
+        features (:obj:`list`, optional): List of lists containing features for predict method for each model.
     """
-    def __init__(self, X, y, source=None, metrics=None, stats=None, h2o_init_params=None):
-        wrappers = {'glm': InsolverGLMWrapper, 'gbm': InsolverGBMWrapper}
+    def __init__(self, X, y, source=None, metrics=None, stats=None, h2o_init_params=None, predict_params=None,
+                 features=None):
+        wrappers = {'glm': InsolverGLMWrapper, 'gbm': InsolverGBMWrapper, 'rf': InsolverRFWrapper}
         self.stats, self.metrics = None, None
         if (source is None) or isinstance(source, str):
             source = os.getcwd() if source is None else source
@@ -41,7 +45,7 @@ def __init__(self, X, y, source=None, metrics=None, stats=None, h2o_init_params=
         else:
             raise TypeError(f'Source of type {type(source)} is not supported.')
 
-        self._calc_metrics(X=X, y=y, metrics=metrics, stats=stats)
+        self._calc_metrics(X=X, y=y, metrics=metrics, stats=stats, predict_params=predict_params, features=features)
 
     def __repr__(self):
         stk = traceback.extract_stack()
@@ -58,7 +62,7 @@ def __repr__(self):
             print(self.metrics)
         return ''
 
-    def _calc_metrics(self, X, y, metrics=None, stats=None):
+    def _calc_metrics(self, X, y, metrics=None, stats=None, predict_params=None, features=None):
         """Computing metrics and statistics for models.
 
         Args:
@@ -66,6 +70,9 @@ def _calc_metrics(self, X, y, metrics=None, stats=None):
             y (:obj:`pd.DataFrame`, :obj:`pd.Series`): Actual target values for X.
             metrics (:obj:`list`, :obj:`tuple`, :obj:`callable`, optional): Metrics or list of metrics to compute.
             stats (:obj:`list`, :obj:`tuple`, :obj:`callable`, optional): Statistics or list of statistics to compute.
+            predict_params (:obj:`list`, optional): List of dictionaries containing parameters passed to predict methods
+         for each model.
+            features (:obj:`list`, optional): List of lists containing features for predict method for each model.
 
         Returns:
             Returns `None`, but results available in `self.stats`, `self.metrics`.
@@ -74,8 +81,12 @@ def _calc_metrics(self, X, y, metrics=None, stats=None):
         trivial = InsolverTrivialWrapper(agg=lambda x: x)
         trivial.fit(X, y)
         models = [trivial] + self.models
+        features = [None] + features if features is not None else None
         for model in models:
-            p = model.predict(X)
+            p = model.predict(X if (features is None) or (features[models.index(model)] is None)
+                              else X[features[models.index(model)]],
+                              **({} if (predict_params is None) or (predict_params[models.index(model)] is None)
+                                 else predict_params[models.index(model)]))
             stats_val = [mean(p), var(p), std(p), min(p), quantile(p, 0.25), median(p), quantile(p, 0.75), max(p)]
             name_stats = ['Mean', 'Variance', 'St. Dev.', 'Min', 'Q1', 'Median', 'Q3', 'Max']
             if stats is not None:
@@ -92,7 +103,11 @@ def _calc_metrics(self, X, y, metrics=None, stats=None):
                 else:
                     raise TypeError(f'Statistics with type {type(stats)} are not supported.')
             stats_df = stats_df.append(DataFrame([stats_val], columns=name_stats))
-            model_names.append(f'{model.algo.upper()} {model.backend.capitalize()}')
+
+            if hasattr(model, 'algo') and hasattr(model, 'backend'):
+                model_names.append(f'{model.algo.upper()} {model.backend.capitalize()}')
+            else:
+                model_names.append(model.__class__.__name__)
             stats_df.index = ['Actual'] + model_names[1:]
             self.stats = stats_df
 
diff --git a/insolver/model_tools/model_utils.py b/insolver/model_tools/model_utils.py
index 0312e050..6f49e2bf 100644
--- a/insolver/model_tools/model_utils.py
+++ b/insolver/model_tools/model_utils.py
@@ -3,7 +3,11 @@
 from urllib.request import urlopen
 from zipfile import ZipFile
 
-from numpy import log, sum
+import numpy as np
+import matplotlib.pyplot as plt
+
+from scipy.special import xlogy
+from pandas import DataFrame, Series, concat, qcut
 from sklearn.model_selection import train_test_split
 
 
@@ -21,7 +25,8 @@ def download_dataset(name, folder='datasets'):
     datasets = {
         'freMPL-R': 'https://github.com/MindSetLib/Insolver/releases/download/v0.4.4/freMPL-R.zip',
         'US_Accidents': 'https://github.com/MindSetLib/Insolver/releases/download/v0.4.4/US_Accidents_June20.zip',
-        'Lending_Club': 'https://github.com/MindSetLib/Insolver/releases/download/v0.4.4/LendingClub.zip',
+        'US_Accidents_small': 'https://github.com/MindSetLib/Insolver/releases/download/v0.4.5/US_Accidents_small.zip',
+        'Lending_Club': 'https://github.com/MindSetLib/Insolver/releases/download/v0.4.4/LendingClub.zip'
     }
     if name not in datasets.keys():
         return f'Dataset {name} is not found. Available datasets are {", ".join(datasets.keys())}'
@@ -95,36 +100,160 @@ def train_test_column_split(x, y, df_column):
             y1[y1[col_name] == 'train'].drop(col_name, axis=1), y1[y1[col_name] == 'test'].drop(col_name, axis=1))
 
 
-def deviance_poisson(y_hat, y, weight=None):
+def deviance_poisson(y, y_pred, weight=None, agg='sum'):
     """Function for Poisson Deviance evaluation.
 
     Args:
-        y_hat: Array with predictions.
         y: Array with target variable.
+        y_pred: Array with predictions.
         weight: Weights for weighted metric.
+        agg: Function to calculate deviance ['sum', 'mean'] or callable are supported.
 
     Returns:
         float, value of the Poisson deviance.
     """
-    t_hat, t = y_hat + 1, y + 1
-    if weight:
-        return 2 * sum(weight*(t * log(t / t_hat) - (t - t_hat)))
-    else:
-        return 2 * sum(t * log(t / t_hat) - (t - t_hat))
+    dict_func = {'sum': np.sum, 'mean': np.mean}
+    func = dict_func[agg] if agg in ['sum', 'mean'] else agg if isinstance(agg, callable) else None
+    if func is None:
+        raise ValueError
+    weight = 1 if weight is None else weight
+    return func(2 * weight * (xlogy(y, y / y_pred) - (y - y_pred)))
 
 
-def deviance_gamma(y_hat, y, weight=None):
+def deviance_gamma(y, y_pred, weight=None, agg='sum'):
     """Function for Gamma Deviance evaluation.
 
     Args:
-        y_hat: Array with predictions.
         y: Array with target variable.
+        y_pred: Array with predictions.
         weight: Weights for weighted metric.
+        agg: Function to calculate deviance ['sum', 'mean'] or callable are supported.
 
     Returns:
         float, value of the Gamma deviance.
     """
-    if weight:
-        return 2 * sum(weight*(-log(y/y_hat) + (y-y_hat)/y_hat))
+    dict_func = {'sum': np.sum, 'mean': np.mean}
+    func = dict_func[agg] if agg in ['sum', 'mean'] else agg if isinstance(agg, callable) else None
+    if func is None:
+        raise ValueError
+    weight = 1 if weight is None else weight
+    return func(2 * weight * (np.log(y_pred/y) + y/y_pred - 1))
+
+
+def inforamtion_value_woe(data, target, bins=10, cat_thresh=10, detail=False):
+    """Function for Information value and Weight of Evidence computation.
+
+    Args:
+        data (pd.DataFrame): DataFrame with data to compute IV and WoE.
+        target (:obj:`str` or :obj:`pd.Series`): Target variable to compute IV and WoE.
+        bins (:obj:`int`, optional): Number of bins for WoE calculation for continuous variables.
+        cat_thresh (:obj:`int`, optional): Maximum number of categories for non-binned WoE calculation.
+        detail (:obj:`bool`, optional):  Whether to return detailed results DataFrame or not. Short by default.
+
+    Returns:
+        pd.DataFrame, DataFrame containing the data on Information Value (depends on detail argument).
+    """
+    detailed_result, short_result = DataFrame(), DataFrame()
+    target = target.name if isinstance(target, Series) else target
+    cols = data.columns
+    for ivars in cols[~cols.isin([target])]:
+        if (data[ivars].dtype.kind in 'bifc') and (len(np.unique(data[ivars])) > cat_thresh):
+            binned_x = qcut(data[ivars], bins,  duplicates='drop')
+            d0 = DataFrame({'x': binned_x, 'y': data[target]})
+        else:
+            d0 = DataFrame({'x': data[ivars], 'y': data[target]})
+        d = d0.groupby("x", as_index=False).agg({"y": ["count", "sum"]})
+        d.columns = ['Cutoff', 'N', 'Events']
+        d['% of Events'] = np.maximum(d['Events'], 0.5) / d['Events'].sum()
+        d['Non-Events'] = d['N'] - d['Events']
+        d['% of Non-Events'] = np.maximum(d['Non-Events'], 0.5) / d['Non-Events'].sum()
+        d['WoE'] = np.log(d['% of Events'] / d['% of Non-Events'])
+        d['IV'] = d['WoE'] * (d['% of Events'] - d['% of Non-Events'])
+        d.insert(loc=0, column='Variable', value=ivars)
+        temp = DataFrame({"Variable": [ivars], "IV": [d['IV'].sum()]}, columns=["Variable", "IV"])
+        detailed_result = concat([detailed_result, temp], axis=0)
+        short_result = concat([short_result, d], axis=0)
+    return short_result if detail else detailed_result
+
+
+def gain_curve(predict, exposure, step=1, figsize=(10, 6), gini_exact=False, output=False):
+    gini_df = DataFrame()
+    plt.figure(figsize=figsize)
+    if isinstance(predict, (Series, np.ndarray)) and isinstance(exposure, Series):
+        temp_df = concat([Series(predict, name='Predict').reset_index(drop=True),
+                          exposure.reset_index(drop=True)], axis=1)
+        temp_df = temp_df.sort_values('Predict', ascending=False).reset_index(drop=True)
+        normalized_df = temp_df.cumsum()/temp_df.sum()
+        if gini_exact:
+            w = sum(temp_df[exposure.name])
+            m = np.true_divide(sum(temp_df[exposure.name] * temp_df['Predict']), sum(temp_df[exposure.name]))
+            temp_df['Rank'] = 0
+            temp_df.loc[0, 'Rank'] = 1 + 0.5 * (temp_df.loc[0, exposure.name] - 1)
+            for x in range(1, len(temp_df)):
+                temp_df.loc[x, 'Rank'] = (temp_df.loc[x-1, 'Rank'] + 0.5 * (temp_df.loc[x-1, exposure.name] + 1)
+                                          + 0.5 * (temp_df.loc[x, exposure.name] - 1))
+            gini = 1 + 1/w - 2/(w**2 * m) * sum(temp_df[exposure.name] * temp_df['Predict'] * temp_df['Rank'])
+        else:
+            auc = (np.sum(normalized_df['Predict'] * np.append(np.diff(normalized_df['Exposure']), 0))
+                   + np.sum(np.append(np.diff(normalized_df['Predict']), 0) *
+                            np.append(np.diff(normalized_df['Exposure']), 0))/2)
+            gini = 2 * auc - 1
+        gini_df = gini_df.append(DataFrame.from_dict({'Gini': {'Predict': gini}}))
+        plt.plot(normalized_df[exposure.name].values[::step], normalized_df['Predict'].values[::step],
+                 label=f'Predict (Gini: {round(gini, 3)})')
+    elif isinstance(predict, DataFrame) and isinstance(exposure, Series):
+        temp_df = concat([predict.reset_index(drop=True), exposure.reset_index(drop=True)], axis=1)
+        for pred_col in temp_df.columns[:-1]:
+            temp_df2 = temp_df[[pred_col, exposure.name]].sort_values(pred_col, ascending=False).reset_index(drop=True)
+            normalized_df = temp_df2.cumsum()/temp_df2.sum()
+            if gini_exact:
+                w = sum(temp_df2[exposure.name])
+                m = np.true_divide(sum(temp_df2[exposure.name] * temp_df2[pred_col]), sum(temp_df2[exposure.name]))
+                temp_df2['Rank'] = 0
+                temp_df2.loc[0, 'Rank'] = 1 + 0.5 * (temp_df2.loc[0, exposure.name] - 1)
+                for x in range(1, len(temp_df2)):
+                    temp_df2.loc[x, 'Rank'] = (temp_df2.loc[x-1, 'Rank'] + 0.5 * (temp_df2.loc[x-1, exposure.name] + 1)
+                                               + 0.5 * (temp_df2.loc[x, exposure.name] - 1))
+                gini = 1 + 1/w - 2/(w**2 * m) * sum(temp_df2[exposure.name] * temp_df2[pred_col] * temp_df2['Rank'])
+            else:
+                auc = (np.sum(normalized_df[pred_col] * np.append(np.diff(normalized_df[exposure.name]), 0))
+                       + np.sum(np.append(np.diff(normalized_df[pred_col]), 0) *
+                                np.append(np.diff(normalized_df[exposure.name]), 0))/2)
+                gini = 2 * auc - 1
+            gini_df = gini_df.append(DataFrame.from_dict({'Gini': {pred_col: gini}}))
+            plt.plot(normalized_df[exposure.name].values[::step], normalized_df[pred_col].values[::step],
+                     label=f'{pred_col} (Gini: {round(gini, 3)})')
+    else:
+        raise Exception
+    plt.legend()
+    plt.plot(np.linspace(0, 1, 2), np.linspace(0, 1, 2), c='red', linestyle='--', linewidth=0.7)
+    plt.title('Gains curve')
+    plt.xlabel('Cumulative exposure')
+    plt.ylabel('Cumulative response')
+    plt.show()
+    if output:
+        return gini_df
+
+
+def lift(predict, column, lift_type='groupby', q=10, output=False, reference='mean'):
+    df = concat([column.reset_index(drop=True), Series(predict, name='Predict')], axis=1)
+    if lift_type == 'groupby':
+        pass
+    elif lift_type == 'quantile':
+        df[column.name] = qcut(column, q=q).reset_index(drop=True)
+    else:
+        raise Exception
+    if reference == 'mean':
+        df = df.groupby(column.name).mean() / np.mean(predict)
+    elif reference == 'min':
+        df = df.groupby(column.name).mean() / df.groupby(column.name).min()
     else:
-        return 2 * sum(-log(y/y_hat) + (y-y_hat)/y_hat)
+        raise Exception
+    plt.bar(df.index.astype(str), height=df['Predict'])
+    plt.title('Lift Metrics')
+    plt.xlabel(column.name)
+    plt.ylabel('Lift Score')
+    plt.xticks(rotation=90)
+    plt.show()
+    if output:
+        return df
diff --git a/insolver/serving/fastapi_app.py b/insolver/serving/fastapi_app.py
index 3348e884..80770cf0 100644
--- a/insolver/serving/fastapi_app.py
+++ b/insolver/serving/fastapi_app.py
@@ -52,7 +52,7 @@ async def predict(data: Data):
     InsTransforms.ins_transform()
 
     # Prediction
-    predicted = model.predict(df)
+    predicted = model.predict(InsTransforms)
 
     result = {
         'predicted': predicted.tolist()
diff --git a/insolver/serving/flask_app.py b/insolver/serving/flask_app.py
index e46bd142..d0d5bdd3 100644
--- a/insolver/serving/flask_app.py
+++ b/insolver/serving/flask_app.py
@@ -70,7 +70,7 @@ def predict():
     InsTransforms.ins_transform()
 
     # Prediction
-    predicted = model.predict(df)
+    predicted = model.predict(InsTransforms)
 
     result = {
         'predicted': predicted.tolist()
diff --git a/insolver/wrappers/__init__.py b/insolver/wrappers/__init__.py
index c33c7fb9..e5eb33de 100644
--- a/insolver/wrappers/__init__.py
+++ b/insolver/wrappers/__init__.py
@@ -1,3 +1,4 @@
 from .glm import InsolverGLMWrapper
 from .gbm import InsolverGBMWrapper
+from .general import InsolverRFWrapper
 from .base import InsolverTrivialWrapper
diff --git a/insolver/wrappers/base.py b/insolver/wrappers/base.py
index 6ac33edb..757bedbb 100644
--- a/insolver/wrappers/base.py
+++ b/insolver/wrappers/base.py
@@ -1,6 +1,7 @@
 import os
 import time
 import pickle
+import joblib
 
 from numpy import array, mean, broadcast_to
 from pandas import concat, merge
@@ -64,6 +65,12 @@ def _pickle_save(self, path, name):
         with open(os.path.join(path, name), 'wb') as _model:
             pickle.dump(self.model, _model, pickle.HIGHEST_PROTOCOL)
 
+    def _joblib_load(self, load_path):
+        self.model = joblib.load(load_path)
+
+    def _joblib_save(self, path, name):
+        joblib.dump(self.model, os.path.join(path, name))
+
     def _update_meta(self):
         self.meta = self.__dict__.copy()
         for key in ['_backends', '_back_load_dict', '_back_save_dict', 'object', 'model', 'meta']:
diff --git a/insolver/wrappers/gbm.py b/insolver/wrappers/gbm.py
index 45200d72..6b608753 100644
--- a/insolver/wrappers/gbm.py
+++ b/insolver/wrappers/gbm.py
@@ -57,9 +57,9 @@ def __init__(self, backend, task=None, objective=None, n_estimators=100, load_pa
                     'gamma': {'xgboost': 'reg:gamma', 'lightgbm': 'gamma',
                               'catboost': 'Tweedie:variance_power=1.9999999'}
                 }
-                self.objective = (objectives[self.objective][self.backend] if self.objective in objectives.keys()
-                                  else self.objective)
-                kwargs.update({'objective': self.objective, 'n_estimators': self.n_estimators})
+                self.objective_ = (objectives[self.objective][self.backend] if self.objective in objectives.keys()
+                                   else self.objective)
+                kwargs.update({'objective': self.objective_, 'n_estimators': self.n_estimators})
                 self.model, self.params = gbm_init[task][self.backend](**(kwargs if kwargs is not None else {})), kwargs
 
                 def __params_gbm(**params):
@@ -71,18 +71,24 @@ def __params_gbm(**params):
                 raise NotImplementedError(f'Task parameter supports values in {self._tasks}.')
         self._update_meta()
 
-    def fit(self, X, y, **kwargs):
+    def fit(self, X, y, report=None, **kwargs):
         """Fit a Gradient Boosting Machine.
 
         Args:
             X (:obj:`pd.DataFrame`, :obj:`pd.Series`): Training data.
             y (:obj:`pd.DataFrame`, :obj:`pd.Series`): Training target values.
+            report (:obj:`list`, :obj:`tuple`, optional): A list of metrics to report after model fitting, optional.
             **kwargs: Other parameters passed to Scikit-learn API .fit().
         """
         self.model.fit(X, y, **kwargs)
         if not hasattr(self.model, 'feature_name_'):
             self.model.feature_name_ = X.columns if isinstance(X, DataFrame) else [X.name]
         self._update_meta()
+        if report is not None:
+            if isinstance(report, (list, tuple)):
+                prediction = self.model.predict(X)
+                print(DataFrame([[x.__name__, x(y, prediction)] for x
+                                 in report]).rename({0: 'Metrics', 1: 'Value'}, axis=1).set_index('Metrics'))
 
     def predict(self, X, **kwargs):
         """Predict using GBM with feature matrix X.
diff --git a/insolver/wrappers/general.py b/insolver/wrappers/general.py
new file mode 100644
index 00000000..0e4583e4
--- /dev/null
+++ b/insolver/wrappers/general.py
@@ -0,0 +1,115 @@
+from numpy import cumsum, diff, exp, true_divide, add, append, nan, concatenate, array
+from pandas import DataFrame, Series
+
+from sklearn.metrics import mean_squared_error, SCORERS
+from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
+
+from .base import InsolverBaseWrapper
+from .extensions import InsolverCVHPExtension, InsolverPDPExtension
+
+
+class InsolverRFWrapper(InsolverBaseWrapper, InsolverCVHPExtension, InsolverPDPExtension):
+    """Insolver wrapper for Random Forest.
+
+    Attributes:
+        backend (str): Framework for building RF, 'sklearn' is supported.
+        task (str): Task that RF should solve: Classification or Regression. Values 'reg' and 'class' are supported.
+        n_estimators (:obj:`int`, optional): Number of trees in the forest. Equals 100 by default.
+        load_path (:obj:`str`, optional): Path to RF model to load from disk.
+        **kwargs: Parameters for RF estimators except `n_estimators`. Will not be changed in hyperopt.
+    """
+    def __init__(self, backend, task=None, n_estimators=100, load_path=None, **kwargs):
+        super(InsolverRFWrapper, self).__init__(backend)
+        self.algo, self._backends = 'Random Forest', ['sklearn']
+        self._tasks = ['class', 'reg']
+        self._back_load_dict = {'sklearn': self._pickle_load}
+        self._back_save_dict = {'sklearn': self._pickle_save}
+        self.n_estimators, self.params = n_estimators, None
+
+        if backend not in self._backends:
+            raise NotImplementedError(f'Error with the backend choice. Supported backends: {self._backends}')
+
+        if load_path is not None:
+            self.load_model(load_path)
+        else:
+            if task in self._tasks:
+                rf_init = {
+                    'class': {'sklearn': RandomForestClassifier},
+                    'reg': {'sklearn': RandomForestRegressor}
+                }
+
+                kwargs.update({'n_estimators': self.n_estimators})
+                self.model, self.params = rf_init[task][self.backend](**(kwargs if kwargs is not None else {})), kwargs
+
+                def __params_rf(**params):
+                    params.update(self.params)
+                    return rf_init[task][self.backend](**params)
+
+                self.object = __params_rf
+            else:
+                raise NotImplementedError(f'Task parameter supports values in {self._tasks}.')
+        self._update_meta()
+
+    def fit(self, X, y, report=None, **kwargs):
+        """Fit a Random Forest.
+
+        Args:
+            X (:obj:`pd.DataFrame`, :obj:`pd.Series`): Training data.
+            y (:obj:`pd.DataFrame`, :obj:`pd.Series`): Training target values.
+            report (:obj:`list`, :obj:`tuple`, optional): A list of metrics to report after model fitting, optional.
+            **kwargs: Other parameters passed to Scikit-learn API .fit().
+        """
+        self.model.fit(X, y, **kwargs)
+        if not hasattr(self.model, 'feature_name_'):
+            self.model.feature_name_ = X.columns if isinstance(X, DataFrame) else [X.name]
+        self._update_meta()
+        if report is not None:
+            if isinstance(report, (list, tuple)):
+                prediction = self.model.predict(X)
+                print(DataFrame([[x.__name__, x(y, prediction)] for x
+                                 in report]).rename({0: 'Metrics', 1: 'Value'}, axis=1).set_index('Metrics'))
+
+    def predict(self, X, **kwargs):
+        """Predict using RF with feature matrix X.
+
+        Args:
+            X (:obj:`pd.DataFrame`, :obj:`pd.Series`): Samples.
+            **kwargs: Other parameters passed to Scikit-learn API .predict().
+
+        Returns:
+            array: Returns predicted values.
+        """
+        return self.model.predict(X if not hasattr(self.model, 'feature_name_')
+                                  else X[self.model.feature_name_], **kwargs)
+
+    def cross_val(self, X, y, scoring=None, cv=None, **kwargs):
+        """Method for performing cross-validation given the hyperparameters of initialized or fitted model.
+
+        Args:
+            X (:obj:`pd.DataFrame`, :obj:`pd.Series`): Training data.
+            y (:obj:`pd.DataFrame`, :obj:`pd.Series`): Training target values.
+            scoring (:obj:`callable`): Metrics passed to sklearn.model_selection.cross_validate calculation.
+            cv (:obj:`int, cross-validation generator or an iterable`, optional): Cross-validation strategy from
+             sklearn. Performs 5-fold cv by default.
+            **kwargs: Other parameters passed to sklearn.model_selection.cross_validate.
+
+        Returns:
+            pd.DataFrame, pd.DataFrame: DataFrame with metrics on folds, DataFrame with shap values on folds.
+        """
+        scoring = mean_squared_error if scoring is None else scoring
+        models, metrics = self._cross_val(X, y, scoring=scoring, cv=cv, **kwargs)
+        if callable(scoring):
+            scorers = {scoring.__name__.replace('_', ' '): array([scoring(y, self.model.predict(X))])}
+        elif isinstance(scoring, (tuple, list)):
+            scorers = {scorer.__name__.replace('_', ' '): array([scorer(y, self.model.predict(X))]) for
+                       scorer in scoring}
+        elif isinstance(scoring, str):
+            if scoring in SCORERS:
+                scorers = {scoring.replace('_', ' '): array([SCORERS[scoring](self.model, X=X, y=y)])}
+            else:
+                raise ValueError(f'Scorer {scoring} is not supported.')
+        else:
+            raise NotImplementedError(f'Scoring of type {scoring} is not supported')
+        metrics = DataFrame({key: concatenate((scorers[key], metrics[key])) for key in scorers.keys()}).T
+        metrics.columns = [f'Fold {i}' if i != 0 else 'Overall' for i in range(metrics.shape[1])]
+        return metrics
diff --git a/insolver/wrappers/glm.py b/insolver/wrappers/glm.py
index ba5ae3fd..87386217 100644
--- a/insolver/wrappers/glm.py
+++ b/insolver/wrappers/glm.py
@@ -69,7 +69,8 @@ def __params_pipe(**glm_pars):
                 self.model, self.object = __params_pipe(**self.params), __params_pipe
         self._update_meta()
 
-    def fit(self, X, y, sample_weight=None, X_valid=None, y_valid=None, sample_weight_valid=None, **kwargs):
+    def fit(self, X, y, sample_weight=None, X_valid=None, y_valid=None, sample_weight_valid=None,
+            report=None, **kwargs):
         """Fit a Generalized Linear Model.
 
         Args:
@@ -79,6 +80,7 @@ def fit(self, X, y, sample_weight=None, X_valid=None, y_valid=None, sample_weigh
             X_valid (:obj:`pd.DataFrame`, :obj:`pd.Series`, optional): Validation data (only h2o supported).
             y_valid (:obj:`pd.DataFrame`, :obj:`pd.Series`, optional): Validation target values (only h2o supported).
             sample_weight_valid (:obj:`pd.DataFrame`, :obj:`pd.Series`, optional): Validation sample weights.
+            report (:obj:`list`, :obj:`tuple`, optional): A list of metrics to report after model fitting, optional.
             **kwargs: Other parameters passed to H2OGeneralizedLinearEstimator.
         """
         if (self.backend == 'sklearn') & isinstance(self.model, Pipeline):
@@ -92,6 +94,14 @@ def fit(self, X, y, sample_weight=None, X_valid=None, y_valid=None, sample_weigh
         else:
             raise NotImplementedError(f'Error with the backend choice. Supported backends: {self._backends}')
         self._update_meta()
+        if report is not None:
+            if isinstance(report, (list, tuple)):
+                if self.backend == 'h2o':
+                    prediction = self.model.predict(train_set).as_data_frame().values.reshape(-1)
+                else:
+                    prediction = self.model.predict(X)
+                print(DataFrame([[x.__name__, x(y, prediction)] for x
+                                 in report]).rename({0: 'Metrics', 1: 'Value'}, axis=1).set_index('Metrics'))
 
     def predict(self, X, sample_weight=None, **kwargs):
         """Predict using GLM with feature matrix X.
@@ -123,7 +133,7 @@ def coef_norm(self):
         """Output GLM coefficients for standardized data.
 
         Returns:
-            dict: {:obj:`str`: :obj:`float`}m Dictionary containing GLM coefficients for standardized data.
+            dict: {:obj:`str`: :obj:`float`} Dictionary containing GLM coefficients for standardized data.
         """
         if self.standardize:
             if (self.backend == 'sklearn') & isinstance(self.model, Pipeline):
@@ -139,15 +149,14 @@ def coef_norm(self):
                 raise NotImplementedError(f'Error with the backend choice. Supported backends: {self._backends}')
 
         else:
-            Exception('Normalized coefficients unavailable since model fitted on non-standardized data.')
-            coefs = dict()
+            raise Exception('Normalized coefficients unavailable since model fitted on non-standardized data.')
         return coefs
 
     def coef(self):
         """Output GLM coefficients for non-standardized data. Also calculated when GLM fitted on standardized data.
 
         Returns:
-            dict: {:obj:`str`: :obj:`float`}m Dictionary containing GLM coefficients for non-standardized data.
+            dict: {:obj:`str`: :obj:`float`} Dictionary containing GLM coefficients for non-standardized data.
         """
         if (self.backend == 'sklearn') & isinstance(self.model, Pipeline):
             if self.model.feature_name_ is None:
diff --git a/requirements-dev.txt b/requirements-dev.txt
index a8656e45..28c3ab96 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,2 +1,3 @@
 pytest==6.0.1
 pytest-cov==2.10.1
+tox==3.21.0
diff --git a/requirements.txt b/requirements.txt
index 0278a6bb..2062784f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,7 @@
 wheel>=0.36.2
+joblib>=1.0.0
 numpy>=1.19.5
+scipy>=1.6.0
 pandas>=1.2.0
 xgboost>=1.3.2
 lightgbm>=3.1.1
@@ -19,5 +21,5 @@ h2o==3.32.0.3
 flask==1.1.2
 fastapi==0.63.0
 uvicorn[standard]==0.13.3
-pydantic==1.7.3
+pydantic==1.7.4
 gunicorn==20.0.4
diff --git a/setup.py b/setup.py
index d5ab23d5..22e27610 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
 
 
 setup(name='insolver',
-      version='0.4.6',
+      version='0.4.10',
       description='Insolver is low-code machine learning library, initially created for the insurance industry, '
                   'but can be used in any other.\n You can find a detailed overview at '
                   'https://insolver.readthedocs.io/en/latest/source/overview.html.',
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..4725226c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,18 @@
+# content of: tox.ini , put in same dir as setup.py
+[tox]
+envlist = py36, py37, py38, py39
+skip_missing_interpreters = true
+skipsdist=True
+
+[gh-actions]
+python =
+    3.6: py36
+    3.7: py37
+    3.8: py38
+    3.9: py39
+
+[testenv]
+deps = -r requirements.txt
+commands =
+    # NOTE: you can run any command line tool here - not just tests
+    pytest --junitxml=test_report.xml


Test patch:
diff --git a/tests/test_InsolverDataFrame.py b/tests/test_InsolverDataFrame.py
index e9a35c40..efa966ca 100644
--- a/tests/test_InsolverDataFrame.py
+++ b/tests/test_InsolverDataFrame.py
@@ -1,8 +1,7 @@
 import pandas as pd
 from insolver import InsolverDataFrame
 
-
-df = pd.read_csv('tests/data/freMPL-R_100.csv')
+df = pd.DataFrame({'col1': [1], 'col2': [2]})
 
 
 def test_InsolverDataFrame():
@@ -10,3 +9,23 @@ def test_InsolverDataFrame():
     assert hasattr(InsDataFrame, 'get_meta_info')
     assert hasattr(InsDataFrame, 'get_batch')
     assert hasattr(InsDataFrame, 'split_frame')
+    assert hasattr(InsDataFrame, 'sample_request')
+
+
+def test_get_meta_info():
+    InsDataFrame = InsolverDataFrame(df)
+    meta_info = InsDataFrame.get_meta_info()
+    assert meta_info['type'] == 'InsolverDataFrame'
+    assert meta_info['len'] == 1
+    assert meta_info['columns'][0]['name'] == 'col1'
+    assert meta_info['columns'][1]['name'] == 'col2'
+    assert meta_info['columns'][0]['use'] == 'unknown'
+    assert meta_info['columns'][1]['use'] == 'unknown'
+    assert meta_info['columns'][0]['dtype'] == df.col1.dtypes
+    assert meta_info['columns'][1]['dtype'] == df.col2.dtypes
+
+
+def test_sample_request():
+    InsDataFrame = InsolverDataFrame(df)
+    request = InsDataFrame.sample_request()
+    assert request['df'] == {'col1': {'0': 1}, 'col2': {'0': 2}}