Repository: zelros/cinnamon (language: python)

Issue:
Add output drift explainer



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index a0071c8..d013de7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,9 +6,11 @@ models/
 data/
 .DS_Store
 figures/
-src/__pycache__/
+**/__pycache__/
 save/
 *_old*
 dist/
 *.egg-info/
 RESEARCH.md
+build/
+catboost_info/
diff --git a/Makefile b/Makefile
index 9519e95..f8249b3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,2 +1,5 @@
 build:
-	python3 -m build
\ No newline at end of file
+	python3 -m build
+
+test:
+	python3 -m pytest src/cinnamon
diff --git a/Pipfile b/Pipfile
index 73b7baa..64855b2 100644
--- a/Pipfile
+++ b/Pipfile
@@ -5,15 +5,11 @@ name = "pypi"
 
 [packages]
 cinnamon = {path = './', editable = true}
-treelib = "*"
-matplotlib = "*"
-jupyter = "*"
-catboost = "*"
-pweave = "*"
+#pweave = "*"
 
 [dev-packages]
 build = "*"
 twine = "*"
-
-[requires]
-python_version = "3.9"
+pytest = "*"
+flake8 = "*"
+jupyter = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 916d3be..ce93b6e 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "b7bc83a0e9feb3bc9df4ed53b3ac7be84d448de8c662af323da362a6f0d816a9"
+            "sha256": "aa498ae773a572827af5c494982dfacee305560029f7c0188b911ff85be15cbf"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -21,25 +21,43 @@
                 "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442",
                 "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"
             ],
-            "markers": "sys_platform == 'darwin' and platform_system == 'Darwin'",
+            "markers": "sys_platform == 'darwin'",
             "version": "==0.1.2"
         },
         "argon2-cffi": {
             "hashes": [
-                "sha256:165cadae5ac1e26644f5ade3bd9c18d89963be51d9ea8817bd671006d7909057",
-                "sha256:217b4f0f853ccbbb5045242946ad2e162e396064575860141b71a85eb47e475a",
-                "sha256:245f64a203012b144b7b8c8ea6d468cb02b37caa5afee5ba4a10c80599334f6a",
-                "sha256:4ad152c418f7eb640eac41ac815534e6aa61d1624530b8e7779114ecfbf327f8",
-                "sha256:566ffb581bbd9db5562327aee71b2eda24a1c15b23a356740abe3c011bbe0dcb",
-                "sha256:65213a9174320a1aee03fe826596e0620783966b49eb636955958b3074e87ff9",
-                "sha256:bc513db2283c385ea4da31a2cd039c33380701f376f4edd12fe56db118a3b21a",
-                "sha256:c7a7c8cc98ac418002090e4add5bebfff1b915ea1cb459c578cd8206fef10378",
-                "sha256:e4d8f0ae1524b7b0372a3e574a2561cbdddb3fdb6c28b70a72868189bda19659",
-                "sha256:f710b61103d1a1f692ca3ecbd1373e28aa5e545ac625ba067ff2feca1b2bb870",
-                "sha256:fa7e7d1fc22514a32b1761fdfa1882b6baa5c36bb3ef557bdd69e6fc9ba14a41"
+                "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80",
+                "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"
             ],
-            "markers": "python_version >= '3.5'",
-            "version": "==21.1.0"
+            "markers": "python_version >= '3.6'",
+            "version": "==21.3.0"
+        },
+        "argon2-cffi-bindings": {
+            "hashes": [
+                "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670",
+                "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f",
+                "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583",
+                "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194",
+                "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c",
+                "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a",
+                "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082",
+                "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5",
+                "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f",
+                "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7",
+                "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d",
+                "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f",
+                "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae",
+                "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3",
+                "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86",
+                "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367",
+                "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d",
+                "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93",
+                "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb",
+                "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e",
+                "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==21.2.0"
         },
         "attrs": {
             "hashes": [
@@ -66,27 +84,30 @@
         },
         "catboost": {
             "hashes": [
-                "sha256:0d853958210a0de866a9d57ef80aa5e11617712660c7e8562038d321bda3b234",
-                "sha256:112f356fe592f921573fe306ef520e1a845083618569a1de21e970a4023b49ce",
-                "sha256:18b47b39126d3140c80a792b4b7bbef1b941e1b5c5db49d048e0fec288ec4987",
-                "sha256:1bb3540e2fe48efe9c1d29ee1d7f4ff933612966ade15552a682da67c090ab53",
-                "sha256:1bfdf317d8be37b163c1103757811ddbdb44836d6fdbe83a4134e18152804ae9",
-                "sha256:1e2a8d2b02b27ed29645a6c12d8a220abf5b8e8f02125e2c44604129b65ddc0f",
-                "sha256:2dbdbe14aae8f58b10bd5f769ca2eb96eb541664f172870874008253bc92e014",
-                "sha256:3a7ed94e33ece0abcba33077282f17ebcf9c929dc927a3f455330c01f4515651",
-                "sha256:552af5ddc5c8d5d37b1fe80665d27238fd980e42f384b6558022bd83456be7d7",
-                "sha256:8354ff250d08a54d02de40061cb505579d0f9fb71b51c342ed76172774a5eb1b",
-                "sha256:8e5d36f1f054b561846aa90beef2e9827b7147f64172f2be375fcbd897038682",
-                "sha256:8e9fd180322491b2ad0dc0bb54d0229154d49609f4b57451aedf7698506ebd41",
-                "sha256:96bab88c4915a5250d3d2a5be3943a24fa3a43d597d66692cc21078b267596cc",
-                "sha256:9b66baec2913247cc3d4ed62ce0f4260ceb0c63a01ce8f62c17483277a1ec312",
-                "sha256:ab31203aae2b7c8a9c8b27514259f6b5d089d9a23c775e8f68a20add9eaf9cfa",
-                "sha256:bb4b9370a31971851a8a5fa1815087b60f4aef822892623daac106c1982b9efa",
-                "sha256:dc9536d63908fa09dc0b18167307a56030480703484919501fad8a1d493df133",
-                "sha256:f1ad1e87465a30e1b73e5cab72ae579bcc3977b553fb50d2c0dfb3da6e81e511"
+                "sha256:054c7c8cf6f3ad4e7507223ca38a4db545f4e97618db1044143bd6980af52a60",
+                "sha256:10b7d1d65f4cd7d5fce1ebbf784eca7527d2c2f70aab5d2ece14e472c116c895",
+                "sha256:1558734e7a947716f291d9b100d29cbfd298bb26e5ae84c6009fccb344160ad6",
+                "sha256:2f9dbae8e044418274919ae7ba860da81e06e4b5506ff06711d6a21c29f77a89",
+                "sha256:45adf23597e6b03989ab741aaa0759ef13da6f9231c66425c3fc1f9bbaadc1c3",
+                "sha256:5356f7aba3193cb38034e4be362ff1b9d19ba506db3e68a9ebfde99c49fe7890",
+                "sha256:566e89a919149260c4de75c90ceacae688d066975b2a9cf62aed61208d6b4fde",
+                "sha256:5ca9f375092a656ed5352716f8033b6e08be5a33d2bc03f6cb2f8010c9821d7a",
+                "sha256:657ea6df049d01ce728e6a2930d0b3930549d9a47190375d1cc0f9ba8e5fd221",
+                "sha256:673eb21d714e72cb851521a08375c43f7322c99ef3560f10208e730861cbf6ae",
+                "sha256:6c661efa6e071885a6b8925465dfc989adcd4e7b62b2647dcd9fba03c75424c6",
+                "sha256:6f306857a49b30dbc9f66fbb2e93c868b07aa14e03a48a4c959e8e11a09e643d",
+                "sha256:7859c3eacd07093331c8dc16dd5c74f9e6f3ecdf7cc4ac7073db1129029d0e5b",
+                "sha256:7f7571a1a32e1ac0fe3a0e34318ac1d90de2d45761aab2f9a811fe33e119bdbb",
+                "sha256:8550e66ab659be8c4a3b6bf5824fe5dcd7e6fffd641fc445e90e6d2a254effad",
+                "sha256:8bb5e2e9c162721d07a138266da8b3ffdd4c120eb77eb3c24cd5549ca4f5ad67",
+                "sha256:b72e72a859bb47b269fa43d94c9332a177fe27caf920946e21c92590202a86c0",
+                "sha256:c435ef13ea0adbfefe68d4426b8d7a22b63f2b02928ca451ae6cce3462e03f1c",
+                "sha256:c89b70328b85ae5bf8ae467c960fdf430cbf7eefe86a15a705fbc0e236a0d82a",
+                "sha256:d0b04c62523876f8c21f636fd3b4c82950f26115352963017ae820b12b299e80",
+                "sha256:e1db7572879ed8d92b8a1cf66cd0b76fe02ace1752b0b124d216bc6dab4985d2"
             ],
             "index": "pypi",
-            "version": "==1.0.0"
+            "version": "==1.0.3"
         },
         "cffi": {
             "hashes": [
@@ -149,10 +170,11 @@
         },
         "cycler": {
             "hashes": [
-                "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d",
-                "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
+                "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3",
+                "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"
             ],
-            "version": "==0.10.0"
+            "markers": "python_version >= '3.6'",
+            "version": "==0.11.0"
         },
         "debugpy": {
             "hashes": [
@@ -205,6 +227,14 @@
             "markers": "python_version >= '2.7'",
             "version": "==0.3"
         },
+        "fonttools": {
+            "hashes": [
+                "sha256:545c05d0f7903a863c2020e07b8f0a57517f2c40d940bded77076397872d14ca",
+                "sha256:edf251d5d2cc0580d5f72de4621c338d8c66c5f61abb50cf486640f73c8194d5"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.28.5"
+        },
         "future": {
             "hashes": [
                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
@@ -214,27 +244,35 @@
         },
         "graphviz": {
             "hashes": [
-                "sha256:5dadec94046d82adaae6019311a30e0487536d9d5a60d85451f0ba32f9fc6559",
-                "sha256:ef6e2c5deb9cdcc0c7eece1d89625fd07b0f2208ea2bcb483520907ddf8b4e12"
+                "sha256:09ed0cde452d015fe77c4845a210eb642f28d245f5bc250d4b97808cb8f49078",
+                "sha256:f34088c08be2ec16279dfa9c3b4ff3d1453c5c67597a33e2819b000e18d4c546"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==0.17"
+            "version": "==0.19.1"
+        },
+        "importlib-metadata": {
+            "hashes": [
+                "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
+                "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
+            ],
+            "markers": "python_version < '3.10'",
+            "version": "==4.10.0"
         },
         "ipykernel": {
             "hashes": [
-                "sha256:a3f6c2dda2ecf63b37446808a70ed825fea04790779ca524889c596deae0def8",
-                "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"
+                "sha256:3a227788216b43982d9ac28195949467627b0d16e6b8af9741d95dcaa8c41a89",
+                "sha256:82ded8919fa7f5483be2b6219c3b13380d93faab1fc49cc2cfcd10e9e24cc158"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==6.4.1"
+            "version": "==6.6.0"
         },
         "ipython": {
             "hashes": [
-                "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11",
-                "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"
+                "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97",
+                "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c"
             ],
-            "markers": "python_version >= '3.3'",
-            "version": "==7.28.0"
+            "markers": "python_version >= '3.7'",
+            "version": "==7.30.1"
         },
         "ipython-genutils": {
             "hashes": [
@@ -252,19 +290,19 @@
         },
         "jedi": {
             "hashes": [
-                "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93",
-                "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"
+                "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d",
+                "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==0.18.0"
+            "version": "==0.18.1"
         },
         "jinja2": {
             "hashes": [
-                "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45",
-                "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"
+                "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
+                "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==3.0.2"
+            "version": "==3.0.3"
         },
         "joblib": {
             "hashes": [
@@ -276,11 +314,11 @@
         },
         "jsonschema": {
             "hashes": [
-                "sha256:2b3cca28580511d44326f0e7fc582eab3cbe31aabd1a1c2cfa74a399796ffd84",
-                "sha256:9dd7c33b4a96138dc37bb86b3610d3b12d30d96433d4d73435ca3025804154a8"
+                "sha256:8697a10a5a5edc922d2eb8556c7f35e814436f3ed8278ec2f65d40e9312d7c80",
+                "sha256:cca171fb7544de15ccda236bf78d58434d769c9a2ce21d44e0d209e39eeb8876"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==4.1.0"
+            "version": "==4.3.2"
         },
         "jupyter": {
             "hashes": [
@@ -293,11 +331,11 @@
         },
         "jupyter-client": {
             "hashes": [
-                "sha256:074bdeb1ffaef4a3095468ee16313938cfdc48fc65ca95cc18980b956c2e5d79",
-                "sha256:8b6e06000eb9399775e0a55c52df6c1be4766666209c22f90c2691ded0e338dc"
+                "sha256:64d93752d8cbfba0c1030c3335c3f0d9797cd1efac012652a14aac1653db11a3",
+                "sha256:a5f995a73cffb314ed262713ae6dfce53c6b8216cea9f332071b8ff44a6e1654"
             ],
             "markers": "python_full_version >= '3.6.1'",
-            "version": "==7.0.6"
+            "version": "==7.1.0"
         },
         "jupyter-console": {
             "hashes": [
@@ -309,11 +347,11 @@
         },
         "jupyter-core": {
             "hashes": [
-                "sha256:8dd262ec8afae95bd512518eb003bc546b76adbf34bf99410e9accdf4be9aa3a",
-                "sha256:ef210dcb4fca04de07f2ead4adf408776aca94d17151d6f750ad6ded0b91ea16"
+                "sha256:1c091f3bbefd6f2a8782f2c1db662ca8478ac240e962ae2c66f0b87c818154ea",
+                "sha256:dce8a7499da5a53ae3afd5a9f4b02e5df1d57250cf48f3ad79da23b4778cd6fa"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==4.8.1"
+            "version": "==4.9.1"
         },
         "jupyterlab-pygments": {
             "hashes": [
@@ -382,17 +420,18 @@
         },
         "markdown": {
             "hashes": [
-                "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49",
-                "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"
+                "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006",
+                "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==3.3.4"
+            "version": "==3.3.6"
         },
         "markupsafe": {
             "hashes": [
                 "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
                 "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
                 "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
+                "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194",
                 "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
                 "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
                 "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
@@ -400,6 +439,7 @@
                 "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
                 "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
                 "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
+                "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a",
                 "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
                 "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
                 "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
@@ -407,27 +447,36 @@
                 "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
                 "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
                 "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
+                "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047",
                 "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
                 "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
+                "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b",
                 "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
                 "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
                 "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
                 "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
+                "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1",
                 "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
                 "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
                 "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
+                "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee",
+                "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f",
                 "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
                 "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
                 "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
                 "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
                 "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
                 "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
+                "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86",
+                "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6",
                 "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
                 "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
                 "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
                 "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
+                "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e",
                 "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
                 "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
+                "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f",
                 "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
                 "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
                 "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
@@ -435,10 +484,14 @@
                 "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
                 "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
                 "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
+                "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a",
+                "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207",
                 "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
                 "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
+                "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd",
                 "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
                 "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
+                "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9",
                 "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
                 "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
                 "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
@@ -450,30 +503,44 @@
         },
         "matplotlib": {
             "hashes": [
-                "sha256:01c9de93a2ca0d128c9064f23709362e7fefb34910c7c9e0b8ab0de8258d5eda",
-                "sha256:41b6e307458988891fcdea2d8ecf84a8c92d53f84190aa32da65f9505546e684",
-                "sha256:48e1e0859b54d5f2e29bb78ca179fd59b971c6ceb29977fb52735bfd280eb0f5",
-                "sha256:54a026055d5f8614f184e588f6e29064019a0aa8448450214c0b60926d62d919",
-                "sha256:556965514b259204637c360d213de28d43a1f4aed1eca15596ce83f768c5a56f",
-                "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913",
-                "sha256:6a724e3a48a54b8b6e7c4ae38cd3d07084508fa47c410c8757e9db9791421838",
-                "sha256:6be8df61b1626e1a142c57e065405e869e9429b4a6dab4a324757d0dc4d42235",
-                "sha256:844a7b0233e4ff7fba57e90b8799edaa40b9e31e300b8d5efc350937fa8b1bea",
-                "sha256:85f0c9cf724715e75243a7b3087cf4a3de056b55e05d4d76cc58d610d62894f3",
-                "sha256:a78a3b51f29448c7f4d4575e561f6b0dbb8d01c13c2046ab6c5220eb25c06506",
-                "sha256:b884715a59fec9ad3b6048ecf3860f3b2ce965e676ef52593d6fa29abcf7d330",
-                "sha256:b8b53f336a4688cfce615887505d7e41fd79b3594bf21dd300531a4f5b4f746a",
-                "sha256:c70b6311dda3e27672f1bf48851a0de816d1ca6aaf3d49365fbdd8e959b33d2b",
-                "sha256:ebfb01a65c3f5d53a8c2a8133fec2b5221281c053d944ae81ff5822a68266617",
-                "sha256:eeb1859efe7754b1460e1d4991bbd4a60a56f366bc422ef3a9c5ae05f0bc70b5",
-                "sha256:f15edcb0629a0801738925fe27070480f446fcaa15de65946ff946ad99a59a40",
-                "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa",
-                "sha256:f72657f1596199dc1e4e7a10f52a4784ead8a711f4e5b59bea95bdb97cf0e4fd",
-                "sha256:fc4f526dfdb31c9bd6b8ca06bf9fab663ca12f3ec9cdf4496fb44bc680140318",
-                "sha256:fcd6f1954943c0c192bfbebbac263f839d7055409f1173f80d8b11a224d236da"
+                "sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a",
+                "sha256:2252bfac85cec7af4a67e494bfccf9080bcba8a0299701eab075f48847cca907",
+                "sha256:2e3484d8455af3fdb0424eae1789af61f6a79da0c80079125112fd5c1b604218",
+                "sha256:34a1fc29f8f96e78ec57a5eff5e8d8b53d3298c3be6df61e7aa9efba26929522",
+                "sha256:3e66497cd990b1a130e21919b004da2f1dc112132c01ac78011a90a0f9229778",
+                "sha256:40e0d7df05e8efe60397c69b467fc8f87a2affeb4d562fe92b72ff8937a2b511",
+                "sha256:456cc8334f6d1124e8ff856b42d2cc1c84335375a16448189999496549f7182b",
+                "sha256:506b210cc6e66a0d1c2bb765d055f4f6bc2745070fb1129203b67e85bbfa5c18",
+                "sha256:53273c5487d1c19c3bc03b9eb82adaf8456f243b97ed79d09dded747abaf1235",
+                "sha256:577ed20ec9a18d6bdedb4616f5e9e957b4c08563a9f985563a31fd5b10564d2a",
+                "sha256:6803299cbf4665eca14428d9e886de62e24f4223ac31ab9c5d6d5339a39782c7",
+                "sha256:68fa30cec89b6139dc559ed6ef226c53fd80396da1919a1b5ef672c911aaa767",
+                "sha256:6c094e4bfecd2fa7f9adffd03d8abceed7157c928c2976899de282f3600f0a3d",
+                "sha256:778d398c4866d8e36ee3bf833779c940b5f57192fa0a549b3ad67bc4c822771b",
+                "sha256:7a350ca685d9f594123f652ba796ee37219bf72c8e0fc4b471473d87121d6d34",
+                "sha256:87900c67c0f1728e6db17c6809ec05c025c6624dcf96a8020326ea15378fe8e7",
+                "sha256:8a77906dc2ef9b67407cec0bdbf08e3971141e535db888974a915be5e1e3efc6",
+                "sha256:8e70ae6475cfd0fad3816dcbf6cac536dc6f100f7474be58d59fa306e6e768a4",
+                "sha256:abf67e05a1b7f86583f6ebd01f69b693b9c535276f4e943292e444855870a1b8",
+                "sha256:b04fc29bcef04d4e2d626af28d9d892be6aba94856cb46ed52bcb219ceac8943",
+                "sha256:b19a761b948e939a9e20173aaae76070025f0024fc8f7ba08bef22a5c8573afc",
+                "sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c",
+                "sha256:b71f3a7ca935fc759f2aed7cec06cfe10bc3100fadb5dbd9c435b04e557971e1",
+                "sha256:b8a4fb2a0c5afbe9604f8a91d7d0f27b1832c3e0b5e365f95a13015822b4cd65",
+                "sha256:bb1c613908f11bac270bc7494d68b1ef6e7c224b7a4204d5dacf3522a41e2bc3",
+                "sha256:d24e5bb8028541ce25e59390122f5e48c8506b7e35587e5135efcb6471b4ac6c",
+                "sha256:d70a32ee1f8b55eed3fd4e892f0286df8cccc7e0475c11d33b5d0a148f5c7599",
+                "sha256:e293b16cf303fe82995e41700d172a58a15efc5331125d08246b520843ef21ee",
+                "sha256:e2f28a07b4f82abb40267864ad7b3a4ed76f1b1663e81c7efc84a9b9248f672f",
+                "sha256:e3520a274a0e054e919f5b3279ee5dbccf5311833819ccf3399dab7c83e90a25",
+                "sha256:e3b6f3fd0d8ca37861c31e9a7cab71a0ef14c639b4c95654ea1dd153158bf0df",
+                "sha256:e486f60db0cd1c8d68464d9484fd2a94011c1ac8593d765d0211f9daba2bd535",
+                "sha256:e8c87cdaf06fd7b2477f68909838ff4176f105064a72ca9d24d3f2a29f73d393",
+                "sha256:edf5e4e1d5fb22c18820e8586fb867455de3b109c309cb4fce3aaed85d9468d1",
+                "sha256:fe8d40c434a8e2c68d64c6d6a04e77f21791a93ff6afe0dce169597c110d3079"
             ],
             "index": "pypi",
-            "version": "==3.4.3"
+            "version": "==3.5.1"
         },
         "matplotlib-inline": {
             "hashes": [
@@ -492,19 +559,19 @@
         },
         "nbclient": {
             "hashes": [
-                "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce",
-                "sha256:95a300c6fbe73721736cf13972a46d8d666f78794b832866ed7197a504269e11"
+                "sha256:8a307be4129cce5f70eb83a57c3edbe45656623c31de54e38bb6fdfbadc428b3",
+                "sha256:99e46ddafacd0b861293bf246fed8540a184adfa3aa7d641f89031ec070701e0"
             ],
             "markers": "python_full_version >= '3.6.1'",
-            "version": "==0.5.4"
+            "version": "==0.5.9"
         },
         "nbconvert": {
             "hashes": [
-                "sha256:16ceecd0afaa8fd26c245fa32e2c52066c02f13aa73387fffafd84750baea863",
-                "sha256:b1b9dc4f1ff6cafae0e6d91f42fb9046fdc32e6beb6d7e2fa2cd7191ad535240"
+                "sha256:5e77d6203854944520105e38f2563a813a4a3708e8563aa598928a3b5ee1081a",
+                "sha256:8f23fbeabda4a500685d788ee091bf22cf34119304314304fb39f16e2fc32f37"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==6.2.0"
+            "version": "==6.3.0"
         },
         "nbformat": {
             "hashes": [
@@ -516,90 +583,94 @@
         },
         "nest-asyncio": {
             "hashes": [
-                "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c",
-                "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"
+                "sha256:3fdd0d6061a2bb16f21fe8a9c6a7945be83521d81a0d15cff52e9edee50101d6",
+                "sha256:f969f6013a16fadb4adcf09d11a68a4f617c6049d7af7ac2c676110169a63abd"
             ],
             "markers": "python_version >= '3.5'",
-            "version": "==1.5.1"
+            "version": "==1.5.4"
         },
         "notebook": {
             "hashes": [
-                "sha256:26b0095c568e307a310fd78818ad8ebade4f00462dada4c0e34cbad632b9085d",
-                "sha256:33488bdcc5cbef23c3cfa12cd51b0b5459a211945b5053d17405980611818149"
+                "sha256:5cad068fa82cd4fb98d341c052100ed50cd69fbfb4118cb9b8ab5a346ef27551",
+                "sha256:7bcdf79bd1cda534735bd9830d2cbedab4ee34d8fe1df6e7b946b3aab0902ba3"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==6.4.4"
+            "version": "==6.4.6"
         },
         "numpy": {
             "hashes": [
-                "sha256:09858463db6dd9f78b2a1a05c93f3b33d4f65975771e90d2cf7aadb7c2f66edf",
-                "sha256:209666ce9d4a817e8a4597cd475b71b4878a85fa4b8db41d79fdb4fdee01dde2",
-                "sha256:298156f4d3d46815eaf0fcf0a03f9625fc7631692bd1ad851517ab93c3168fc6",
-                "sha256:30fc68307c0155d2a75ad19844224be0f2c6f06572d958db4e2053f816b859ad",
-                "sha256:423216d8afc5923b15df86037c6053bf030d15cc9e3224206ef868c2d63dd6dc",
-                "sha256:426a00b68b0d21f2deb2ace3c6d677e611ad5a612d2c76494e24a562a930c254",
-                "sha256:466e682264b14982012887e90346d33435c984b7fead7b85e634903795c8fdb0",
-                "sha256:51a7b9db0a2941434cd930dacaafe0fc9da8f3d6157f9d12f761bbde93f46218",
-                "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13",
-                "sha256:550564024dc5ceee9421a86fc0fb378aa9d222d4d0f858f6669eff7410c89bef",
-                "sha256:5de64950137f3a50b76ce93556db392e8f1f954c2d8207f78a92d1f79aa9f737",
-                "sha256:640c1ccfd56724f2955c237b6ccce2e5b8607c3bc1cc51d3933b8c48d1da3723",
-                "sha256:7fdc7689daf3b845934d67cb221ba8d250fdca20ac0334fea32f7091b93f00d3",
-                "sha256:805459ad8baaf815883d0d6f86e45b3b0b67d823a8f3fa39b1ed9c45eaf5edf1",
-                "sha256:92a0ab128b07799dd5b9077a9af075a63467d03ebac6f8a93e6440abfea4120d",
-                "sha256:9f2dc79c093f6c5113718d3d90c283f11463d77daa4e83aeeac088ec6a0bda52",
-                "sha256:a5109345f5ce7ddb3840f5970de71c34a0ff7fceb133c9441283bb8250f532a3",
-                "sha256:a55e4d81c4260386f71d22294795c87609164e22b28ba0d435850fbdf82fc0c5",
-                "sha256:a9da45b748caad72ea4a4ed57e9cd382089f33c5ec330a804eb420a496fa760f",
-                "sha256:b160b9a99ecc6559d9e6d461b95c8eec21461b332f80267ad2c10394b9503496",
-                "sha256:b342064e647d099ca765f19672696ad50c953cac95b566af1492fd142283580f",
-                "sha256:b5e8590b9245803c849e09bae070a8e1ff444f45e3f0bed558dd722119eea724",
-                "sha256:bf75d5825ef47aa51d669b03ce635ecb84d69311e05eccea083f31c7570c9931",
-                "sha256:c01b59b33c7c3ba90744f2c695be571a3bd40ab2ba7f3d169ffa6db3cfba614f",
-                "sha256:d96a6a7d74af56feb11e9a443150216578ea07b7450f7c05df40eec90af7f4a7",
-                "sha256:dd0e3651d210068d13e18503d75aaa45656eef51ef0b261f891788589db2cc38",
-                "sha256:e167b9805de54367dcb2043519382be541117503ce99e3291cc9b41ca0a83557",
-                "sha256:e42029e184008a5fd3d819323345e25e2337b0ac7f5c135b7623308530209d57",
-                "sha256:f545c082eeb09ae678dd451a1b1dbf17babd8a0d7adea02897a76e639afca310",
-                "sha256:fde50062d67d805bc96f1a9ecc0d37bfc2a8f02b937d2c50824d186aa91f2419"
-            ],
-            "markers": "python_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64'",
-            "version": "==1.21.2"
+                "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb",
+                "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939",
+                "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc",
+                "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced",
+                "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441",
+                "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b",
+                "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166",
+                "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640",
+                "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13",
+                "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8",
+                "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5",
+                "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611",
+                "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76",
+                "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c",
+                "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7",
+                "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a",
+                "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5",
+                "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee",
+                "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449",
+                "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02",
+                "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a",
+                "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815",
+                "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593",
+                "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e",
+                "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa",
+                "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954",
+                "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354",
+                "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9",
+                "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a",
+                "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089"
+            ],
+            "markers": "python_version < '3.11' and python_version >= '3.7'",
+            "version": "==1.21.5"
         },
         "packaging": {
             "hashes": [
-                "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
-                "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
+                "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
+                "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==21.0"
+            "version": "==21.3"
         },
         "pandas": {
             "hashes": [
-                "sha256:003ba92db58b71a5f8add604a17a059f3068ef4e8c0c365b088468d0d64935fd",
-                "sha256:10e10a2527db79af6e830c3d5842a4d60383b162885270f8cffc15abca4ba4a9",
-                "sha256:22808afb8f96e2269dcc5b846decacb2f526dd0b47baebc63d913bf847317c8f",
-                "sha256:2d1dc09c0013d8faa7474574d61b575f9af6257ab95c93dcf33a14fd8d2c1bab",
-                "sha256:35c77609acd2e4d517da41bae0c11c70d31c87aae8dd1aabd2670906c6d2c143",
-                "sha256:372d72a3d8a5f2dbaf566a5fa5fa7f230842ac80f29a931fb4b071502cf86b9a",
-                "sha256:42493f8ae67918bf129869abea8204df899902287a7f5eaf596c8e54e0ac7ff4",
-                "sha256:5298a733e5bfbb761181fd4672c36d0c627320eb999c59c65156c6a90c7e1b4f",
-                "sha256:5ba0aac1397e1d7b654fccf263a4798a9e84ef749866060d19e577e927d66e1b",
-                "sha256:a2aa18d3f0b7d538e21932f637fbfe8518d085238b429e4790a35e1e44a96ffc",
-                "sha256:a388960f979665b447f0847626e40f99af8cf191bce9dc571d716433130cb3a7",
-                "sha256:a51528192755f7429c5bcc9e80832c517340317c861318fea9cea081b57c9afd",
-                "sha256:b528e126c13816a4374e56b7b18bfe91f7a7f6576d1aadba5dee6a87a7f479ae",
-                "sha256:c1aa4de4919358c5ef119f6377bc5964b3a7023c23e845d9db7d9016fa0c5b1c",
-                "sha256:c2646458e1dce44df9f71a01dc65f7e8fa4307f29e5c0f2f92c97f47a5bf22f5",
-                "sha256:d47750cf07dee6b55d8423471be70d627314277976ff2edd1381f02d52dbadf9",
-                "sha256:d99d2350adb7b6c3f7f8f0e5dfb7d34ff8dd4bc0a53e62c445b7e43e163fce63",
-                "sha256:dd324f8ee05925ee85de0ea3f0d66e1362e8c80799eb4eb04927d32335a3e44a",
-                "sha256:eaca36a80acaacb8183930e2e5ad7f71539a66805d6204ea88736570b2876a7b",
-                "sha256:f567e972dce3bbc3a8076e0b675273b4a9e8576ac629149cf8286ee13c259ae5",
-                "sha256:fe48e4925455c964db914b958f6e7032d285848b7538a5e1b19aeb26ffaea3ec"
+                "sha256:1e4285f5de1012de20ca46b188ccf33521bff61ba5c5ebd78b4fb28e5416a9f1",
+                "sha256:2651d75b9a167cc8cc572cf787ab512d16e316ae00ba81874b560586fa1325e0",
+                "sha256:2c21778a688d3712d35710501f8001cdbf96eb70a7c587a3d5613573299fdca6",
+                "sha256:32e1a26d5ade11b547721a72f9bfc4bd113396947606e00d5b4a5b79b3dcb006",
+                "sha256:3345343206546545bc26a05b4602b6a24385b5ec7c75cb6059599e3d56831da2",
+                "sha256:344295811e67f8200de2390093aeb3c8309f5648951b684d8db7eee7d1c81fb7",
+                "sha256:37f06b59e5bc05711a518aa10beaec10942188dccb48918bb5ae602ccbc9f1a0",
+                "sha256:552020bf83b7f9033b57cbae65589c01e7ef1544416122da0c79140c93288f56",
+                "sha256:5cce0c6bbeb266b0e39e35176ee615ce3585233092f685b6a82362523e59e5b4",
+                "sha256:5f261553a1e9c65b7a310302b9dbac31cf0049a51695c14ebe04e4bfd4a96f02",
+                "sha256:60a8c055d58873ad81cae290d974d13dd479b82cbb975c3e1fa2cf1920715296",
+                "sha256:62d5b5ce965bae78f12c1c0df0d387899dd4211ec0bdc52822373f13a3a022b9",
+                "sha256:7d28a3c65463fd0d0ba8bbb7696b23073efee0510783340a44b08f5e96ffce0c",
+                "sha256:8025750767e138320b15ca16d70d5cdc1886e8f9cc56652d89735c016cd8aea6",
+                "sha256:8b6dbec5f3e6d5dc80dcfee250e0a2a652b3f28663492f7dab9a24416a48ac39",
+                "sha256:a395692046fd8ce1edb4c6295c35184ae0c2bbe787ecbe384251da609e27edcb",
+                "sha256:a62949c626dd0ef7de11de34b44c6475db76995c2064e2d99c6498c3dba7fe58",
+                "sha256:aaf183a615ad790801fa3cf2fa450e5b6d23a54684fe386f7e3208f8b9bfbef6",
+                "sha256:adfeb11be2d54f275142c8ba9bf67acee771b7186a5745249c7d5a06c670136b",
+                "sha256:b6b87b2fb39e6383ca28e2829cddef1d9fc9e27e55ad91ca9c435572cdba51bf",
+                "sha256:bd971a3f08b745a75a86c00b97f3007c2ea175951286cdda6abe543e687e5f2f",
+                "sha256:c69406a2808ba6cf580c2255bcf260b3f214d2664a3a4197d0e640f573b46fd3",
+                "sha256:d3bc49af96cd6285030a64779de5b3688633a07eb75c124b0747134a63f4c05f",
+                "sha256:fd541ab09e1f80a2a1760032d665f6e032d8e44055d602d65eeea6e6e85498cb",
+                "sha256:fe95bae4e2d579812865db2212bb733144e34d0c6785c0685329e5b60fcb85dd"
             ],
             "markers": "python_full_version >= '3.7.1'",
-            "version": "==1.3.4"
+            "version": "==1.3.5"
         },
         "pandocfilters": {
             "hashes": [
@@ -611,11 +682,11 @@
         },
         "parso": {
             "hashes": [
-                "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398",
-                "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"
+                "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0",
+                "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==0.8.2"
+            "version": "==0.8.3"
         },
         "pexpect": {
             "hashes": [
@@ -681,27 +752,27 @@
         },
         "plotly": {
             "hashes": [
-                "sha256:6598393e898a9c5ae78397f76f07002ec41fd92e5f746d3b9806248d53885643",
-                "sha256:c3da90d41cab36722048eaa5d0eee7a286a55982835980e5f183e5536b823f07"
+                "sha256:20b8a1a0f0434f9b8d10eb7caa66e947a9a1d698e5a53d40d447bbc0d2ae41f0",
+                "sha256:bc7d19272560f73fe4c2c989c31b00774a35d3a76891fab0b72c17616862d0e0"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==5.3.1"
+            "version": "==5.5.0"
         },
         "prometheus-client": {
             "hashes": [
-                "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86",
-                "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"
+                "sha256:1b12ba48cee33b9b0b9de64a1047cbd3c5f2d0ab6ebcead7ddda613a750ec3c5",
+                "sha256:317453ebabff0a1b02df7f708efbab21e3489e7072b61cb6957230dd004a0af0"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.11.0"
+            "version": "==0.12.0"
         },
         "prompt-toolkit": {
             "hashes": [
-                "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c",
-                "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"
+                "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6",
+                "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"
             ],
             "markers": "python_full_version >= '3.6.2'",
-            "version": "==3.0.20"
+            "version": "==3.0.24"
         },
         "ptyprocess": {
             "hashes": [
@@ -721,11 +792,11 @@
         },
         "pycparser": {
             "hashes": [
-                "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
-                "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
+                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.20"
+            "version": "==2.21"
         },
         "pygments": {
             "hashes": [
@@ -737,11 +808,11 @@
         },
         "pyparsing": {
             "hashes": [
-                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
-                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+                "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
+                "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
             ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.4.7"
+            "markers": "python_version >= '3.6'",
+            "version": "==3.0.6"
         },
         "pyrsistent": {
             "hashes": [
@@ -840,75 +911,85 @@
         },
         "qtconsole": {
             "hashes": [
-                "sha256:73994105b0369bb99f4164df4a131010f3c7b33a7b5169c37366358d8744675b",
-                "sha256:bbc34bca14f65535afcb401bc74b752bac955e5313001ba640383f7e5857dc49"
+                "sha256:4aa6a3e600e0c8cf16853f2378311bc2371f57cb0f22ecfc28994f4cf409ee2e",
+                "sha256:8f9db97b27782184efd0a0f2d57ea3bd852d053747a2e442a9011329c082976d"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==5.1.1"
+            "version": "==5.2.2"
         },
         "qtpy": {
             "hashes": [
-                "sha256:83c502973e9fdd7b648d8267a421229ea3d9a0651c22e4c65a4d9228479c39b6",
-                "sha256:d6e4ae3a41f1fcb19762b58f35ad6dd443b4bdc867a4cb81ef10ccd85403c92b"
+                "sha256:d427addd37386a8d786db81864a5536700861d95bf085cb31d1bea855d699557",
+                "sha256:e121fbee8e95645af29c5a4aceba8d657991551fc1aa3b6b6012faf4725a1d20"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==1.11.2"
+            "version": "==1.11.3"
         },
         "scikit-learn": {
             "hashes": [
-                "sha256:121f78d6564000dc5e968394f45aac87981fcaaf2be40cfcd8f07b2baa1e1829",
-                "sha256:14bd46639b2149b3ed613adc095511313a0db62ba9fa31117bdcb5c23722e93b",
-                "sha256:190c178028f9073d9f61cd30a19c685993236b9b2df884f16608cbb3ff03800b",
-                "sha256:29559c207616604bbaa664bf98eed81b32d9f3d4c975065a206a5e2b268fe784",
-                "sha256:4cb5ccb2b63c617ead48c6d92001273ad1b0e8e2bd4a4857edb58749a88b6d82",
-                "sha256:555f4b4c10d3bef9e3cda63c3b45670a091fb50328fccd54948cd8a7cf887198",
-                "sha256:56ab58978c7aa181856a42f8f491be953b755105040aeb070ebd6b180896f146",
-                "sha256:663a6aaad92e5690b03d931f849016c9718beaa654e9a15f08bfcac750241036",
-                "sha256:6a056637f7f9876e4c9db9b5434d340e0c97e25f00c4c04458f0ff906e82488e",
-                "sha256:6d8bdacde73f5f484325179f466ce2011f79360e9a152100179c3dafb88f2a35",
-                "sha256:776800194e757cd212b47cd05907e0eb67a554ad333fe76776060dbb729e3427",
-                "sha256:83ab0d0447b8de8450c554952a8399791544605caf274fc3c904e247e1584ced",
-                "sha256:9d8caf7fa58791b6b26e912e44d5056818b7bb3142bfa7806f54bde47c189078",
-                "sha256:9f103cd6d7e15fa537a844c1a85c9beeeee8ec38357287c9efd3ee4bb8354e1d",
-                "sha256:af94b89a8f7759603c696b320e86e57f4b2bb4911e02bf2bae33c714ac498fb8",
-                "sha256:b1df4d1151dd6d945324583125e6449bb74ec7cd91ffd7f850015cdb75f151b5",
-                "sha256:b9f10b85dcd9ce80f738e33f55a32b3a538b47409dc1a59eec30b46ea96759db",
-                "sha256:c1f710bba72925aa96e60828df5d2a4872f5d4a4ad7bb4a4c9a6a41c9ce9a198",
-                "sha256:c9c329ec195cdea6a4dee3cebdb1602f4e0f69351c63bc58a4812f3c8a9f4f2d",
-                "sha256:e35135657b7103a70298cf557e4fad06af97607cb0780d8f44a2f91ca7769458",
-                "sha256:e8a6074f7d505bbfd30bcc1c57dc7cb150cc9c021459c2e2729854be1aefb5f7",
-                "sha256:eed33b7ca2bf3fdd585339db42838ab0b641952e064564bff6e9a10573ea665c",
-                "sha256:efeac34d0ce6bf9404d268545867cbde9d6ecadd0e9bd7e6b468e5f4e2349875",
-                "sha256:f7053801ceb7c51ce674c6a8e37a18fcc221c292f66ef7da84744ecf13b4a0c0",
-                "sha256:f8aecb3edc443e5625725ae1ef8f500fa78ce7cb0e864115864bb9f234d18290"
+                "sha256:02aee3b257617da0ec98dee9572b10523dc00c25b68c195ddf100c1a93b1854b",
+                "sha256:059c5be0c0365321ddbcac7abf0db806fad8ecb64ee6c7cbcd58313c7d61634d",
+                "sha256:116e05fd990d9b363fc29bd3699ec2117d7da9088f6ca9a90173b240c5a063f1",
+                "sha256:11a57405c1c3514227d0c6a0bee561c94cd1284b41e236f7a1d76b3975f77593",
+                "sha256:32d941f12fd7e245f01da2b82943c5ce6f1133fa5375eb80caa51457532b3e7e",
+                "sha256:46248cc6a8b72490f723c73ff2e65e62633d14cafe9d2df3a7b3f87d332a6f7e",
+                "sha256:515b227f01f569145dc9f86e56f4cea9f00a613fc4d074bbfc0a92ca00bff467",
+                "sha256:538f3a85c4980c7572f3e754f0ba8489363976ef3e7f6a94e8f1af5ae45f6f6a",
+                "sha256:53bb7c605427ab187869d7a05cd3f524a3015a90e351c1788fc3a662e7f92b69",
+                "sha256:59b1d6df8724003fa16b7365a3b43449ee152aa6e488dd7a19f933640bb2d7fb",
+                "sha256:62ce4e3ddb6e6e9dcdb3e5ac7f0575dbaf56f79ce2b2edee55192b12b52df5be",
+                "sha256:648f4dbfdd0a1b45bf6e2e4afe3f431774c55dee05e2d28f8394d6648296f373",
+                "sha256:944f47b2d881b9d24aee40d643bfdc4bd2b6dc3d25b62964411c6d8882f940a1",
+                "sha256:a51fdbc116974d9715957366df73e5ec6f0a7a2afa017864c2e5f5834e6f494d",
+                "sha256:a800665527c1a63f7395a0baae3c89b0d97b54d2c23769c1c9879061bb80bc19",
+                "sha256:ac2ca9dbb754d61cfe1c83ba8483498ef951d29b93ec09d6f002847f210a99da",
+                "sha256:bd78a2442c948536f677e2744917c37cff014559648102038822c23863741c27",
+                "sha256:c604a813df8e7d6dfca3ae0db0a8fd7e5dff4ea9d94081ab263c81bf0b61ab4b",
+                "sha256:c6b9510fd2e1642314efb7aa951a0d05d963f3523e01c30b2dadde2395ebe6b4",
+                "sha256:ebbe4275556d3c02707bd93ae8b96d9651acd4165126e0ae64b336afa2a6dcb1",
+                "sha256:ee59da47e18b703f6de17d5d51b16ce086c50969d5a83db5217f0ae9372de232",
+                "sha256:fb7214103f6c36c1371dd8c166897e3528264a28f2e2e42573ba8c61ed4d7142",
+                "sha256:fc60e0371e521995a6af2ef3f5d911568506124c272889b318b8b6e497251231",
+                "sha256:fc75f81571137b39f9b31766e15a0e525331637e7fe8f8000a3fbfba7da3add9",
+                "sha256:fecb5102f0a36c16c1361ec519a7bb0260776ef40e17393a81f530569c916a7b"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==1.0"
+            "version": "==1.0.1"
         },
         "scipy": {
             "hashes": [
-                "sha256:2a0eeaab01258e0870c4022a6cd329aef3b7c6c2b606bd7cf7bb2ba9820ae561",
-                "sha256:3304bd5bc32e00954ac4b3f4cc382ca8824719bf348aacbec6347337d6b125fe",
-                "sha256:3f52470e0548cdb74fb8ddf06773ffdcca7c97550f903b1c51312ec19243a7f7",
-                "sha256:4729b41a4cdaf4cd011aeac816b532f990bdf97710cef59149d3e293115cf467",
-                "sha256:4ee952f39a4a4c7ba775a32b664b1f4b74818548b65f765987adc14bb78f5802",
-                "sha256:611f9cb459d0707dd8e4de0c96f86e93f61aac7475fcb225e9ec71fecdc5cebf",
-                "sha256:6b47d5fa7ea651054362561a28b1ccc8da9368a39514c1bbf6c0977a1c376764",
-                "sha256:71cfc96297617eab911e22216e8a8597703202e95636d9406df9af5c2ac99a2b",
-                "sha256:787749110a23502031fb1643c55a2236c99c6b989cca703ea2114d65e21728ef",
-                "sha256:90c07ba5f34f33299a428b0d4fa24c30d2ceba44d63f8385b2b05be460819fcb",
-                "sha256:a496b42dbcd04ea9924f5e92be63af3d8e0f43a274b769bfaca0a297327d54ee",
-                "sha256:bc61e3e5ff92d2f32bb263621d54a9cff5e3f7c420af3d1fa122ce2529de2bd9",
-                "sha256:c9951e3746b68974125e5e3445008a4163dd6d20ae0bbdae22b38cb8951dc11b",
-                "sha256:d1388fbac9dd591ea630da75c455f4cc637a7ca5ecb31a6b6cef430914749cde",
-                "sha256:d13f31457f2216e5705304d9f28e2826edf75487410a57aa99263fa4ffd792c2",
-                "sha256:d648aa85dd5074b1ed83008ae987c3fbb53d68af619fce1dee231f4d8bd40e2f",
-                "sha256:da9c6b336e540def0b7fd65603da8abeb306c5fc9a5f4238665cbbb5ff95cf58",
-                "sha256:e101bceeb9e65a90dadbc5ca31283403a2d4667b9c178db29109750568e8d112",
-                "sha256:efdd3825d54c58df2cc394366ca4b9166cf940a0ebddeb87b6c10053deb625ea"
-            ],
-            "markers": "python_version < '3.10' and python_version >= '3.7'",
-            "version": "==1.7.1"
+                "sha256:033ce76ed4e9f62923e1f8124f7e2b0800db533828c853b402c7eec6e9465d80",
+                "sha256:173308efba2270dcd61cd45a30dfded6ec0085b4b6eb33b5eb11ab443005e088",
+                "sha256:21b66200cf44b1c3e86495e3a436fc7a26608f92b8d43d344457c54f1c024cbc",
+                "sha256:2c56b820d304dffcadbbb6cbfbc2e2c79ee46ea291db17e288e73cd3c64fefa9",
+                "sha256:304dfaa7146cffdb75fbf6bb7c190fd7688795389ad060b970269c8576d038e9",
+                "sha256:3f78181a153fa21c018d346f595edd648344751d7f03ab94b398be2ad083ed3e",
+                "sha256:4d242d13206ca4302d83d8a6388c9dfce49fc48fdd3c20efad89ba12f785bf9e",
+                "sha256:5d1cc2c19afe3b5a546ede7e6a44ce1ff52e443d12b231823268019f608b9b12",
+                "sha256:5f2cfc359379c56b3a41b17ebd024109b2049f878badc1e454f31418c3a18436",
+                "sha256:65bd52bf55f9a1071398557394203d881384d27b9c2cad7df9a027170aeaef93",
+                "sha256:7edd9a311299a61e9919ea4192dd477395b50c014cdc1a1ac572d7c27e2207fa",
+                "sha256:8499d9dd1459dc0d0fe68db0832c3d5fc1361ae8e13d05e6849b358dc3f2c279",
+                "sha256:866ada14a95b083dd727a845a764cf95dd13ba3dc69a16b99038001b05439709",
+                "sha256:87069cf875f0262a6e3187ab0f419f5b4280d3dcf4811ef9613c605f6e4dca95",
+                "sha256:93378f3d14fff07572392ce6a6a2ceb3a1f237733bd6dcb9eb6a2b29b0d19085",
+                "sha256:95c2d250074cfa76715d58830579c64dff7354484b284c2b8b87e5a38321672c",
+                "sha256:ab5875facfdef77e0a47d5fd39ea178b58e60e454a4c85aa1e52fcb80db7babf",
+                "sha256:b0e0aeb061a1d7dcd2ed59ea57ee56c9b23dd60100825f98238c06ee5cc4467e",
+                "sha256:b78a35c5c74d336f42f44106174b9851c783184a85a3fe3e68857259b37b9ffb",
+                "sha256:c9e04d7e9b03a8a6ac2045f7c5ef741be86727d8f49c45db45f244bdd2bcff17",
+                "sha256:ca36e7d9430f7481fc7d11e015ae16fbd5575615a8e9060538104778be84addf",
+                "sha256:ceebc3c4f6a109777c0053dfa0282fddb8893eddfb0d598574acfb734a926168",
+                "sha256:e2c036492e673aad1b7b0d0ccdc0cb30a968353d2c4bf92ac8e73509e1bf212c",
+                "sha256:eb326658f9b73c07081300daba90a8746543b5ea177184daed26528273157294",
+                "sha256:eb7ae2c4dbdb3c9247e07acc532f91077ae6dbc40ad5bd5dca0bb5a176ee9bda",
+                "sha256:edad1cf5b2ce1912c4d8ddad20e11d333165552aba262c882e28c78bbc09dbf6",
+                "sha256:eef93a446114ac0193a7b714ce67659db80caf940f3232bad63f4c7a81bc18df",
+                "sha256:f7eaea089345a35130bc9a39b89ec1ff69c208efa97b3f8b25ea5d4c41d88094",
+                "sha256:f99d206db1f1ae735a8192ab93bd6028f3a42f6fa08467d37a14eb96c9dd34a3"
+            ],
+            "markers": "python_version < '3.11' and python_version >= '3.7'",
+            "version": "==1.7.3"
         },
         "send2trash": {
             "hashes": [
@@ -917,6 +998,14 @@
             ],
             "version": "==1.8.0"
         },
+        "setuptools": {
+            "hashes": [
+                "sha256:032e5949c02878c405552fb800743aad96940097460bc6f115340402852dcdde",
+                "sha256:989ab2d3e632ba23358b8d43950bd46babde5bbb1516760dc5f5ccfe7accdd45"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==60.0.3"
+        },
         "six": {
             "hashes": [
                 "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
@@ -1006,11 +1095,11 @@
         },
         "traitlets": {
             "hashes": [
-                "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4",
-                "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"
+                "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7",
+                "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==5.1.0"
+            "version": "==5.1.1"
         },
         "treelib": {
             "hashes": [
@@ -1036,24 +1125,40 @@
         },
         "widgetsnbextension": {
             "hashes": [
-                "sha256:079f87d87270bce047512400efd70238820751a11d2d8cb137a5a5bdbaf255c7",
-                "sha256:bd314f8ceb488571a5ffea6cc5b9fc6cba0adaf88a9d2386b93a489751938bcd"
+                "sha256:763a9fdc836d141fa080005a886d63f66f73d56dba1fb5961afc239c77708569",
+                "sha256:e0731a60ba540cd19bbbefe771a9076dcd2dde90713a8f87f27f53f2d1db7727"
             ],
-            "version": "==3.5.1"
+            "version": "==3.5.2"
         },
         "xgboost": {
             "hashes": [
-                "sha256:6b7c34a18474c1b73b5c6dcf1231f5fe102d6f26490a65465e3879048bf1f3d4",
-                "sha256:9502a9ed5c52669c83207380eeaaa3862cbf38b271040c6d5226914e07cf196c",
-                "sha256:ae122406c2c1d2a407c18a49c2a05f9bbbaa3249fd39e91c11cb223d538dba4e",
-                "sha256:ebe36ee21516a37f645bcd1f3ca1247485fe77d96f1c3d605f970c469b6a9015",
-                "sha256:edaad84317a53671069b4477152915c25b4121d997bfa531711f0a18ba1402fe"
+                "sha256:8668e86a2ab13bccc16bc4c295cb118771dc70d02ef82752289e5d3e7a7a5357",
+                "sha256:a30485a84bbecf0d6a0c648e3efee94e3ecf99134cc6408df11cdab46da995de",
+                "sha256:ab50d064861aa6f27396e3879bcccc720b1bdcaff501367b7ea1325c4ab61e63",
+                "sha256:c6a3ed369decf0ebee1c7af18fa87d632843b5dd2edaca2dd4b2c5e66034c316",
+                "sha256:e399dcb4ce6bb669c8cc707aaea2f3da50af9cc78f6f5019a0bb6246a433b736"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==1.5.0"
+            "version": "==1.5.1"
+        },
+        "zipp": {
+            "hashes": [
+                "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832",
+                "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==3.6.0"
         }
     },
     "develop": {
+        "attrs": {
+            "hashes": [
+                "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
+                "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==21.2.0"
+        },
         "bleach": {
             "hashes": [
                 "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
@@ -1079,11 +1184,11 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0",
-                "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"
+                "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
+                "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
             ],
             "markers": "python_version >= '3'",
-            "version": "==2.0.7"
+            "version": "==2.0.9"
         },
         "colorama": {
             "hashes": [
@@ -1095,11 +1200,11 @@
         },
         "docutils": {
             "hashes": [
-                "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
-                "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
+                "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
+                "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==0.17.1"
+            "version": "==0.18.1"
         },
         "idna": {
             "hashes": [
@@ -1111,27 +1216,34 @@
         },
         "importlib-metadata": {
             "hashes": [
-                "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15",
-                "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"
+                "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
+                "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==4.8.1"
+            "markers": "python_version < '3.10'",
+            "version": "==4.10.0"
+        },
+        "iniconfig": {
+            "hashes": [
+                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
+                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
+            ],
+            "version": "==1.1.1"
         },
         "keyring": {
             "hashes": [
-                "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe",
-                "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"
+                "sha256:3dc0f66062a4f8f6f2ce30d6a516e6e623e6c3c2e76864204ceaf64695408f07",
+                "sha256:88f206024295e3c6fb16bb0a60fb4bb7ec1185629dc5a729f12aa7c236d01387"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==23.2.1"
+            "version": "==23.4.0"
         },
         "packaging": {
             "hashes": [
-                "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
-                "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
+                "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
+                "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==21.0"
+            "version": "==21.3"
         },
         "pep517": {
             "hashes": [
@@ -1142,10 +1254,26 @@
         },
         "pkginfo": {
             "hashes": [
-                "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779",
-                "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"
+                "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff",
+                "sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"
+            ],
+            "version": "==1.8.2"
+        },
+        "pluggy": {
+            "hashes": [
+                "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+                "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
             ],
-            "version": "==1.7.1"
+            "markers": "python_version >= '3.6'",
+            "version": "==1.0.0"
+        },
+        "py": {
+            "hashes": [
+                "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
+                "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==1.11.0"
         },
         "pygments": {
             "hashes": [
@@ -1157,18 +1285,27 @@
         },
         "pyparsing": {
             "hashes": [
-                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
-                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+                "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
+                "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
             ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.4.7"
+            "markers": "python_version >= '3.6'",
+            "version": "==3.0.6"
+        },
+        "pytest": {
+            "hashes": [
+                "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
+                "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
+            ],
+            "index": "pypi",
+            "version": "==6.2.5"
         },
         "readme-renderer": {
             "hashes": [
-                "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc",
-                "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"
+                "sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
+                "sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
             ],
-            "version": "==30.0"
+            "markers": "python_version >= '3.6'",
+            "version": "==32.0"
         },
         "requests": {
             "hashes": [
@@ -1200,13 +1337,21 @@
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==1.16.0"
         },
+        "toml": {
+            "hashes": [
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+            ],
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.10.2"
+        },
         "tomli": {
             "hashes": [
-                "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f",
-                "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"
+                "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224",
+                "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.2.1"
+            "markers": "python_version >= '3.7'",
+            "version": "==2.0.0"
         },
         "tqdm": {
             "hashes": [
@@ -1218,11 +1363,11 @@
         },
         "twine": {
             "hashes": [
-                "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218",
-                "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"
+                "sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
+                "sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
             ],
             "index": "pypi",
-            "version": "==3.4.2"
+            "version": "==3.7.1"
         },
         "urllib3": {
             "hashes": [
diff --git a/examples/.gitignore b/examples/.gitignore
index 945c182..4946e86 100644
--- a/examples/.gitignore
+++ b/examples/.gitignore
@@ -4,3 +4,5 @@ models/
 catboost_info
 data/
 *pending.ipynb
+test*.ipynb
+*debug*.ipynb
diff --git a/examples/boston_CatBoost_ModelDriftExplainer.ipynb b/examples/boston_CatBoost_ModelDriftExplainer.ipynb
new file mode 100644
index 0000000..115fd07
--- /dev/null
+++ b/examples/boston_CatBoost_ModelDriftExplainer.ipynb
@@ -0,0 +1,221 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "bfff3f51",
+   "metadata": {},
+   "source": [
+    "The experiment is done with a random split so we should not detect a data drift"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "52e27f10",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from sklearn import datasets\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from catboost import CatBoostRegressor\n",
+    "\n",
+    "from cinnamon.drift import ModelDriftExplainer"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "77b0c4b7",
+   "metadata": {},
+   "source": [
+    "# Boston Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1ac4bce2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston = datasets.load_boston()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "68e500b2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston_df = pd.DataFrame(boston.data, columns = boston.feature_names)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1e3f78a4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston_df.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a890efc5",
+   "metadata": {},
+   "source": [
+    "# Build CatBoost model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b950476c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train, X_test, y_train, y_test = train_test_split(boston_df, boston.target, test_size=0.3, random_state=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "621aa207",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model = CatBoostRegressor(loss_function='RMSE',\n",
+    "                         learning_rate=0.1,\n",
+    "                         iterations=1000,\n",
+    "                         max_depth=6,\n",
+    "                         early_stopping_rounds=20,\n",
+    "                         random_seed=2021,\n",
+    "                         verbose=10\n",
+    "                       )"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4f6d0ca3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9071a255",
+   "metadata": {},
+   "source": [
+    "# Analyze data drift with ModelDriftExplainer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5cd0feda",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer = ModelDriftExplainer(model)\n",
+    "drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b317faab",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_prediction_drift()\n",
+    "drift_explainer.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5afccb92",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_target_drift()\n",
+    "drift_explainer.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "633a3f47",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "858ac743",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_tree_based_drift_values(type='node_size')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a3bf92e1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_tree_based_drift_values(type='mean')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1eb791b0",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_feature_drift('LSTAT')\n",
+    "drift_explainer.get_feature_drift('LSTAT')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "611ccc6f",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.9.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/boston_OutputDriftDetector.ipynb b/examples/boston_OutputDriftDetector.ipynb
new file mode 100644
index 0000000..a1fb033
--- /dev/null
+++ b/examples/boston_OutputDriftDetector.ipynb
@@ -0,0 +1,212 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e655314b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from sklearn import datasets\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from xgboost import XGBRegressor\n",
+    "\n",
+    "from cinnamon.drift import OutputDriftDetector\n",
+    "\n",
+    "RANDOM_SEED = 2021"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "77b0c4b7",
+   "metadata": {},
+   "source": [
+    "# Boston Housing Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1ac4bce2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston = datasets.load_boston()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "68e500b2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston_df = pd.DataFrame(boston.data, columns=boston.feature_names)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1e3f78a4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston_df.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "17045336",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "boston_df.shape"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a890efc5",
+   "metadata": {},
+   "source": [
+    "# Build XGBoost model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b950476c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train, X_test, y_train, y_test = train_test_split(boston_df, boston.target, test_size=0.3, random_state=RANDOM_SEED)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "621aa207",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model = XGBRegressor(n_estimators=1000,\n",
+    "                   booster=\"gbtree\",\n",
+    "                   objective=\"reg:squarederror\",\n",
+    "                   learning_rate=0.05,\n",
+    "                   max_depth=6,\n",
+    "                   seed=RANDOM_SEED,\n",
+    "                   use_label_encoder=False)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4f6d0ca3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "23174b96",
+   "metadata": {},
+   "source": [
+    "# OutputDriftDetector\n",
+    "\n",
+    "The experiment is done with a random split so we should not detect a data drift"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "10f62e94",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector = OutputDriftDetector(task='regression')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "790420be",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.fit(model.predict(X_train), model.predict(X_test), y_train, y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7745b260",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.plot_prediction_drift()\n",
+    "output_drift_detector.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b2854296",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.plot_target_drift()\n",
+    "output_drift_detector.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c3dc2cce",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "92266de8",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3b80c628",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.9.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/boston_xgboost_regression.ipynb b/examples/boston_XGBoost_ModelDriftExplainer.ipynb
similarity index 80%
rename from examples/boston_xgboost_regression.ipynb
rename to examples/boston_XGBoost_ModelDriftExplainer.ipynb
index 099691d..c805375 100644
--- a/examples/boston_xgboost_regression.ipynb
+++ b/examples/boston_XGBoost_ModelDriftExplainer.ipynb
@@ -12,7 +12,9 @@
     "from sklearn.model_selection import train_test_split\n",
     "from xgboost import XGBRegressor\n",
     "\n",
-    "from cinnamon.drift import ModelDriftExplainer, AdversarialDriftExplainer"
+    "from cinnamon.drift import ModelDriftExplainer, AdversarialDriftExplainer\n",
+    "\n",
+    "RANDOM_SEED = 2021"
    ]
   },
   {
@@ -40,7 +42,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "boston_df = pd.DataFrame(boston.data, columns = boston.feature_names)"
+    "boston_df = pd.DataFrame(boston.data, columns=boston.feature_names)"
    ]
   },
   {
@@ -78,7 +80,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "X_train, X_valid, y_train, y_valid = train_test_split(boston_df, boston.target, test_size=0.3, random_state=2021)"
+    "X_train, X_test, y_train, y_test = train_test_split(boston_df, boston.target, test_size=0.3, random_state=RANDOM_SEED)"
    ]
   },
   {
@@ -93,7 +95,7 @@
     "                   objective=\"reg:squarederror\",\n",
     "                   learning_rate=0.05,\n",
     "                   max_depth=6,\n",
-    "                   seed=2021,\n",
+    "                   seed=RANDOM_SEED,\n",
     "                   use_label_encoder=False)"
    ]
   },
@@ -104,7 +106,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "model.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20, verbose=10)"
+    "model.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
    ]
   },
   {
@@ -134,7 +136,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer.fit(X_train, X_valid, y_train, y_valid)"
+    "drift_explainer.fit(X_train, X_test, y_train, y_test)"
    ]
   },
   {
@@ -169,41 +171,52 @@
    "outputs": [],
    "source": [
     "# here we would detect a data drift but the difference in performance metrics\n",
-    "# is due to comparing train and valid dataset\n",
+    "# is due to comparing train and test dataset\n",
     "drift_explainer.get_performance_metrics_drift()"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "c69a67a8",
+   "id": "93a40e55",
    "metadata": {},
    "outputs": [],
    "source": [
     "# plot drift values computed with the tree based approach\n",
-    "drift_explainer.plot_tree_based_drift_values()"
+    "drift_explainer.plot_tree_based_drift_values(type='node_size')\n",
+    "drift_explainer.get_tree_based_drift_values(type='node_size')"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "adaab9fc",
+   "id": "00492cba",
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer.get_tree_based_drift_values()"
+    "# no apparent drift for LSTAT the first feature indicated by drift values\n",
+    "drift_explainer.plot_feature_drift('LSTAT')\n",
+    "drift_explainer.get_feature_drift('LSTAT')"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "00492cba",
+   "id": "bd3735a0",
    "metadata": {},
    "outputs": [],
    "source": [
-    "# no apparent drift for LSTAT the first feature indicated by drift values\n",
-    "drift_explainer.plot_feature_drift('LSTAT')\n",
-    "drift_explainer.get_feature_drift('LSTAT')"
+    "drift_explainer.get_feature_drift(0)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0e895cfa",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_feature_drifts()"
    ]
   },
   {
@@ -222,7 +235,7 @@
    "outputs": [],
    "source": [
     "# logs corresponds training logs of adversarial classifiers\n",
-    "drift_explainer2 = AdversarialDriftExplainer().fit(X_train, X_valid, y_train, y_valid)"
+    "drift_explainer2 = AdversarialDriftExplainer(seed=RANDOM_SEED).fit(X_train, X_test, y_train, y_test)"
    ]
   },
   {
@@ -253,12 +266,24 @@
    "id": "a32c4400",
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "drift_explainer2.get_feature_drift('LSTAT')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "10e4a1ef",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer2.get_feature_drift(0)"
+   ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "527eb166",
+   "id": "eb1e35e2",
    "metadata": {},
    "outputs": [],
    "source": []
@@ -280,7 +305,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.1"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/examples/breast_cancer_CatBoost_ModelDriftExplainer.ipynb b/examples/breast_cancer_CatBoost_ModelDriftExplainer.ipynb
new file mode 100644
index 0000000..3c0eb1a
--- /dev/null
+++ b/examples/breast_cancer_CatBoost_ModelDriftExplainer.ipynb
@@ -0,0 +1,277 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "c9282a05",
+   "metadata": {},
+   "source": [
+    "The experiment is done with a random split so we should not detect a data drift"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "da27f578",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from sklearn import datasets\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from catboost import CatBoostClassifier, Pool\n",
+    "\n",
+    "from cinnamon.drift import ModelDriftExplainer"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2e16e864",
+   "metadata": {},
+   "source": [
+    "# Breast Cancer Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f582c773",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset = datasets.load_breast_cancer()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b20301d9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X = pd.DataFrame(dataset.data, columns = dataset.feature_names)\n",
+    "y = dataset.target"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "df4a4c87",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a6ea631c",
+   "metadata": {},
+   "source": [
+    "# Build CatBoost model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "cbf5e852",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "60c94965",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf = CatBoostClassifier(loss_function='Logloss',\n",
+    "                         learning_rate=0.1,\n",
+    "                         iterations=1000,\n",
+    "                         max_depth=6,\n",
+    "                         early_stopping_rounds=20,\n",
+    "                         random_seed=2021,\n",
+    "                         verbose=False\n",
+    "                        )"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b55c669b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf.fit(X=Pool(X_train, y_train), eval_set=Pool(X_test, y_test))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "57797a5f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer = ModelDriftExplainer(clf)\n",
+    "drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5917d285",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b7ab4a61",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c62370f3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_prediction_drift(prediction_type='proba')\n",
+    "drift_explainer.get_prediction_drift(prediction_type='proba')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d3c807cf",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_target_drift()\n",
+    "drift_explainer.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c3b79a7a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a617e7e4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_tree_based_drift_values(type='node_size')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1c107b53",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_tree_based_drift_values(type='node_size')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "205d9252",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_tree_based_drift_values(type='mean_norm')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "254208c7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_tree_based_drift_values(type='mean_norm')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e6111be0",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_feature_drift('mean texture')\n",
+    "drift_explainer.get_feature_drift('mean texture')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a391200a",
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_feature_drift(0)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7209b9b8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_feature_drifts()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "47d2aca7",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.9.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/breast_cancer_OutputDriftDetector.ipynb b/examples/breast_cancer_OutputDriftDetector.ipynb
new file mode 100644
index 0000000..3826b49
--- /dev/null
+++ b/examples/breast_cancer_OutputDriftDetector.ipynb
@@ -0,0 +1,359 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "da27f578",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "from sklearn import datasets\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from xgboost import XGBClassifier\n",
+    "import xgboost as xgb\n",
+    "\n",
+    "from cinnamon.drift import OutputDriftDetector"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2e16e864",
+   "metadata": {},
+   "source": [
+    "# Breast Cancer Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f582c773",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset = datasets.load_breast_cancer()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b20301d9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X = pd.DataFrame(dataset.data, columns = dataset.feature_names)\n",
+    "y = dataset.target"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "df4a4c87",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a6ea631c",
+   "metadata": {},
+   "source": [
+    "# Build XGBoost model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "cbf5e852",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9be4d156",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf = XGBClassifier(n_estimators=1000,\n",
+    "                    booster=\"gbtree\",\n",
+    "                    objective=\"binary:logistic\",\n",
+    "                    learning_rate=0.05,\n",
+    "                    max_depth=6,\n",
+    "                    use_label_encoder=False,\n",
+    "                    seed=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8001d44d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7b265d66",
+   "metadata": {},
+   "source": [
+    "# Analyze data drift with OutputDriftDetector\n",
+    "\n",
+    "The experiment is done with a random split so we should not detect a data drift"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3da8b162",
+   "metadata": {},
+   "source": [
+    "### prediction_type='proba'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "74f9b603",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector = OutputDriftDetector(task='classification', prediction_type='proba')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a865f9ca",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf.predict_proba(X_train)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "32ccdf95",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.fit(clf.predict_proba(X_train), clf.predict_proba(X_test),\n",
+    "                           y_train, y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bb58e7b4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.plot_prediction_drift()\n",
+    "output_drift_detector.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "da37eae4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.plot_target_drift()\n",
+    "output_drift_detector.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "015dfec1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "da9a4315",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1e5b5850",
+   "metadata": {},
+   "source": [
+    "### prediction_type='label'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "197d7ea4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2 = OutputDriftDetector(task='classification', prediction_type='label')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "48ce3836",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.fit(clf.predict(X_train), clf.predict(X_test),\n",
+    "                            y_train, y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e9bacaed",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.plot_prediction_drift()\n",
+    "output_drift_detector2.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7d80440a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.plot_target_drift()\n",
+    "output_drift_detector2.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "13051c77",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "21aaccb4",
+   "metadata": {},
+   "source": [
+    "### prediction_type='raw'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4df437e4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3 = OutputDriftDetector(task='classification', prediction_type='raw')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7e7c6fbb",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.fit(clf.predict(X_train, output_margin=True), clf.predict(X_test, output_margin=True),\n",
+    "                            y_train, y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a6de8689",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.plot_prediction_drift()\n",
+    "output_drift_detector3.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ea722a0a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.plot_target_drift()\n",
+    "output_drift_detector3.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a11bce1c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2a73300c",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ca322e8a",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3662bd77",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.9.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/breast_cancer_xgboost_binary_classif.ipynb b/examples/breast_cancer_XGBoost_ModelDriftExplainer.ipynb
similarity index 86%
rename from examples/breast_cancer_xgboost_binary_classif.ipynb
rename to examples/breast_cancer_XGBoost_ModelDriftExplainer.ipynb
index 607bf6d..32939a5 100644
--- a/examples/breast_cancer_xgboost_binary_classif.ipynb
+++ b/examples/breast_cancer_XGBoost_ModelDriftExplainer.ipynb
@@ -70,7 +70,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.3, random_state=2021)"
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2021)"
    ]
   },
   {
@@ -96,7 +96,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "clf.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20, verbose=10)"
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
    ]
   },
   {
@@ -126,7 +126,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer.fit(X1=X_train, X2=X_valid, y1=y_train, y2=y_valid)"
+    "drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)"
    ]
   },
   {
@@ -165,6 +165,16 @@
     "drift_explainer.get_target_drift()"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e71945db",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_performance_metrics_drift()"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -184,7 +194,7 @@
    "execution_count": null,
    "id": "60518cba",
    "metadata": {
-    "scrolled": true
+    "scrolled": false
    },
    "outputs": [],
    "source": [
@@ -217,6 +227,17 @@
     "drift_explainer.get_tree_based_drift_values(type='mean_norm')"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f079c170",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_feature_drift('mean perimeter')\n",
+    "drift_explainer.get_feature_drift('mean perimeter')"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -224,15 +245,17 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer.plot_feature_drift(0)\n",
-    "drift_explainer.get_feature_drift(0)"
+    "drift_explainer.plot_feature_drift(4)\n",
+    "drift_explainer.get_feature_drift(4)"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
    "id": "a890b9d8",
-   "metadata": {},
+   "metadata": {
+    "scrolled": true
+   },
    "outputs": [],
    "source": [
     "drift_explainer.get_feature_drifts()"
@@ -241,7 +264,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "5621b63e",
+   "id": "48ce3836",
    "metadata": {},
    "outputs": [],
    "source": []
@@ -263,7 +286,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.1"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/examples/ieee_fraud_no_data_drift.ipynb b/examples/ieee_fraud_no_data_drift.ipynb
index c816705..045f6cc 100644
--- a/examples/ieee_fraud_no_data_drift.ipynb
+++ b/examples/ieee_fraud_no_data_drift.ipynb
@@ -150,7 +150,7 @@
     "\n",
     "We replicate a typical production situation where we would have:\n",
     "- training data\n",
-    "- validation data\n",
+    "- test data\n",
     "- production data"
    ]
   },
@@ -173,8 +173,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# we do a time split (shuffle=False) to seperate between df_temp (training-validation data)\n",
-    "# and df_prod (production data)\n",
+    "# we do a time split (shuffle=False) to seperate between df_temp (train + test data) and df_prod (production data)\n",
     "df_temp, df_prod = train_test_split(df, test_size=0.25, shuffle=False, random_state=seed)"
    ]
   },
@@ -186,7 +185,7 @@
    "outputs": [],
    "source": [
     "# we do a time split (shuffle=False) to seperate between training data and validation data\n",
-    "X_train, X_valid, y_train, y_valid = train_test_split(df_temp[features], df_temp['isFraud'].values,\n",
+    "X_train, X_test, y_train, y_test = train_test_split(df_temp[features], df_temp['isFraud'].values,\n",
     "                                                      test_size=1/3, shuffle=False, random_state=seed)"
    ]
   },
@@ -231,7 +230,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "clf.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20,\n",
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20,\n",
     "        verbose=10, eval_metric=['auc', 'logloss'])"
    ]
   },
@@ -254,7 +253,7 @@
    "source": [
     "# initialize and fit a model drift explainer on valid and prod data\n",
     "drift_explainer = ModelDriftExplainer(clf)\n",
-    "drift_explainer.fit(X1=X_valid, X2=X_prod, y1=y_valid, y2=y_prod)"
+    "drift_explainer.fit(X1=X_test, X2=X_prod, y1=y_test, y2=y_prod)"
    ]
   },
   {
@@ -291,7 +290,7 @@
    "outputs": [],
    "source": [
     "# no apparent drift in performance metrics\n",
-    "print(f'log_loss valid: {log_loss(y_valid, clf.predict_proba(X_valid))}')\n",
+    "print(f'log_loss test: {log_loss(y_test, clf.predict_proba(X_test))}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf.predict_proba(X_prod))}')"
    ]
   },
@@ -318,22 +317,6 @@
     "drift_explainer.get_feature_drift('card6')"
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "64c549a7",
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "a20807ce",
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -359,7 +342,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.1"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/examples/ieee_fraud_simulated_concept_drift_card6.ipynb b/examples/ieee_fraud_simulated_concept_drift_card6.ipynb
index 20b6cec..f470b41 100644
--- a/examples/ieee_fraud_simulated_concept_drift_card6.ipynb
+++ b/examples/ieee_fraud_simulated_concept_drift_card6.ipynb
@@ -148,7 +148,7 @@
     "\n",
     "We replicate a typical production situation where we would have:\n",
     "- training data\n",
-    "- validation data\n",
+    "- test data\n",
     "- production data\n",
     "\n",
     "Also, we introduce so data drift on the variable `card6` by keeping only transactions which correspond to credit card. In a real application, this would correspond to the case where we are not able to identify fraud (the target label) for debit card transactions.\n",
@@ -175,7 +175,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# we do a time split (shuffle=False) to seperate between df_temp (training-validation data)\n",
+    "# we do a time split (shuffle=False) to seperate between df_temp (training-test data)\n",
     "# and df_prod (production data)\n",
     "df_temp, df_prod = train_test_split(df.copy(), test_size=0.25, shuffle=False)"
    ]
@@ -198,13 +198,13 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# drop all debit card transactions in training-validaton data\n",
-    "# we do a time split (shuffle=False) to seperate between training data and validation data\n",
-    "X_train, X_valid, y_train, y_valid = train_test_split(df_temp.loc[df_temp['card6'].values == 'credit', features].copy(),\n",
-    "                                                      df_temp.loc[df_temp['card6'].values == 'credit', 'isFraud'].values,\n",
-    "                                                      test_size=1/3,\n",
-    "                                                      shuffle=False,\n",
-    "                                                      random_state=seed)"
+    "# drop all debit card transactions in train-test data\n",
+    "# we do a time split (shuffle=False) to seperate between train data and test data\n",
+    "X_train, X_test, y_train, y_test = train_test_split(df_temp.loc[df_temp['card6'].values == 'credit', features].copy(),\n",
+    "                                                    df_temp.loc[df_temp['card6'].values == 'credit', 'isFraud'].values,\n",
+    "                                                    test_size=1/3,\n",
+    "                                                    shuffle=False,\n",
+    "                                                    random_state=seed)"
    ]
   },
   {
@@ -248,7 +248,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "clf.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20,\n",
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20,\n",
     "        verbose=10, eval_metric=['auc', 'logloss'])"
    ]
   },
@@ -285,7 +285,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer.fit(X1=X_valid, X2=X_prod, y1=y_valid, y2=y_prod)"
+    "drift_explainer.fit(X1=X_test, X2=X_prod, y1=y_test, y2=y_prod)"
    ]
   },
   {
@@ -319,10 +319,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "print(f'log_loss valid: {log_loss(y_valid, clf.predict_proba(X_valid))}')\n",
+    "print(f'log_loss test: {log_loss(y_test, clf.predict_proba(X_test))}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf.predict_proba(X_prod))}')\n",
     "\n",
-    "print(f'AUC valid: {roc_auc_score(y_valid, clf.predict_proba(X_valid)[:, 1])}')\n",
+    "print(f'AUC test: {roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1])}')\n",
     "print(f'AUC prod: {roc_auc_score(y_prod, clf.predict_proba(X_prod)[:, 1])}')"
    ]
   },
@@ -414,9 +414,9 @@
    "id": "f8d69a54",
    "metadata": {},
    "source": [
-    "## Correction on validation dataset\n",
+    "## Correction on test dataset\n",
     "\n",
-    "We apply our methodology which uses adversarial learning to correct data drift between valid and prod data.\n",
+    "We apply our methodology which uses adversarial learning to correct data drift between test and prod data.\n",
     "\n",
     "We then check our three indicators of data drift in order to see if we get improvement."
    ]
@@ -433,9 +433,9 @@
     "# weights computed with the adversarial method\n",
     "# feature_subset=['D1', 'C13', 'C2', 'TransactionAmt']: only the first fourth features in terms of\n",
     "# drift value are selected\n",
-    "sample_weights_valid_adversarial = (AdversarialDriftExplainer(feature_subset=['D1', 'C13', 'C2', 'TransactionAmt'],\n",
+    "sample_weights_test_adversarial = (AdversarialDriftExplainer(feature_subset=['D1', 'C13'], #, 'C2', 'TransactionAmt'\n",
     "                                                              seed=2021)\n",
-    "                                    .fit(X_valid, X_prod)\n",
+    "                                    .fit(X_test, X_prod)\n",
     "                                    .get_adversarial_correction_weights(max_ratio=10))"
    ]
   },
@@ -446,8 +446,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer2 = ModelDriftExplainer(clf).fit(X1=X_valid, X2=X_prod, y1=y_valid, y2=y_prod,\n",
-    "                                                sample_weights1=sample_weights_valid_adversarial)"
+    "drift_explainer2 = ModelDriftExplainer(clf).fit(X1=X_test, X2=X_prod, y1=y_test, y2=y_prod,\n",
+    "                                                sample_weights1=sample_weights_test_adversarial)"
    ]
   },
   {
@@ -482,10 +482,10 @@
    "outputs": [],
    "source": [
     "# valid loss is closer to prod loss, but there is still a difference\n",
-    "print(f'log_loss valid: {log_loss(y_valid, clf.predict_proba(X_valid), sample_weight=sample_weights_valid_adversarial)}')\n",
+    "print(f'log_loss valid: {log_loss(y_test, clf.predict_proba(X_test), sample_weight=sample_weights_test_adversarial)}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf.predict_proba(X_prod))}')\n",
     "\n",
-    "print(f'AUC valid: {roc_auc_score(y_valid, clf.predict_proba(X_valid)[:, 1], sample_weight=sample_weights_valid_adversarial)}')\n",
+    "print(f'AUC valid: {roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1], sample_weight=sample_weights_test_adversarial)}')\n",
     "print(f'AUC prod: {roc_auc_score(y_prod, clf.predict_proba(X_prod)[:, 1])}')"
    ]
   },
@@ -494,11 +494,11 @@
    "id": "dbdb694b",
    "metadata": {},
    "source": [
-    "## Correction on validation dataset and train dataset (in order to retrain the model)\n",
+    "## Correction on test dataset and train dataset (in order to retrain the model)\n",
     "\n",
     "We apply the same adversarial strategy on training data.\n",
     "\n",
-    "With the model retrain on re-weighted samples, new weights, we observe there is no obviouos improvement in model performance on production data. This needs to be further investigated."
+    "With the model retrain on re-weighted samples, new weights, we observe there is no obvious improvement in model performance on production data. This needs to be further investigated."
    ]
   },
   {
@@ -511,7 +511,7 @@
    "outputs": [],
    "source": [
     "# weights computed with the adversarial method on training data\n",
-    "sample_weights_train_adversarial = (AdversarialDriftExplainer(feature_subset=['D1', 'C13', 'C2', 'TransactionAmt'],\n",
+    "sample_weights_train_adversarial = (AdversarialDriftExplainer(feature_subset=['D1', 'C13', ], #'C2', 'TransactionAmt'\n",
     "                                                              seed=2021)\n",
     "                                    .fit(X_train, X_prod)\n",
     "                                    .get_adversarial_correction_weights(max_ratio=10))"
@@ -542,9 +542,9 @@
    "source": [
     "# train a new classifier with the reweighted samples\n",
     "# we use a power factor 0.3 on sample_weights_train_adversarial weights to smooth them\n",
-    "clf2.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], sample_weight=sample_weights_train_adversarial**0.3,\n",
+    "clf2.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], sample_weight=sample_weights_train_adversarial**0.3,\n",
     "         early_stopping_rounds=20, verbose=10, eval_metric=['auc', 'logloss'],\n",
-    "         sample_weight_eval_set=[sample_weights_valid_adversarial])"
+    "         sample_weight_eval_set=[sample_weights_test_adversarial])"
    ]
   },
   {
@@ -555,10 +555,10 @@
    "outputs": [],
    "source": [
     "# with the reweighting, we see a small improvement for performance on production data, but is it significative ?\n",
-    "print(f'log_loss valid: {log_loss(y_valid, clf2.predict_proba(X_valid), sample_weight=sample_weights_valid_adversarial)}')\n",
+    "print(f'log_loss test: {log_loss(y_test, clf2.predict_proba(X_test), sample_weight=sample_weights_test_adversarial)}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf2.predict_proba(X_prod))}')\n",
     "\n",
-    "print(f'AUC valid: {roc_auc_score(y_valid, clf2.predict_proba(X_valid)[:, 1], sample_weight=sample_weights_valid_adversarial)}')\n",
+    "print(f'AUC test: {roc_auc_score(y_test, clf2.predict_proba(X_test)[:, 1], sample_weight=sample_weights_test_adversarial)}')\n",
     "print(f'AUC prod: {roc_auc_score(y_prod, clf2.predict_proba(X_prod)[:, 1])}')"
    ]
   },
@@ -587,7 +587,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.1"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/examples/ieee_fraud_simulated_covariate_shift_card6.ipynb b/examples/ieee_fraud_simulated_covariate_shift_card6.ipynb
index 4b2e261..0113aaf 100644
--- a/examples/ieee_fraud_simulated_covariate_shift_card6.ipynb
+++ b/examples/ieee_fraud_simulated_covariate_shift_card6.ipynb
@@ -149,7 +149,7 @@
     "\n",
     "We replicate a typical production situation where we would have:\n",
     "- training data\n",
-    "- validation data\n",
+    "- test data\n",
     "- production data\n",
     "\n",
     "Also, we introduce so data drift on the variable `card6` by using downsampling. This data drift corresponds to covariate shift."
@@ -174,8 +174,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# we do a time split (shuffle=False) to seperate between df_temp (training-validation data)\n",
-    "# and df_prod (production data)\n",
+    "# we do a time split (shuffle=False) to seperate between df_temp (train-test data) and df_prod (production data)\n",
     "df_temp, df_prod = train_test_split(df.copy(), test_size=0.25, shuffle=False, random_state=seed)"
    ]
   },
@@ -220,8 +219,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# we do a time split (shuffle=False) to seperate between training data and validation data\n",
-    "X_train, X_valid, y_train, y_valid = train_test_split(df_temp[features].copy(),\n",
+    "# we do a time split (shuffle=False) to seperate between training data and test data\n",
+    "X_train, X_test, y_train, y_test = train_test_split(df_temp[features].copy(),\n",
     "                                                      df_temp['isFraud'].values,\n",
     "                                                      test_size=1/3,\n",
     "                                                      shuffle=False,\n",
@@ -269,7 +268,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "clf.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20,\n",
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20,\n",
     "        verbose=10, eval_metric=['auc', 'logloss'])"
    ]
   },
@@ -295,9 +294,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# initialize and fit a model drift explainer on valid and prod data\n",
+    "# initialize and fit a model drift explainer on test and prod data\n",
     "drift_explainer = ModelDriftExplainer(clf)\n",
-    "drift_explainer.fit(X1=X_valid, X2=X_prod, y1=y_valid, y2=y_prod)"
+    "drift_explainer.fit(X1=X_test, X2=X_prod, y1=y_test, y2=y_prod)"
    ]
   },
   {
@@ -331,7 +330,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "print(f'log_loss valid: {log_loss(y_valid, clf.predict_proba(X_valid))}')\n",
+    "print(f'log_loss test: {log_loss(y_test, clf.predict_proba(X_test))}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf.predict_proba(X_prod))}')"
    ]
   },
@@ -361,7 +360,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# first drift value feature correspond is 'card6' : the one we voluntarily add drift to.\n",
+    "# first drift value feature is 'card6' : the one we voluntarily add drift to.\n",
     "drift_explainer.plot_feature_drift('card6', as_discrete=True)\n",
     "drift_explainer.get_feature_drift('card6')"
    ]
@@ -413,9 +412,9 @@
    "id": "45b56fc5",
    "metadata": {},
    "source": [
-    "## Correction on validation dataset\n",
+    "## Correction on test dataset\n",
     "\n",
-    "We apply our methodology which uses adversarial learning to correct data drift between valid and prod data.\n",
+    "We apply our methodology which uses adversarial learning to correct data drift between test and prod data.\n",
     "\n",
     "We then check our three indicators of  data drift in order to see if we get improvement."
    ]
@@ -429,8 +428,8 @@
    "source": [
     "# weights computed with the adversarial method\n",
     "# feature_subset=['card6']: only the first feature in terms of drift value is selected here\n",
-    "sample_weights_valid_adversarial = (AdversarialDriftExplainer(feature_subset=['card6'], seed=2021)\n",
-    "                                    .fit(X_valid, X_prod)\n",
+    "sample_weights_test_adversarial = (AdversarialDriftExplainer(feature_subset=['card6'], seed=2021)\n",
+    "                                    .fit(X_test, X_prod)\n",
     "                                    .get_adversarial_correction_weights(max_ratio=10))"
    ]
   },
@@ -441,10 +440,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# study the drift with the new weights on validation data\n",
+    "# study the drift with the new weights on test data\n",
     "drift_explainer2 = ModelDriftExplainer(clf)\n",
-    "drift_explainer2.fit(X1=X_valid, X2=X_prod, y1=y_valid, y2=y_prod,\n",
-    "                     sample_weights1=sample_weights_valid_adversarial)"
+    "drift_explainer2.fit(X1=X_test, X2=X_prod, y1=y_test, y2=y_prod,\n",
+    "                     sample_weights1=sample_weights_test_adversarial)"
    ]
   },
   {
@@ -454,7 +453,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# with new weigts on valid data, distributions of predictions are much closer\n",
+    "# with new weigts on test data, distributions of predictions are much closer\n",
     "drift_explainer2.plot_prediction_drift(bins=100)\n",
     "drift_explainer2.get_prediction_drift()"
    ]
@@ -479,7 +478,7 @@
    "outputs": [],
    "source": [
     "# we now replicates much better what happens in production (also in terms of log loss)\n",
-    "print(f'log_loss valid: {log_loss(y_valid, clf.predict_proba(X_valid), sample_weight=sample_weights_valid_adversarial)}')\n",
+    "print(f'log_loss test: {log_loss(y_test, clf.predict_proba(X_test), sample_weight=sample_weights_test_adversarial)}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf.predict_proba(X_prod))}')"
    ]
   },
@@ -534,9 +533,9 @@
    "outputs": [],
    "source": [
     "# train a new classifier with the reweighted samples\n",
-    "clf2.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], sample_weight=sample_weights_train_adversarial,\n",
+    "clf2.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], sample_weight=sample_weights_train_adversarial,\n",
     "         early_stopping_rounds=20, verbose=10, eval_metric=['auc', 'logloss'],\n",
-    "         sample_weight_eval_set=[sample_weights_valid_adversarial])"
+    "         sample_weight_eval_set=[sample_weights_test_adversarial])"
    ]
   },
   {
@@ -546,15 +545,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# we see no improvement on prod loss when we train with both train and valid datasets reweighted\n",
-    "print(f'log_loss valid: {log_loss(y_valid, clf2.predict_proba(X_valid), sample_weight=sample_weights_valid_adversarial)}')\n",
+    "# we see no improvement on prod loss when we train with both train and test datasets reweighted\n",
+    "print(f'log_loss test: {log_loss(y_test, clf2.predict_proba(X_test), sample_weight=sample_weights_test_adversarial)}')\n",
     "print(f'log_loss prod: {log_loss(y_prod, clf2.predict_proba(X_prod))}')"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "5a5a4743",
+   "id": "12ae1ff1",
    "metadata": {},
    "outputs": [],
    "source": []
@@ -576,7 +575,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.1"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/examples/iris_CatBoost_ModelDriftExplainer.ipynb b/examples/iris_CatBoost_ModelDriftExplainer.ipynb
new file mode 100644
index 0000000..615d366
--- /dev/null
+++ b/examples/iris_CatBoost_ModelDriftExplainer.ipynb
@@ -0,0 +1,293 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "c9282a05",
+   "metadata": {},
+   "source": [
+    "The experiment is done with a random split so we should not detect a data drift"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "da27f578",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from sklearn import datasets\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from catboost import CatBoostClassifier\n",
+    "\n",
+    "from cinnamon.drift import ModelDriftExplainer"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2e16e864",
+   "metadata": {},
+   "source": [
+    "# Iris Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f582c773",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset = datasets.load_iris()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b20301d9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X = pd.DataFrame(dataset.data, columns = dataset.feature_names)\n",
+    "y = dataset.target"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "df4a4c87",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "85a7a6c1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# iris is a multiclass classification problem\n",
+    "np.unique(y, return_counts=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a6ea631c",
+   "metadata": {},
+   "source": [
+    "# Build CatBoost model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "cbf5e852",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9be4d156",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf = CatBoostClassifier(loss_function='MultiClass', \n",
+    "                         learning_rate=0.1,\n",
+    "                         iterations=1000,\n",
+    "                         max_depth=6,\n",
+    "                         early_stopping_rounds=20,\n",
+    "                         random_seed=2021,\n",
+    "                         verbose=10,\n",
+    "                        )"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8001d44d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "89a3fef8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer = ModelDriftExplainer(clf)\n",
+    "drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a7b53a15",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f0f39b51",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a681a2bf",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_prediction_drift(prediction_type='proba')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1c2201bb",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_prediction_drift(prediction_type='proba')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "53186e34",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ca837448",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "20752580",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e7834b47",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_tree_based_drift_values()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3b2a6653",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_tree_based_drift_values()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "82201979",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_tree_based_drift_values(type='mean')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "183fd966",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_tree_based_drift_values(type='mean')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "15a9ce7e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.plot_feature_drift(2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5e102759",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_feature_drift(0)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "385dd16a",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.9.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/iris_OutputDriftDetector.ipynb b/examples/iris_OutputDriftDetector.ipynb
new file mode 100644
index 0000000..6f85249
--- /dev/null
+++ b/examples/iris_OutputDriftDetector.ipynb
@@ -0,0 +1,362 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ba5dfdb1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from sklearn import datasets\n",
+    "from sklearn.model_selection import train_test_split\n",
+    "from xgboost import XGBClassifier\n",
+    "\n",
+    "from cinnamon.drift import OutputDriftDetector"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2e16e864",
+   "metadata": {},
+   "source": [
+    "# Iris Data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f582c773",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dataset = datasets.load_iris()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b20301d9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X = pd.DataFrame(dataset.data, columns = dataset.feature_names)\n",
+    "y = dataset.target"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "df4a4c87",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e69c4f3c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# This is multiclass classification pbm with 3 classes\n",
+    "np.unique(y, return_counts=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a6ea631c",
+   "metadata": {},
+   "source": [
+    "# Build XGBoost model"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "cbf5e852",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9be4d156",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf = XGBClassifier(n_estimators=1000,\n",
+    "                    booster=\"gbtree\",\n",
+    "                    learning_rate=0.05,\n",
+    "                    max_depth=6,\n",
+    "                    use_label_encoder=False,\n",
+    "                    seed=2021)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8001d44d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2042fc37",
+   "metadata": {},
+   "source": [
+    "# Analyze data drift with ModelDriftExplainer\n",
+    "\n",
+    "The experiment is done with a random split so we should not detect a data drift"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "477eb2df",
+   "metadata": {},
+   "source": [
+    "### prediction_type == \"proba\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a0b2739b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector = OutputDriftDetector(task='classification', prediction_type='proba')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "89a3fef8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.fit(clf.predict_proba(X_train), clf.predict_proba(X_test), y1=y_train, y2=y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "84b01f28",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# no apparent drift in distributions of log-softmax predictions\n",
+    "output_drift_detector.plot_prediction_drift()\n",
+    "output_drift_detector.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d1f7d044",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.plot_target_drift()\n",
+    "output_drift_detector.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e666797a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1c6cf71c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector.class_names"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3d7e4e97",
+   "metadata": {},
+   "source": [
+    "### prediction_type == \"label\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e85b71d9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2 = OutputDriftDetector(task='classification', prediction_type='label')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f98d02c8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.fit(clf.predict(X_train), clf.predict(X_test),\n",
+    "                            y_train, y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ba395f61",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.plot_prediction_drift()\n",
+    "output_drift_detector2.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "01d187b6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.plot_target_drift()\n",
+    "output_drift_detector2.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c48fea03",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector2.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d7ce02ee",
+   "metadata": {},
+   "source": [
+    "### prediction_type == \"raw\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bfadd1e0",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3 = OutputDriftDetector(task='classification', prediction_type='raw')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "18d2bb8f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.fit(clf.predict(X_train, output_margin=True), clf.predict(X_test, output_margin=True),\n",
+    "                           y1=y_train, y2=y_test)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6098cf0f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# no apparent drift in distributions of log-softmax predictions\n",
+    "output_drift_detector3.plot_prediction_drift()\n",
+    "output_drift_detector3.get_prediction_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "757dcfc2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.plot_target_drift()\n",
+    "output_drift_detector3.get_target_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "86b06e6c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "output_drift_detector3.get_performance_metrics_drift()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d9f7fc3c",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "59e0f4f5",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d8ac178a",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "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.9.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/iris_xgboost_multi_classif.ipynb b/examples/iris_XGBoost_ModelDriftExplainer.ipynb
similarity index 86%
rename from examples/iris_xgboost_multi_classif.ipynb
rename to examples/iris_XGBoost_ModelDriftExplainer.ipynb
index 9336865..fa4ebf1 100644
--- a/examples/iris_xgboost_multi_classif.ipynb
+++ b/examples/iris_XGBoost_ModelDriftExplainer.ipynb
@@ -21,7 +21,7 @@
    "id": "2e16e864",
    "metadata": {},
    "source": [
-    "# Breast Cancer Data"
+    "# Iris Data"
    ]
   },
   {
@@ -81,7 +81,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.3, random_state=2021)"
+    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2021)"
    ]
   },
   {
@@ -106,7 +106,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "clf.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20, verbose=10)"
+    "clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=10)"
    ]
   },
   {
@@ -136,7 +136,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "drift_explainer.fit(X1=X_train, X2=X_valid, y1=y_train, y2=y_valid)"
+    "drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)"
    ]
   },
   {
@@ -151,6 +151,16 @@
     "drift_explainer.get_prediction_drift()"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d1f7d044",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "drift_explainer.get_target_drift()"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -185,6 +195,16 @@
     }
    },
    "outputs": [],
+   "source": [
+    "drift_explainer.get_feature_drifts()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "02b9dda3",
+   "metadata": {},
+   "outputs": [],
    "source": []
   }
  ],
@@ -204,7 +224,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.1"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/setup.py b/setup.py
index 3f76ca8..0e2c810 100644
--- a/setup.py
+++ b/setup.py
@@ -28,10 +28,14 @@
     package_dir={"": "src"},
     packages=find_packages(where="src"),
     license="MIT",
-    python_requires=">=3.9",
-    install_requires=["pandas>=1.2.4", "scipy>=1.7.1", "scikit-learn>=1.0", "xgboost>=1.4.2",
-                      "numpy>=1.21.2", "matplotlib>=3.4.3", "treelib>=1.6.1"],
+    python_requires=">=3.6",
+    install_requires=["pandas>=1.0",
+                      "scikit-learn>=0.24",  # can switch to 1.0 when I switch to Python 3.7 support
+                      "xgboost>=1.4",
+                      "catboost>=1.0",
+                      "matplotlib",
+                      "treelib"],
     #extras_require={"graphs": ["matplotlib>=3.4.3", "treelib>=1.6.1"]},
     keywords=["data drift", "covariate shift", "concept drift", "monitoring",
               "adversarial learning", "machine learning"],
-)
\ No newline at end of file
+)
diff --git a/src/cinnamon/common/constants.py b/src/cinnamon/common/constants.py
index 766107d..2c8af83 100644
--- a/src/cinnamon/common/constants.py
+++ b/src/cinnamon/common/constants.py
@@ -1,5 +1,9 @@
 from enum import Enum
 
+NUMPY_atol = 1e-8
+FLOAT_atol = 1e-12
+
+
 class TreeBasedDriftValueType(Enum):
     NODE_SIZE = "node_size"
     MEAN = "mean"
diff --git a/src/cinnamon/common/dev_utils.py b/src/cinnamon/common/dev_utils.py
index 5ef39cd..1c566f6 100644
--- a/src/cinnamon/common/dev_utils.py
+++ b/src/cinnamon/common/dev_utils.py
@@ -1,4 +1,5 @@
 import sys
+import numpy as np
 
 
 def safe_isinstance(obj, class_path_str):
@@ -54,3 +55,12 @@ def safe_isinstance(obj, class_path_str):
             return True
 
     return False
+
+
+def find_uniques(*arrays: np.array):
+    uniques = set()
+    for a in arrays:
+        for x in np.unique(a):
+            uniques.add(x)
+    uniques.discard(None)
+    return list(uniques)
diff --git a/src/cinnamon/common/logging.py b/src/cinnamon/common/logging.py
new file mode 100644
index 0000000..d4718ef
--- /dev/null
+++ b/src/cinnamon/common/logging.py
@@ -0,0 +1,5 @@
+import logging
+
+logging.basicConfig(format='%(levelname)s:%(asctime)s - (%(pathname)s) %(message)s', level=logging.INFO)
+
+cinnamon_logger = logging.getLogger('Cinnamon')
diff --git a/src/cinnamon/common/math_utils.py b/src/cinnamon/common/math_utils.py
index 9a04f80..abacd5b 100644
--- a/src/cinnamon/common/math_utils.py
+++ b/src/cinnamon/common/math_utils.py
@@ -1,5 +1,4 @@
 import numpy as np
-from sklearn.metrics import log_loss, mean_squared_error, explained_variance_score, roc_auc_score, accuracy_score
 
 
 def threshold(x: float, max_ratio: float):
@@ -14,18 +13,28 @@ def threshold_array(a: np.array, max_ratio: float):
 
 def softmax(a: np.array):
     """
-    :param a: array of log_softmax
+    :param a: array of log_softmax (should have ndim = 2)
     :return:
     """
     return np.exp(a) / np.sum(np.exp(a), axis=1)[:, None]
 
 
-def compute_classification_metrics(y_true: np.array, y_pred: np.array, sample_weights: np.array) -> dict:
-    # TODO: add more metrics here: accuracy, AUC, etc.
-    return {'log_loss': log_loss(y_true, y_pred, sample_weight=sample_weights)}
-
-
-def compute_regression_metrics(y_true: np.array, y_pred: np.array, sample_weights: np.array) -> dict:
-    # TODO: add more metrics here
-    return {'mse': mean_squared_error(y_true, y_pred, sample_weight=sample_weights),
-            'explained_variance': explained_variance_score(y_true, y_pred, sample_weight=sample_weights)}
+def sigmoid(a: np.array):
+    '''
+    :param a: array of logit (should have ndim = 1)
+    :return:
+    '''
+    return np.exp(a) / (1 + np.exp(a))
+
+
+def reverse_binary_representation(n: int, n_bits: int):
+    # return the integer obtained via reversing the order of the binary representation of the integer
+    # -------- example -----------
+    # n = 4, n_bits = 3
+    # binary representation = '100', reversed in '001'
+    # hence the returned integer is 1
+    # ---------------------------
+    assert 0 <= n < 2 ** n_bits
+    binary_representation = np.binary_repr(n, width=n_bits)
+    # we do the loop from left to right so no need to use binary_representation[::-1]
+    return sum([2**i for i, b in enumerate(binary_representation) if b == '1'])
diff --git a/src/cinnamon/common/stat_utils.py b/src/cinnamon/common/stat_utils.py
new file mode 100644
index 0000000..d74377a
--- /dev/null
+++ b/src/cinnamon/common/stat_utils.py
@@ -0,0 +1,264 @@
+import pandas as pd
+import numpy as np
+from numpy.testing import assert_allclose
+from scipy.stats import chi2_contingency, distributions
+from sklearn.metrics import log_loss, mean_squared_error, explained_variance_score, roc_auc_score, accuracy_score
+from sklearn.preprocessing import OneHotEncoder
+from dataclasses import dataclass
+from pandas.testing import assert_frame_equal
+from .math_utils import sigmoid, softmax
+from typing import List
+from scipy.spatial.distance import jensenshannon
+from.constants import FLOAT_atol
+
+# ---------------------------------------
+#        Compute performance metrics ML
+# ---------------------------------------
+
+
+@dataclass
+class PerformanceMetrics:
+    def assert_equal(self, other) -> None:
+        pass
+
+
+@dataclass
+class RegressionMetrics(PerformanceMetrics):
+    mse: float
+    explained_variance: float
+
+    def assert_equal(self, other) -> None:
+        assert isinstance(other, RegressionMetrics)
+        assert_allclose(self.mse, other.mse, atol=FLOAT_atol)
+        assert_allclose(self.explained_variance, other.explained_variance, atol=FLOAT_atol)
+
+
+@dataclass
+class ClassificationMetrics(PerformanceMetrics):
+    accuracy: float
+    log_loss: float = None
+
+    def assert_equal(self, other) -> None:
+        assert isinstance(other, ClassificationMetrics)
+        assert_allclose(self.accuracy, other.accuracy, atol=FLOAT_atol)
+        self.__assert_equal_or_none(self.log_loss, other.log_loss)
+
+    @staticmethod
+    def __assert_equal_or_none(x, y):
+        assert (x is not None) == (y is not None)
+        if x is not None:
+            assert_allclose(x, y, atol=FLOAT_atol)
+
+
+def compute_classification_metrics(y_true: np.array, y_pred: np.array, sample_weights: np.array,
+                                   class_names: List[str], prediction_type: str = 'proba',
+                                   threshold: float = 0.5) -> ClassificationMetrics:
+    '''
+
+    :param y_true: with labels in (0, ..., n_class - 1)
+    :param y_pred: if prediction_type == 'label', y_pred should be with
+    :param sample_weights:
+    :param class_names:
+    :param prediction_type:
+    :param threshold:
+    :return:
+    '''
+    ohe_y_true = OneHotEncoder(categories=[class_names]).fit_transform(y_true.reshape(-1, 1))
+
+    # case prediction_type in ['raw', 'proba']
+    if prediction_type in ['raw', 'proba']:
+
+        # Compute y_pred_proba
+        if prediction_type == 'raw':  # predictions are logit (binary classif) or log softmax (multiclass classif):
+            if y_pred.ndim == 1:  # binary classif
+                y_pred_proba = sigmoid(y_pred)
+            else:  # y_pred.ndim > 1
+                y_pred_proba = softmax(y_pred)
+        else:  # prediction_type == 'proba
+            y_pred_proba = y_pred
+
+        # Compute y_pred_label
+        if y_pred.ndim == 1:  # binary classif
+            y_pred_label = np.array([int(x > threshold) for x in y_pred_proba])
+        else:  # y_pred.ndim > 1
+            y_pred_label = np.argmax(y_pred_proba, axis=1)
+
+        return ClassificationMetrics(accuracy=accuracy_score(y_true, y_pred_label, sample_weight=sample_weights),
+                                     log_loss=log_loss(ohe_y_true, y_pred_proba, sample_weight=sample_weights))
+
+    else:  # case prediction_type == 'label'
+        return ClassificationMetrics(accuracy=accuracy_score(y_true, y_pred, sample_weight=sample_weights))
+
+
+def compute_regression_metrics(y_true: np.array, y_pred: np.array, sample_weights: np.array) -> RegressionMetrics:
+    # TODO: add more metrics here
+    return RegressionMetrics(mse=mean_squared_error(y_true, y_pred, sample_weight=sample_weights),
+                             explained_variance=explained_variance_score(y_true, y_pred, sample_weight=sample_weights))
+
+
+# --------------------------------
+#        Compute distribution
+# --------------------------------
+
+def compute_distributions_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None,
+                             max_n_cat: int = None):
+    if sample_weights1 is None:
+        sample_weights1 = np.ones_like(a1)
+    if sample_weights2 is None:
+        sample_weights2 = np.ones_like(a2)
+
+    # compute cat_map is needed
+    def _compute_cat_map(a: np.array, sample_weights: np.array, max_n_cat: float):
+        # sort categories by size
+        unique_cat = np.unique(a).tolist()
+        total_weight = np.sum(sample_weights)
+        cat_weights = [np.sum(sample_weights[a == cat]) / total_weight for cat in unique_cat]
+        sorted_cat = [cat for _, cat in sorted(zip(cat_weights, unique_cat), reverse=True)]
+
+        # create category mapping to reduce number of categories
+        cat_map = {}
+        for i, cat in enumerate(sorted_cat):
+            if i < (max_n_cat - 1):
+                cat_map[cat] = cat
+            else:
+                cat_map[cat] = 'other_cat_agg'
+        return cat_map
+
+    if max_n_cat is not None:
+        cat_map = _compute_cat_map(np.concatenate((a1, a2)), np.concatenate((sample_weights1, sample_weights2)),
+                                   max_n_cat)
+    else:
+        cat_map = None
+
+    # compute the distribution
+    unique_cat1 = np.unique(a1).tolist()
+    unique_cat2 = np.unique(a2).tolist()
+    unique_cat = unique_cat1 + [cat for cat in unique_cat2 if cat not in unique_cat1]
+    total_weight1 = np.sum(sample_weights1)
+    total_weight2 = np.sum(sample_weights2)
+    if cat_map is not None:
+        distrib = {cat: [0, 0] for cat in cat_map.values()}
+        for cat in unique_cat:
+            distrib[cat_map[cat]] = [distrib[cat_map[cat]][0] + np.sum(sample_weights1[a1 == cat]) / total_weight1,
+                                     distrib[cat_map[cat]][1] + np.sum(sample_weights2[a2 == cat]) / total_weight2]
+    else:
+        distrib = {}
+        for cat in unique_cat:
+            distrib[cat] = [np.sum(sample_weights1[a1 == cat]) / total_weight1,
+                            np.sum(sample_weights2[a2 == cat]) / total_weight2]
+
+    return distrib
+
+
+def compute_distributions_num(a1: np.array, a2: np.array, bins, sample_weights1=None, sample_weights2=None):
+    bin_edges = np.histogram_bin_edges(np.concatenate((a1, a2)), bins=bins)
+    hist1 = np.histogram(a1, bins=bin_edges, weights=sample_weights1, density=False)[0]
+    hist2 = np.histogram(a2, bins=bin_edges, weights=sample_weights2, density=False)[0]
+    return {'bin_edges': bin_edges, 'hist1': hist1, 'hist2': hist2}
+
+
+# --------------------------------
+#        Distances
+# --------------------------------
+
+def compute_mean_diff(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
+    if sample_weights1 is None:
+        sample_weights1 = np.ones_like(a1)
+    if sample_weights2 is None:
+        sample_weights2 = np.ones_like(a2)
+    mean1 = np.sum(a1 * sample_weights1) / np.sum(sample_weights1)
+    mean2 = np.sum(a2 * sample_weights2) / np.sum(sample_weights2)
+    return mean2 - mean1
+
+
+def wasserstein_distance_for_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
+    # this correspond to wasserstein distance where we assume a distance 1 between two categories of the feature
+    distrib = compute_distributions_cat(a1, a2, sample_weights1, sample_weights2)
+    drift = 0
+    for cat in distrib.keys():
+        drift += abs(distrib[cat][0] - distrib[cat][1]) / 2
+    return drift
+
+
+def jensen_shannon_distance(a1: np.array, a2: np.array, base=None, sample_weights1=None, sample_weights2=None):
+    distrib = compute_distributions_cat(a1, a2, sample_weights1, sample_weights2)
+    p = [v[0] for v in distrib.values()]
+    q = [v[1] for v in distrib.values()]
+    return jensenshannon(p, q, base=base)
+
+
+# --------------------------------
+#        Statistical tests
+# --------------------------------
+
+def chi2_test(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
+    # TODO: generalization of Chi2 for weights != np.ones is complicated (need verif)
+    # chi2 do not take max_n_cat into account. If pbm with number of cat, should be handled by
+    # chi2_test internally with a proper solution
+    distrib = compute_distributions_cat(a1, a2, sample_weights1, sample_weights2)
+    contingency_table = pd.DataFrame({cat: pd.Series({'X1': distrib[cat][0] * len(a1), 'X2': distrib[cat][1] * len(a2)})
+                                      for cat in distrib.keys()})
+    statistic, pvalue, dof, expected = chi2_contingency(contingency_table)
+    return Chi2TestResult(statistic, pvalue, dof, contingency_table)
+
+
+def ks_weighted(data1, data2, wei1, wei2, alternative='two-sided'):
+    # kolmogorov smirnov test for weighted samples
+    # taken from https://stackoverflow.com/questions/40044375/how-to-calculate-the-kolmogorov-smirnov-statistic-between-two-weighted-samples
+    # see also: https://github.com/scipy/scipy/issues/12315
+    # TODO: verify p-value computation is good
+    ix1 = np.argsort(data1)
+    ix2 = np.argsort(data2)
+    data1 = data1[ix1]
+    data2 = data2[ix2]
+    wei1 = wei1[ix1]
+    wei2 = wei2[ix2]
+    data = np.concatenate([data1, data2])
+    cwei1 = np.hstack([0, np.cumsum(wei1) / sum(wei1)])
+    cwei2 = np.hstack([0, np.cumsum(wei2) / sum(wei2)])
+    cdf1we = cwei1[np.searchsorted(data1, data, side='right')]
+    cdf2we = cwei2[np.searchsorted(data2, data, side='right')]
+    d = np.max(np.abs(cdf1we - cdf2we))
+    # calculate p-value
+    n1 = data1.shape[0]
+    n2 = data2.shape[0]
+    m, n = sorted([float(n1), float(n2)], reverse=True)
+    en = m * n / (m + n)
+    if alternative == 'two-sided':
+        prob = distributions.kstwo.sf(d, np.round(en))
+    else:
+        z = np.sqrt(en) * d
+        # Use Hodges' suggested approximation Eqn 5.3
+        # Requires m to be the larger of (n1, n2)
+        expt = -2 * z ** 2 - 2 * z * (m + 2 * n) / np.sqrt(m * n * (m + n)) / 3.0
+        prob = np.exp(expt)
+    return BaseStatisticalTestResult(statistic=d, pvalue=prob)
+
+
+# -----------------------------------
+#        Statistical tests results
+# -----------------------------------
+
+@dataclass(frozen=True)
+class BaseStatisticalTestResult:
+    statistic: float
+    pvalue: float
+
+    def assert_equal(self, other):
+        assert isinstance(other, BaseStatisticalTestResult)
+        assert_allclose(self.statistic, other.statistic, atol=FLOAT_atol)
+        assert_allclose(self.pvalue, other.pvalue, atol=FLOAT_atol)
+
+
+@dataclass(frozen=True)
+class Chi2TestResult(BaseStatisticalTestResult):
+    dof: int = None
+    contingency_table: pd.DataFrame = None
+
+    def assert_equal(self, other):
+        assert isinstance(other, Chi2TestResult)
+        # atol != FLOAT_atol, bellow. Because pbm with CI in Python 3.6 (result of chi2 test differs in Python 3.6 CI env)
+        assert_allclose(self.statistic, other.statistic, atol=2e-3)
+        assert_allclose(self.pvalue, other.pvalue, atol=4e-2)
+        assert self.dof == other.dof
+        assert_frame_equal(self.contingency_table, other.contingency_table)
diff --git a/src/cinnamon/drift/__init__.py b/src/cinnamon/drift/__init__.py
index d10f2d5..b12ad44 100644
--- a/src/cinnamon/drift/__init__.py
+++ b/src/cinnamon/drift/__init__.py
@@ -1,2 +1,3 @@
 from .adversarial_drift_explainer import AdversarialDriftExplainer
 from .model_drift_explainer import ModelDriftExplainer
+from .output_drift_detector import OutputDriftDetector
diff --git a/src/cinnamon/drift/abstract_drift_explainer.py b/src/cinnamon/drift/abstract_drift_explainer.py
index ddbb23e..f7ed70c 100644
--- a/src/cinnamon/drift/abstract_drift_explainer.py
+++ b/src/cinnamon/drift/abstract_drift_explainer.py
@@ -1,14 +1,10 @@
-import logging
 import numpy as np
 import pandas as pd
 import matplotlib.pyplot as plt
 from typing import List, Tuple, Union
 
-from .drift_utils import compute_drift_cat, compute_drift_num, plot_drift_cat, plot_drift_num
-
-logging.basicConfig(format='%(levelname)s:%(asctime)s - (%(pathname)s) %(message)s', level=logging.INFO)
-
-cinnamon_logger = logging.getLogger('Cinnamon')
+from .drift_utils import compute_drift_cat, compute_drift_num, plot_drift_cat, plot_drift_num, AbstractDriftMetrics
+from ..common.logging import cinnamon_logger
 
 
 class AbstractDriftExplainer:
@@ -32,7 +28,7 @@ def __init__(self):
         self.y2 = None
 
     @staticmethod
-    def _compute_feature_drifts(X1, X2, n_features, cat_feature_indices, sample_weights1, sample_weights2) -> List[dict]:
+    def _compute_feature_drifts(X1, X2, n_features, cat_feature_indices, sample_weights1, sample_weights2) -> List[AbstractDriftMetrics]:
         feature_drifts = []
         for i in range(n_features):
             if i in cat_feature_indices:
@@ -44,7 +40,7 @@ def _compute_feature_drifts(X1, X2, n_features, cat_feature_indices, sample_weig
             feature_drifts.append(feature_drift)
         return feature_drifts
 
-    def get_target_drift(self) -> dict:
+    def get_target_drift(self) -> AbstractDriftMetrics:
         """
         Compute drift measures for the labels y.
 
@@ -65,7 +61,7 @@ def get_target_drift(self) -> dict:
         target_drift : dict
             Dictionary of drift measures for the labels
         """
-        if isinstance(self.target_drift, dict):
+        if self.target_drift is not None:
             return self.target_drift
 
         if self.y1 is None or self.y2 is None:
@@ -111,7 +107,7 @@ def plot_target_drift(self, max_n_cat: int = 20, figsize: Tuple[int, int] = (7,
             plot_drift_num(self.y1, self.y2, self.sample_weights1, self.sample_weights2, title='target',
                            figsize=figsize, bins=bins)
 
-    def get_feature_drifts(self) -> List[dict]:
+    def get_feature_drifts(self) -> List[AbstractDriftMetrics]:
         """
         Compute drift measures for all features in X.
 
@@ -138,7 +134,7 @@ def get_feature_drifts(self) -> List[dict]:
                                                                self.sample_weights1, self.sample_weights2)
         return self.feature_drifts
 
-    def get_feature_drift(self, feature: Union[int, str]) -> dict:
+    def get_feature_drift(self, feature: Union[int, str]) -> AbstractDriftMetrics:
         """
         Compute drift measures for a given feature in X.
 
diff --git a/src/cinnamon/drift/adversarial_drift_explainer.py b/src/cinnamon/drift/adversarial_drift_explainer.py
index becfb7a..8c50693 100644
--- a/src/cinnamon/drift/adversarial_drift_explainer.py
+++ b/src/cinnamon/drift/adversarial_drift_explainer.py
@@ -273,12 +273,12 @@ def _get_feature_names(X1: pd.DataFrame):
 
     @staticmethod
     def _get_cat_feature_indices():
-        # FIXME: problem here with cat features...
+        # FIXME: problem here with cat features... and add it to the test
         return []
 
     @staticmethod
     def _get_class_names(task: str, y1, y2):
-        # FIXME: problem here with feature names...
+        # FIXME: problem here with feature names... and add it to the test
         return []
 
     @staticmethod
diff --git a/src/cinnamon/drift/drift_utils.py b/src/cinnamon/drift/drift_utils.py
index 817f753..f0cf244 100644
--- a/src/cinnamon/drift/drift_utils.py
+++ b/src/cinnamon/drift/drift_utils.py
@@ -1,154 +1,56 @@
 import numpy as np
-import pandas as pd
-from scipy.stats import chi2_contingency, wasserstein_distance, ks_2samp, distributions
+from numpy.testing import assert_allclose
+from scipy.stats import wasserstein_distance, ks_2samp
 import matplotlib.pyplot as plt
-
-
-def compute_distribution_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None,
-                             max_n_cat: int = None):
-    if sample_weights1 is None:
-        sample_weights1 = np.ones_like(a1)
-    if sample_weights2 is None:
-        sample_weights2 = np.ones_like(a2)
-
-    # compute cat_map is needed
-    def _compute_cat_map(a: np.array, sample_weights: np.array, max_n_cat: float):
-        # sort categories by size
-        unique_cat = np.unique(a).tolist()
-        total_weight = np.sum(sample_weights)
-        cat_weights = [np.sum(sample_weights[a == cat]) / total_weight for cat in unique_cat]
-        sorted_cat = [cat for _, cat in sorted(zip(cat_weights, unique_cat), reverse=True)]
-
-        # create category mapping to reducce number of categories
-        cat_map = {}
-        for i, cat in enumerate(sorted_cat):
-            if i < (max_n_cat-1):
-                cat_map[cat] = cat
-            else:
-                cat_map[cat] = 'other_cat_agg'
-        return cat_map
-
-    if max_n_cat is not None:
-        cat_map = _compute_cat_map(np.concatenate((a1, a2)), np.concatenate((sample_weights1, sample_weights2)),
-                                   max_n_cat)
-    else:
-        cat_map = None
-
-    # compute the distribution
-    unique_cat1 = np.unique(a1).tolist()
-    unique_cat2 = np.unique(a2).tolist()
-    unique_cat = unique_cat1 + [cat for cat in unique_cat2 if cat not in unique_cat1]
-    total_weight1 = np.sum(sample_weights1)
-    total_weight2 = np.sum(sample_weights2)
-    if cat_map is not None:
-        distrib = {cat: [0, 0] for cat in cat_map.values()}
-        for cat in unique_cat:
-            distrib[cat_map[cat]] = [distrib[cat_map[cat]][0] + np.sum(sample_weights1[a1 == cat]) / total_weight1,
-                                     distrib[cat_map[cat]][1] + np.sum(sample_weights2[a2 == cat]) / total_weight2]
-    else:
-        distrib = {}
-        for cat in unique_cat:
-            distrib[cat] = [np.sum(sample_weights1[a1 == cat]) / total_weight1,
-                            np.sum(sample_weights2[a2 == cat]) / total_weight2]
-
-    return distrib
-
-
-def compute_mean_diff(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
-    if sample_weights1 is None:
-        sample_weights1 = np.ones_like(a1)
-    if sample_weights2 is None:
-        sample_weights2 = np.ones_like(a2)
-    mean1 = np.sum(a1 * sample_weights1) / np.sum(sample_weights1)
-    mean2 = np.sum(a2 * sample_weights2) / np.sum(sample_weights2)
-    return mean2 - mean1
-
-
-def wasserstein_distance_for_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
-    # this correspond to wasserstein distance where we assume a distance 1 between two categories of the feature
-    distrib = compute_distribution_cat(a1, a2, sample_weights1, sample_weights2)
-    drift = 0
-    for cat in distrib.keys():
-        drift += abs(distrib[cat][0] - distrib[cat][1]) / 2
-    return drift
-
-
-def chi2_test(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
-    # TODO: generalization of Chi2 for weights != np.ones is complicated (need verif)
-    # chi2 do not take max_n_cat into account. If pbm with number of cat, should be handled by
-    # chi2_test internally with a proper solution
-    distrib = compute_distribution_cat(a1, a2, sample_weights1, sample_weights2)
-    contingency_table = pd.DataFrame({cat: pd.Series({'X1': distrib[cat][0] * len(a1), 'X2': distrib[cat][1] * len(a2)})
-                                      for cat in distrib.keys()})
-    chi2_stat, p_value, dof, expected = chi2_contingency(contingency_table)
-    return {'chi2_stat': chi2_stat,
-            'p_value': p_value,
-            'dof': dof,
-            'contingency_table': contingency_table}
-
-
-def compute_drift_num(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
+from dataclasses import dataclass
+from typing import List
+from ..common.stat_utils import (compute_distributions_cat,
+                                 compute_distributions_num,
+                                 compute_mean_diff,
+                                 wasserstein_distance_for_cat,
+                                 jensen_shannon_distance,
+                                 chi2_test,
+                                 ks_weighted,
+                                 BaseStatisticalTestResult,
+                                 Chi2TestResult,
+                                 PerformanceMetrics,
+                                 )
+from ..common.constants import FLOAT_atol
+
+
+def compute_drift_num(a1: np.array, a2: np.array, sample_weights1: np.array = None, sample_weights2: np.array = None,
+                      js_bins: int = 10, js_base=None):
     if (sample_weights1 is None and sample_weights2 is None or
             np.all(sample_weights1 == sample_weights1[0]) and np.all(sample_weights2 == sample_weights2[0])):
-        kolmogorov_smirnov = ks_2samp(a1, a2)
-    else:
-        kolmogorov_smirnov = ks_weighted(a1, a2, sample_weights1, sample_weights2)
-    return {'mean_difference': compute_mean_diff(a1, a2, sample_weights1, sample_weights2),
-            'wasserstein': wasserstein_distance(a1, a2, sample_weights1, sample_weights2),
-            'kolmogorov_smirnov': kolmogorov_smirnov}
-
-
-def ks_weighted(data1, data2, wei1, wei2, alternative='two-sided'):
-    # kolmogorov smirnov test for weighted samples
-    # taken from https://stackoverflow.com/questions/40044375/how-to-calculate-the-kolmogorov-smirnov-statistic-between-two-weighted-samples
-    # see also: https://github.com/scipy/scipy/issues/12315
-    # TODO: verify p-value computation is good
-    ix1 = np.argsort(data1)
-    ix2 = np.argsort(data2)
-    data1 = data1[ix1]
-    data2 = data2[ix2]
-    wei1 = wei1[ix1]
-    wei2 = wei2[ix2]
-    data = np.concatenate([data1, data2])
-    cwei1 = np.hstack([0, np.cumsum(wei1)/sum(wei1)])
-    cwei2 = np.hstack([0, np.cumsum(wei2)/sum(wei2)])
-    cdf1we = cwei1[np.searchsorted(data1, data, side='right')]
-    cdf2we = cwei2[np.searchsorted(data2, data, side='right')]
-    d = np.max(np.abs(cdf1we - cdf2we))
-    # calculate p-value
-    n1 = data1.shape[0]
-    n2 = data2.shape[0]
-    m, n = sorted([float(n1), float(n2)], reverse=True)
-    en = m * n / (m + n)
-    if alternative == 'two-sided':
-        prob = distributions.kstwo.sf(d, np.round(en))
+        ks_test_object = ks_2samp(a1, a2)
+        ks_test = BaseStatisticalTestResult(statistic=ks_test_object.statistic,
+                                            pvalue=ks_test_object.pvalue)
     else:
-        z = np.sqrt(en) * d
-        # Use Hodges' suggested approximation Eqn 5.3
-        # Requires m to be the larger of (n1, n2)
-        expt = -2 * z**2 - 2 * z * (m + 2*n)/np.sqrt(m*n*(m+n))/3.0
-        prob = np.exp(expt)
-    return {'statistic': d, 'pvalue': prob}
+        # 'ks_weighted' return a dictionnary with the good format
+        ks_test = ks_weighted(a1, a2, sample_weights1, sample_weights2)
+    return DriftMetricsNum(mean_difference=compute_mean_diff(a1, a2, sample_weights1, sample_weights2),
+                           wasserstein=wasserstein_distance(a1, a2, sample_weights1, sample_weights2),
+                           ks_test=ks_test)
 
 
-def compute_drift_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
-    return {'wasserstein': wasserstein_distance_for_cat(a1, a2, sample_weights1, sample_weights2),
-            'chi2_test': chi2_test(a1, a2, sample_weights1, sample_weights2)}
+def compute_drift_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None, js_base=None):
+    return DriftMetricsCat(wasserstein=wasserstein_distance_for_cat(a1, a2, sample_weights1, sample_weights2),
+                           jensen_shannon=jensen_shannon_distance(a1, a2, js_base, sample_weights1, sample_weights2),
+                           chi2_test=chi2_test(a1, a2, sample_weights1, sample_weights2))
 
 
 def plot_drift_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None, title=None,
-                   max_n_cat: float = None, figsize=(10, 6)):
-
+                   max_n_cat: int = None, figsize=(10, 6)):
     # compute both distributions
-    distrib = compute_distribution_cat(a1, a2, sample_weights1, sample_weights2, max_n_cat)
-    bar_height = np.array([v for v in distrib.values()]) # len(distrib) rows and 2 columns
+    distrib = compute_distributions_cat(a1, a2, sample_weights1, sample_weights2, max_n_cat)
+    bar_height = np.array([v for v in distrib.values()])  # len(distrib) rows and 2 columns
 
-    #plot
+    # plot
     index = np.arange(len(distrib))
     bar_width = 0.35
     fig, ax = plt.subplots(figsize=figsize)
     ax.bar(index, bar_height[:, 0], bar_width, label="Dataset 1")
-    ax.bar(index+bar_width, bar_height[:, 1], bar_width, label="Dataset 2")
+    ax.bar(index + bar_width, bar_height[:, 1], bar_width, label="Dataset 2")
 
     ax.set_xlabel('Category')
     ax.set_ylabel('Percentage')
@@ -159,16 +61,67 @@ def plot_drift_cat(a1: np.array, a2: np.array, sample_weights1=None, sample_weig
     plt.show()
 
 
-def plot_drift_num(a1: np.array, a2: np.array, sample_weights1: np.array=None, sample_weights2: np.array=None,
-                   title=None, figsize=(7,5), bins=10):
-    #distrib = compute_distribution_num(a1, a2, sample_weights1, sample_weights2)
+def plot_drift_num(a1: np.array, a2: np.array, sample_weights1: np.array = None, sample_weights2: np.array = None,
+                   title=None, figsize=(7, 5), bins=10):
+    distribs = compute_distributions_num(a1, a2, bins, sample_weights1, sample_weights2)
     fig, ax = plt.subplots(figsize=figsize)
-    ax.hist(a1, bins=bins, density=True, weights=sample_weights1, alpha=0.3)
-    ax.hist(a2, bins=bins, density=True, weights=sample_weights2, alpha=0.3)
+    ax.hist(a1, bins=distribs['bin_edges'], density=True, weights=sample_weights1, alpha=0.3)
+    ax.hist(a2, bins=distribs['bin_edges'], density=True, weights=sample_weights2, alpha=0.3)
     ax.legend(['Dataset 1', 'Dataset 2'])
     plt.title(title)
     plt.show()
 
 
-def compute_distribution_num(a1: np.array, a2: np.array, sample_weights1=None, sample_weights2=None):
-    pass
+@dataclass
+class AbstractDriftMetrics:
+    def assert_equal(self, other) -> None:
+        pass
+
+
+@dataclass
+class DriftMetricsNum(AbstractDriftMetrics):
+    mean_difference: float
+    wasserstein: float
+    ks_test: BaseStatisticalTestResult
+
+    def assert_equal(self, other) -> None:
+        assert isinstance(other, DriftMetricsNum)
+        assert_allclose(self.mean_difference, other.mean_difference, atol=FLOAT_atol)
+        assert_allclose(self.wasserstein, other.wasserstein, atol=FLOAT_atol)
+        self.ks_test.assert_equal(other.ks_test)
+
+
+@dataclass
+class DriftMetricsCat(AbstractDriftMetrics):
+    wasserstein: float
+    jensen_shannon: float
+    chi2_test: Chi2TestResult
+
+    def assert_equal(self, other) -> None:
+        assert isinstance(other, DriftMetricsCat)
+        assert_allclose(self.wasserstein, other.wasserstein, atol=FLOAT_atol)
+        assert_allclose(self.jensen_shannon, other.jensen_shannon, atol=FLOAT_atol)
+        self.chi2_test.assert_equal(other.chi2_test)
+
+
+def assert_drift_metrics_equal(drift_metrics1: AbstractDriftMetrics,
+                               drift_metrics2: AbstractDriftMetrics) -> None:
+    drift_metrics1.assert_equal(drift_metrics2)
+
+
+def assert_drift_metrics_list_equal(l1: List[AbstractDriftMetrics], l2: List[AbstractDriftMetrics]) -> None:
+    assert len(l1) == len(l2)
+    for i in range(len(l1)):
+        assert_drift_metrics_equal(l1[i], l2[i])
+
+
+@dataclass
+class PerformanceMetricsDrift:
+    dataset1: PerformanceMetrics
+    dataset2: PerformanceMetrics
+
+
+def assert_performance_metrics_drift_equal(performance_metrics_drift1: PerformanceMetricsDrift,
+                                           performance_metrics_drift2: PerformanceMetricsDrift):
+    performance_metrics_drift1.dataset1.assert_equal(performance_metrics_drift2.dataset1)
+    performance_metrics_drift1.dataset2.assert_equal(performance_metrics_drift2.dataset2)
diff --git a/src/cinnamon/drift/model_drift_explainer.py b/src/cinnamon/drift/model_drift_explainer.py
index 70fc989..7795cbe 100644
--- a/src/cinnamon/drift/model_drift_explainer.py
+++ b/src/cinnamon/drift/model_drift_explainer.py
@@ -1,19 +1,22 @@
-import logging
 import numpy as np
 import pandas as pd
 from typing import List, Tuple
+from scipy.stats import wasserstein_distance
 
 from .abstract_drift_explainer import AbstractDriftExplainer
 from ..model_parser.i_model_parser import IModelParser
 from .adversarial_drift_explainer import AdversarialDriftExplainer
 from ..model_parser.xgboost_parser import XGBoostParser
+from ..model_parser.catboost_parser import CatBoostParser
 
-from .drift_utils import compute_drift_num, plot_drift_num
+from .drift_utils import (compute_drift_num, plot_drift_num,
+                          DriftMetricsNum, PerformanceMetricsDrift)
 from ..common.dev_utils import safe_isinstance
-from ..common.math_utils import (compute_classification_metrics,
+from ..common.stat_utils import (compute_classification_metrics,
                                  compute_regression_metrics)
 from ..common.constants import TreeBasedDriftValueType
 
+
 class ModelDriftExplainer(AbstractDriftExplainer):
     """
     Tool to study data drift between two datasets, in a context where "model" is
@@ -98,6 +101,7 @@ def __init__(self, model, iteration_range: Tuple[int, int] = None):
         self.pred_proba1 = None
         self.pred_proba2 = None
         self._prediction_dim = None
+        self._tree_based_drift_values_sum_check = False
 
     def fit(self, X1: pd.DataFrame, X2: pd.DataFrame, y1: np.array=None, y2: np.array= None,
             sample_weights1: np.array = None, sample_weights2: np.array = None):
@@ -158,7 +162,7 @@ def fit(self, X1: pd.DataFrame, X2: pd.DataFrame, y1: np.array=None, y2: np.arra
 
         return self
 
-    def get_prediction_drift(self, prediction_type: str = "raw") -> List[dict]:
+    def get_prediction_drift(self, prediction_type: str = "raw") -> List[DriftMetricsNum]:
         """
         Compute drift measures based on model predictions.
 
@@ -176,7 +180,7 @@ def get_prediction_drift(self, prediction_type: str = "raw") -> List[dict]:
 
         Returns
         -------
-        prediction_drift : list of dict
+        prediction_drift : list of DriftMetricsNum object
             Drift measures for each predicted dimension.
         """
         if prediction_type not in ['raw', 'proba']:
@@ -189,7 +193,8 @@ def get_prediction_drift(self, prediction_type: str = "raw") -> List[dict]:
                                                   self.sample_weights1, self.sample_weights2)
 
     @staticmethod
-    def _compute_prediction_drift(predictions1, predictions2, task, prediction_dim, sample_weights1=None, sample_weights2=None):
+    def _compute_prediction_drift(predictions1, predictions2, task, prediction_dim,
+                                  sample_weights1=None, sample_weights2=None) -> List[DriftMetricsNum]:
         prediction_drift = []
         if task == 'classification':
             if prediction_dim == 1:  # binary classif
@@ -247,7 +252,7 @@ def plot_prediction_drift(self, prediction_type='raw', bins: int = 10,
             plot_drift_num(pred1, pred2, self.sample_weights1, self.sample_weights2, title=f'Predictions',
                            figsize=figsize, bins=bins)
 
-    def get_performance_metrics_drift(self) -> dict:
+    def get_performance_metrics_drift(self) -> PerformanceMetricsDrift:
         """
         Compute performance metrics on dataset 1 and dataset 2.
 
@@ -258,11 +263,15 @@ def get_performance_metrics_drift(self) -> dict:
         if self.y1 is None or self.y2 is None:
             self._raise_no_target_error()
         if self.task == 'classification':
-            return {'dataset 1': compute_classification_metrics(self.y1, self.pred_proba1, self.sample_weights1),
-                    'dataset 2': compute_classification_metrics(self.y2, self.pred_proba2, self.sample_weights2)}
+            return PerformanceMetricsDrift(dataset1=compute_classification_metrics(self.y1, self.pred_proba1,
+                                                                                   self.sample_weights1, self.class_names),
+                                           dataset2=compute_classification_metrics(self.y2, self.pred_proba2,
+                                                                                   self.sample_weights2, self.class_names))
         elif self.task == 'regression':
-            return {'dataset 1': compute_regression_metrics(self.y1, self.predictions1, self.sample_weights1),
-                    'dataset 2': compute_regression_metrics(self.y2, self.predictions2, self.sample_weights2)}
+            return PerformanceMetricsDrift(dataset1=compute_regression_metrics(self.y1, self.predictions1,
+                                                                               self.sample_weights1),
+                                           dataset2=compute_regression_metrics(self.y2, self.predictions2,
+                                                                               self.sample_weights2))
         else:  # ranking
             raise NotImplementedError('No metrics currently implemented for ranking model')
 
@@ -288,9 +297,13 @@ def get_tree_based_drift_values(self, type: str = TreeBasedDriftValueType.NODE_S
         -------
         drift_values : numpy array
         """
+        if not self._tree_based_drift_values_sum_check:
+            self._model_parser.check_tree_based_drift_values_sum(self.X1, self.X2, self.sample_weights1,
+                                                                  self.sample_weights2)
+            self._tree_based_drift_values_sum_check = True
         return self._model_parser.compute_tree_based_drift_values(type)
 
-    def plot_tree_based_drift_values(self, n: int = 10, type: str = TreeBasedDriftValueType.NODE_SIZE.value):
+    def plot_tree_based_drift_values(self, n: int = 10, type: str = TreeBasedDriftValueType.NODE_SIZE.value) -> None:
         """
         Plot drift values computed using the tree structures present in the model.
 
@@ -343,14 +356,14 @@ def _compute_prediction_based_drift_values(self, X1: pd.DataFrame, X2: pd.DataFr
             drift_values.append(drift_value)
         return np.array(drift_values).reshape(-1, 1)
 
-    def plot_prediction_based_drift_values(self, n: int = 10):
+    def plot_prediction_based_drift_values(self, n: int = 10) -> None:
         """Not implemented"""
         self._raise_not_implem_feature_error()
         drift_values = self.get_prediction_based_drift_values()
         print(drift_values)
         self._plot_drift_values(drift_values, n, self.feature_names)
 
-    def plot_tree_drift(self, tree_idx: int, type: str = TreeBasedDriftValueType.NODE_SIZE.value):
+    def plot_tree_drift(self, tree_idx: int, type: str = TreeBasedDriftValueType.NODE_SIZE.value) -> None:
         """
         Plot the representation of a given tree in the model, to illustrate how
         drift values are computed.
@@ -376,7 +389,7 @@ def plot_tree_drift(self, tree_idx: int, type: str = TreeBasedDriftValueType.NOD
         -------
         None
         """
-        return self._model_parser.plot_tree_drift(tree_idx, type, self.feature_names)
+        self._model_parser.plot_tree_drift(tree_idx, type, self.feature_names)
 
     def get_tree_based_correction_weights(self, max_depth: int = None, max_ratio: int = 10) -> np.array:
         """
@@ -406,7 +419,7 @@ def get_tree_based_correction_weights(self, max_depth: int = None, max_ratio: in
         return self._model_parser.compute_tree_based_correction_weights(self.X1, max_depth, max_ratio, self.sample_weights1)
 
     @staticmethod
-    def _get_n_features(model_parser: IModelParser):
+    def _get_n_features(model_parser: IModelParser) -> int:
         return model_parser.n_features
 
     @staticmethod
@@ -457,10 +470,13 @@ def _parse_model(self, model, iteration_range):
         elif safe_isinstance(model, 'xgboost.sklearn.XGBRanker'):
             self._model_parser: IModelParser = XGBoostParser(model.get_booster(), 'xgboost.sklearn.XGBRanker',
                                                             iteration_range)
-# because of unresolved pbms with CatBoost().calc_leaf_indexes(), CatBoost is not supported in CinnaMon
-#        elif safe_isinstance(model, 'catboost.core.CatBoostClassifier'):
-#            self._model_parser: IModelParser = CatBoostParser(model, 'catboost.core.CatBoostClassifier',
-#                                                             iteration_range, task='classification')
+        # TODO: because of unresolved pbms with CatBoost().calc_leaf_indexes(), CatBoost is not supported in CinnaMon
+        elif safe_isinstance(model, 'catboost.core.CatBoostClassifier'):
+            self._model_parser: IModelParser = CatBoostParser(model, 'catboost.core.CatBoostClassifier',
+                                                             iteration_range, task='classification')
+        elif safe_isinstance(model, 'catboost.core.CatBoostRegressor'):
+            self._model_parser: IModelParser = CatBoostParser(model, 'catboost.core.CatBoostRegressor',
+                                                              iteration_range, task='regression')
         else:
             raise TypeError(f'The type of model {type(model).__name__} is not supported in ModelDriftExplainer. Only '
                             f'XGBoost is supported currently. Support for other tree based algorithms and model '
diff --git a/src/cinnamon/drift/output_drift_detector.py b/src/cinnamon/drift/output_drift_detector.py
new file mode 100644
index 0000000..6e415ce
--- /dev/null
+++ b/src/cinnamon/drift/output_drift_detector.py
@@ -0,0 +1,349 @@
+import numpy as np
+from .drift_utils import AbstractDriftMetrics, compute_drift_num, compute_drift_cat, plot_drift_num, plot_drift_cat
+from typing import List, Tuple
+from ..common.logging import cinnamon_logger
+from ..common.dev_utils import find_uniques
+from ..common.stat_utils import (compute_regression_metrics, compute_classification_metrics,
+                                 RegressionMetrics, ClassificationMetrics)
+from .drift_utils import PerformanceMetricsDrift
+
+
+class OutputDriftDetector:
+
+    logger = cinnamon_logger.getChild('OutputDriftDetector')
+
+    def __init__(self, task: str, prediction_type: str = None, class_names: List[str] = None):
+        '''
+        :param task: 'regression' or 'classification'
+        :param prediction_type: for task=classification only ('raw', 'proba', or 'label'). label for the case where the user provides
+        predicted labels and not probabilities (or equivalent). In the regression mode, it will be set to "raw" by default.
+        :param class_names: leave None if regression. If classification, labels will based on "y" and "predictions"
+        passed to fit, and sorted in alphanumeric order.
+        '''
+        self.predictions1 = None
+        self.predictions2 = None
+        self.y1 = None
+        self.y2 = None
+        self.sample_weights1 = None
+        self.sample_weights2 = None
+        self._prediction_dim = None
+        self.task = self._check_input_task(task)
+        self.prediction_type = self._check_input_prediction_type(prediction_type, self.task)
+        self.class_names = self._check_input_class_names(class_names)
+
+    def fit(self, predictions1: np.array, predictions2: np.array, y1: np.array = None, y2: np.array = None,
+            sample_weights1: np.array = None, sample_weights2: np.array = None):
+
+        # faire les checks ici
+        # if dim == 1, should be of dim (n, ),
+        # else, (n, n_class)
+        #
+        # rmk on parameter : on classif, we only accept pred_proba and raw predictions (not predicted label with threshold)
+
+        # y : we accept ground truth labels only (OHE labels are not accepted). GT labels can be str or int.
+        #
+        self.predictions1 = self._check_input_predictions(predictions1, 1, self.task, self.prediction_type)
+        self.predictions2 = self._check_input_predictions(predictions2, 2, self.task, self.prediction_type)
+        self.y1 = self._check_input_y(y1, 1, self.task, self.prediction_type)
+        self.y2 = self._check_input_y(y2, 2, self.task, self.prediction_type)
+        self.sample_weights1 = self._check_input_sample_weights(sample_weights1, 1, self.predictions1.shape[0])
+        self.sample_weights2 = self._check_input_sample_weights(sample_weights2, 2, self.predictions2.shape[0])
+        self._check_inputs_consistency()
+        self._prediction_dim = self._get_prediction_dim(self.predictions1, self.predictions2, self.y1, self.y2,
+                                                        task=self.task, prediction_type=self.prediction_type)
+        self.class_names = self._get_class_names(self.predictions1, self.predictions2, self.y1, self.y2, self.task,
+                                                 self.prediction_type, self._prediction_dim, self.class_names)
+
+    @staticmethod
+    def _check_input_task(task: str) -> str:
+        if task not in ['regression', 'classification']:
+            raise ValueError(f'Bad value for "task"')
+        else:
+            return task
+
+    @staticmethod
+    def _check_input_prediction_type(prediction_type: str, task: str) -> str:
+        if task == 'classification':
+            if prediction_type is None:
+                raise ValueError(f'When task == "classification", "prediction_type" parameter should be specified')
+            if prediction_type not in ['raw', 'proba', 'label']:
+                raise ValueError(f'Bad value for "prediction_type"')
+            else:
+                return prediction_type
+        else:  # task == 'regression'
+            if prediction_type is not None:
+                OutputDriftDetector.logger.warning('Provided value for "prediction_type" is override to "raw"'
+                                                    'because task == "regression"')
+            return 'raw'
+
+    @staticmethod
+    def _check_input_class_names(class_names: List[str]) -> List[str]:
+        if class_names is None:
+            return None
+        else:
+            return [str(x) for x in class_names]
+
+    @staticmethod
+    def _check_input_predictions(predictions: np.array, n: int, task: str, prediction_type: str) -> np.array:
+        predictions = np.array(np.squeeze(predictions))
+        if task == 'regression':
+            if predictions.ndim != 1:
+                raise ValueError(f'Bad shape for "predictions{n}"')
+        else:  # task == 'classification':
+            if prediction_type in ['raw', 'proba']:
+                if predictions.ndim not in [1, 2]:
+                    raise ValueError(f'Bad shape for "predictions{n}"')
+                if predictions.ndim == 2 and predictions.shape[1] == 2:
+                    predictions = predictions[:, 1]
+            else:  # prediction_type == 'label'
+                if predictions.ndim != 1:
+                    raise ValueError(f'Bad shape for "predictions{n}"')
+        return predictions
+
+    @staticmethod
+    def _check_input_y(y: np.array, n: int, task: str, prediction_type: str) -> np.array:
+        if y is None:
+            return None
+        y = np.squeeze(y)
+        if task == 'regression':
+            if y.ndim != 1:
+                raise ValueError(f'Bad shape for "y{n}"')
+        else:  # task == 'classification':
+            if prediction_type in ['raw', 'proba']:
+                if y.ndim != 1:
+                    raise ValueError(f'Bad shape for "y{n}"')
+            else:  # prediction_type == 'label'
+                if y.ndim != 1:
+                    raise ValueError(f'Bad shape for "y{n}"')
+        return y
+
+    @staticmethod
+    def _check_input_sample_weights(sample_weights: np.array, n:int, expected_length: int) -> np.array:
+        if sample_weights is None:
+            return np.ones(expected_length)
+        sample_weights = np.squeeze(sample_weights)
+        if sample_weights.ndim != 1:
+            raise ValueError(f'Bad shape for "sample_weights{n}"')
+        elif sample_weights.shape[0] != expected_length:
+            raise ValueError(f'"sample_weights{n}" and "predictions{n}" shapes are not consistent')
+        elif np.any(sample_weights < 0):
+            raise ValueError(f'Elements in "sample_weights{n}" must be non negative')
+        elif np.sum(sample_weights) == 0:
+            raise ValueError(f'The sum of "sample_weights{n}" must be positive')
+        else:
+            return sample_weights * len(sample_weights) / np.sum(sample_weights)
+
+    def _check_inputs_consistency(self):
+        # check consistency between y1 == None and y2 == None
+        if (self.y1 is None) != (self.y2 is None):  # xor operation
+            raise ValueError(f'y1 and y2 should be either both None or non None')
+
+        # check consistency between predictions1 and predictions2 dimensions
+        if self.predictions1.ndim == 2 and self.predictions2.ndim == 2:
+            if self.predictions1.shape[1] != self.predictions2.shape[1]:
+                raise ValueError(f'Not the same shape for "predictions1" and "predictions2"')
+
+        # check consistency between class_names and prediction dimensions
+        if self.class_names is not None:
+            # case of binary classif
+            if self.predictions1.ndim == 1:
+                if len(self.class_names) != 2:
+                    raise ValueError(f'"predictions" inputs indicates a binary classification but "class_names"'
+                                     f'do not have 2 elements')
+            # case of multiclass classif
+            elif len(self.class_names) != self.predictions1.shape[1]:
+                raise ValueError(f'Number of elements in "class_names" do not match with number of dimensions in'
+                                 f'"predictions"')
+
+    @staticmethod
+    def _get_prediction_dim(predictions1: np.array, predictions2: np.array, y1: np.array, y2: np.array,
+                            task: str, prediction_type: str) -> int:
+        if task == 'regression':
+            return 1
+        else:  # task == 'classification'
+            if prediction_type in ['raw', 'proba']:
+                if predictions1.ndim == 1:  # binary classif
+                    return 1
+                else:  # multiclass classif
+                    return predictions1.shape[1]
+            else:  # prediction_type == 'label'
+                return len(find_uniques(predictions1, predictions2, y1, y2))
+
+    @staticmethod
+    def _get_class_names(predictions1: np.array, predictions2: np.array, y1: np.array, y2: np.array, task: str,
+                         prediction_type: str, prediction_dim: int, class_names: List[str]) -> List[str]:
+        if task == 'regression':
+            return None
+        else:  # task == 'classification
+            if prediction_type in ['raw', 'proba']:
+                if class_names is not None:
+                    return class_names
+                elif y1 is not None:
+                    return [str(x) for x in sorted(find_uniques(y1, y2))]
+                else:  # prediction_type == 'label'
+                    n_class = 2 if prediction_dim == 1 else prediction_dim
+                    return [str(i) for i in range(n_class)]
+            else:
+                if class_names is not None:
+                    return class_names
+                else:
+                    return [str(x) for x in sorted(find_uniques(predictions1, predictions2, y1, y2))]
+
+    def get_prediction_drift(self) -> List[AbstractDriftMetrics]:
+        """
+        Compute drift measures based on predictions.
+
+        See the documentation in README for explanations about how it is computed,
+        especially the slide presentation.
+
+        Returns
+        -------
+        prediction_drift : list of dict
+            Drift measures for each predicted dimension.
+        """
+        self._raise_no_fit_error()
+        return self._compute_prediction_drift(self.predictions1, self.predictions2, self.prediction_type,
+                                              self._prediction_dim, self.sample_weights1, self.sample_weights2)
+
+    @staticmethod
+    def _compute_prediction_drift(predictions1, predictions2, prediction_type, prediction_dim, sample_weights1, sample_weights2):
+        prediction_drift = []
+        if prediction_type in ['raw', 'proba']:
+            if prediction_dim == 1:
+                prediction_drift.append(compute_drift_num(predictions1, predictions2, sample_weights1, sample_weights2))
+            else:  # multiclass classif
+                for i in range(prediction_dim):
+                    drift = compute_drift_num(predictions1[:, i], predictions2[:, i],
+                                              sample_weights1, sample_weights2)
+                    prediction_drift.append(drift)
+        else:  # prediction_type == "label"
+            prediction_drift.append(compute_drift_cat(predictions1, predictions2, sample_weights1, sample_weights2))
+        return prediction_drift
+
+    def plot_prediction_drift(self, bins: int = 10, figsize: Tuple[int, int] = (7, 5),
+                              max_n_cat: int = 20) -> None:
+        """
+        Plot histogram of distribution of predictions1 and predictions2
+        in order to visualize a potential drift of the predicted values.
+        See the documentation in README for explanations about how it is computed,
+        especially the slide presentation.
+
+        Parameters
+        ----------
+        bins : int (default=100)
+            "bins" parameter passed to matplotlib.pyplot.hist function.
+
+        figsize : Tuple[int, int], optional (default=(7, 5))
+            Graphic size passed to matplotlib
+
+        max_n_cat : int (default=20)
+            For multiclass classification only. Maximum number of classes to
+            represent on the plot.
+        Returns
+        -------
+        None
+        """
+        self._raise_no_fit_error()
+        if self.prediction_type in ['raw', 'proba']:
+            if self._prediction_dim == 1:  # regression or binary classif
+                plot_drift_num(self.predictions1, self.predictions2, self.sample_weights1, self.sample_weights2,
+                               title=f'Predictions', figsize=figsize, bins=bins)
+            else:  # multiclass classif
+                for i in range(self._prediction_dim):
+                    plot_drift_num(self.predictions1[:, i], self.predictions2[:, i], self.sample_weights1,
+                                   self.sample_weights2, title=f'{self.class_names[i]}', figsize=figsize, bins=bins)
+        if self.prediction_type == 'label':
+            plot_drift_cat(self.predictions1, self.predictions2, self.sample_weights1, self.sample_weights2,
+                           title=f'Predictions', figsize=figsize, max_n_cat=max_n_cat)
+
+    def _raise_no_fit_error(self):
+        if self.predictions1 is None or self.predictions2 is None:
+            raise ValueError('You have to call the fit method first')
+
+    def get_target_drift(self) -> AbstractDriftMetrics:
+        """
+        Compute drift measures for the labels y.
+
+        For classification :
+        - Wasserstein distance
+        - Result of Chi2 2 sample test
+
+        For regression:
+        - Difference of means
+        - Wasserstein distance
+        - Result of Kolmogorov 2 sample test
+
+        See the documentation in README for explanations about how it is computed,
+        especially the slide presentation.
+
+        Returns
+        -------
+        target_drift : dict
+            Dictionary of drift measures for the labels
+        """
+        if self.y1 is None:  # then y2 is also None
+            self._raise_no_target_error()
+        else:
+            if self.task == 'classification':
+                return compute_drift_cat(self.y1, self.y2, self.sample_weights1, self.sample_weights2)
+            elif self.task in ['regression', 'ranking']:
+                return compute_drift_num(self.y1, self.y2, self.sample_weights1, self.sample_weights2)
+
+    @staticmethod
+    def _raise_no_target_error():
+        # TODO: put this in AbstractDriftAnalyzer
+        raise ValueError('Either y1 or y2 was not passed to "fit"')
+
+    def plot_target_drift(self, max_n_cat: int = 20, figsize: Tuple[int, int] = (7, 5), bins: int = 10) -> None:
+        # TODO: put this in AbstractDriftAnalyzer
+        """
+        Plot distributions of labels in order to
+        visualize a potential drift of the target labels.
+
+        Parameters
+        ----------
+        max_n_cat : int (default=20)
+            For multiclass classification only. Maximum number of classes to
+            represent on the plot.
+
+        bins : int (default=100)
+            For regression only. "bins" parameter passed to matplotlib.pyplot.hist function.
+
+        figsize : Tuple[int, int] (default=(7, 5))
+            Graphic size passed to matplotlib
+
+        Returns
+        -------
+        None
+        """
+        if self.y1 is None:  # then y2 is also None
+            self._raise_no_target_error()
+        if self.task == 'classification':
+            plot_drift_cat(self.y1, self.y2, self.sample_weights1, self.sample_weights2, title='target',
+                           max_n_cat=max_n_cat, figsize=figsize)
+        elif self.task == 'regression':
+            plot_drift_num(self.y1, self.y2, self.sample_weights1, self.sample_weights2, title='target',
+                           figsize=figsize, bins=bins)
+
+    def get_performance_metrics_drift(self) -> PerformanceMetricsDrift:
+        """
+        Compute performance metrics on dataset 1 and dataset 2.
+
+        Returns
+        -------
+        Dictionary of performance metrics
+        """
+        if self.y1 is None or self.y2 is None:
+            self._raise_no_target_error()
+        if self.task == 'regression':
+            return PerformanceMetricsDrift(dataset1=compute_regression_metrics(self.y1, self.predictions1,
+                                                                               self.sample_weights1),
+                                           dataset2=compute_regression_metrics(self.y2, self.predictions2,
+                                                                               self.sample_weights2))
+        else:  # task == 'classification':
+            return PerformanceMetricsDrift(dataset1=compute_classification_metrics(self.y1, self.predictions1,
+                                                                                   self.sample_weights1, self.class_names,
+                                                                                   self.prediction_type),
+                                           dataset2=compute_classification_metrics(self.y2, self.predictions2,
+                                                                                   self.sample_weights2, self.class_names,
+                                                                                   self.prediction_type))
diff --git a/src/cinnamon/model_parser/abstract_tree_ensemble_parser.py b/src/cinnamon/model_parser/abstract_tree_ensemble_parser.py
index 17bb68a..3e60761 100644
--- a/src/cinnamon/model_parser/abstract_tree_ensemble_parser.py
+++ b/src/cinnamon/model_parser/abstract_tree_ensemble_parser.py
@@ -1,14 +1,18 @@
 from .i_tree_ensemble_parser import ITreeEnsembleParser
-from typing import List
+from typing import List, Tuple
 import pandas as pd
 import numpy as np
 
 from .single_tree import BinaryTree
 from ..common.math_utils import threshold
 from ..common.constants import TreeBasedDriftValueType
+from ..common.logging import cinnamon_logger
+
 
 class AbstractTreeEnsembleParser(ITreeEnsembleParser):
 
+    logger = cinnamon_logger.getChild('TreeEnsembleParser')
+
     def __init__(self, model, model_type, iteration_range):
         super().__init__()
         self.original_model = model
@@ -16,11 +20,11 @@ def __init__(self, model, model_type, iteration_range):
         self.iteration_range = iteration_range
         self.trees = None
         self.parse(iteration_range)
+        self.original_model_total_iterations = None
 
     def fit(self, X1, X2, sample_weights1, sample_weights2):
         self.node_weights1 = self.get_node_weights(X1, sample_weights=sample_weights1)
         self.node_weights2 = self.get_node_weights(X2, sample_weights=sample_weights2)
-        #self._check_drift_values_mean(X1, X2, sample_weights1, sample_weights2)
 
     def get_predictions(self, X: pd.DataFrame, prediction_type: str) -> np.array:
         # return array of shape (nb. obs, nb. class) for multiclass and shape array of shape (nb. obs, )
@@ -68,11 +72,18 @@ def get_node_weights(self, X: pd.DataFrame, sample_weights: np.array) -> List[np
         return node_weights
 
     @staticmethod
-    def _get_iteration_range(iteration_range, initial_n_trees):
+    def _get_iteration_range(iteration_range: Tuple[int, int], original_model_total_iterations: int,
+                             best_iteration: int = None):
         if iteration_range is None:
-            iteration_range = (0, initial_n_trees)
-        elif iteration_range[1] > initial_n_trees:
-            raise ValueError(f'"iteration_range" values exceeds {initial_n_trees} which is the number of trees in the model')
+            if best_iteration is not None:
+                iteration_range = (0, best_iteration)
+                AbstractTreeEnsembleParser.logger.warning('By default, the best iteration given by early stopping is used '
+                                                          'to compute "iteration_range". This behavior is consistent with '
+                                                          'model.predict XGBoost default behavior.')
+            else:
+                iteration_range = (0, original_model_total_iterations)
+        elif iteration_range[1] > original_model_total_iterations:
+            raise ValueError(f'"iteration_range" values exceeds the total number of trees in the model')
         else:
             pass
         return iteration_range
@@ -85,7 +96,7 @@ def _check_parsing_with_leaf_predictions(self, X):
         if not np.array_equal(self.predict_leaf_with_model_parser(X), self.predict_leaf(X)):
             self._model_parser_error()
 
-    def _check_drift_values_mean(self, X1, X2, sample_weights1, sample_weights2):
+    def check_tree_based_drift_values_sum(self, X1, X2, sample_weights1, sample_weights2) -> None:
         sample_weights1_norm = sample_weights1 / np.sum(sample_weights1)
         sample_weights2_norm = sample_weights2 / np.sum(sample_weights2)
         if self.prediction_dim == 1:
@@ -95,8 +106,9 @@ def _check_drift_values_mean(self, X1, X2, sample_weights1, sample_weights2):
             mean_prediction_diff = np.sum(sample_weights2_norm[:, np.newaxis] * self.predict_raw(X2), axis=0) - \
                                    np.sum(sample_weights1_norm[:, np.newaxis] * self.predict_raw(X1), axis=0)
         stat = abs(self.compute_tree_based_drift_values(type=TreeBasedDriftValueType.MEAN.value).sum(axis=0) - mean_prediction_diff)
-        if any(stat > 10**(-3)):  # any works because difference is an array
-            raise ValueError('Error in computation of feature contributions')
+        if any(stat > 10**(-6)):  # any works because difference is an array
+            raise ValueError('Error in computation of tree based drift values. Your model may not be properly parsed '
+                             'by CinnaMon. You can report the error here: https://github.com/zelros/cinnamon/issues')
 
     def compute_tree_based_drift_values(self, type: str):
         """
@@ -117,12 +129,12 @@ def compute_tree_based_drift_values(self, type: str):
         for i, tree in enumerate(self.trees):
             drift_values_tree = tree.compute_drift_values(self.node_weights1[i], self.node_weights2[i],
                                                                   type=type)
-            drift_values = self.add_drift_values(drift_values, drift_values_tree, i,
+            drift_values = self._add_drift_values(drift_values, drift_values_tree, i,
                                                          self.prediction_dim, type)
             drift_values_details.append(drift_values_tree)
         return drift_values  #, drift_values_details
 
-    def plot_tree_drift(self, tree_idx: int, type: str, feature_names: List[str]):
+    def plot_tree_drift(self, tree_idx: int, type: str, feature_names: List[str]) -> None:
         if self.node_weights1 is None:
             raise ValueError('You need to run drift_explainer.fit before calling plot_tree_drift')
         if type not in [e.value for e in TreeBasedDriftValueType]:
@@ -167,7 +179,7 @@ def _get_weights_tree(tree: BinaryTree, predicted_leaves: np.array, node_weights
         return weights
 
     @staticmethod
-    def add_drift_values(drift_values, drift_values_tree, i, prediction_dim, type):
+    def _add_drift_values(drift_values, drift_values_tree, i, prediction_dim, type):
         drift_values += drift_values_tree
         return drift_values
 
diff --git a/src/cinnamon/model_parser/catboost_parser.py b/src/cinnamon/model_parser/catboost_parser.py
index 0ea4c78..b5dcb58 100644
--- a/src/cinnamon/model_parser/catboost_parser.py
+++ b/src/cinnamon/model_parser/catboost_parser.py
@@ -6,6 +6,7 @@
 import tempfile
 from typing import Tuple
 from .abstract_tree_ensemble_parser import AbstractTreeEnsembleParser
+from ..common.math_utils import reverse_binary_representation
 
 
 class CatBoostParser(AbstractTreeEnsembleParser):
@@ -27,10 +28,9 @@ def parse(self, iteration_range: Tuple[int, int] = None):
         self.json_cb_model = json.load(open(tmp_file.name, "r"))
         tmp_file.close()
 
-        self.iteration_range = self._get_iteration_range(iteration_range, len(self.json_cb_model['oblivious_trees']))
+        self.original_model_total_iterations = len(self.json_cb_model['oblivious_trees'])
+        self.iteration_range = self._get_iteration_range(iteration_range, self.original_model_total_iterations)
         self.n_iterations = self.iteration_range[1] - self.iteration_range[0]  # corresponds to n trees after iteration_range
-        self.n_trees = self.n_iterations
-        self.trees = self._get_trees(self.json_cb_model, self.iteration_range, self.original_model.n_features_in_)
 
         # load the CatBoost oblivious trees specific parameters
         self.model_objective = self.json_cb_model['model_info']['params']['loss_function']['type']
@@ -40,7 +40,7 @@ def parse(self, iteration_range: Tuple[int, int] = None):
         self.cat_feature_indices = self.original_model.get_cat_feature_indices()
         self.feature_names = self.original_model.feature_names_
         self.n_features = len(self.feature_names)
-        self.class_names = self.original_model.classes_.tolist()
+        self.class_names = [str(x) for x in self.original_model.classes_.tolist()]
         if self.class_names is not None and len(self.class_names) > 2:  # if multiclass
             self.prediction_dim = len(self.class_names)
         else:
@@ -55,12 +55,22 @@ def parse(self, iteration_range: Tuple[int, int] = None):
             self.cat_label_map = {feat['feature_index']: feat['values'] if 'values' in feat else None
                                   for feat in self.json_cb_model['features_info']['categorical_features']}
 
+        self.n_trees = self.n_iterations
+        self.trees = self._get_trees(self.json_cb_model, self.iteration_range, self.n_features)
+
     def _get_trees(self, json_cb_model, iteration_range, n_features):
         # load all trees
         trees = []
         for tree_index in range(iteration_range[0], iteration_range[1]):
             # leaf weights
             leaf_weights = json_cb_model['oblivious_trees'][tree_index]['leaf_weights']
+            # ------------------------------------------------------------------------------------------------
+            # BugFix : correcting the way CatBoost numbers the predicted leaves (with order of splits inversed)
+            n_bits = int(np.log2(len(leaf_weights)))
+            mapping = [reverse_binary_representation(x, n_bits) for x in range(2**n_bits)]
+            # ------------------------------------------------------------------------------------------------
+
+            leaf_weights = [leaf_weights[mapping[i]] for i in range(len(leaf_weights))]
             # re-compute the number of samples that pass through each node
             leaf_weights_unraveled = [0] * (len(leaf_weights) - 1) + leaf_weights
             for index in range(len(leaf_weights) - 2, -1, -1):
@@ -71,9 +81,11 @@ def _get_trees(self, json_cb_model, iteration_range, n_features):
             # leaf values = log softmax if multiclass classification
             leaf_values = json_cb_model['oblivious_trees'][tree_index]['leaf_values']
             n_class = int(len(leaf_values) / len(leaf_weights))
+
             # re-compute leaf values within each node
             leaf_values_unraveled = np.concatenate((np.zeros((len(leaf_weights) - 1, n_class)),
-                                                    np.array(leaf_values).reshape(len(leaf_weights), n_class)), axis=0)
+                                                    np.array(leaf_values).reshape(len(leaf_weights), n_class)[mapping]),
+                                                   axis=0)
             for index in range(len(leaf_weights) - 2, -1, -1):
                 if leaf_weights_unraveled[2 * index + 1] + leaf_weights_unraveled[2 * index + 2] == 0:
                     leaf_values_unraveled[index, :] = [-1] * n_class  # equal probabilities
@@ -107,7 +119,7 @@ def _get_trees(self, json_cb_model, iteration_range, n_features):
                     split_features_index.append(json_cb_model['features_info']['categorical_features'][elem.get('cat_feature_index')]['flat_feature_index'])
                     borders.append(elem['value'])
                 elif split_type == 'OnlineCtr':
-                    # FIXME: afraid of the 0 bellow there could be more than 1 element ?
+                    # TODO: only the first feature in the list is considered here (multiple feature
                     corresponding_cat_index = json_cb_model['features_info']['ctrs'][elem.get('ctr_target_border_idx')]['elements'][0]['cat_feature_index']
                     split_features_index.append(self.cat_feature_index_map[corresponding_cat_index])
                     borders.append(elem['border'])
@@ -148,10 +160,20 @@ def predict_leaf(self, X: pd.DataFrame):
         predicted_leaves = self.original_model.calc_leaf_indexes(pool,
                                                      ntree_start=self.iteration_range[0],
                                                      ntree_end=self.iteration_range[1])
+        # ------------------------------------------------------------------------------------------------
+        # BugFix : correcting the way CatBoost numbers the predicted leaves (with order of splits inversed)
+        for i in range(predicted_leaves.shape[1]):
+            n_leaves = int((self.trees[i].n_nodes + 1)/2)
+            n_bits = int(np.log2(n_leaves))
+            # numerical optimization: precompute all values
+            mapping = [reverse_binary_representation(x, n_bits) for x in range(2**n_bits)]
+            predicted_leaves[:, i] = np.array([mapping[x] for x in predicted_leaves[:, i]])
+        # ------------------------------------------------------------------------------------------------
+
         # By default catboost numbers leaves from 0 to n_leaves - 1, hence we need to add the number of nodes
         # The number we had may be different depending on the depth of the tree
         for i in range(predicted_leaves.shape[1]):
-            # this assumes that trees are perfect binary trees (which is default inn CatBoost)
+            # this assumes that trees are perfect binary trees (which is default in CatBoost)
             predicted_leaves[:, i] += int((self.trees[i].n_nodes - 1)/2)
         return predicted_leaves
 
diff --git a/src/cinnamon/model_parser/i_model_parser.py b/src/cinnamon/model_parser/i_model_parser.py
index 11c218b..1b91fcc 100644
--- a/src/cinnamon/model_parser/i_model_parser.py
+++ b/src/cinnamon/model_parser/i_model_parser.py
@@ -34,5 +34,8 @@ def compute_tree_based_correction_weights(self, X1: pd.DataFrame, max_depth: int
                                               sample_weights1: np.array) -> np.array:
         pass
 
-    def plot_tree_drift(self, tree_idx: int, type: str, feature_names: List[str]):
+    def plot_tree_drift(self, tree_idx: int, type: str, feature_names: List[str]) -> None:
+        pass
+
+    def check_tree_based_drift_values_sum(self, X1, X2, sample_weights1, sample_weights2) -> None:
         pass
diff --git a/src/cinnamon/model_parser/single_tree.py b/src/cinnamon/model_parser/single_tree.py
index 049c3f4..650a06a 100644
--- a/src/cinnamon/model_parser/single_tree.py
+++ b/src/cinnamon/model_parser/single_tree.py
@@ -53,8 +53,6 @@ def _compute_split_contribs_mean(self, node_weight_fractions1, node_weight_fract
         for i in range(self.n_nodes):
             if self.children_left[i] == -1:  # if leaf
                 continue  # equal to 0 by default
-            if node_weight_fractions1[i] == 0 or node_weight_fractions2[i] == 0:
-                continue
             else:
                 mean_log_softmax1 = (node_weight_fractions1[self.children_left[i]] * self.values[self.children_left[i]] +
                                      node_weight_fractions1[self.children_right[i]] * self.values[self.children_right[i]] -
@@ -62,8 +60,6 @@ def _compute_split_contribs_mean(self, node_weight_fractions1, node_weight_fract
                 mean_log_softmax2 = (node_weight_fractions2[self.children_left[i]] * self.values[self.children_left[i]] +
                                      node_weight_fractions2[self.children_right[i]] * self.values[self.children_right[i]] -
                                      node_weight_fractions2[i] * self.values[i])
-                # the diff of mean_log_softmax2 and mean_log_softmax1 is weighted by the minimum of
-                # node_weight_fractions1[i] and node_weight_fractions2[i]
                 split_contribs[i] = mean_log_softmax2 - mean_log_softmax1
         return split_contribs
 
diff --git a/src/cinnamon/model_parser/model_parser.py b/src/cinnamon/model_parser/unknown_model_parser.py
similarity index 90%
rename from src/cinnamon/model_parser/model_parser.py
rename to src/cinnamon/model_parser/unknown_model_parser.py
index fb4c37b..f57b002 100644
--- a/src/cinnamon/model_parser/model_parser.py
+++ b/src/cinnamon/model_parser/unknown_model_parser.py
@@ -28,11 +28,14 @@ def compute_prediction_based_drift_values(self) -> np.array:
     def compute_tree_based_drift_values(self, type: str) -> np.array:
         self._not_tree_based_model_error()
 
+    def check_tree_based_drift_values_sum(self, X1, X2, sample_weights1, sample_weights2):
+        self._not_tree_based_model_error()
+
     def compute_tree_based_correction_weights(self, X1: pd.DataFrame, max_depth: int, max_ratio: int,
                                               sample_weights1: np.array) -> np.array:
         self._not_tree_based_model_error()
 
-    def plot_tree_drift(self, tree_idx: int, type: str, feature_names: List[str]):
+    def plot_tree_drift(self, tree_idx: int, type: str, feature_names: List[str]) -> None:
         self._not_tree_based_model_error()
 
     @staticmethod
diff --git a/src/cinnamon/model_parser/xgboost_parser.py b/src/cinnamon/model_parser/xgboost_parser.py
index 8e6e416..caf7e47 100644
--- a/src/cinnamon/model_parser/xgboost_parser.py
+++ b/src/cinnamon/model_parser/xgboost_parser.py
@@ -68,8 +68,16 @@ def parse(self, iteration_range: Tuple[int, int]):
             self.task = self.objective_task_map[self.model_objective]
         else:
             raise ValueError(f"{self.model_objective} objective for XGBoost model is not supported")
+
+        # handle iteration range
+        self.original_model_total_iterations = parsed_info['num_trees'] // self.prediction_dim
+        if self.original_model.best_ntree_limit != self.original_model_total_iterations:
+            best_iteration = self.original_model.best_ntree_limit
+        else:
+            best_iteration = None
         self.iteration_range = self._get_iteration_range(iteration_range,
-                                                         parsed_info['num_trees'] // self.prediction_dim)
+                                                         self.original_model_total_iterations,
+                                                         best_iteration)
         self.n_iterations = self.iteration_range[1] - self.iteration_range[0]
         # n_trees is not equal to n_iterations if multiclass
         self.n_trees = self.n_iterations * self.prediction_dim
@@ -268,7 +276,7 @@ def down(node_idx: int, i: int, tree: BinaryTree) -> int:
         return np.array(leaf_indexes, dtype=np.float32)
 
     @staticmethod
-    def add_drift_values(drift_values, drift_values_tree, i, prediction_dim, type):
+    def _add_drift_values(drift_values, drift_values_tree, i, prediction_dim, type):
         if type in [TreeBasedDriftValueType.MEAN.value,
                     TreeBasedDriftValueType.MEAN_NORM.value]:
             drift_values[:, (i % prediction_dim)] += drift_values_tree[:, 0]
diff --git a/src/cinnamon/report/drift_report_template.pmd b/src/cinnamon/report/drift_report_template.pmd
index 1c76f49..7611623 100644
--- a/src/cinnamon/report/drift_report_template.pmd
+++ b/src/cinnamon/report/drift_report_template.pmd
@@ -50,7 +50,7 @@ else:
             plt.show()
 
             print(f"Wasserstein distance: {drift_explainer.prediction_drift[i]['wasserstein']}")
-            print(f"Kolmogorov Smirnov test: {drift_explainer.prediction_drift[i]['kolmogorov_smirnov']}")
+            print(f"Kolmogorov Smirnov test: {drift_explainer.prediction_drift[i]['ks_test']}")
 ```
 
 # 2.2 Explanation of the drift: feature contributions
@@ -73,5 +73,5 @@ for i, feature_name in enumerate(drift_explainer.feature_names):
           print(f"Chi2 test: {drift_explainer.feature_drifts[i]['chi2_test']}")
     else:
         print(f"Wasserstein distance: {drift_explainer.feature_drifts[i]['wasserstein']}")
-        print(f"Kolmogorov Smirnov test: {drift_explainer.feature_drifts[i]['kolmogorov_smirnov']}")
+        print(f"Kolmogorov Smirnov test: {drift_explainer.feature_drifts[i]['ks_test']}")
 ```


Test patch:
diff --git a/src/cinnamon/drift/tests/__init__.py b/src/cinnamon/drift/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/cinnamon/drift/tests/test_adversarial_drift_explainer.py b/src/cinnamon/drift/tests/test_adversarial_drift_explainer.py
new file mode 100644
index 0000000..b201c77
--- /dev/null
+++ b/src/cinnamon/drift/tests/test_adversarial_drift_explainer.py
@@ -0,0 +1,121 @@
+import pandas as pd, numpy as np
+from numpy.testing import assert_allclose
+from sklearn import datasets
+from sklearn.model_selection import train_test_split
+from cinnamon.common.stat_utils import BaseStatisticalTestResult, Chi2TestResult
+from cinnamon.drift import AdversarialDriftExplainer
+from cinnamon.drift.drift_utils import (DriftMetricsCat, DriftMetricsNum,
+                                        assert_drift_metrics_equal,
+                                        assert_drift_metrics_list_equal)
+from ...common.constants import NUMPY_atol
+
+RANDOM_SEED = 2021
+
+
+def test_adversarial_drift_explainer():
+    dataset = datasets.load_iris()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = pd.Series(dataset.target).map({0: 'Iris-Setosa', 1: 'Iris-Versicolour', 2: 'Iris-Virginica'})
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    drift_explainer = AdversarialDriftExplainer(seed=RANDOM_SEED, verbosity=False).fit(X_train, X_test, y_train, y_test)
+
+    # adversarial_drift_values
+    assert_allclose(drift_explainer.get_adversarial_drift_values(),
+                    np.array([[0.27580956],
+                              [0.3008618],
+                              [0.18539894],
+                              [0.23792973]]),
+                    atol=NUMPY_atol)
+
+    # adversarial_correction_weights
+    assert_allclose(drift_explainer.get_adversarial_correction_weights(),
+                    np.array([0.90810081, 0.76698897, 0.99673733, 1.05017385, 1.26964731,
+                              0.76698897, 1.00623148, 1.11323871, 1.26964731, 0.95846368,
+                              1.28322436, 0.95846368, 0.93051401, 0.93051401, 0.69659946,
+                              0.69659946, 0.83154643, 0.69659946, 0.95846368, 0.95846368,
+                              1.11323871, 1.05017385, 1.26964731, 1.26964731, 0.90810081,
+                              1.05017385, 0.83154643, 0.69659946, 0.85965477, 0.99673733,
+                              1.26964731, 0.69659946, 0.69659946, 1.11323871, 1.00623148,
+                              1.00979752, 0.95846368, 1.26964731, 1.11323871, 1.01535569,
+                              1.13832889, 0.76698897, 1.01535569, 1.00979752, 1.11323871,
+                              1.26964731, 1.01535569, 0.83154643, 1.26964731, 1.11323871,
+                              1.13832889, 0.95846368, 0.69659946, 0.95846368, 0.76698897,
+                              0.90810081, 1.26964731, 0.95846368, 1.16355731, 1.26964731,
+                              1.00623148, 0.85965477, 1.05017385, 0.98884336, 1.00623148,
+                              0.93051401, 0.93051401, 1.26964731, 1.00623148, 1.26964731,
+                              1.11323871, 1.28322436, 0.93051401, 0.99673733, 1.26964731,
+                              0.69659946, 0.76698897, 1.26964731, 0.76698897, 1.00623148,
+                              0.83154643, 0.99673733, 1.26246993, 0.90810081, 1.13832889,
+                              1.05017385, 1.13832889, 1.11323871, 0.93051401, 1.05017385,
+                              0.69659946, 0.90810081, 1.00623148, 0.76698897, 1.11323871,
+                              1.05017385, 1.00623148, 0.95592598, 1.00623148, 0.90810081,
+                              0.69659946, 1.11323871, 1.05017385, 1.01535569, 0.99673733]),
+                    atol=NUMPY_atol)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsCat(wasserstein=0.09523809523809523,
+                                               jensen_shannon=0.07382902143706498,
+                                               chi2_test=Chi2TestResult(
+                                                   statistic=1.3333333333333333,
+                                                   pvalue=0.5134171190325922,
+                                                   dof=2,
+                                                   contingency_table=pd.DataFrame(
+                                                       [[33.0, 34.0, 38.0],
+                                                        [17.0, 16.0, 12.0]],
+                                                       index=['X1', 'X2'],
+                                                       columns=['Iris-Setosa',
+                                                                'Iris-Versicolour',
+                                                                'Iris-Virginica']))))
+
+    # feature_drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-0.18571428571428505, wasserstein=0.19968253968253974,
+                                          ks_test=BaseStatisticalTestResult(statistic=0.16507936507936508,
+                                                                            pvalue=0.3237613427576299)),
+                          DriftMetricsNum(mean_difference=-0.08825396825396803, wasserstein=0.1301587301587301,
+                                          ks_test=BaseStatisticalTestResult(statistic=0.14285714285714285,
+                                                                            pvalue=0.499646880472137)),
+                          DriftMetricsNum(mean_difference=-0.2765079365079357, wasserstein=0.2777777777777778,
+                                          ks_test=BaseStatisticalTestResult(statistic=0.1523809523809524,
+                                                                            pvalue=0.41885114043708227)),
+                          DriftMetricsNum(mean_difference=-0.16412698412698457, wasserstein=0.16412698412698412,
+                                          ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143,
+                                                                            pvalue=0.2821678346768163))]
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('sepal length (cm)'),
+                               DriftMetricsNum(mean_difference=-0.18571428571428505,
+                                               wasserstein=0.19968253968253974,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.16507936507936508,
+                                                                                 pvalue=0.3237613427576299)))
+
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(2),
+                               DriftMetricsNum(mean_difference=-0.2765079365079357,
+                                               wasserstein=0.2777777777777778,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.1523809523809524,
+                                                                                 pvalue=0.41885114043708227)))
+
+    assert drift_explainer.feature_names == ['sepal length (cm)',
+                                             'sepal width (cm)',
+                                             'petal length (cm)',
+                                             'petal width (cm)']
+
+    assert drift_explainer.feature_subset == ['sepal length (cm)',
+                                              'sepal width (cm)',
+                                              'petal length (cm)',
+                                              'petal width (cm)']
+
+    assert drift_explainer.n_features == 4
+
+    assert drift_explainer.task == 'classification'
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
diff --git a/src/cinnamon/drift/tests/test_catboost_model_drift_explainer.py b/src/cinnamon/drift/tests/test_catboost_model_drift_explainer.py
new file mode 100644
index 0000000..91b657c
--- /dev/null
+++ b/src/cinnamon/drift/tests/test_catboost_model_drift_explainer.py
@@ -0,0 +1,715 @@
+import pandas as pd, numpy as np
+from numpy.testing import assert_allclose
+from sklearn import datasets
+from sklearn.model_selection import train_test_split
+from catboost import CatBoostRegressor, CatBoostClassifier
+from cinnamon.drift import ModelDriftExplainer
+from cinnamon.drift.drift_utils import (DriftMetricsNum, DriftMetricsCat,
+                                        PerformanceMetricsDrift,
+                                        assert_drift_metrics_equal,
+                                        assert_drift_metrics_list_equal,
+                                        assert_performance_metrics_drift_equal)
+from cinnamon.common.stat_utils import (BaseStatisticalTestResult, Chi2TestResult,
+                                        RegressionMetrics, ClassificationMetrics)
+from ...common.constants import NUMPY_atol
+
+RANDOM_SEED = 2021
+
+
+def test_boston_catboost_ModelDriftExplainer():
+    boston = datasets.load_boston()
+    boston_df = pd.DataFrame(boston.data, columns=boston.feature_names)
+    X_train, X_test, y_train, y_test = train_test_split(boston_df, boston.target, test_size=0.3,
+                                                        random_state=RANDOM_SEED)
+    model = CatBoostRegressor(loss_function='RMSE',
+                              learning_rate=0.1,
+                              iterations=1000,
+                              max_depth=6,
+                              early_stopping_rounds=20,
+                              random_seed=RANDOM_SEED,
+                              verbose=False)
+    model.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)])
+    drift_explainer = ModelDriftExplainer(model)
+    drift_explainer.fit(X_train, X_test, y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=-0.8779339801393569, wasserstein=1.250335807497859, ks_test=BaseStatisticalTestResult(statistic=0.05764942016057092, pvalue=0.8464257838033299))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsNum(mean_difference=-0.609240261671129, wasserstein=1.3178114778471604, ks_test=BaseStatisticalTestResult(statistic=0.07857567647933393, pvalue=0.4968030078636394)))
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(drift_explainer.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(RegressionMetrics(mse=1.9584429193171917,
+                                                                                     explained_variance=0.9788618728948271),
+                                                                   RegressionMetrics(mse=9.57426222359859,
+                                                                                     explained_variance=0.8537685044683866)))
+
+    # tree_based_drift_values "node_size"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='node_size'),
+                    np.array([[1.90483198],
+                              [0.42289995],
+                              [0.84419323],
+                              [0.40268871],
+                              [2.2042409 ],
+                              [2.75792564],
+                              [1.13996793],
+                              [2.4437588 ],
+                              [1.46726763],
+                              [1.05893021],
+                              [1.13820808],
+                              [1.9729567 ],
+                              [4.08308837]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean'),
+                    np.array([[ 0.06869412],
+                              [-0.00890033],
+                              [ 0.00371266],
+                              [ 0.04273852],
+                              [ 0.09479128],
+                              [-0.34271389],
+                              [-0.04711113],
+                              [-0.03455269],
+                              [-0.09711201],
+                              [-0.0245336 ],
+                              [-0.07489948],
+                              [-0.09540242],
+                              [-0.362645  ]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean_norm"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean_norm'),
+                    np.array([[ 0.0028605 ],
+                              [-0.01031604],
+                              [-0.02142061],
+                              [ 0.03696658],
+                              [ 0.03681141],
+                              [-0.3557283 ],
+                              [-0.04529067],
+                              [-0.1080007 ],
+                              [-0.09353176],
+                              [-0.02250316],
+                              [-0.07035532],
+                              [-0.029536  ],
+                              [-0.43489742]]),
+                    atol=NUMPY_atol)
+
+    # feature_drift_LSTAT
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('LSTAT'),
+                               DriftMetricsNum(mean_difference=0.7378638864109419, wasserstein=0.8023078352661315, ks_test=BaseStatisticalTestResult(statistic=0.08887154326494201, pvalue=0.3452770147763923)))
+
+    # feature_drift_feature_0
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(0),
+                               DriftMetricsNum(mean_difference=-1.1253475613291695, wasserstein=1.1305975918079103, ks_test=BaseStatisticalTestResult(statistic=0.0618123699078204, pvalue=0.7813257636577198)))
+
+    # all feature drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-1.1253475613291695, wasserstein=1.1305975918079103, ks_test=BaseStatisticalTestResult(statistic=0.0618123699078204, pvalue=0.7813257636577198)),
+                          DriftMetricsNum(mean_difference=-0.7548691644365135, wasserstein=0.8747398156407966, ks_test=BaseStatisticalTestResult(statistic=0.02951234017246506, pvalue=0.9999448615410187)),
+                          DriftMetricsNum(mean_difference=-0.17914585191793364, wasserstein=0.7212444246208749, ks_test=BaseStatisticalTestResult(statistic=0.07381801962533452, pvalue=0.5764298225321844)),
+                          DriftMetricsNum(mean_difference=0.02337942313410645, wasserstein=0.02337942313410646, ks_test=BaseStatisticalTestResult(statistic=0.02337942313410645, pvalue=0.9999998980177341)),
+                          DriftMetricsNum(mean_difference=-0.010584444692238959, wasserstein=0.01748104742789176, ks_test=BaseStatisticalTestResult(statistic=0.07296312815938151, pvalue=0.5907168134992118)),
+                          DriftMetricsNum(mean_difference=-0.015463871543265562, wasserstein=0.07063187630092177, ks_test=BaseStatisticalTestResult(statistic=0.056645851917930416, pvalue=0.86067051746252)),
+                          DriftMetricsNum(mean_difference=-0.5575044603033064, wasserstein=1.22835637823372, ks_test=BaseStatisticalTestResult(statistic=0.04114629794826048, pvalue=0.9894125780752614)),
+                          DriftMetricsNum(mean_difference=0.12717342030924828, wasserstein=0.17319668822479928, ks_test=BaseStatisticalTestResult(statistic=0.07244275944097532, pvalue=0.5998930415655818)),
+                          DriftMetricsNum(mean_difference=-0.28690900981266765, wasserstein=0.3868941421349984, ks_test=BaseStatisticalTestResult(statistic=0.03631430270591734, pvalue=0.9978241882813342)),
+                          DriftMetricsNum(mean_difference=-13.692387749033628, wasserstein=14.388492417484393, ks_test=BaseStatisticalTestResult(statistic=0.08039696699375558, pvalue=0.4679944796162433)),
+                          DriftMetricsNum(mean_difference=0.20367603330359785, wasserstein=0.205839280404401, ks_test=BaseStatisticalTestResult(statistic=0.06322479928635147, pvalue=0.7594689122671989)),
+                          DriftMetricsNum(mean_difference=6.062043190603617, wasserstein=6.4615822925958994, ks_test=BaseStatisticalTestResult(statistic=0.06341064525721082, pvalue=0.7559278097560319)),
+                          DriftMetricsNum(mean_difference=0.7378638864109419, wasserstein=0.8023078352661315, ks_test=BaseStatisticalTestResult(statistic=0.08887154326494201, pvalue=0.3452770147763923))]
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    # tree_based_correction_weights with default params
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(),
+                    np.array([1.26204467, 1.12758195, 1.1098125 , 1.13271794, 1.13462246,
+                              1.1165066 , 1.13961283, 1.11612841, 0.6649825 , 0.9101491 ,
+                              1.11189125, 1.03815182, 1.03340836, 1.10880066, 0.75487182,
+                              0.62898398, 1.07190854, 0.97157506, 0.72438018, 0.73757673,
+                              1.21736199, 0.93459267, 1.12398518, 1.04348364, 0.96185557,
+                              1.1577552 , 1.01165243, 1.11628833, 0.91056533, 0.83375788,
+                              0.70681885, 1.01313382, 1.31589968, 1.01646162, 1.0793867 ,
+                              1.05104272, 1.08126388, 1.06460182, 1.06142385, 1.08647292,
+                              1.0602133 , 1.08808216, 1.06097316, 1.0085106 , 0.71690766,
+                              1.0964748 , 1.12949296, 1.13137799, 1.12525403, 0.79226995,
+                              1.1735033 , 1.0767666 , 0.7389624 , 0.92409138, 1.02928402,
+                              0.87390194, 1.04918248, 0.98114919, 0.90461318, 0.99690141,
+                              1.04944021, 0.84043304, 0.91081296, 0.55272801, 1.06356075,
+                              1.04406892, 1.14332477, 0.95402909, 1.07448815, 1.06314665,
+                              1.15305845, 1.08667437, 1.08295325, 0.61808778, 0.93841517,
+                              1.05701348, 1.12788455, 0.89833351, 1.03701816, 0.76605064,
+                              0.79993235, 1.01853021, 1.00812235, 0.68440514, 1.08131822,
+                              1.15944491, 1.10028033, 1.02133312, 0.77126167, 1.1169331 ,
+                              0.87226883, 1.0274817 , 1.01806625, 0.8738874 , 0.82350401,
+                              1.05955957, 0.90911545, 0.40651477, 1.04803514, 1.12623313,
+                              1.12227116, 1.14571827, 1.04230305, 1.12393889, 1.16377785,
+                              0.82612925, 1.01915541, 1.01503278, 0.8620892 , 0.98212787,
+                              1.03699855, 1.15657293, 1.06550115, 1.0064774 , 1.11731286,
+                              0.85131301, 1.20523173, 0.99942746, 0.80044317, 0.71548786,
+                              1.10341947, 0.93963463, 0.61569436, 1.00081533, 1.08776728,
+                              1.07295692, 1.10010529, 0.87705107, 0.71708581, 0.92747423,
+                              0.95394448, 1.01897125, 1.07292986, 1.09759025, 1.05769623,
+                              1.05323663, 1.14615865, 0.75403556, 0.83882016, 0.93634525,
+                              1.08743475, 1.18833951, 0.99177265, 1.05426211, 0.78516348,
+                              0.48991769, 0.73260998, 1.0319473 , 0.98482995, 0.94403372,
+                              1.37041794, 1.11271977, 1.07681627, 1.13313276, 0.37633866,
+                              0.6236818 , 1.10345547, 1.05265211, 1.1262599 , 1.08363078,
+                              0.94800955, 1.10693143, 0.98511127, 1.06251833, 1.27042363,
+                              0.71259725, 0.92953185, 1.22249787, 1.13422245, 1.08934502,
+                              1.01907369, 1.09034165, 0.99168541, 1.09420003, 1.07536856,
+                              0.8122953 , 1.12414347, 1.00207014, 1.05484035, 0.90979186,
+                              0.79486343, 0.88513653, 1.13279839, 1.14051842, 0.83545873,
+                              1.26441103, 1.05784774, 1.00351022, 0.97224466, 1.08296292,
+                              1.12561801, 1.15469868, 1.04479062, 0.98524393, 1.14416384,
+                              1.16673851, 1.05295793, 1.0901144 , 0.98444583, 1.27904676,
+                              0.97641288, 0.93157474, 0.92839532, 0.8393623 , 1.00766852,
+                              1.16061036, 0.60164609, 1.19944905, 0.82551414, 1.10334377,
+                              1.0347387 , 1.01459291, 0.60733288, 1.07325684, 0.94225207,
+                              0.81292041, 0.79177656, 0.97445209, 1.03076143, 1.11147617,
+                              0.84454846, 0.79125186, 1.05158967, 0.94814929, 0.83491393,
+                              1.03337443, 1.08380137, 1.08361233, 1.06407962, 1.05943645,
+                              1.11413293, 0.93745831, 1.01865618, 1.0756428 , 1.09025293,
+                              1.34224054, 1.08469619, 0.99515584, 0.99791534, 1.14609149,
+                              1.10220546, 1.10655145, 1.02036823, 1.10529888, 1.27661895,
+                              1.20303298, 1.11570198, 1.04415163, 0.75277445, 1.03027215,
+                              0.89199167, 1.10340212, 1.04283617, 0.8981111 , 0.42991246,
+                              1.09663151, 1.256944  , 1.24974654, 1.10685389, 0.79354265,
+                              0.91414439, 1.13016238, 0.89852017, 1.06255957, 0.92109072,
+                              0.75752724, 0.99058815, 0.96723553, 1.11404277, 0.90148599,
+                              0.83184323, 0.97127245, 0.81092146, 0.64699283, 0.70473045,
+                              1.11558755, 0.8553589 , 1.02399408, 1.10955695, 0.86333844,
+                              1.04051355, 1.16375158, 1.14611548, 1.19964363, 1.21960475,
+                              1.05222397, 1.01633969, 1.17325167, 1.14014116, 0.92470836,
+                              1.01147851, 1.20072323, 1.1313567 , 0.82869624, 1.05976579,
+                              1.23110071, 1.0830754 , 0.79357543, 1.08449987, 0.96710123,
+                              0.82443848, 0.98763578, 1.03064906, 1.07669648, 1.08406061,
+                              1.07438381, 1.12997196, 0.99596552, 0.90128289, 1.12868713,
+                              1.17248793, 0.97608255, 1.11212892, 0.9704317 , 1.06249169,
+                              0.98339119, 1.10227236, 1.03327927, 0.92720463, 1.08533577,
+                              0.98078947, 1.10147139, 0.67812663, 1.11703186, 1.10053492,
+                              0.79891288, 0.89857003, 0.54973991, 1.03118822, 0.76857038,
+                              1.14446561, 0.90206737, 0.89310273, 1.02372394, 1.09747072,
+                              1.22432677, 1.12042835, 0.82996937, 0.94792183, 1.07949222,
+                              0.71751433, 0.93075069, 1.15650086, 0.92698566, 1.13077359,
+                              1.08510617, 1.0996725 , 1.03192748, 0.81301031, 1.12839233,
+                              1.02642602, 0.98966379, 0.84655256, 1.07963336]),
+                    atol=NUMPY_atol)
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    assert drift_explainer.cat_feature_indices == []
+    assert drift_explainer.class_names == []
+    assert drift_explainer.feature_names == ['CRIM',
+                                             'ZN',
+                                             'INDUS',
+                                             'CHAS',
+                                             'NOX',
+                                             'RM',
+                                             'AGE',
+                                             'DIS',
+                                             'RAD',
+                                             'TAX',
+                                             'PTRATIO',
+                                             'B',
+                                             'LSTAT']
+    assert drift_explainer.iteration_range == (0, 123)
+    assert drift_explainer.n_features == 13
+    assert drift_explainer.task == 'regression'
+
+
+def test_breast_cancer_catboost_ModelDriftExplainer():
+    dataset = datasets.load_breast_cancer()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    clf = CatBoostClassifier(loss_function='Logloss',
+                             learning_rate=0.1,
+                             iterations=1000,
+                             max_depth=6,
+                             early_stopping_rounds=20,
+                             random_seed=RANDOM_SEED,
+                             verbose=False)
+    clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)])
+    drift_explainer = ModelDriftExplainer(clf)
+    drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)
+
+    # prediction drift "raw"
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=0.04634869138730369, wasserstein=0.5538123284880192, ks_test=BaseStatisticalTestResult(statistic=0.0658702871080549, pvalue=0.6459307033330981))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # prediction drift "proba"
+    prediction_drift_proba_ref = [DriftMetricsNum(mean_difference=0.016349784083660057, wasserstein=0.0339516727427027, ks_test=BaseStatisticalTestResult(statistic=0.0658702871080549, pvalue=0.6459307033330981))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(prediction_type='proba'),
+                                    prediction_drift_proba_ref)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsCat(wasserstein=0.0024097093655411628,
+                                               jensen_shannon=0.0017616379091961293,
+                                               chi2_test=Chi2TestResult(statistic=0.0,
+                                                                        pvalue=1.0,
+                                                                        dof=1,
+                                                                        contingency_table=pd.DataFrame([[148.0, 250.0], [64.0, 107.0]],
+                                                                                                       index=['X1', 'X2'], columns=[0, 1]))))
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(drift_explainer.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.00748801449692523),
+                                                                   ClassificationMetrics(accuracy=0.9590643274853801, log_loss=0.10225262245942275)))
+
+
+    # tree_based_drift_values "node_size"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='node_size'),
+                    np.array([[0.32509379],
+                              [1.32677547],
+                              [0.80774132],
+                              [0.66001433],
+                              [0.32493723],
+                              [0.45099504],
+                              [0.61364896],
+                              [0.90952567],
+                              [0.34701315],
+                              [0.44619327],
+                              [0.50151929],
+                              [0.76307014],
+                              [0.47727041],
+                              [0.49002657],
+                              [0.51454348],
+                              [0.97118271],
+                              [0.41764427],
+                              [0.49172829],
+                              [0.36100245],
+                              [0.41368567],
+                              [0.83342952],
+                              [0.93508863],
+                              [0.80483778],
+                              [0.95622554],
+                              [1.29879061],
+                              [0.41801119],
+                              [1.00313047],
+                              [1.51256486],
+                              [0.74948057],
+                              [0.48788657]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean'),
+                    np.array([[-0.00512973],
+                              [-0.07137614],
+                              [-0.03603572],
+                              [-0.00214852],
+                              [ 0.00424277],
+                              [ 0.0151429 ],
+                              [ 0.06059671],
+                              [ 0.09650109],
+                              [ 0.00792461],
+                              [ 0.0054325 ],
+                              [-0.02852552],
+                              [ 0.00407939],
+                              [-0.03285494],
+                              [ 0.00963248],
+                              [-0.01979541],
+                              [ 0.01209277],
+                              [-0.00048218],
+                              [ 0.02427688],
+                              [-0.00064145],
+                              [-0.03105579],
+                              [-0.04912936],
+                              [-0.06080663],
+                              [-0.03693812],
+                              [-0.03793038],
+                              [ 0.07092418],
+                              [ 0.02045349],
+                              [ 0.04684453],
+                              [ 0.1217826 ],
+                              [-0.01263006],
+                              [-0.02809828]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean_norm"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean_norm'),
+                    np.array([[-0.00105982],
+                              [-0.07325502],
+                              [-0.03451459],
+                              [-0.02184468],
+                              [ 0.00142696],
+                              [ 0.00066276],
+                              [ 0.06964488],
+                              [ 0.05645905],
+                              [-0.00341184],
+                              [-0.00472395],
+                              [-0.02023157],
+                              [-0.01013813],
+                              [-0.03178182],
+                              [-0.00809401],
+                              [-0.01765605],
+                              [-0.01061387],
+                              [ 0.00070906],
+                              [ 0.01189473],
+                              [ 0.00576327],
+                              [-0.01932325],
+                              [-0.04673837],
+                              [-0.03106766],
+                              [-0.04590803],
+                              [-0.05081389],
+                              [ 0.04648351],
+                              [-0.00369685],
+                              [ 0.02293893],
+                              [ 0.01151517],
+                              [-0.0271206 ],
+                              [-0.00963751]]),
+                    atol=NUMPY_atol)
+
+    # feature_drift mean perimeter
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('mean perimeter'),
+                               DriftMetricsNum(mean_difference=-0.5394598724617197,
+                                               wasserstein=3.3009656469481903,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.07401040289165124,
+                                                                                 pvalue=0.4998149146505402)))
+
+    # feature_drift_feature_4
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(4),
+                               DriftMetricsNum(mean_difference=-0.0011612596608774894,
+                                               wasserstein=0.0016581176349584157,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.10195715419201269,
+                                                                                 pvalue=0.1528888570418342)))
+
+    # all feature drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-0.06832955714243738, wasserstein=0.5165887184460309, ks_test=BaseStatisticalTestResult(statistic=0.07067501248934732, pvalue=0.5585303796186362)),
+                          DriftMetricsNum(mean_difference=0.014966205295483093, wasserstein=0.48581195450938897, ks_test=BaseStatisticalTestResult(statistic=0.07262922801140204, pvalue=0.5235102002710166)),
+                          DriftMetricsNum(mean_difference=-0.5394598724617197, wasserstein=3.3009656469481903, ks_test=BaseStatisticalTestResult(statistic=0.07401040289165124, pvalue=0.4998149146505402)),
+                          DriftMetricsNum(mean_difference=-17.377639660289788, wasserstein=51.29123100884536, ks_test=BaseStatisticalTestResult(statistic=0.06404831173410915, pvalue=0.6798041178199972)),
+                          DriftMetricsNum(mean_difference=-0.0011612596608774894, wasserstein=0.0016581176349584157, ks_test=BaseStatisticalTestResult(statistic=0.10195715419201269, pvalue=0.1528888570418342)),
+                          DriftMetricsNum(mean_difference=-0.003067542684181135, wasserstein=0.006741856651679452, ks_test=BaseStatisticalTestResult(statistic=0.06678127479502777, pvalue=0.6292065348385405)),
+                          DriftMetricsNum(mean_difference=-0.006204282970407593, wasserstein=0.006991872085574072, ks_test=BaseStatisticalTestResult(statistic=0.05695142378559464, pvalue=0.8061165226745349)),
+                          DriftMetricsNum(mean_difference=-0.002757498369038165, wasserstein=0.0030981202797613783, ks_test=BaseStatisticalTestResult(statistic=0.07233536101560434, pvalue=0.5289539515277676)),
+                          DriftMetricsNum(mean_difference=-0.0015799582708866666, wasserstein=0.0029724940491933365, ks_test=BaseStatisticalTestResult(statistic=0.07430426988744894, pvalue=0.4945853439694361)),
+                          DriftMetricsNum(mean_difference=-0.0005908483940168588, wasserstein=0.0007386144171148141, ks_test=BaseStatisticalTestResult(statistic=0.08001998295571425, pvalue=0.4016092513985777)),
+                          DriftMetricsNum(mean_difference=0.0037102162861089028, wasserstein=0.024305223485850316, ks_test=BaseStatisticalTestResult(statistic=0.07703723294836756, pvalue=0.4491840353305204)),
+                          DriftMetricsNum(mean_difference=0.02886939669105759, wasserstein=0.05198642040612427, ks_test=BaseStatisticalTestResult(statistic=0.0476211466690176, pvalue=0.9335508976473271)),
+                          DriftMetricsNum(mean_difference=0.07566076581739134, wasserstein=0.21045070968879498, ks_test=BaseStatisticalTestResult(statistic=0.07721355314584619, pvalue=0.44639850985093277)),
+                          DriftMetricsNum(mean_difference=-1.0084665432425268, wasserstein=3.717517793646603, ks_test=BaseStatisticalTestResult(statistic=0.07694907284962826, pvalue=0.4505043319421054)),
+                          DriftMetricsNum(mean_difference=-0.0003356947309647645, wasserstein=0.0005335116518263824, ks_test=BaseStatisticalTestResult(statistic=0.0786828881248347, pvalue=0.4227052366505618)),
+                          DriftMetricsNum(mean_difference=-0.0007019634870257772, wasserstein=0.0019131883246642555, ks_test=BaseStatisticalTestResult(statistic=0.08865967263216668, pvalue=0.28293492039886137)),
+                          DriftMetricsNum(mean_difference=-0.0013409832539892468, wasserstein=0.003182003266331656, ks_test=BaseStatisticalTestResult(statistic=0.08345822680654735, pvalue=0.3510440105305035)),
+                          DriftMetricsNum(mean_difference=2.3271988597960494e-05, wasserstein=0.0006830131799347609, ks_test=BaseStatisticalTestResult(statistic=0.0452555173528461, pvalue=0.9552787666453139)),
+                          DriftMetricsNum(mean_difference=0.0011282174468835414, wasserstein=0.0012097365629316194, ks_test=BaseStatisticalTestResult(statistic=0.07890328837168298, pvalue=0.41933341366069177)),
+                          DriftMetricsNum(mean_difference=-0.0002275110538070466, wasserstein=0.000301250179258867, ks_test=BaseStatisticalTestResult(statistic=0.05733345088013165, pvalue=0.7993870994743999)),
+                          DriftMetricsNum(mean_difference=-0.06736449792823507, wasserstein=0.5360777131270384, ks_test=BaseStatisticalTestResult(statistic=0.05892033265743924, pvalue=0.772495994489099)),
+                          DriftMetricsNum(mean_difference=-0.1677551500191079, wasserstein=0.4373399159540389, ks_test=BaseStatisticalTestResult(statistic=0.05468864791795233, pvalue=0.8423784026321328)),
+                          DriftMetricsNum(mean_difference=-0.2752897528578728, wasserstein=3.3780036145640473, ks_test=BaseStatisticalTestResult(statistic=0.0704986922918687, pvalue=0.5621042713248987)),
+                          DriftMetricsNum(mean_difference=-20.36132563401793, wasserstein=61.92498604131765, ks_test=BaseStatisticalTestResult(statistic=0.057495077727820386, pvalue=0.7963855471533337)),
+                          DriftMetricsNum(mean_difference=-0.0031193050045549287, wasserstein=0.0032626057186517362, ks_test=BaseStatisticalTestResult(statistic=0.09193628963531106, pvalue=0.24521731583934214)),
+                          DriftMetricsNum(mean_difference=-0.006255693526110079, wasserstein=0.013996743806753077, ks_test=BaseStatisticalTestResult(statistic=0.09212730318257957, pvalue=0.24304491278560958)),
+                          DriftMetricsNum(mean_difference=-0.014588629316171553, wasserstein=0.01665632422345645, ks_test=BaseStatisticalTestResult(statistic=0.08436921449352024, pvalue=0.3384205412836392)),
+                          DriftMetricsNum(mean_difference=-0.0035156151077022635, wasserstein=0.005018610787857414, ks_test=BaseStatisticalTestResult(statistic=0.04593141144318082, pvalue=0.9493671446153595)),
+                          DriftMetricsNum(mean_difference=0.001843304240500776, wasserstein=0.007164130594492942, ks_test=BaseStatisticalTestResult(statistic=0.07614093861118458, pvalue=0.4637484909517612)),
+                          DriftMetricsNum(mean_difference=-0.0014582808780746054, wasserstein=0.0027211234535249344, ks_test=BaseStatisticalTestResult(statistic=0.07605277851244527, pvalue=0.46509038807816905))]
+
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    # tree_based_correction_weights with default params
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(),
+                    np.array([0.92165749, 1.0180526 , 1.01528637, 1.15786976, 1.15251736,
+                              1.10880001, 1.08356881, 0.68413624, 0.9424286 , 0.98766633,
+                              1.41433958, 1.08706384, 0.97873393, 1.10629262, 1.01031163,
+                              0.95405252, 1.0493201 , 1.1201864 , 0.99148762, 0.98701252,
+                              1.08090489, 1.05935291, 0.92962193, 0.99033665, 1.12470821,
+                              0.97368892, 1.03117403, 1.11728288, 1.01424376, 1.0172446 ,
+                              1.15340733, 1.04214744, 1.00748081, 0.99182202, 0.99066358,
+                              0.9775729 , 0.88190388, 0.99897512, 0.85684419, 1.11343334,
+                              1.00427673, 1.0137275 , 1.02612415, 1.02836285, 0.96133991,
+                              0.90516637, 1.08759653, 0.98257095, 1.02172571, 0.91802906,
+                              0.93808599, 0.99696338, 1.07369889, 1.05587756, 0.83185497,
+                              0.97812855, 1.04413349, 1.01891141, 0.90346127, 0.99362902,
+                              0.9684525 , 0.90370031, 1.10388789, 0.92171006, 0.89300965,
+                              0.98234068, 0.96709105, 1.07491421, 1.0715786 , 0.91088615,
+                              0.98381211, 1.05858935, 0.97279471, 1.05012173, 0.96044355,
+                              0.99105465, 0.91613061, 1.01055213, 1.0386696 , 0.94040953,
+                              1.13212958, 0.95137864, 0.97603354, 1.04687388, 0.95370118,
+                              0.99246408, 1.00257267, 0.96993912, 1.02519882, 0.93810277,
+                              0.48268112, 1.12485058, 0.84108957, 1.10272162, 0.94508517,
+                              1.07946573, 0.94977558, 0.93663486, 0.98652869, 0.98037468,
+                              0.87961358, 1.06685576, 1.01100015, 0.99671888, 0.95890097,
+                              1.00447804, 0.92971345, 0.91349528, 1.02323946, 1.02577859,
+                              0.91211533, 1.03094459, 1.11474476, 1.14168737, 1.02649575,
+                              0.92977089, 0.97661125, 0.976821  , 1.12167864, 1.01383215,
+                              0.98997489, 0.92519519, 1.04306992, 0.97149484, 1.32099152,
+                              1.0367465 , 0.69665508, 0.91062362, 1.09735028, 0.94284757,
+                              1.14077798, 0.96975988, 0.99445788, 0.97743503, 1.11935207,
+                              0.99061245, 0.97508935, 1.11131188, 0.84821667, 1.02849404,
+                              0.91821426, 0.53449279, 1.00486452, 1.08051941, 0.99521843,
+                              0.96221783, 1.0440186 , 0.81521703, 1.0894057 , 1.11203717,
+                              1.01419199, 0.8865133 , 1.0886448 , 0.91025983, 1.02400012,
+                              1.00608243, 0.93732792, 1.00516116, 1.15640212, 1.05316762,
+                              1.1603465 , 1.1700486 , 1.08210802, 0.97742172, 1.05970082,
+                              0.96771   , 0.91606506, 1.03760101, 0.923343  , 1.04202743,
+                              1.00836815, 1.04797098, 0.90173025, 1.02029034, 0.90919946,
+                              1.10546591, 1.07202822, 0.93411116, 0.95355117, 0.96518171,
+                              0.97356549, 1.01825159, 1.10041902, 1.0306647 , 1.00240573,
+                              1.17424778, 0.84697649, 0.71144744, 1.00979486, 1.02608188,
+                              1.03503633, 0.94085382, 0.89019966, 1.04144332, 1.01900468,
+                              1.09237709, 1.1005501 , 1.13816495, 1.18921656, 1.07641217,
+                              1.02610131, 0.94688992, 1.00916288, 0.99520259, 1.04383419,
+                              0.8914629 , 0.82632783, 0.82218549, 0.95617706, 1.21362093,
+                              1.01690773, 1.02284861, 0.96508569, 0.93323201, 0.85838486,
+                              1.20105611, 0.77928097, 0.83495525, 0.99575798, 0.96114947,
+                              1.1761569 , 0.94595579, 1.10951379, 1.11344293, 0.60714757,
+                              1.03140788, 0.99712   , 1.00687262, 0.97267929, 1.07229012,
+                              1.09275373, 1.09082862, 0.787288  , 1.07361439, 1.0813897 ,
+                              1.04333335, 0.714547  , 1.02120617, 1.10718908, 1.05357347,
+                              1.03012204, 0.9114963 , 0.93601976, 1.04242308, 1.12041318,
+                              1.00101232, 1.22969326, 1.15161829, 0.99415276, 1.02143429,
+                              0.94636112, 1.08114318, 0.96079992, 0.94653426, 1.02658629,
+                              1.06325884, 0.86697461, 0.99407943, 0.95985893, 1.03540461,
+                              1.1099155 , 1.03512613, 0.88609987, 0.88210044, 1.02402373,
+                              1.07324776, 0.84010564, 1.0249757 , 0.9487804 , 0.76399759,
+                              0.93586195, 1.10872997, 0.98853891, 0.99840971, 1.10876968,
+                              1.03350015, 1.10303379, 1.08268589, 1.04787402, 1.01740861,
+                              1.04618087, 1.16730026, 0.98164013, 1.00976541, 1.04040524,
+                              1.06680264, 1.1000673 , 0.7044813 , 1.0315956 , 0.99394837,
+                              1.03531513, 0.9105652 , 1.03146222, 1.06874793, 1.31614444,
+                              0.90292881, 0.96053845, 1.05036673, 0.83121104, 1.17975858,
+                              1.1212491 , 0.95549658, 1.04059506, 0.91478524, 0.89108609,
+                              1.1266986 , 0.89964061, 0.99946036, 0.96647233, 1.04281517,
+                              0.78827454, 1.15204341, 0.92453591, 0.96305923, 0.94900471,
+                              1.08408539, 0.90617385, 0.98667806, 0.92399051, 1.10660771,
+                              0.90871755, 1.08292277, 0.76650403, 1.01491741, 1.12331611,
+                              1.02522223, 1.02110709, 1.01852973, 0.92150599, 1.07809778,
+                              1.14257374, 0.98380398, 0.99664231, 1.12776558, 0.94948592,
+                              0.85051449, 0.85900297, 1.05544595, 0.91412861, 1.0484394 ,
+                              0.97829921, 0.98202048, 1.14047031, 0.95379987, 0.91766237,
+                              1.01589703, 1.0824786 , 1.12013037, 0.88112188, 1.00863211,
+                              0.92110846, 1.01895688, 1.02187464, 0.98720769, 0.85154246,
+                              1.1011457 , 1.02685091, 1.09628736, 0.98466108, 1.04139473,
+                              1.06269857, 1.03416093, 1.03975772, 0.98137741, 1.14413735,
+                              0.96292444, 1.01744469, 1.22381866, 0.92417892, 1.06754583,
+                              0.97316372, 0.93383873, 1.02873736, 0.80390796, 1.03196751,
+                              0.64875596, 0.98344423, 1.1250535 , 0.87262625, 0.87091717,
+                              1.13906108, 1.05110702, 0.98772745, 1.21762225, 0.98077481,
+                              1.05500001, 1.05349047, 0.93513032, 0.98487021, 0.67353588,
+                              1.03058237, 1.00919155, 1.13908548, 0.92211026, 0.93359272,
+                              0.95502617, 0.88087176, 0.8987916 ]),
+                    atol=NUMPY_atol)
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    assert drift_explainer.cat_feature_indices == []
+    assert drift_explainer.class_names == ['0', '1']
+    assert drift_explainer.feature_names == ['mean radius',
+                                             'mean texture',
+                                             'mean perimeter',
+                                             'mean area',
+                                             'mean smoothness',
+                                             'mean compactness',
+                                             'mean concavity',
+                                             'mean concave points',
+                                             'mean symmetry',
+                                             'mean fractal dimension',
+                                             'radius error',
+                                             'texture error',
+                                             'perimeter error',
+                                             'area error',
+                                             'smoothness error',
+                                             'compactness error',
+                                             'concavity error',
+                                             'concave points error',
+                                             'symmetry error',
+                                             'fractal dimension error',
+                                             'worst radius',
+                                             'worst texture',
+                                             'worst perimeter',
+                                             'worst area',
+                                             'worst smoothness',
+                                             'worst compactness',
+                                             'worst concavity',
+                                             'worst concave points',
+                                             'worst symmetry',
+                                             'worst fractal dimension']
+    assert drift_explainer.iteration_range == (0, 105)
+    assert drift_explainer.n_features == 30
+    assert drift_explainer.task == 'classification'
+
+
+def test_iris_catboost_ModelDriftExplainer():
+    dataset = datasets.load_iris()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    clf = CatBoostClassifier(loss_function='MultiClass',
+                             learning_rate=0.1,
+                             iterations=1000,
+                             max_depth=6,
+                             early_stopping_rounds=20,
+                             random_seed=RANDOM_SEED,
+                             verbose=False)
+    clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)])
+    drift_explainer = ModelDriftExplainer(clf)
+    drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)
+
+    # prediction drift "raw"
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=0.2556635888899628, wasserstein=0.2931763654790207, ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143, pvalue=0.2821678346768163)),
+                            DriftMetricsNum(mean_difference=0.2536708490324236, wasserstein=0.2625697569861726, ks_test=BaseStatisticalTestResult(statistic=0.10793650793650794, pvalue=0.8205934119780005)),
+                            DriftMetricsNum(mean_difference=-0.5093344379223861, wasserstein=0.5240813705758235, ks_test=BaseStatisticalTestResult(statistic=0.18095238095238095, pvalue=0.22712915347334828))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # prediction drift "proba"
+    prediction_drift_proba_ref = [DriftMetricsNum(mean_difference=0.06068744478888638, wasserstein=0.062073873098040216, ks_test=BaseStatisticalTestResult(statistic=0.12063492063492064, pvalue=0.7069803362151523)),
+                                  DriftMetricsNum(mean_difference=0.07308349137417464, wasserstein=0.07352443840551189, ks_test=BaseStatisticalTestResult(statistic=0.12698412698412698, pvalue=0.6467769104301901)),
+                                  DriftMetricsNum(mean_difference=-0.13377093616306102, wasserstein=0.13459925728761946, ks_test=BaseStatisticalTestResult(statistic=0.18095238095238095, pvalue=0.22712915347334828))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(prediction_type='proba'),
+                                    prediction_drift_proba_ref)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsCat(wasserstein=0.09523809523809523,
+                                               jensen_shannon=0.07382902143706498,
+                                               chi2_test=Chi2TestResult(statistic=1.3333333333333333,
+                                                                        pvalue=0.5134171190325922,
+                                                                        dof=2,
+                                                                        contingency_table=pd.DataFrame([[33.0, 34.0, 38.0], [17.0, 16.0, 12.0]],
+                                                                                                       index=['X1', 'X2'], columns=[0, 1, 2]))))
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(drift_explainer.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=0.9904761904761905, log_loss=0.06196156247077523),
+                                                                   ClassificationMetrics(accuracy=0.9333333333333333, log_loss=0.14716856908904924)))
+
+    # tree_based_drift_values "node_size"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='node_size'),
+                    np.array([[ 3.88114597],
+                              [ 7.84748116],
+                              [ 6.75366501],
+                              [11.90863769]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean'),
+                    np.array([[-0.01354953, -0.00602339,  0.01957293],
+                              [-0.05742398, -0.01429501, -0.06161435],
+                              [ 0.14828041,  0.05454545, -0.00282586],
+                              [ 0.17835669,  0.2194438 , -0.46446716]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean_norm"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean_norm'),
+                    np.array([[-0.09855691, -0.09208058, -0.07090097],
+                              [-0.09248865, -0.05182248, -0.0942603 ],
+                              [-0.06083239, -0.14343543, -0.21452551],
+                              [ 0.03280696,  0.07820104, -0.60642925]]),
+                    atol=NUMPY_atol)
+
+    # feature_drift - argument as a string
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('petal width (cm)'),
+                               DriftMetricsNum(mean_difference=-0.16412698412698457,
+                                               wasserstein=0.16412698412698412,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143,
+                                                                                 pvalue=0.2821678346768163)))
+
+    # feature_drift - argument as integer
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(2),
+                               DriftMetricsNum(mean_difference=-0.2765079365079357,
+                                               wasserstein=0.2777777777777778,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.1523809523809524,
+                                                                                 pvalue=0.41885114043708227)))
+
+    # all feature drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-0.18571428571428505, wasserstein=0.19968253968253974, ks_test=BaseStatisticalTestResult(statistic=0.16507936507936508, pvalue=0.3237613427576299)),
+                          DriftMetricsNum(mean_difference=-0.08825396825396803, wasserstein=0.1301587301587301, ks_test=BaseStatisticalTestResult(statistic=0.14285714285714285, pvalue=0.499646880472137)),
+                          DriftMetricsNum(mean_difference=-0.2765079365079357, wasserstein=0.2777777777777778, ks_test=BaseStatisticalTestResult(statistic=0.1523809523809524, pvalue=0.41885114043708227)),
+                          DriftMetricsNum(mean_difference=-0.16412698412698457, wasserstein=0.16412698412698412, ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143, pvalue=0.2821678346768163))]
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    # tree_based_correction_weights with default params
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(),
+                    np.array([1.46778441, 0.52294752, 0.63499851, 1.11395665, 1.51159197,
+                              0.73016553, 0.70492996, 0.68508753, 1.32705675, 1.19768229,
+                              1.6776906 , 1.26519898, 0.38068631, 0.68758125, 0.91815123,
+                              0.84310013, 0.61681574, 1.34260229, 1.56001491, 1.443861  ,
+                              0.70120618, 0.65058177, 1.78956352, 1.31837229, 0.61456506,
+                              0.8542716 , 0.71941464, 0.45689895, 1.19019827, 0.63635147,
+                              1.89923028, 1.09247435, 0.72878899, 0.74354047, 0.63547501,
+                              1.21954902, 1.44866555, 1.04356531, 0.69035447, 1.3739053 ,
+                              1.73660237, 0.33077669, 1.39210873, 1.52170161, 0.63702699,
+                              1.86402732, 1.18316883, 0.40800216, 1.16027329, 0.79378744,
+                              1.14638635, 1.14222676, 1.2546594 , 1.63006777, 0.59401333,
+                              0.68203216, 1.52611396, 1.07152711, 1.39406928, 1.33558325,
+                              0.51887858, 1.17583738, 1.23719303, 0.72742515, 0.91513678,
+                              0.50291088, 0.72376554, 0.97899664, 0.84310013, 1.8191181 ,
+                              0.66860961, 0.78368263, 0.31702045, 0.62884497, 1.51467351,
+                              0.62243733, 0.35189584, 2.25980288, 0.67056316, 0.68112491,
+                              0.7548836 , 0.71009471, 0.93523464, 0.47412222, 1.5718063 ,
+                              1.49699883, 1.69508587, 0.74675178, 0.68287764, 2.04223207,
+                              1.36987599, 0.61456506, 0.62397505, 0.329149  , 0.84031679,
+                              0.77182041, 1.02214697, 0.93128777, 0.84400149, 1.3790245 ,
+                              0.49808655, 0.78799818, 0.82521918, 1.21908353, 0.6493135 ]),
+                    atol=NUMPY_atol)
+
+    # tree_based_correction_weights with "max_depth = 1"
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(max_depth=1),
+                    np.array([0.9674496 , 0.78989541, 0.76883768, 1.10634011, 1.10160442,
+                              0.79566687, 1.14505217, 0.76883768, 1.1784106 , 1.10420233,
+                              1.19467364, 1.10774096, 0.80356229, 0.85297475, 1.15812415,
+                              1.15090691, 0.78878774, 1.15812415, 1.10160442, 1.10160442,
+                              0.76883768, 1.10907358, 1.19467364, 1.17241594, 0.76991734,
+                              1.14364165, 0.76883768, 0.75560207, 1.11460987, 0.78878774,
+                              1.19467364, 1.16721319, 1.15812415, 0.76883768, 1.14471797,
+                              1.11121168, 1.10420233, 1.16510963, 0.76883768, 1.05986901,
+                              1.09714384, 0.82007931, 1.0726481 , 1.00569346, 0.76991734,
+                              1.1784106 , 1.07040247, 0.75560207, 1.1784106 , 0.76883768,
+                              1.08195813, 1.11354292, 1.1784106 , 1.07040247, 0.78878774,
+                              0.76883768, 1.1784106 , 1.11354292, 1.06896562, 1.1784106 ,
+                              1.14265495, 1.10651888, 1.10420233, 0.76991734, 1.15812415,
+                              0.78323856, 0.76991734, 1.17106693, 1.15090691, 1.1784106 ,
+                              0.76991734, 1.20509256, 0.82007931, 0.76883768, 1.10160442,
+                              1.14849744, 0.91122631, 1.19467364, 0.76883768, 1.14128403,
+                              0.76883768, 0.76883768, 1.02605152, 0.80356229, 1.07040247,
+                              1.1784106 , 1.08195813, 0.76883768, 0.76991734, 1.1784106 ,
+                              1.1784106 , 0.76991734, 1.15090691, 0.82007931, 0.76883768,
+                              1.13375469, 1.1522327 , 1.09307662, 1.14522398, 1.10160442,
+                              0.75560207, 0.76883768, 1.13375469, 1.02672997, 0.78878774]),
+                    atol=NUMPY_atol)
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    assert drift_explainer.cat_feature_indices == []
+    assert drift_explainer.class_names == ['0', '1', '2']
+    assert drift_explainer.feature_names == ['sepal length (cm)',
+                                             'sepal width (cm)',
+                                             'petal length (cm)',
+                                             'petal width (cm)']
+    assert drift_explainer.iteration_range == (0, 82)
+    assert drift_explainer.n_features == 4
+    assert drift_explainer.task == 'classification'
diff --git a/src/cinnamon/drift/tests/test_output_drift_detector.py b/src/cinnamon/drift/tests/test_output_drift_detector.py
new file mode 100644
index 0000000..02f6308
--- /dev/null
+++ b/src/cinnamon/drift/tests/test_output_drift_detector.py
@@ -0,0 +1,277 @@
+import pandas as pd
+from sklearn import datasets
+from sklearn.model_selection import train_test_split
+from xgboost import XGBClassifier, XGBRegressor
+
+from cinnamon.drift import OutputDriftDetector
+from cinnamon.drift.drift_utils import (DriftMetricsNum, DriftMetricsCat,
+                                        PerformanceMetricsDrift,
+                                        assert_drift_metrics_equal,
+                                        assert_drift_metrics_list_equal,
+                                        assert_performance_metrics_drift_equal)
+from cinnamon.common.stat_utils import (BaseStatisticalTestResult, Chi2TestResult,
+                                        RegressionMetrics, ClassificationMetrics)
+
+RANDOM_SEED = 2021
+
+
+def test_breast_cancer_OutputDriftDetector():
+    dataset = datasets.load_breast_cancer()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    clf = XGBClassifier(n_estimators=1000,
+                        booster="gbtree",
+                        objective="binary:logistic",
+                        learning_rate=0.05,
+                        max_depth=6,
+                        use_label_encoder=False,
+                        seed=RANDOM_SEED)
+    clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=0)
+
+    # ---------------------------------
+    #  case: prediction_type='proba'
+    # ---------------------------------
+
+    output_drift_detector = OutputDriftDetector(task='classification', prediction_type='proba')
+    output_drift_detector.fit(clf.predict_proba(X_train), clf.predict_proba(X_test),
+                               y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=0.00798452225707491,
+                                            wasserstein=0.024082025832758043,
+                                            ks_test=BaseStatisticalTestResult(statistic=0.08323782655969908,
+                                                                              pvalue=0.3542889176877513))]
+    assert_drift_metrics_list_equal(output_drift_detector.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # target drift
+    target_drift_ref = DriftMetricsCat(wasserstein=0.0024097093655411628,
+                                       jensen_shannon=0.0017616379091961293,
+                                       chi2_test=Chi2TestResult(statistic=0.0,
+                                                                pvalue=1.0,
+                                                                dof=1,
+                                                                contingency_table=pd.DataFrame(
+                                                                    [[148.0, 250.0], [64.0, 107.0]],
+                                                                    index=['X1', 'X2'], columns=[0, 1])))
+    assert_drift_metrics_equal(output_drift_detector.get_target_drift(),
+                               target_drift_ref)
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.016039305599991362),
+                                                                   ClassificationMetrics(accuracy=0.9473684210526315, log_loss=0.11116574995208815)))
+
+    # ---------------------------------
+    #   case: prediction_type='label'
+    # ---------------------------------
+
+    output_drift_detector2 = OutputDriftDetector(task='classification', prediction_type='label')
+    output_drift_detector2.fit(clf.predict(X_train), clf.predict(X_test),
+                                y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref2 = [DriftMetricsCat(wasserstein=0.015134150283581616,
+                                             jensen_shannon=0.011119138338504947,
+                                             chi2_test=Chi2TestResult(statistic=0.06175606172739494,
+                                                                      pvalue=0.8037416368764607,
+                                                                      dof=1,
+                                                                      contingency_table=pd.DataFrame(
+                                                                          [[148.0, 250.0], [61.0, 110.0]],
+                                                                          index=['X1', 'X2'], columns=[0, 1])))]
+    assert_drift_metrics_list_equal(output_drift_detector2.get_prediction_drift(),
+                                    prediction_drift_ref2)
+
+    # target drift
+    assert_drift_metrics_equal(output_drift_detector2.get_target_drift(),
+                               target_drift_ref)  # target_drift_ref is the same as in previous case
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector2.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0),
+                                                                   ClassificationMetrics(accuracy=0.9473684210526315)))
+
+    # ---------------------------------
+    #   case: prediction_type='raw'
+    # ---------------------------------
+
+    output_drift_detector3 = OutputDriftDetector(task='classification', prediction_type='raw')
+    output_drift_detector3.fit(pd.DataFrame(clf.predict(X_train, output_margin=True)),
+                                clf.predict(X_test, output_margin=True),
+                                y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref3 = [DriftMetricsNum(mean_difference=0.005498574879272855,
+                                             wasserstein=0.3764544601013494,
+                                             ks_test=BaseStatisticalTestResult(statistic=0.08323782655969908,
+                                                                               pvalue=0.3542889176877513))]
+    assert_drift_metrics_list_equal(output_drift_detector3.get_prediction_drift(),
+                                    prediction_drift_ref3)
+
+    # target drift
+    assert_drift_metrics_equal(output_drift_detector3.get_target_drift(),
+                               target_drift_ref)  # target_drift_ref is the same as in previous case
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector3.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.016039305803571925),
+                                                                   ClassificationMetrics(accuracy=0.9473684210526315, log_loss=0.11116572790125613)))
+
+
+def test_iris_OutputDriftDetector():
+    dataset = datasets.load_iris()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    clf = XGBClassifier(n_estimators=1000,
+                        booster="gbtree",
+                        learning_rate=0.05,
+                        max_depth=6,
+                        use_label_encoder=False,
+                        seed=2021)
+    clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=0)
+
+    # ---------------------------------
+    #  case: prediction_type='proba'
+    # ---------------------------------
+
+    output_drift_detector = OutputDriftDetector(task='classification', prediction_type='proba')
+    output_drift_detector.fit(clf.predict_proba(X_train), clf.predict_proba(X_test),
+                               y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=0.06122570359665486,
+                                            wasserstein=0.06138405880136859,
+                                            ks_test=BaseStatisticalTestResult(statistic=0.1111111111111111,
+                                                                              pvalue=0.793799989988573)),
+                            DriftMetricsNum(mean_difference=0.08154049228640303,
+                                            wasserstein=0.08205600790975118,
+                                            ks_test=BaseStatisticalTestResult(statistic=0.12698412698412698,
+                                                                              pvalue=0.6467769104301901)),
+                            DriftMetricsNum(mean_difference=-0.1427661934187488,
+                                            wasserstein=0.14276781702443725,
+                                            ks_test=BaseStatisticalTestResult(statistic=0.19047619047619047,
+                                                                              pvalue=0.1805850065949114))]
+    assert_drift_metrics_list_equal(output_drift_detector.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # target drift
+    target_drift_ref = DriftMetricsCat(wasserstein=0.09523809523809523,
+                                       jensen_shannon=0.07382902143706498,
+                                       chi2_test=Chi2TestResult(statistic=1.3333333333333333,
+                                                                pvalue=0.5134171190325922,
+                                                                dof=2,
+                                                                contingency_table=pd.DataFrame(
+                                                                    [[33.0, 34.0, 38.0], [17.0, 16.0, 12.0]],
+                                                                    index=['X1', 'X2'], columns=[0, 1, 2])))
+    assert_drift_metrics_equal(output_drift_detector.get_target_drift(),
+                               target_drift_ref)
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.045063073312242824),
+                                                                   ClassificationMetrics(accuracy=0.9333333333333333, log_loss=0.16192325585418277)))
+
+    # ---------------------------------
+    #   case: prediction_type='label'
+    # ---------------------------------
+
+    output_drift_detector2 = OutputDriftDetector(task='classification', prediction_type='label')
+    output_drift_detector2.fit(clf.predict(X_train), clf.predict(X_test),
+                                y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref2 = [DriftMetricsCat(wasserstein=0.16190476190476188,
+                                             jensen_shannon=0.12861925049715453,
+                                             chi2_test=Chi2TestResult(statistic=3.879642904933953,
+                                                                      pvalue=0.14372961005414284,
+                                                                      dof=2,
+                                                                      contingency_table=pd.DataFrame(
+                                                                          [[33.0, 34.0, 38.0], [17.0, 19.0, 9.0]],
+                                                                          index=['X1', 'X2'], columns=[0, 1, 2])))]
+    assert_drift_metrics_list_equal(output_drift_detector2.get_prediction_drift(),
+                                    prediction_drift_ref2)
+
+    # target drift
+    assert_drift_metrics_equal(output_drift_detector2.get_target_drift(),
+                               target_drift_ref)  # target_drift_ref is the same as in previous case
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector2.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0),
+                                                                   ClassificationMetrics(accuracy=0.9333333333333333)))
+
+    # ---------------------------------
+    #   case: prediction_type='raw'
+    # ---------------------------------
+
+    output_drift_detector3 = OutputDriftDetector(task='classification', prediction_type='raw')
+    output_drift_detector3.fit(pd.DataFrame(clf.predict(X_train, output_margin=True)),
+                                clf.predict(X_test, output_margin=True),
+                                y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref3 = [DriftMetricsNum(mean_difference=0.31093458145383807,
+                                             wasserstein=0.310934581453838,
+                                             ks_test=BaseStatisticalTestResult(statistic=0.06349206349206349,
+                                                                               pvalue=0.9987212484986797)),
+                             DriftMetricsNum(mean_difference=0.3232848411632908,
+                                             wasserstein=0.3318073130907522,
+                                             ks_test=BaseStatisticalTestResult(statistic=0.12698412698412698,
+                                                                               pvalue=0.6467769104301901)),
+                             DriftMetricsNum(mean_difference=-0.5564053781212321,
+                                             wasserstein=0.5568392310587188,
+                                             ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143,
+                                                                               pvalue=0.2821678346768163))]
+    assert_drift_metrics_list_equal(output_drift_detector3.get_prediction_drift(),
+                                    prediction_drift_ref3)
+
+    # target drift
+    assert_drift_metrics_equal(output_drift_detector3.get_target_drift(),
+                               target_drift_ref)  # target_drift_ref is the same as in previous case
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector3.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.04506306932086036),
+                                                                   ClassificationMetrics(accuracy=0.9333333333333333, log_loss=0.1619232536604007)))
+
+
+def test_boston_OutputDriftDetector():
+    dataset = datasets.load_boston()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    model = XGBRegressor(n_estimators=1000,
+                         booster="gbtree",
+                         objective="reg:squarederror",
+                         learning_rate=0.05,
+                         max_depth=6,
+                         seed=RANDOM_SEED,
+                         use_label_encoder=False)
+    model.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=0)
+
+    output_drift_detector = OutputDriftDetector(task='regression')
+    output_drift_detector.fit(model.predict(X_train), model.predict(X_test), y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=-0.7889487954289152,
+                                            wasserstein=1.0808420273082935,
+                                            ks_test=BaseStatisticalTestResult(statistic=0.052743086529884034,
+                                                                              pvalue=0.9096081584010306))]
+    assert_drift_metrics_list_equal(output_drift_detector.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # target drift
+    target_drift_ref = DriftMetricsNum(mean_difference=-0.609240261671129,
+                                       wasserstein=1.3178114778471604,
+                                       ks_test=BaseStatisticalTestResult(statistic=0.07857567647933393,
+                                                                         pvalue=0.4968030078636394))
+    assert_drift_metrics_equal(output_drift_detector.get_target_drift(),
+                               target_drift_ref)
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(output_drift_detector.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(RegressionMetrics(mse=0.3643813701486243,
+                                                                                     explained_variance=0.9960752192224699),
+                                                                   RegressionMetrics(mse=12.419719495108291,
+                                                                                     explained_variance=0.8095694395593922)))
diff --git a/src/cinnamon/drift/tests/test_xgboost_model_drift_explainer.py b/src/cinnamon/drift/tests/test_xgboost_model_drift_explainer.py
new file mode 100644
index 0000000..0fa614a
--- /dev/null
+++ b/src/cinnamon/drift/tests/test_xgboost_model_drift_explainer.py
@@ -0,0 +1,882 @@
+import pandas as pd, numpy as np
+from numpy.testing import assert_allclose
+from sklearn import datasets
+from sklearn.model_selection import train_test_split
+from xgboost import XGBRegressor, XGBClassifier
+from cinnamon.drift import ModelDriftExplainer
+from cinnamon.drift.drift_utils import (DriftMetricsNum, DriftMetricsCat,
+                                        PerformanceMetricsDrift,
+                                        assert_drift_metrics_equal,
+                                        assert_drift_metrics_list_equal,
+                                        assert_performance_metrics_drift_equal)
+from cinnamon.common.stat_utils import (BaseStatisticalTestResult, Chi2TestResult,
+                                        RegressionMetrics, ClassificationMetrics)
+from ...common.constants import NUMPY_atol
+
+RANDOM_SEED = 2021
+
+
+def test_boston_xgboost_ModelDriftExplainer():
+    boston = datasets.load_boston()
+    boston_df = pd.DataFrame(boston.data, columns=boston.feature_names)
+    X_train, X_test, y_train, y_test = train_test_split(boston_df, boston.target, test_size=0.3,
+                                                        random_state=RANDOM_SEED)
+    model = XGBRegressor(n_estimators=1000,
+                         booster="gbtree",
+                         objective="reg:squarederror",
+                         learning_rate=0.05,
+                         max_depth=6,
+                         seed=RANDOM_SEED,
+                         use_label_encoder=False)
+    model.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=0)
+    drift_explainer = ModelDriftExplainer(model)
+    drift_explainer.fit(X_train, X_test, y_train, y_test)
+
+    # prediction drift
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=-0.7889487954289152, wasserstein=1.0808420273082935, ks_test=BaseStatisticalTestResult(statistic=0.052743086529884034, pvalue=0.9096081584010306))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsNum(mean_difference=-0.609240261671129,
+                                               wasserstein=1.3178114778471604,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.07857567647933393,
+                                                                                 pvalue=0.4968030078636394)))
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(drift_explainer.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(RegressionMetrics(mse=0.3643813701486243,
+                                                                                     explained_variance=0.9960752192224699),
+                                                                   RegressionMetrics(mse=12.419719495108291,
+                                                                                     explained_variance=0.8095694395593922)))
+
+    # tree_based_drift_values "node_size"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='node_size'),
+                    np.array([[4.31765569],
+                              [0.53994982],
+                              [1.2299105 ],
+                              [0.05466186],
+                              [2.40478511],
+                              [5.62793724],
+                              [2.3531374 ],
+                              [3.78890353],
+                              [0.73237733],
+                              [1.65802895],
+                              [1.39955313],
+                              [2.47726252],
+                              [6.99832357]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean'),
+                    np.array([[-0.04545056],
+                              [ 0.00084117],
+                              [ 0.18597606],
+                              [-0.00062653],
+                              [ 0.24192436],
+                              [-0.55327918],
+                              [-0.05149254],
+                              [-0.25770117],
+                              [ 0.03011812],
+                              [ 0.0020817 ],
+                              [-0.15965889],
+                              [-0.13940995],
+                              [-0.04227164]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean_norm"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean_norm'),
+                    np.array([[ 0.05234698],
+                              [ 0.0058806 ],
+                              [ 0.00328171],
+                              [-0.00030556],
+                              [ 0.07280695],
+                              [-0.29861327],
+                              [-0.00603697],
+                              [-0.2603778 ],
+                              [ 0.01561826],
+                              [-0.02779742],
+                              [-0.01725041],
+                              [-0.03128068],
+                              [-0.03379194]]),
+                    atol=NUMPY_atol)
+
+    # feature_drift_LSTAT
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('LSTAT'),
+                               DriftMetricsNum(mean_difference=0.7378638864109419,
+                                                                         wasserstein=0.8023078352661315,
+                                                                         ks_test=BaseStatisticalTestResult(statistic=0.08887154326494201,
+                                                                                                           pvalue=0.3452770147763923)))
+
+    # feature_drift_feature_0
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(0),
+                               DriftMetricsNum(mean_difference=-1.1253475613291695,
+                                                                   wasserstein=1.1305975918079103,
+                                                                   ks_test=BaseStatisticalTestResult(statistic=0.0618123699078204,
+                                                                                                     pvalue=0.7813257636577198)))
+
+    # all feature drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-1.1253475613291695, wasserstein=1.1305975918079103, ks_test=BaseStatisticalTestResult(statistic=0.0618123699078204, pvalue=0.7813257636577198)),
+                      DriftMetricsNum(mean_difference=-0.7548691644365135, wasserstein=0.8747398156407966, ks_test=BaseStatisticalTestResult(statistic=0.02951234017246506, pvalue=0.9999448615410187)),
+                      DriftMetricsNum(mean_difference=-0.17914585191793364, wasserstein=0.7212444246208749, ks_test=BaseStatisticalTestResult(statistic=0.07381801962533452, pvalue=0.5764298225321844)),
+                      DriftMetricsNum(mean_difference=0.02337942313410645, wasserstein=0.02337942313410646, ks_test=BaseStatisticalTestResult(statistic=0.02337942313410645, pvalue=0.9999998980177341)),
+                      DriftMetricsNum(mean_difference=-0.010584444692238959, wasserstein=0.01748104742789176, ks_test=BaseStatisticalTestResult(statistic=0.07296312815938151, pvalue=0.5907168134992118)),
+                      DriftMetricsNum(mean_difference=-0.015463871543265562, wasserstein=0.07063187630092177, ks_test=BaseStatisticalTestResult(statistic=0.056645851917930416, pvalue=0.86067051746252)),
+                      DriftMetricsNum(mean_difference=-0.5575044603033064, wasserstein=1.22835637823372, ks_test=BaseStatisticalTestResult(statistic=0.04114629794826048, pvalue=0.9894125780752614)),
+                      DriftMetricsNum(mean_difference=0.12717342030924828, wasserstein=0.17319668822479928, ks_test=BaseStatisticalTestResult(statistic=0.07244275944097532, pvalue=0.5998930415655818)),
+                      DriftMetricsNum(mean_difference=-0.28690900981266765, wasserstein=0.3868941421349984, ks_test=BaseStatisticalTestResult(statistic=0.03631430270591734, pvalue=0.9978241882813342)),
+                      DriftMetricsNum(mean_difference=-13.692387749033628, wasserstein=14.388492417484393, ks_test=BaseStatisticalTestResult(statistic=0.08039696699375558, pvalue=0.4679944796162433)),
+                      DriftMetricsNum(mean_difference=0.20367603330359785, wasserstein=0.205839280404401, ks_test=BaseStatisticalTestResult(statistic=0.06322479928635147, pvalue=0.7594689122671989)),
+                      DriftMetricsNum(mean_difference=6.062043190603617, wasserstein=6.4615822925958994, ks_test=BaseStatisticalTestResult(statistic=0.06341064525721082, pvalue=0.7559278097560319)),
+                      DriftMetricsNum(mean_difference=0.7378638864109419, wasserstein=0.8023078352661315, ks_test=BaseStatisticalTestResult(statistic=0.08887154326494201, pvalue=0.3452770147763923))]
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    # tree_based_correction_weights with default params
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(),
+                    np.array([1.16419732, 1.10087397, 1.21368666, 1.13727639, 1.13989036,
+                              1.18977695, 1.01215575, 1.10059681, 0.72558209, 1.00675234,
+                              1.10666682, 0.88555298, 1.12977301, 1.16608115, 0.91400419,
+                              0.44987693, 1.04860138, 1.15972868, 0.74749602, 0.78955673,
+                              1.23054107, 1.0929581 , 1.15461204, 0.93743351, 0.89136442,
+                              1.18001834, 1.09512827, 1.20852801, 1.07037641, 0.85541822,
+                              0.71586788, 1.10300282, 1.25814986, 0.96320127, 0.96420466,
+                              1.01631317, 1.31017151, 0.99762216, 0.91570282, 1.23407839,
+                              1.01356292, 1.03317556, 0.98659978, 1.14176654, 0.8256704 ,
+                              1.05326507, 1.15819153, 0.96044076, 1.04398531, 0.97956093,
+                              1.13806087, 0.92484557, 0.60986512, 0.91123648, 1.06589494,
+                              0.84968641, 1.1733163 , 0.93350917, 0.88924206, 1.05747077,
+                              0.93490889, 0.79767375, 0.92145929, 0.37230812, 1.10293867,
+                              1.09235481, 1.37000448, 0.89159463, 1.05314073, 1.04337542,
+                              1.17859982, 1.05619742, 1.19299521, 0.49073022, 1.12006103,
+                              1.10935394, 1.20617108, 0.98542949, 1.19876176, 0.49210116,
+                              0.76742674, 1.06288453, 0.98133668, 0.79451027, 1.05343443,
+                              1.17409533, 1.0322569 , 0.84641052, 0.72454594, 1.0972863 ,
+                              1.11972538, 1.0166767 , 1.12840246, 0.90672729, 0.70155724,
+                              1.18006586, 0.92539464, 0.32431158, 1.04676889, 1.07684215,
+                              1.12142957, 1.12868632, 0.80044461, 1.11108876, 1.07978027,
+                              0.83810027, 0.97369048, 0.94380991, 1.06643724, 1.03637159,
+                              1.09826332, 1.21987827, 1.1017478 , 0.92381585, 1.05592795,
+                              0.77292046, 1.26144311, 1.11049506, 0.50631903, 0.69105845,
+                              1.05058848, 0.92704126, 0.59578928, 1.09463525, 1.01638428,
+                              0.96861518, 1.06710481, 1.0882454 , 0.69635315, 0.84551532,
+                              1.16587031, 0.91786233, 1.21218501, 1.11080734, 0.86970289,
+                              1.09885839, 1.23491866, 0.61032684, 0.94564681, 0.94738328,
+                              1.059863  , 1.13433497, 0.83917301, 1.15907066, 0.84877355,
+                              0.33947135, 0.49498701, 0.88139004, 0.99945658, 0.95302861,
+                              1.48348648, 1.09300884, 1.11657693, 1.13850024, 0.33491379,
+                              0.50668992, 1.24825669, 1.13978931, 1.20383783, 0.94804158,
+                              0.99665375, 1.22177126, 1.0945194 , 1.02675212, 1.30485921,
+                              0.58660552, 0.55305576, 1.17434394, 1.17382957, 1.1202571 ,
+                              0.96642816, 1.01531836, 1.10319784, 0.86347228, 1.00810461,
+                              0.56946217, 1.08268008, 0.86923329, 1.0317189 , 0.93847969,
+                              0.845586  , 0.97831139, 1.00289034, 1.14807371, 0.86903538,
+                              1.13722926, 1.1006587 , 1.16877497, 1.01227611, 1.07845423,
+                              1.11708513, 1.19417547, 1.2778751 , 0.98797798, 1.07728855,
+                              1.05499927, 1.04710719, 1.08808864, 0.96846122, 1.32186046,
+                              0.8530053 , 1.04979525, 1.09932135, 0.7429571 , 0.92881187,
+                              1.30223646, 0.42061162, 1.19721629, 0.73952189, 1.04644862,
+                              1.17597349, 1.0840867 , 0.74224707, 1.13574442, 0.94866608,
+                              0.95590973, 0.94671777, 0.9254706 , 0.9839863 , 1.0244442 ,
+                              0.77062547, 0.84602691, 0.9576317 , 0.88120068, 0.89514653,
+                              1.15687421, 1.05989279, 1.13865161, 1.04575364, 1.11247464,
+                              1.17972616, 0.95284698, 0.99251302, 0.98733136, 1.10005399,
+                              1.28689456, 1.00010831, 1.28234576, 0.89869426, 1.11778495,
+                              1.05600081, 1.12962457, 1.03721409, 1.07050115, 1.31205803,
+                              1.22222613, 1.1449303 , 0.94381272, 0.47570943, 0.98423073,
+                              0.57225095, 1.09066898, 0.96422086, 0.89040329, 0.32512642,
+                              1.02420319, 1.32294277, 1.08759698, 1.10158941, 0.61790862,
+                              0.96842455, 1.13527867, 0.85848503, 1.1100173 , 0.86949728,
+                              0.81562783, 1.15702292, 0.84311207, 1.12182998, 1.06690836,
+                              0.76243479, 0.92992144, 0.93082927, 0.51646473, 0.75581889,
+                              1.13794611, 0.88634101, 1.01195516, 1.0558324 , 1.05306641,
+                              1.08135681, 1.06816819, 1.09851691, 1.33354986, 1.13348136,
+                              1.09352752, 1.01020482, 1.16867974, 1.10760027, 0.95324276,
+                              0.9707388 , 1.1306543 , 1.1445239 , 0.96796771, 0.96245229,
+                              1.2453499 , 0.9909731 , 0.64256786, 1.0459765 , 1.05809618,
+                              0.92863222, 1.04914003, 0.94390974, 0.88043042, 1.12183045,
+                              1.21387541, 1.06464791, 0.9586424 , 0.95162604, 1.14590447,
+                              1.11224052, 0.76397902, 1.09903285, 1.02689233, 1.02270401,
+                              1.06316529, 1.09481799, 1.05671481, 0.92147167, 1.14338271,
+                              0.97165919, 1.05255944, 0.66238535, 1.11540372, 1.10107548,
+                              0.84405474, 1.11217045, 0.77474588, 1.01380398, 0.83058349,
+                              1.16788829, 0.85915848, 0.75478956, 1.10948121, 1.03640738,
+                              1.16700158, 1.07234547, 0.91683386, 0.931275  , 1.06521399,
+                              0.73163952, 1.05447144, 1.14416767, 1.07729212, 1.18850243,
+                              1.10836293, 1.1817005 , 1.1238247 , 1.09186981, 1.17053314,
+                              0.99844788, 1.01820715, 1.25297799, 1.09458313]),
+                    atol=NUMPY_atol)
+
+    # tree_based_correction_weights with "max_depth = 2"
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(max_depth=2),
+                    np.array([1.06705916, 1.00782503, 1.03194615, 1.01143602, 1.04150287,
+                              1.00490137, 1.00716356, 1.01372945, 1.01984372, 1.0415846 ,
+                              1.03286222, 0.99840509, 1.06628184, 1.03063623, 0.96939166,
+                              0.8038806 , 1.03194615, 1.06036313, 0.98854907, 0.97907688,
+                              1.013492  , 0.88872912, 1.05886575, 1.02508135, 1.04243298,
+                              1.02569367, 1.01785115, 1.02850978, 0.9878603 , 0.96039109,
+                              1.05294481, 1.05615545, 1.05709789, 1.02261963, 1.05662473,
+                              1.00474926, 1.03737369, 1.01897035, 1.01220578, 1.06800669,
+                              1.05100332, 1.01640816, 1.02526032, 1.05518344, 0.99446514,
+                              1.00321176, 1.03888114, 1.04169193, 1.01280588, 1.0248593 ,
+                              1.0249929 , 1.02327516, 0.80835604, 0.92040462, 1.01850082,
+                              1.02153529, 0.90401362, 1.02508135, 1.04124058, 1.02249592,
+                              1.00350377, 1.05380244, 1.00418732, 0.71625784, 1.03046312,
+                              1.06836601, 1.06196184, 1.03166289, 1.00493671, 1.00978167,
+                              1.00664191, 1.04896893, 1.03335223, 0.80286263, 1.01784813,
+                              1.0230165 , 1.03333042, 0.84973661, 1.03394422, 0.76627792,
+                              1.05628382, 1.01554171, 1.01177573, 0.7684792 , 1.00713751,
+                              1.02989149, 1.05380244, 1.00484896, 0.76382491, 1.01011393,
+                              0.95893291, 1.01303789, 1.05380244, 0.90396531, 0.80832806,
+                              1.03219578, 1.05623757, 0.70414909, 1.03159602, 1.01011393,
+                              1.02227181, 1.02247586, 1.04243298, 1.03888114, 1.03306075,
+                              0.97684241, 1.01143497, 1.00390841, 1.01690221, 1.023098  ,
+                              1.05092364, 1.02605504, 1.03239116, 1.04243298, 1.03159602,
+                              1.00770838, 1.01753144, 1.06355041, 0.80286263, 0.75878782,
+                              1.00042124, 0.86210717, 0.81805437, 1.01595349, 1.01736054,
+                              1.0028074 , 1.01347956, 1.02344056, 0.81918853, 1.02189   ,
+                              1.027844  , 1.06352663, 1.03735179, 1.0109767 , 1.06018503,
+                              1.02446896, 1.04069356, 0.93041787, 0.92321909, 1.02691114,
+                              1.0261104 , 1.05804732, 1.00032953, 1.04715949, 1.01143497,
+                              0.64937092, 0.75612285, 1.02438357, 1.01978483, 1.02372891,
+                              1.06364573, 1.0249929 , 1.02795647, 1.02206413, 0.62444111,
+                              0.81441905, 1.04560518, 1.02249592, 1.0202202 , 1.04272037,
+                              0.85373068, 1.05937769, 1.01905612, 1.02037054, 1.05917458,
+                              0.70315177, 0.84265141, 1.03564249, 1.03158103, 1.05625255,
+                              1.01155734, 1.02473125, 1.0248593 , 1.03110024, 1.0105692 ,
+                              0.76655379, 1.01929869, 1.00111594, 1.03218851, 1.05161489,
+                              1.05380244, 0.9806052 , 1.00321176, 1.03293578, 1.00571659,
+                              1.08229672, 1.0087436 , 1.08268708, 1.04243298, 1.01435934,
+                              1.03569911, 1.01924758, 1.07414099, 1.04461879, 1.01836087,
+                              1.02202067, 1.05894745, 1.03006066, 1.02643593, 1.05709789,
+                              1.01168096, 1.06689953, 1.02196073, 0.83395358, 1.00043922,
+                              1.15795501, 0.71625784, 1.03587309, 0.75878782, 1.05380244,
+                              1.05484844, 1.04243298, 0.84244514, 1.03735179, 1.02792702,
+                              1.02966407, 1.03335223, 0.96005168, 1.02455794, 1.01088373,
+                              1.04243298, 1.04050496, 1.02249592, 1.02036971, 1.0415846 ,
+                              1.05424294, 1.03218851, 1.01418897, 1.03564249, 1.01220578,
+                              1.01143497, 0.8492266 , 1.02931471, 1.01486963, 1.03774678,
+                              1.05063759, 1.01603176, 1.06950223, 1.02451342, 1.01562316,
+                              1.02864778, 1.0332124 , 1.01418897, 1.02793579, 1.03888114,
+                              1.05004129, 1.01177573, 1.00748309, 0.94426584, 1.01767718,
+                              0.86049152, 1.01984239, 1.03821867, 1.05709789, 0.70139784,
+                              1.0416767 , 1.05709789, 1.03913362, 1.00816457, 0.76974082,
+                              1.0415846 , 1.04090503, 1.04243298, 1.04440392, 1.01973511,
+                              1.03564249, 1.02943286, 1.02796848, 1.0157813 , 1.02045684,
+                              1.02864778, 0.8525487 , 1.00884064, 0.74854892, 1.01284357,
+                              1.03997235, 1.02153529, 1.00782503, 1.03267649, 1.02778381,
+                              1.01143497, 1.02772656, 1.02722247, 1.03643174, 1.03194615,
+                              1.04763263, 1.04621989, 1.02569346, 1.05618285, 1.04243298,
+                              1.04243298, 1.01450463, 1.01649063, 0.89104104, 1.05294481,
+                              1.03799457, 1.01188382, 0.9310142 , 1.01736054, 1.01340682,
+                              1.03173974, 1.01418897, 1.02446896, 1.00350377, 1.03888114,
+                              1.02809208, 1.01160488, 1.00987981, 1.03888114, 1.02291403,
+                              1.00799433, 1.0415846 , 1.03267649, 1.01177573, 0.9816398 ,
+                              0.96714353, 1.03913362, 1.01143497, 1.00839094, 1.02525736,
+                              1.01476836, 1.03806175, 1.02312352, 1.04720395, 1.03598462,
+                              0.99556151, 0.90878994, 0.98549045, 0.89692268, 1.04950095,
+                              1.03267649, 1.01143497, 0.87793868, 1.02596249, 1.03568617,
+                              1.03888114, 1.0181819 , 0.99735705, 1.00522002, 1.0416767 ,
+                              1.01143497, 1.02867809, 1.01459792, 0.8948014 , 1.05592543,
+                              1.01303789, 1.03286268, 1.03194615, 1.0187867 , 1.02966407,
+                              1.03347725, 0.86450652, 1.07528078, 1.00782503]),
+                    atol=NUMPY_atol)
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    assert drift_explainer.cat_feature_indices == []
+    assert drift_explainer.class_names == []
+    assert drift_explainer.feature_names == ['CRIM',
+                                             'ZN',
+                                             'INDUS',
+                                             'CHAS',
+                                             'NOX',
+                                             'RM',
+                                             'AGE',
+                                             'DIS',
+                                             'RAD',
+                                             'TAX',
+                                             'PTRATIO',
+                                             'B',
+                                             'LSTAT']
+    assert drift_explainer.iteration_range == (0, 143)
+    assert drift_explainer.n_features == 13
+    assert drift_explainer.task == 'regression'
+
+
+def test_breast_cancer_xgboost_ModelDriftExplainer():
+    dataset = datasets.load_breast_cancer()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    clf = XGBClassifier(n_estimators=1000,
+                        booster="gbtree",
+                        objective="binary:logistic",
+                        learning_rate=0.05,
+                        max_depth=6,
+                        use_label_encoder=False,
+                        seed=RANDOM_SEED)
+    clf.fit(X=X_train, y=y_train, eval_set=[(X_valid, y_valid)], early_stopping_rounds=20, verbose=0)
+    drift_explainer = ModelDriftExplainer(clf)
+    drift_explainer.fit(X1=X_train, X2=X_valid, y1=y_train, y2=y_valid)
+
+    # prediction drift "raw"
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=0.005498574879272855, wasserstein=0.3764544601013494, ks_test=BaseStatisticalTestResult(statistic=0.08323782655969908, pvalue=0.3542889176877513))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # prediction drift "proba"
+    prediction_drift_proba_ref = [DriftMetricsNum(mean_difference=0.00798452225707491, wasserstein=0.024082025832758043, ks_test=BaseStatisticalTestResult(statistic=0.08323782655969908, pvalue=0.3542889176877513))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(prediction_type='proba'),
+                                    prediction_drift_proba_ref)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsCat(wasserstein=0.0024097093655411628,
+                                               jensen_shannon=0.0017616379091961293,
+                                               chi2_test=Chi2TestResult(statistic=0.0,
+                                                                        pvalue=1.0,
+                                                                        dof=1,
+                                                                        contingency_table=pd.DataFrame([[148.0, 250.0], [64.0, 107.0]],
+                                                                                                       index=['X1', 'X2'], columns=[0, 1]))))
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(drift_explainer.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.016039305599991362),
+                                                                   ClassificationMetrics(accuracy=0.9473684210526315, log_loss=0.11116574995208815)))
+
+    # tree_based_drift_values "node_size"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='node_size'),
+                    np.array([[0.1263785 ],
+                              [0.86245768],
+                              [0.03015075],
+                              [0.5015792 ],
+                              [0.345185  ],
+                              [0.01076813],
+                              [0.48651996],
+                              [0.5553289 ],
+                              [0.00502513],
+                              [0.02160804],
+                              [0.05284761],
+                              [0.16747726],
+                              [0.01615219],
+                              [0.21016901],
+                              [0.06186914],
+                              [0.61067109],
+                              [0.13051384],
+                              [0.00904523],
+                              [0.1728764 ],
+                              [0.0583929 ],
+                              [0.970708  ],
+                              [0.78384477],
+                              [1.04602337],
+                              [1.23239211],
+                              [1.55243827],
+                              [0.09348557],
+                              [0.55188395],
+                              [1.57008003],
+                              [0.27572543],
+                              [0.05313921]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean'),
+                    np.array([[-9.33158904e-03],
+                              [ 1.15870292e-02],
+                              [ 2.25198024e-03],
+                              [-5.45754250e-03],
+                              [ 5.29567213e-03],
+                              [ 1.24361599e-03],
+                              [ 2.21942302e-02],
+                              [-3.17637413e-03],
+                              [ 9.47460649e-06],
+                              [ 4.13937125e-03],
+                              [ 1.80148733e-03],
+                              [-1.81682108e-03],
+                              [ 7.62334093e-05],
+                              [ 1.50764895e-03],
+                              [-2.89777474e-03],
+                              [-1.19719725e-02],
+                              [ 4.80747869e-03],
+                              [ 1.99959485e-03],
+                              [-4.34269903e-04],
+                              [ 1.51119076e-03],
+                              [ 2.62156736e-03],
+                              [-9.64497653e-04],
+                              [-1.93085931e-02],
+                              [-2.76468329e-03],
+                              [ 1.09498474e-02],
+                              [-5.65097244e-03],
+                              [ 1.16655661e-03],
+                              [-1.93422930e-03],
+                              [ 8.26090050e-04],
+                              [-2.78114137e-03]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean_norm"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean_norm'),
+                    np.array([[ 0.00128017],
+                              [-0.00862285],
+                              [ 0.00227804],
+                              [-0.00702226],
+                              [ 0.01434936],
+                              [ 0.00044783],
+                              [ 0.01191792],
+                              [ 0.00368157],
+                              [-0.0001583 ],
+                              [-0.00016892],
+                              [ 0.00055728],
+                              [ 0.00208376],
+                              [-0.00037917],
+                              [-0.00193379],
+                              [-0.00055455],
+                              [-0.0112573 ],
+                              [ 0.00458787],
+                              [-0.00033537],
+                              [ 0.00311544],
+                              [-0.00054474],
+                              [-0.00204518],
+                              [ 0.00557142],
+                              [-0.01681621],
+                              [-0.02567387],
+                              [ 0.01606102],
+                              [-0.00331497],
+                              [ 0.00925461],
+                              [-0.00053606],
+                              [ 0.00542589],
+                              [-0.00056559]]),
+                    atol=NUMPY_atol)
+
+    # feature_drift mean perimeter
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('mean perimeter'),
+                               DriftMetricsNum(mean_difference=-0.5394598724617197,
+                                               wasserstein=3.3009656469481903,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.07401040289165124,
+                                                                                 pvalue=0.4998149146505402)))
+
+    # feature_drift_feature_4
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(4),
+                               DriftMetricsNum(mean_difference=-0.0011612596608774894,
+                                               wasserstein=0.0016581176349584157,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.10195715419201269,
+                                                                                 pvalue=0.1528888570418342)))
+
+    # all feature drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-0.06832955714243738, wasserstein=0.5165887184460309, ks_test=BaseStatisticalTestResult(statistic=0.07067501248934732, pvalue=0.5585303796186362)),
+                          DriftMetricsNum(mean_difference=0.014966205295483093, wasserstein=0.48581195450938897, ks_test=BaseStatisticalTestResult(statistic=0.07262922801140204, pvalue=0.5235102002710166)),
+                          DriftMetricsNum(mean_difference=-0.5394598724617197, wasserstein=3.3009656469481903, ks_test=BaseStatisticalTestResult(statistic=0.07401040289165124, pvalue=0.4998149146505402)),
+                          DriftMetricsNum(mean_difference=-17.377639660289788, wasserstein=51.29123100884536, ks_test=BaseStatisticalTestResult(statistic=0.06404831173410915, pvalue=0.6798041178199972)),
+                          DriftMetricsNum(mean_difference=-0.0011612596608774894, wasserstein=0.0016581176349584157, ks_test=BaseStatisticalTestResult(statistic=0.10195715419201269, pvalue=0.1528888570418342)),
+                          DriftMetricsNum(mean_difference=-0.003067542684181135, wasserstein=0.006741856651679452, ks_test=BaseStatisticalTestResult(statistic=0.06678127479502777, pvalue=0.6292065348385405)),
+                          DriftMetricsNum(mean_difference=-0.006204282970407593, wasserstein=0.006991872085574072, ks_test=BaseStatisticalTestResult(statistic=0.05695142378559464, pvalue=0.8061165226745349)),
+                          DriftMetricsNum(mean_difference=-0.002757498369038165, wasserstein=0.0030981202797613783, ks_test=BaseStatisticalTestResult(statistic=0.07233536101560434, pvalue=0.5289539515277676)),
+                          DriftMetricsNum(mean_difference=-0.0015799582708866666, wasserstein=0.0029724940491933365, ks_test=BaseStatisticalTestResult(statistic=0.07430426988744894, pvalue=0.4945853439694361)),
+                          DriftMetricsNum(mean_difference=-0.0005908483940168588, wasserstein=0.0007386144171148141, ks_test=BaseStatisticalTestResult(statistic=0.08001998295571425, pvalue=0.4016092513985777)),
+                          DriftMetricsNum(mean_difference=0.0037102162861089028, wasserstein=0.024305223485850316, ks_test=BaseStatisticalTestResult(statistic=0.07703723294836756, pvalue=0.4491840353305204)),
+                          DriftMetricsNum(mean_difference=0.02886939669105759, wasserstein=0.05198642040612427, ks_test=BaseStatisticalTestResult(statistic=0.0476211466690176, pvalue=0.9335508976473271)),
+                          DriftMetricsNum(mean_difference=0.07566076581739134, wasserstein=0.21045070968879498, ks_test=BaseStatisticalTestResult(statistic=0.07721355314584619, pvalue=0.44639850985093277)),
+                          DriftMetricsNum(mean_difference=-1.0084665432425268, wasserstein=3.717517793646603, ks_test=BaseStatisticalTestResult(statistic=0.07694907284962826, pvalue=0.4505043319421054)),
+                          DriftMetricsNum(mean_difference=-0.0003356947309647645, wasserstein=0.0005335116518263824, ks_test=BaseStatisticalTestResult(statistic=0.0786828881248347, pvalue=0.4227052366505618)),
+                          DriftMetricsNum(mean_difference=-0.0007019634870257772, wasserstein=0.0019131883246642555, ks_test=BaseStatisticalTestResult(statistic=0.08865967263216668, pvalue=0.28293492039886137)),
+                          DriftMetricsNum(mean_difference=-0.0013409832539892468, wasserstein=0.003182003266331656, ks_test=BaseStatisticalTestResult(statistic=0.08345822680654735, pvalue=0.3510440105305035)),
+                          DriftMetricsNum(mean_difference=2.3271988597960494e-05, wasserstein=0.0006830131799347609, ks_test=BaseStatisticalTestResult(statistic=0.0452555173528461, pvalue=0.9552787666453139)),
+                          DriftMetricsNum(mean_difference=0.0011282174468835414, wasserstein=0.0012097365629316194, ks_test=BaseStatisticalTestResult(statistic=0.07890328837168298, pvalue=0.41933341366069177)),
+                          DriftMetricsNum(mean_difference=-0.0002275110538070466, wasserstein=0.000301250179258867, ks_test=BaseStatisticalTestResult(statistic=0.05733345088013165, pvalue=0.7993870994743999)),
+                          DriftMetricsNum(mean_difference=-0.06736449792823507, wasserstein=0.5360777131270384, ks_test=BaseStatisticalTestResult(statistic=0.05892033265743924, pvalue=0.772495994489099)),
+                          DriftMetricsNum(mean_difference=-0.1677551500191079, wasserstein=0.4373399159540389, ks_test=BaseStatisticalTestResult(statistic=0.05468864791795233, pvalue=0.8423784026321328)),
+                          DriftMetricsNum(mean_difference=-0.2752897528578728, wasserstein=3.3780036145640473, ks_test=BaseStatisticalTestResult(statistic=0.0704986922918687, pvalue=0.5621042713248987)),
+                          DriftMetricsNum(mean_difference=-20.36132563401793, wasserstein=61.92498604131765, ks_test=BaseStatisticalTestResult(statistic=0.057495077727820386, pvalue=0.7963855471533337)),
+                          DriftMetricsNum(mean_difference=-0.0031193050045549287, wasserstein=0.0032626057186517362, ks_test=BaseStatisticalTestResult(statistic=0.09193628963531106, pvalue=0.24521731583934214)),
+                          DriftMetricsNum(mean_difference=-0.006255693526110079, wasserstein=0.013996743806753077, ks_test=BaseStatisticalTestResult(statistic=0.09212730318257957, pvalue=0.24304491278560958)),
+                          DriftMetricsNum(mean_difference=-0.014588629316171553, wasserstein=0.01665632422345645, ks_test=BaseStatisticalTestResult(statistic=0.08436921449352024, pvalue=0.3384205412836392)),
+                          DriftMetricsNum(mean_difference=-0.0035156151077022635, wasserstein=0.005018610787857414, ks_test=BaseStatisticalTestResult(statistic=0.04593141144318082, pvalue=0.9493671446153595)),
+                          DriftMetricsNum(mean_difference=0.001843304240500776, wasserstein=0.007164130594492942, ks_test=BaseStatisticalTestResult(statistic=0.07614093861118458, pvalue=0.4637484909517612)),
+                          DriftMetricsNum(mean_difference=-0.0014582808780746054, wasserstein=0.0027211234535249344, ks_test=BaseStatisticalTestResult(statistic=0.07605277851244527, pvalue=0.46509038807816905))]
+
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    # tree_based_correction_weights with default params
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(),
+                    np.array([1.09688861, 0.9762864 , 1.00014947, 1.01476245, 1.01354627,
+                              0.9767973 , 1.04556855, 0.85788803, 0.90761728, 0.95980924,
+                              1.46309695, 1.01011298, 0.96974536, 1.05906244, 0.97232291,
+                              0.99180543, 0.98516202, 1.00942547, 1.26310403, 0.9748277 ,
+                              1.01451781, 0.97868663, 0.97123145, 0.9733829 , 1.20145142,
+                              0.9586891 , 0.97539291, 0.9768325 , 0.97842638, 0.98211348,
+                              1.02099763, 0.95255366, 0.95910583, 0.97749069, 0.91711393,
+                              1.0435607 , 0.92132381, 0.9767973 , 0.84949809, 1.08825167,
+                              0.99230282, 0.97842638, 0.94620344, 0.99724424, 0.9036093 ,
+                              0.98324182, 1.01734828, 0.96964598, 1.06762684, 0.96974536,
+                              0.9949011 , 1.2057241 , 1.01011298, 0.99340268, 0.9664361 ,
+                              0.97450892, 0.97488189, 0.9762864 , 1.01691498, 0.98512894,
+                              1.00555434, 0.96905592, 0.96974536, 0.91122304, 0.93102404,
+                              0.96065739, 0.92940275, 0.98132391, 1.12690028, 0.96900604,
+                              0.94353279, 0.9762864 , 0.94343761, 1.0492391 , 0.93949154,
+                              0.9934877 , 0.96645436, 1.03202716, 0.93938445, 0.92132381,
+                              1.11859089, 1.02045142, 1.09223617, 0.98554884, 1.00350732,
+                              0.94494111, 0.90379319, 0.90379319, 1.06169248, 0.96778998,
+                              1.15590687, 1.01416578, 0.90558469, 1.00045202, 0.86778472,
+                              1.07007739, 1.12243864, 0.98477793, 0.98689794, 1.10220873,
+                              0.98352155, 0.9797077 , 0.95116113, 1.12059577, 0.92132381,
+                              0.98599024, 0.95152545, 0.93661173, 0.97842638, 1.00527655,
+                              1.19862131, 0.9949011 , 1.01011298, 1.00984077, 0.93130539,
+                              0.92132381, 0.96974536, 1.05286115, 1.01476245, 0.9767973 ,
+                              0.92061277, 0.90067896, 0.9949011 , 0.9767973 , 1.39335122,
+                              0.94681491, 0.94807998, 0.93084373, 0.95346645, 0.96806721,
+                              1.05752417, 0.97923458, 0.9207142 , 1.03982842, 1.03194528,
+                              0.96923814, 0.96976656, 1.11572012, 0.91239922, 0.99233025,
+                              1.15136868, 0.89028972, 0.96380939, 1.10862893, 0.98877476,
+                              0.90374205, 0.97767417, 0.96089942, 1.04194554, 1.06896318,
+                              0.9818003 , 0.91027932, 1.03095402, 1.09436284, 0.93486541,
+                              1.01214388, 0.91603461, 0.89441691, 1.15426558, 0.93306278,
+                              1.21696406, 1.07087497, 0.98775857, 0.9748277 , 1.18333091,
+                              0.9311599 , 1.01432607, 0.96806721, 0.96493244, 0.94681491,
+                              0.9767973 , 1.02117961, 0.9818436 , 0.97395557, 0.90716718,
+                              1.00951905, 1.11828809, 0.98335911, 0.88741634, 0.94236893,
+                              0.98855728, 0.9610783 , 1.00511946, 0.9610783 , 1.05466407,
+                              0.99150881, 0.89978799, 0.92824642, 0.9733829 , 0.98657956,
+                              0.9767973 , 0.95808787, 0.96570979, 0.9767973 , 0.98211348,
+                              1.00552185, 1.01538974, 1.07342214, 1.24891905, 0.97457285,
+                              0.9818003 , 0.97151205, 1.05201347, 1.02833475, 0.96964598,
+                              1.04067146, 1.29794378, 1.01433355, 1.11445973, 1.17866278,
+                              0.97602717, 0.97940093, 0.96672796, 0.9036093 , 1.04055059,
+                              1.08716264, 0.91713342, 0.97842638, 0.97929098, 0.91711393,
+                              1.07971923, 0.94648404, 1.06588569, 0.93929923, 0.86900744,
+                              0.9733829 , 0.99358489, 0.93222083, 0.981091  , 0.98941224,
+                              1.14817263, 1.00549738, 0.9264055 , 0.9748277 , 1.00375663,
+                              0.99860652, 1.03187757, 1.00944462, 1.01416578, 1.19213111,
+                              0.9861027 , 1.12463125, 0.98891136, 0.9488511 , 1.18800708,
+                              0.93474089, 1.15864599, 0.99223952, 0.9513338 , 0.99024567,
+                              1.16595902, 0.97767417, 1.02270935, 0.91477253, 0.9767973 ,
+                              1.15087793, 0.94273799, 0.95093785, 0.96452201, 1.02728696,
+                              1.01577319, 0.97395557, 0.97675389, 1.03329837, 0.95444102,
+                              1.24367776, 0.93817417, 0.91544856, 0.99685911, 0.96439299,
+                              0.95498178, 1.01416578, 0.96520215, 1.00139353, 1.00549738,
+                              0.97190444, 1.00996603, 1.00851361, 1.02635158, 0.99412489,
+                              0.92994705, 1.1962142 , 1.00936136, 0.92669589, 1.00291292,
+                              1.02880622, 0.9610783 , 0.89966342, 0.93919222, 0.94716181,
+                              1.00306662, 0.97671976, 0.97785108, 0.99097539, 0.96974536,
+                              1.04667349, 0.97762583, 0.99467277, 0.93224644, 1.0239402 ,
+                              1.00218875, 0.9767973 , 0.97933463, 0.93395503, 0.98953181,
+                              0.96951773, 1.04147533, 0.97395557, 0.9767973 , 1.05049822,
+                              0.85858325, 1.00839429, 0.95997819, 0.91703499, 0.9764657 ,
+                              1.02576597, 1.05536556, 0.95116113, 0.9767973 , 1.00944462,
+                              0.99493695, 1.00070629, 0.89818716, 0.97842638, 1.15284526,
+                              0.9926688 , 0.97577954, 0.97078205, 0.99270135, 1.10890484,
+                              1.0400484 , 0.9949011 , 0.96735107, 0.99550516, 0.98019335,
+                              0.89467117, 0.85698742, 1.04524239, 0.91764191, 0.99135506,
+                              0.98274187, 1.13238986, 1.04141147, 0.95901436, 0.96155602,
+                              0.97395557, 1.116924  , 1.01011298, 1.38139845, 1.06746572,
+                              1.23827099, 0.9767973 , 0.9782942 , 0.91954068, 0.9436986 ,
+                              1.00983344, 0.97842638, 0.96974536, 0.99754785, 1.0792625 ,
+                              0.9782942 , 0.9347743 , 1.07728331, 0.98969799, 0.96230798,
+                              1.00826171, 0.97846367, 1.0976199 , 0.87795576, 0.98224721,
+                              0.9818003 , 0.888337  , 0.95865103, 0.90196546, 0.99737843,
+                              0.85545711, 0.97395557, 1.09080592, 0.88192188, 0.90172028,
+                              1.10812858, 1.02735467, 0.97096186, 1.11491658, 1.00854637,
+                              1.01446387, 1.00485001, 0.89966342, 1.10834215, 1.07745083,
+                              0.9554235 , 1.02499975, 1.05634889, 1.04846522, 1.07746242,
+                              0.99881543, 0.98052599, 0.99233025]),
+                    atol=NUMPY_atol)
+
+    # tree_based_correction_weights with "max_depth = 1"
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(max_depth=1),
+                    np.array([0.99060717, 1.00097782, 1.01195055, 0.99190102, 0.99296686,
+                              1.00097782, 1.02115759, 0.95965535, 0.99190102, 0.99190102,
+                              1.03780921, 0.99190102, 1.00097782, 1.02115759, 1.00097782,
+                              1.00097782, 0.99190102, 0.99190102, 1.03818753, 1.00097782,
+                              0.99190102, 0.99190102, 1.00097782, 0.99190102, 1.02609205,
+                              1.01195055, 1.00097782, 1.00097782, 0.99190102, 0.98308249,
+                              0.99190102, 0.98862601, 1.00097782, 0.99190102, 0.99190102,
+                              1.00097782, 0.99190102, 1.00097782, 0.98176558, 1.02377208,
+                              0.99296686, 0.99190102, 0.98964166, 0.99190102, 0.98308249,
+                              0.98176558, 1.00097782, 1.00097782, 1.02115759, 1.00097782,
+                              1.00097782, 1.03348572, 0.99190102, 1.00295377, 0.98308249,
+                              1.00097782, 1.00097782, 1.00097782, 1.00097782, 0.99190102,
+                              1.01977884, 1.00097782, 1.00097782, 0.98308249, 0.98308249,
+                              0.99190102, 1.00913713, 0.99190102, 1.01802648, 1.00232049,
+                              0.98964166, 1.00097782, 0.99296686, 1.02115759, 1.01195055,
+                              0.98176558, 1.00097782, 1.02115759, 0.99190102, 0.99190102,
+                              1.01693375, 1.00097782, 1.01350404, 0.99190102, 0.97093225,
+                              0.99070507, 0.99190102, 0.99190102, 1.00778359, 1.00097782,
+                              1.03391585, 0.99190102, 0.98960065, 1.00097782, 0.98308249,
+                              1.02115759, 1.01514945, 1.00097782, 0.99190102, 1.01345846,
+                              1.00407111, 1.00097782, 0.99190102, 1.02115759, 0.99190102,
+                              0.98308249, 0.99296686, 0.98308249, 0.99190102, 1.01195055,
+                              1.02400318, 1.00097782, 0.99190102, 0.99190102, 0.9900491 ,
+                              0.99190102, 1.00097782, 1.01011723, 0.99190102, 1.00097782,
+                              0.99190102, 0.99190102, 1.00097782, 1.00097782, 1.02609205,
+                              0.99190102, 0.96963163, 0.98862601, 0.99190102, 1.00097782,
+                              1.01693375, 1.01195055, 0.99190102, 0.99193593, 1.02115759,
+                              1.00097782, 1.00097782, 1.02400318, 0.98308249, 0.99190102,
+                              1.02719463, 0.96867101, 1.00097782, 1.02115759, 1.01454525,
+                              0.99190102, 0.99190102, 0.99990338, 1.02115759, 1.02115759,
+                              0.99190102, 0.98308249, 1.00097782, 1.03710965, 1.02477633,
+                              1.00097782, 0.99190102, 0.99296686, 1.02643535, 0.99190102,
+                              1.0377968 , 1.02115759, 1.00295377, 1.00097782, 1.0215352 ,
+                              0.98968833, 1.00097782, 1.00097782, 1.00097782, 0.99190102,
+                              1.00097782, 0.99190102, 1.00097782, 0.99190102, 0.99190102,
+                              0.99190102, 1.02115759, 0.99190102, 0.98308249, 0.98964166,
+                              1.01095792, 1.00097782, 1.02115759, 1.00097782, 1.00093062,
+                              0.99190102, 0.99190102, 0.98176558, 0.99190102, 0.99190102,
+                              1.00097782, 1.00097782, 1.00083388, 1.00097782, 0.98308249,
+                              1.00097782, 0.99190102, 1.02115759, 1.02443358, 1.00097782,
+                              0.99190102, 0.99990338, 1.00083388, 1.0229736 , 1.00097782,
+                              1.00305635, 1.02380811, 0.99831398, 1.02487216, 1.02238979,
+                              1.00097782, 0.99190102, 1.00097782, 0.98308249, 1.00124462,
+                              1.02115759, 0.99070507, 0.99190102, 0.99190102, 0.99190102,
+                              1.02115759, 0.99990338, 1.02115759, 0.98743885, 0.98175514,
+                              0.99190102, 1.01195055, 0.98308249, 0.99190102, 0.99190102,
+                              1.00300905, 0.99190102, 1.00207785, 1.00097782, 0.99190102,
+                              1.01195055, 1.02643535, 0.99190102, 0.99190102, 1.01331097,
+                              0.99190102, 0.98964166, 1.02106398, 0.99096913, 1.01350404,
+                              0.99190102, 1.02609205, 0.99190102, 0.99990338, 0.99190102,
+                              1.03639558, 0.99190102, 0.98308249, 0.98964166, 1.00097782,
+                              1.02719463, 0.99190102, 0.99930399, 0.99070507, 1.01195055,
+                              0.99190102, 0.99190102, 0.98968833, 1.00097782, 0.99190102,
+                              1.02506037, 0.97603687, 0.99190102, 1.00097782, 1.00097782,
+                              0.99190102, 0.99190102, 0.99296686, 1.01207895, 0.99190102,
+                              0.99296686, 0.99190102, 0.99190102, 1.01195055, 1.00295377,
+                              0.99190102, 1.03776028, 1.00097782, 0.99190102, 1.00097782,
+                              0.99190102, 1.00097782, 0.99190102, 0.98862601, 0.99990338,
+                              1.00097782, 0.99070507, 0.99190102, 1.00097782, 1.00097782,
+                              1.00166545, 0.98308249, 1.00097782, 0.98743885, 0.99190102,
+                              0.99190102, 1.00097782, 0.99190102, 0.98964166, 0.98176558,
+                              0.99990338, 1.00310274, 0.99190102, 1.00097782, 1.02115759,
+                              1.01207895, 0.99190102, 1.00295377, 0.98308249, 0.99190102,
+                              1.01207895, 0.99060717, 0.99190102, 1.00097782, 0.99190102,
+                              1.00097782, 0.99190102, 0.98308249, 0.99190102, 1.03545037,
+                              1.00026788, 1.01195055, 1.00097782, 0.99190102, 1.02715847,
+                              1.02115759, 1.00097782, 0.99990338, 0.99190102, 0.99162617,
+                              0.98308249, 0.98308249, 1.02115759, 0.97603687, 0.99190102,
+                              0.99190102, 1.01331097, 1.01195055, 0.97972288, 0.99190102,
+                              0.99190102, 1.02115759, 0.99190102, 1.02747846, 1.02377208,
+                              1.03353577, 1.00097782, 1.00097782, 0.99190102, 0.99203397,
+                              0.99190102, 0.99190102, 1.00097782, 0.99190102, 1.03035393,
+                              1.00097782, 0.99190102, 1.02119353, 1.00097782, 1.00097782,
+                              0.98308249, 0.99990338, 1.02377208, 0.99190102, 1.00097782,
+                              0.99190102, 0.98308249, 0.99190102, 0.98743885, 1.01195055,
+                              0.97603687, 0.99190102, 1.02115759, 0.99190102, 0.99059663,
+                              1.02377208, 1.00097782, 1.00134271, 1.02115759, 1.00097782,
+                              0.99190102, 1.00097782, 0.99190102, 1.0013055 , 0.9577269 ,
+                              0.99190102, 1.00097782, 1.02115759, 1.00077649, 1.01928347,
+                              0.99070507, 0.98308249, 0.99190102]),
+                    atol=NUMPY_atol)
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    assert drift_explainer.cat_feature_indices == []
+    assert drift_explainer.class_names == ['0', '1']
+    assert drift_explainer.feature_names == ['mean radius',
+                                             'mean texture',
+                                             'mean perimeter',
+                                             'mean area',
+                                             'mean smoothness',
+                                             'mean compactness',
+                                             'mean concavity',
+                                             'mean concave points',
+                                             'mean symmetry',
+                                             'mean fractal dimension',
+                                             'radius error',
+                                             'texture error',
+                                             'perimeter error',
+                                             'area error',
+                                             'smoothness error',
+                                             'compactness error',
+                                             'concavity error',
+                                             'concave points error',
+                                             'symmetry error',
+                                             'fractal dimension error',
+                                             'worst radius',
+                                             'worst texture',
+                                             'worst perimeter',
+                                             'worst area',
+                                             'worst smoothness',
+                                             'worst compactness',
+                                             'worst concavity',
+                                             'worst concave points',
+                                             'worst symmetry',
+                                             'worst fractal dimension']
+    assert drift_explainer.iteration_range == (0, 126)
+    assert drift_explainer.n_features == 30
+    assert drift_explainer.task == 'classification'
+
+
+def test_iris_xgboost_XGBClassifier():
+    dataset = datasets.load_iris()
+    X = pd.DataFrame(dataset.data, columns=dataset.feature_names)
+    y = dataset.target
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_SEED)
+    clf = XGBClassifier(n_estimators=1000,
+                        booster="gbtree",
+                        objective="binary:logistic",
+                        learning_rate=0.05,
+                        max_depth=6,
+                        use_label_encoder=False,
+                        seed=RANDOM_SEED)
+    clf.fit(X=X_train, y=y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20, verbose=0)
+    drift_explainer = ModelDriftExplainer(clf)
+    drift_explainer.fit(X1=X_train, X2=X_test, y1=y_train, y2=y_test)
+
+    # prediction drift "raw"
+    prediction_drift_ref = [DriftMetricsNum(mean_difference=0.31093458145383807, wasserstein=0.310934581453838, ks_test=BaseStatisticalTestResult(statistic=0.06349206349206349, pvalue=0.9987212484986797)),
+                            DriftMetricsNum(mean_difference=0.3232848411632908, wasserstein=0.3318073130907522, ks_test=BaseStatisticalTestResult(statistic=0.12698412698412698, pvalue=0.6467769104301901)),
+                            DriftMetricsNum(mean_difference=-0.5564053781212321, wasserstein=0.5568392310587188, ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143, pvalue=0.2821678346768163))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(),
+                                    prediction_drift_ref)
+
+    # prediction drift "proba"
+    prediction_drift_proba_ref = [DriftMetricsNum(mean_difference=0.06122570359665486, wasserstein=0.06138405880136859, ks_test=BaseStatisticalTestResult(statistic=0.1111111111111111, pvalue=0.793799989988573)),
+                                  DriftMetricsNum(mean_difference=0.08154049228640303, wasserstein=0.08205600790975118, ks_test=BaseStatisticalTestResult(statistic=0.12698412698412698, pvalue=0.6467769104301901)),
+                                  DriftMetricsNum(mean_difference=-0.1427661934187488, wasserstein=0.14276781702443725, ks_test=BaseStatisticalTestResult(statistic=0.19047619047619047, pvalue=0.1805850065949114))]
+    assert_drift_metrics_list_equal(drift_explainer.get_prediction_drift(prediction_type='proba'),
+                                    prediction_drift_proba_ref)
+
+    # target drift
+    assert_drift_metrics_equal(drift_explainer.get_target_drift(),
+                               DriftMetricsCat(wasserstein=0.09523809523809523,
+                                               jensen_shannon=0.07382902143706498,
+                                               chi2_test=Chi2TestResult(statistic=1.3333333333333333,
+                                                                        pvalue=0.5134171190325922,
+                                                                        dof=2,
+                                                                        contingency_table=pd.DataFrame([[33.0, 34.0, 38.0], [17.0, 16.0, 12.0]],
+                                                                                                       index=['X1', 'X2'], columns=[0, 1, 2]))))
+
+    # performance_metrics_drift
+    assert_performance_metrics_drift_equal(drift_explainer.get_performance_metrics_drift(),
+                                           PerformanceMetricsDrift(ClassificationMetrics(accuracy=1.0, log_loss=0.045063073312242824),
+                                                                   ClassificationMetrics(accuracy=0.9333333333333333, log_loss=0.16192325585418277)))
+
+    # tree_based_drift_values "node_size"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='node_size'),
+                    np.array([[ 1.96002375],
+                              [ 4.03749391],
+                              [16.37810068],
+                              [24.55940758]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean'),
+                    np.array([[ 0.        ,  0.18063744, -0.0278192 ],
+                              [ 0.        , -0.00393914, -0.06837239],
+                              [ 0.31093455,  0.03153828, -0.26869759],
+                              [ 0.        ,  0.11504829, -0.19151622]]),
+                    atol=NUMPY_atol)
+
+    # tree_based_drift_values "mean_norm"
+    assert_allclose(drift_explainer.get_tree_based_drift_values(type='mean_norm'),
+                    np.array([[ 0.        ,  0.01680469,  0.00507903],
+                              [ 0.        , -0.00718861,  0.02268815],
+                              [ 0.31093455, -0.12144406,  0.0417556 ],
+                              [ 0.        ,  0.06756211, -0.16671063]]),
+                    atol=NUMPY_atol)
+
+    # feature_drift - argument as a string
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift('petal width (cm)'),
+                               DriftMetricsNum(mean_difference=-0.16412698412698457,
+                                               wasserstein=0.16412698412698412,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143,
+                                                                                 pvalue=0.2821678346768163)))
+
+    # feature_drift - argument as integer
+    assert_drift_metrics_equal(drift_explainer.get_feature_drift(2),
+                               DriftMetricsNum(mean_difference=-0.2765079365079357,
+                                               wasserstein=0.2777777777777778,
+                                               ks_test=BaseStatisticalTestResult(statistic=0.1523809523809524,
+                                                                                 pvalue=0.41885114043708227)))
+
+    # all feature drifts
+    feature_drifts_ref = [DriftMetricsNum(mean_difference=-0.18571428571428505, wasserstein=0.19968253968253974, ks_test=BaseStatisticalTestResult(statistic=0.16507936507936508, pvalue=0.3237613427576299)),
+                          DriftMetricsNum(mean_difference=-0.08825396825396803, wasserstein=0.1301587301587301, ks_test=BaseStatisticalTestResult(statistic=0.14285714285714285, pvalue=0.499646880472137)),
+                          DriftMetricsNum(mean_difference=-0.2765079365079357, wasserstein=0.2777777777777778, ks_test=BaseStatisticalTestResult(statistic=0.1523809523809524, pvalue=0.41885114043708227)),
+                          DriftMetricsNum(mean_difference=-0.16412698412698457, wasserstein=0.16412698412698412, ks_test=BaseStatisticalTestResult(statistic=0.17142857142857143, pvalue=0.2821678346768163))]
+    assert_drift_metrics_list_equal(drift_explainer.get_feature_drifts(),
+                                    feature_drifts_ref)
+
+    # tree_based_correction_weights with default params
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(),
+                    np.array([1.52938066, 0.70303783, 0.69585122, 1.14692382, 1.14698728,
+                              0.72033464, 1.22632416, 0.72033464, 1.22632416, 1.142113  ,
+                              1.23736756, 1.142113  , 0.56368673, 0.86018518, 1.22632416,
+                              1.22632416, 0.7208303 , 1.22632416, 1.14698728, 1.14698728,
+                              0.69775839, 1.04277591, 1.23736756, 1.22632416, 0.70428396,
+                              1.13630004, 0.7208303 , 0.69775839, 1.142113  , 0.73174406,
+                              1.23736756, 1.22632416, 1.22632416, 0.69785836, 1.19780314,
+                              1.15211681, 1.14698728, 1.22632416, 0.69585122, 1.03271783,
+                              1.15703378, 0.57917234, 1.1944477 , 1.40219276, 0.70476857,
+                              1.23736756, 1.03271783, 0.69775839, 1.22632416, 0.71906011,
+                              1.12826509, 1.142113  , 1.22632416, 1.19985586, 0.70585718,
+                              0.72652107, 1.22632416, 1.142113  , 1.15703378, 1.22632416,
+                              1.19780314, 1.142113  , 1.142113  , 0.72028392, 1.22632416,
+                              0.78968558, 0.71906011, 1.19780314, 1.22632416, 1.24462716,
+                              0.68456622, 1.23933794, 0.39611022, 0.69585122, 1.15703378,
+                              1.19780314, 0.61547879, 1.24462716, 0.7208303 , 1.22632416,
+                              0.7018943 , 0.69785836, 1.32591399, 0.82419426, 1.19011568,
+                              1.22632416, 1.15703378, 0.69775839, 0.68852529, 1.24462716,
+                              1.22632416, 0.70428396, 1.19780314, 0.34753964, 0.71906011,
+                              0.84691392, 1.22632416, 1.04749734, 1.22632416, 1.142113  ,
+                              0.69775839, 0.72702098, 0.84691392, 1.02224718, 0.72028392]),
+                    atol=NUMPY_atol)
+
+    # tree_based_correction_weights with "max_depth = 1"
+    assert_allclose(drift_explainer.get_tree_based_correction_weights(max_depth=1),
+                    np.array([0.94703727, 0.81154305, 0.80612326, 1.0295544 , 1.0295544 ,
+                              0.81154305, 1.20640805, 0.81154305, 1.20640805, 1.0295544 ,
+                              1.21451906, 1.0295544 , 0.82080803, 0.82908081, 1.20640805,
+                              1.20640805, 0.81154305, 1.20640805, 1.0295544 , 1.0295544 ,
+                              0.80612326, 1.0295544 , 1.21451906, 1.20640805, 0.81154305,
+                              1.0295544 , 0.81154305, 0.80612326, 1.0295544 , 0.81154305,
+                              1.21451906, 1.20640805, 1.20640805, 0.81154305, 1.20640805,
+                              1.0295544 , 1.0295544 , 1.20640805, 0.80612326, 0.9700073 ,
+                              1.0295544 , 0.83122672, 0.9700073 , 0.94703727, 0.81154305,
+                              1.21451906, 0.9700073 , 0.80612326, 1.20640805, 0.81154305,
+                              1.0295544 , 1.0295544 , 1.20640805, 0.9700073 , 0.81154305,
+                              0.81154305, 1.20640805, 1.0295544 , 1.0295544 , 1.20640805,
+                              1.20640805, 1.0295544 , 1.0295544 , 0.81154305, 1.20640805,
+                              0.82080803, 0.81154305, 1.20640805, 1.20640805, 1.21451906,
+                              0.81154305, 1.21451906, 0.83122672, 0.80612326, 1.0295544 ,
+                              1.20640805, 0.84918986, 1.21451906, 0.81154305, 1.20640805,
+                              0.81154305, 0.81154305, 0.95784911, 0.82080803, 0.9700073 ,
+                              1.20640805, 1.0295544 , 0.80612326, 0.81154305, 1.21451906,
+                              1.20640805, 0.81154305, 1.20640805, 0.83122672, 0.81154305,
+                              1.0295544 , 1.20640805, 1.0295544 , 1.20640805, 1.0295544 ,
+                              0.80612326, 0.81154305, 1.0295544 , 0.96352923, 0.81154305]),
+                    atol=NUMPY_atol)
+
+    # sample_weights1
+    assert_allclose(drift_explainer.sample_weights1[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    # sample_weights2
+    assert_allclose(drift_explainer.sample_weights2[:5],
+                    np.array([1., 1., 1., 1., 1.]),
+                    atol=NUMPY_atol)
+
+    assert drift_explainer.cat_feature_indices == []
+    assert drift_explainer.class_names == ['0', '1', '2']
+    assert drift_explainer.feature_names == ['sepal length (cm)',
+                                             'sepal width (cm)',
+                                             'petal length (cm)',
+                                             'petal width (cm)']
+    assert drift_explainer.iteration_range == (0, 97)
+    assert drift_explainer.n_features == 4
+    assert drift_explainer.task == 'classification'
diff --git a/src/cinnamon/model_parser/tests/.gitignore b/src/cinnamon/model_parser/tests/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/src/cinnamon/model_parser/tests/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file