diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 6ac82de..e325410 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,10 @@
# ExternalExplainers
-External explainer implementations for PD-EXPLAIN
+External explainer implementations for [PD-EXPLAIN](https://github.com/analysis-bots/pd-explain).\
+While you can use the explainer implementations in this repository directly, it is recommended to use them through the PD-EXPLAIN library,
+for a much better and more user-friendly experience.
+## Included Explainers
+### Outlier explainer
+This explainer is based on the [SCORPION](https://sirrice.github.io/files/papers/scorpion-vldb13.pdf) paper.\
+Its goal is to provide explanations for outliers in the data, explaining why a certain data point is an outlier.\
+This explainer is meant to work on series created as a result of groupby + aggregation operations.\
+Explainer author: [@Itay Elyashiv](https://github.com/ItayELY)
diff --git a/dist/external_explainers-1.0.0-py3-none-any.whl b/dist/external_explainers-1.0.0-py3-none-any.whl
new file mode 100644
index 0000000..2b0aba1
Binary files /dev/null and b/dist/external_explainers-1.0.0-py3-none-any.whl differ
diff --git a/dist/external_explainers-1.0.0.tar.gz b/dist/external_explainers-1.0.0.tar.gz
new file mode 100644
index 0000000..8139646
Binary files /dev/null and b/dist/external_explainers-1.0.0.tar.gz differ
diff --git a/examples/notebooks/Outlier explainer demo.ipynb b/examples/notebooks/Outlier explainer demo.ipynb
new file mode 100644
index 0000000..04d3260
--- /dev/null
+++ b/examples/notebooks/Outlier explainer demo.ipynb
@@ -0,0 +1,380 @@
+{
+ "cells": [
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "# Outlier explainer demo",
+ "id": "e604881e784a9f9a"
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "This notebook demonstrates the usage of the outlier explainer.\\\n",
+ "Note that it is recommended to use our other package, pd-explain, for better ease of use and more features."
+ ],
+ "id": "ca1fceec8df719a6"
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "## Imports and loading data",
+ "id": "2ac929f2c20f6248"
+ },
+ {
+ "cell_type": "code",
+ "id": "initial_id",
+ "metadata": {
+ "collapsed": true,
+ "ExecuteTime": {
+ "end_time": "2025-02-10T14:28:34.539967Z",
+ "start_time": "2025-02-10T14:28:33.726878Z"
+ }
+ },
+ "source": [
+ "import pandas as pd\n",
+ "from external_explainers import OutlierExplainer"
+ ],
+ "outputs": [],
+ "execution_count": 1
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-02-10T14:28:37.085917Z",
+ "start_time": "2025-02-10T14:28:34.548929Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "spotify_data = pd.read_csv('https://raw.githubusercontent.com/analysis-bots/pd-explain/refs/heads/main/Examples/Datasets/spotify_all.csv')\n",
+ "spotify_data.head()"
+ ],
+ "id": "4a877eb58c908b7d",
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ " acousticness artists danceability energy explicit \\\n",
+ "0 0.991000 ['Mamie Smith'] 0.598 0.224 0 \n",
+ "1 0.643000 [\"Screamin' Jay Hawkins\"] 0.852 0.517 0 \n",
+ "2 0.993000 ['Mamie Smith'] 0.647 0.186 0 \n",
+ "3 0.000173 ['Oscar Velazquez'] 0.730 0.798 0 \n",
+ "4 0.295000 ['Mixe'] 0.704 0.707 1 \n",
+ "\n",
+ " id instrumentalness key liveness loudness ... \\\n",
+ "0 0cS0A1fUEUd1EW3FcF8AEI 0.000522 5 0.3790 -12.628 ... \n",
+ "1 0hbkKFIJm7Z05H8Zl9w30f 0.026400 5 0.0809 -7.261 ... \n",
+ "2 11m7laMUgmOKqI3oYzuhne 0.000018 0 0.5190 -12.098 ... \n",
+ "3 19Lc5SfJJ5O1oaxY0fpwfh 0.801000 2 0.1280 -7.311 ... \n",
+ "4 2hJjbsLCytGsnAHfdsLejp 0.000246 10 0.4020 -6.036 ... \n",
+ "\n",
+ " name popularity speechiness \\\n",
+ "0 Keep A Song In Your Soul 12 0.0936 \n",
+ "1 I Put A Spell On You 7 0.0534 \n",
+ "2 Golfing Papa 4 0.1740 \n",
+ "3 True House Music - Xavier Santos & Carlos Gomi... 17 0.0425 \n",
+ "4 Xuniverxe 2 0.0768 \n",
+ "\n",
+ " tempo valence year decade popularity_score main_artist \\\n",
+ "0 149.976 0.6340 1920 1920 10 Mamie Smith \n",
+ "1 86.889 0.9500 1920 1920 0 Screamin' Jay Hawkins \n",
+ "2 97.600 0.6890 1920 1920 0 Mamie Smith \n",
+ "3 127.997 0.0422 1920 1920 10 Oscar Velazquez \n",
+ "4 122.076 0.2990 1920 1920 0 Mixe \n",
+ "\n",
+ " duration_minutes \n",
+ "0 2.805550 \n",
+ "1 2.503333 \n",
+ "2 2.730450 \n",
+ "3 7.034783 \n",
+ "4 2.753733 \n",
+ "\n",
+ "[5 rows x 21 columns]"
+ ],
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
acousticness
\n",
+ "
artists
\n",
+ "
danceability
\n",
+ "
energy
\n",
+ "
explicit
\n",
+ "
id
\n",
+ "
instrumentalness
\n",
+ "
key
\n",
+ "
liveness
\n",
+ "
loudness
\n",
+ "
...
\n",
+ "
name
\n",
+ "
popularity
\n",
+ "
speechiness
\n",
+ "
tempo
\n",
+ "
valence
\n",
+ "
year
\n",
+ "
decade
\n",
+ "
popularity_score
\n",
+ "
main_artist
\n",
+ "
duration_minutes
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
0.991000
\n",
+ "
['Mamie Smith']
\n",
+ "
0.598
\n",
+ "
0.224
\n",
+ "
0
\n",
+ "
0cS0A1fUEUd1EW3FcF8AEI
\n",
+ "
0.000522
\n",
+ "
5
\n",
+ "
0.3790
\n",
+ "
-12.628
\n",
+ "
...
\n",
+ "
Keep A Song In Your Soul
\n",
+ "
12
\n",
+ "
0.0936
\n",
+ "
149.976
\n",
+ "
0.6340
\n",
+ "
1920
\n",
+ "
1920
\n",
+ "
10
\n",
+ "
Mamie Smith
\n",
+ "
2.805550
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
0.643000
\n",
+ "
[\"Screamin' Jay Hawkins\"]
\n",
+ "
0.852
\n",
+ "
0.517
\n",
+ "
0
\n",
+ "
0hbkKFIJm7Z05H8Zl9w30f
\n",
+ "
0.026400
\n",
+ "
5
\n",
+ "
0.0809
\n",
+ "
-7.261
\n",
+ "
...
\n",
+ "
I Put A Spell On You
\n",
+ "
7
\n",
+ "
0.0534
\n",
+ "
86.889
\n",
+ "
0.9500
\n",
+ "
1920
\n",
+ "
1920
\n",
+ "
0
\n",
+ "
Screamin' Jay Hawkins
\n",
+ "
2.503333
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
0.993000
\n",
+ "
['Mamie Smith']
\n",
+ "
0.647
\n",
+ "
0.186
\n",
+ "
0
\n",
+ "
11m7laMUgmOKqI3oYzuhne
\n",
+ "
0.000018
\n",
+ "
0
\n",
+ "
0.5190
\n",
+ "
-12.098
\n",
+ "
...
\n",
+ "
Golfing Papa
\n",
+ "
4
\n",
+ "
0.1740
\n",
+ "
97.600
\n",
+ "
0.6890
\n",
+ "
1920
\n",
+ "
1920
\n",
+ "
0
\n",
+ "
Mamie Smith
\n",
+ "
2.730450
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
0.000173
\n",
+ "
['Oscar Velazquez']
\n",
+ "
0.730
\n",
+ "
0.798
\n",
+ "
0
\n",
+ "
19Lc5SfJJ5O1oaxY0fpwfh
\n",
+ "
0.801000
\n",
+ "
2
\n",
+ "
0.1280
\n",
+ "
-7.311
\n",
+ "
...
\n",
+ "
True House Music - Xavier Santos & Carlos Gomi...
\n",
+ "
17
\n",
+ "
0.0425
\n",
+ "
127.997
\n",
+ "
0.0422
\n",
+ "
1920
\n",
+ "
1920
\n",
+ "
10
\n",
+ "
Oscar Velazquez
\n",
+ "
7.034783
\n",
+ "
\n",
+ "
\n",
+ "
4
\n",
+ "
0.295000
\n",
+ "
['Mixe']
\n",
+ "
0.704
\n",
+ "
0.707
\n",
+ "
1
\n",
+ "
2hJjbsLCytGsnAHfdsLejp
\n",
+ "
0.000246
\n",
+ "
10
\n",
+ "
0.4020
\n",
+ "
-6.036
\n",
+ "
...
\n",
+ "
Xuniverxe
\n",
+ "
2
\n",
+ "
0.0768
\n",
+ "
122.076
\n",
+ "
0.2990
\n",
+ "
1920
\n",
+ "
1920
\n",
+ "
0
\n",
+ "
Mixe
\n",
+ "
2.753733
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
5 rows × 21 columns
\n",
+ "
"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 2
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "## Outlier detection",
+ "id": "84056d9e106e6990"
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "The outlier explainer is meant for usage on series that are a result of aggregation operations.\\\n",
+ "First, we perform a groupby and aggregation on the data."
+ ],
+ "id": "2c10b272c0aaa9ce"
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-02-10T14:28:37.372620Z",
+ "start_time": "2025-02-10T14:28:37.345307Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "new_songs_df = spotify_data[spotify_data['year'] >= 1990]\n",
+ "gb_decade = new_songs_df.groupby('decade')['popularity'].mean()\n",
+ "gb_decade"
+ ],
+ "id": "c38c258934cdca14",
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "decade\n",
+ "1990 43.120769\n",
+ "2000 43.167320\n",
+ "2010 29.579203\n",
+ "2020 19.171014\n",
+ "Name: popularity, dtype: float64"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 3
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "Now we can the outlier to explain suspected outliers.",
+ "id": "36ce2fef356a95ba"
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-02-10T14:29:22.957533Z",
+ "start_time": "2025-02-10T14:29:21.682728Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "outlier_explainer = OutlierExplainer()\n",
+ "outlier_explainer.explain(df_agg=gb_decade, df_in=new_songs_df, g_att='decade', g_agg='popularity', agg_method='mean', target=2020, dir=-1)"
+ ],
+ "id": "545e243e9f81631a",
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ],
+ "image/png": "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YYKIiIhkYZggIiIiWRgmiIiISBaGCSIiIpKFYYKIiIhkYZggIiIiWRgmiIiISBbDwix069YtHDhwADExMVCr1RrTJk2apJPCiIiIqHTQOkysWLECw4cPh4ODA5ycnKBQKKRpCoWCYYKIiKiM0TpMzJgxA19//TW++OKLoqiHiIiIShmt+0zExcWhZ8+eRVELERERlUJah4mePXtiz549RVELERERlUJan+aoVq0aJk6ciBMnTsDb2xtGRkYa00ePHq2z4oiIiKjk0zpMLF++HBYWFjh48CAOHjyoMU2hUDBMEBERlTFah4nw8PCiqIOIiIhKKV60ioiIiGQp1EWrHj58iL///hsRERFIT0/XmPb999/rpDAiIiIqHbQOE/v27UOXLl1QtWpV3LhxA15eXrh37x6EEKhfv35R1EhEREQlmNanOUJCQjB+/HhcvnwZJiYm+OOPP/DgwQM0b96c158gIiIqg7QOE9evX8eAAQMAAIaGhnjx4gUsLCwwbdo0fPvttzovkIiIiEo2rcOEubm51E+iQoUKuHPnjjTt6dOnuquMiIiISgWt+0w0btwYR44cgaenJzp27Ihx48bh8uXL+PPPP9G4ceOiqJGIiIhKMK3DxPfff4/k5GQAwNSpU5GcnIzffvsN1atX50gOIiKiMkjrMFG1alXp/+bm5vjxxx91WhARERGVLoW6aFV8fDxWrlyJkJAQxMbGAgDOnTuHR48e6bQ4IiIiKvm0PjJx6dIltGnTBtbW1rh37x4++eQT2NnZ4c8//0RERAR+/vnnoqiTiIiISiitj0wEBwcjMDAQt27dgomJidTesWNHHDp0SKfFERERUcmndZg4ffo0hg0blqu9YsWKiIqK0klRREREVHpoHSZUKhUSExNztd+8eRPlypXTSVFERERUemgdJrp06YJp06YhIyMDAKBQKBAREYEvvvgC3bt313mBREREVLJpHSbmzp2L5ORklC9fHi9evEDz5s1RrVo1WFpa4uuvvy6KGomIiKgE03o0h7W1Nfbu3YsjR47g0qVLSE5ORv369dGmTZuiqI+IiIhKOK3DRI4mTZqgSZMmuqyFiIiISqFChYnTp0/jwIEDiImJgVqt1pjGS2oTERGVLVqHiW+++QZfffUVatasCUdHRygUCmnay/8nIiKiskHrMLFgwQL89NNPCAwMLIJyiIiIqLTRejSHUqmEv79/UdRCREREpZDWYWLs2LFYsmRJUdRCREREpZDWpznGjx+PTp06wd3dHbVq1YKRkZHG9D///FNnxREREVHJp/WRidGjR+PAgQOoUaMG7O3tYW1trfGjjaVLl8LHxwdWVlawsrKCn58fdu7cKU1PTU3FiBEjYG9vDwsLC3Tv3h3R0dHalkxERERFSOsjE2vXrsUff/yBTp06yd54pUqVMGvWLFSvXh1CCKxduxbvv/8+zp8/j9q1a2Ps2LHYsWMHNm3aBGtra4wcORLdunXD0aNHZW+biIiIdEPrMGFnZwd3d3edbLxz584aj7/++mssXboUJ06cQKVKlbBq1SqsX78erVq1AgCsXr0anp6eOHHiBBo3bpznOtPS0pCWliY9zuumZERERKQ7Wp/mmDJlCiZPnoznz5/rtJCsrCxs3LgRKSkp8PPzw9mzZ5GRkaFxmW4PDw9UrlwZx48fz3c9M2fO1Djt4uLiotM6iYiISJPWRyYWLlyIO3fuwNHREVWqVMnVAfPcuXNare/y5cvw8/NDamoqLCws8Ndff6FWrVq4cOECjI2NYWNjozG/o6MjoqKi8l1fSEgIgoODpceJiYkMFEREREVI6zDRtWtXnRZQs2ZNXLhwAQkJCdi8eTMGDhyIgwcPFnp9KpUKKpVKhxUSERHR62gdJiZPnqzTAoyNjVGtWjUAgK+vL06fPo0FCxagd+/eSE9PR3x8vMbRiejoaDg5Oem0BiLSrypf7tB3CQVyz0TfFRCVTFr3mShqarUaaWlp8PX1hZGREfbt2ydNCwsLQ0REBPz8/PRYIREREb2s0Lcg14WQkBB06NABlStXRlJSEtavX4/Q0FDs3r0b1tbWGDx4MIKDg2FnZwcrKyuMGjUKfn5++Y7kICIiouKn1zARExODAQMG4PHjx7C2toaPjw92796Ntm3bAgDmzZsHpVKJ7t27Iy0tDQEBAfjhhx/0WTIRERG9Qq9hYtWqVa+dbmJigiVLlvBeIERERCWY1n0mDhw4UBR1EBERUSmldZho37493N3dMWPGDDx48KAoaiIiIqJSROsw8ejRI4wcORKbN29G1apVERAQgN9//x3p6elFUR8RERGVcFqHCQcHB4wdOxYXLlzAyZMnUaNGDXz22WdwdnbG6NGjcfHixaKok4iIiEooWdeZqF+/PkJCQjBy5EgkJyfjp59+gq+vL5o2bYqrV6/qqkYiIiIqwQoVJjIyMrB582Z07NgRrq6u2L17NxYvXozo6Gjcvn0brq6u6Nmzp65rJSIiohJI66Gho0aNwoYNGyCEwEcffYTZs2fDy8tLmm5ubo45c+bA2dlZp4USERFRyaR1mLh27RoWLVqEbt265XtDLQcHBw4hJSIiKiO0Ps0xefJk9OzZM1eQyMzMxKFDhwAAhoaGaN68uW4qJCIiohJN6zDRsmVLxMbG5mpPSEhAy5YtdVIUERERlR5ahwkhBBQKRa72Z8+ewdzcXCdFERERUelR4D4T3bp1AwAoFAoEBgZqnObIysrCpUuX8O677+q+QiIiIirRChwmrK2tAWQfmbC0tISpqak0zdjYGI0bN8Ynn3yi+wqJiIioRCtwmFi9ejUAoEqVKhg/fjxPaRARERGAQgwNnTx5clHUQURERKVUgcJE/fr1sW/fPtja2qJevXp5dsDMce7cOZ0VR0RERCVfgcLE+++/L3W47Nq1a1HWQ0RERKVMgcJEzqmNrKwstGzZEj4+PrCxsSnKuoiIiKiU0Oo6EwYGBmjXrh3i4uKKqh4iIiIqZbS+aJWXlxfu3r1bFLUQERFRKaR1mJgxYwbGjx+P7du34/Hjx0hMTNT4ISIiorJF66GhHTt2BAB06dJFY1RHzmW2s7KydFcdERERlXhahwneWpyIiIhepnWY4K3FiYiI6GVah4kcz58/R0REBNLT0zXafXx8ZBdFREREpYfWYeLJkycYNGgQdu7cmed09pkgIiIqW7QezREUFIT4+HicPHkSpqam2LVrF9auXYvq1avj77//LooaiYiIqATT+sjE/v37sXXrVjRo0ABKpRKurq5o27YtrKysMHPmTHTq1Kko6iQiIqISSusjEykpKShfvjwAwNbWFk+ePAEAeHt78yZfREREZZDWYaJmzZoICwsDANSpUwfLli3Do0eP8OOPP6JChQo6L5CIiIhKNq1Pc4wZMwaPHz8GkH0DsPbt22PdunUwNjbGmjVrdF0fERERlXBah4n+/ftL//f19cX9+/dx48YNVK5cGQ4ODjotjoiIiEq+Ql9nIoeZmRnq16+vi1qIiIioFCpQmAgODi7wCr///vtCF0NERESlT4HCxPnz5wu0spdv/EVERERlQ4HCBG/uRURERPnRemgoERER0cu07oDZsmXL157O2L9/v6yCiIiIqHTROkzUrVtX43FGRgYuXLiAK1euYODAgbqqi4iIiEoJrcPEvHnz8myfMmUKkpOTZRdEREREpYvO+kz0798fP/30k65WR0RERKWEzsLE8ePHYWJioqvVERERUSmh9WmObt26aTwWQuDx48c4c+YMJk6cqLPCiIiIqHTQOkxYW1trPFYqlahZsyamTZuGdu3a6awwIiIiKh20DhOrV68uijqIiIiolCr0jb7OnDmD69evAwBq1aoFX19fnRVFREREpYfWYeLhw4fo27cvjh49ChsbGwBAfHw83n33XWzcuBGVKlXSdY1ERERUgmk9mmPIkCHIyMjA9evXERsbi9jYWFy/fh1qtRpDhgwpihqJiIioBNP6yMTBgwdx7Ngx1KxZU2qrWbMmFi1ahKZNm+q0OCIiIir5tD4y4eLigoyMjFztWVlZcHZ21klRREREVHpoHSa+++47jBo1CmfOnJHazpw5gzFjxmDOnDk6LY6IiIhKPq1PcwQGBuL58+do1KgRDA2zF8/MzIShoSE+/vhjfPzxx9K8sbGxuquUiIiISiStw8T8+fOLoAwiIiIqrbQOE7zNOBEREb2sUBetysrKwpYtW6SLVtWuXRtdunSBgYGBTosjIiKikk/rDpi3b9+Gp6cnBgwYgD///BN//vkn+vfvj9q1a+POnTtarWvmzJl45513YGlpifLly6Nr164ICwvTmCc1NRUjRoyAvb09LCws0L17d0RHR2tbNhERERURrcPE6NGj4e7ujgcPHuDcuXM4d+4cIiIi4ObmhtGjR2u1roMHD2LEiBE4ceIE9u7di4yMDLRr1w4pKSnSPGPHjsW2bduwadMmHDx4EJGRkbnuXEpERET6U6iLVp04cQJ2dnZSm729PWbNmgV/f3+t1rVr1y6Nx2vWrEH58uVx9uxZNGvWDAkJCVi1ahXWr1+PVq1aAci+0ZinpydOnDiBxo0ba1s+ERER6ZjWRyZUKhWSkpJytScnJ8PY2FhWMQkJCQAgBZWzZ88iIyMDbdq0kebx8PBA5cqVcfz48TzXkZaWhsTERI0fIiIiKjpah4n33nsPQ4cOxcmTJyGEgBACJ06cwKeffoouXboUuhC1Wo2goCD4+/vDy8sLABAVFQVjY2PphmI5HB0dERUVled6Zs6cCWtra+nHxcWl0DURERHRm2kdJhYuXAh3d3f4+fnBxMQEJiYm8Pf3R7Vq1bBgwYJCFzJixAhcuXIFGzduLPQ6ACAkJAQJCQnSz4MHD2Stj4iIiF5P6z4TNjY22Lp1K27duoXr169DoVDA09MT1apVK3QRI0eOxPbt23Ho0CGNW5g7OTkhPT0d8fHxGkcnoqOj4eTklOe6VCoVVCpVoWshIiIi7RTqOhMAUL16dSlAKBSKQq1DCIFRo0bhr7/+QmhoKNzc3DSm+/r6wsjICPv27UP37t0BAGFhYYiIiICfn19hSyciIiIdKlSYWLVqFebNm4dbt24ByA4WQUFBGDJkiFbrGTFiBNavX4+tW7fC0tJS6gdhbW0NU1NTWFtbY/DgwQgODoadnR2srKwwatQo+Pn5cSQHEVEx6r29N56+eKrvMgqktNT5NtE6TEyaNAnff/+99EcdAI4fP46xY8ciIiIC06ZNK/C6li5dCgBo0aKFRvvq1asRGBgIAJg3bx6USiW6d++OtLQ0BAQE4IcfftC2bCIikuHpi6eIeR6j7zKohNI6TCxduhQrVqxA3759pbYuXbrAx8cHo0aN0ipMCCHeOI+JiQmWLFmCJUuWaFsqERHpmBAKiExLfZfxWgrDRBTy7DsVktZhIiMjAw0aNMjV7uvri8zMTJ0URUREJZPItETK7f/ou4zXsvAIAfDmL6ukO1oPDf3oo4+k0xMvW758Ofr166eTooiIiKj0KHQHzD179kidIE+ePImIiAgMGDAAwcHB0nzff/+9bqokIiKiEkvrMHHlyhXUr18fAKS7hDo4OMDBwQFXrlyR5ivscFEiIiIqXbQOEwcOHCiKOoiIiKiU0rrPBBEREdHLGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikoVhgoiIiGRhmCAiIiJZGCaIiIhIFoYJIiIikkWvYeLQoUPo3LkznJ2doVAosGXLFo3pQghMmjQJFSpUgKmpKdq0aYNbt27pp1giIiLKk17DREpKCurUqYMlS5bkOX327NlYuHAhfvzxR5w8eRLm5uYICAhAampqMVdKRERE+THU58Y7dOiADh065DlNCIH58+fjq6++wvvvvw8A+Pnnn+Ho6IgtW7agT58+eS6XlpaGtLQ06XFiYqLuCyciIiJJie0zER4ejqioKLRp00Zqs7a2RqNGjXD8+PF8l5s5cyasra2lHxcXl+Iol4iIqMwqsWEiKioKAODo6KjR7ujoKE3LS0hICBISEqSfBw8eFGmdREREZZ1eT3MUBZVKBZVKpe8yiIiIyowSe2TCyckJABAdHa3RHh0dLU0jIiIi/SuxYcLNzQ1OTk7Yt2+f1JaYmIiTJ0/Cz89Pj5URERHRy/R6miM5ORm3b9+WHoeHh+PChQuws7ND5cqVERQUhBkzZqB69epwc3PDxIkT4ezsjK5du+qvaCIiItKg1zBx5swZtGzZUnocHBwMABg4cCDWrFmDzz//HCkpKRg6dCji4+PRpEkT7Nq1CyYmJvoqmYiIiF6h1zDRokULCCHyna5QKDBt2jRMmzatGKsiIiIibZTYPhNERERUOjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBERkSwME0RERCQLwwQRERHJwjBBREREsjBMEBER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"
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "execution_count": 5
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "Once again, we recommend doing this via pd-explain for a more streamlined experience, where most of these parameters are automatically inferred via the user's previous actions.",
+ "id": "4e3338ab3267da02"
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..8cf3256
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..62768ef
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+numpy~=2.1.3
+pandas~=2.2.3
+matplotlib~=3.9.2
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..7b7f978
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,42 @@
+import os
+
+from setuptools import setup, find_packages
+
+
+def read(rel_path):
+ here = os.path.abspath(os.path.dirname(__file__))
+ with open(os.path.join(here, rel_path), 'r') as fp:
+ return fp.read()
+
+
+def get_version():
+ for line in read('src/fedex_generator/__init__.py').splitlines():
+ if line.startswith('__version__'):
+ delim = '"' if '"' in line else "'"
+ return line.split(delim)[1]
+ else:
+ raise RuntimeError("Unable to find version string.")
+
+
+def get_long_description():
+ with open('README.md', 'r') as fh:
+ return fh.read()
+
+
+setup(
+ name='external_explainers',
+ version='1.0.0',#get_version(),
+ package_dir={'': 'src'},
+ packages=find_packages(where='src'),
+ long_description_content_type="text/markdown",
+ long_description=get_long_description(), # Long description read from the readme file
+ project_urls={
+ 'Git': 'https://github.com/analysis-bots/ExternalExplainers',
+ },
+ install_requires=[
+ 'wheel',
+ 'pandas',
+ 'numpy',
+ 'matplotlib',
+ ]
+)
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/external_explainers/__init__.py b/src/external_explainers/__init__.py
new file mode 100644
index 0000000..562c41a
--- /dev/null
+++ b/src/external_explainers/__init__.py
@@ -0,0 +1 @@
+from external_explainers.outlier_explainer.outlier_explainer import OutlierExplainer
\ No newline at end of file
diff --git a/src/external_explainers/commons/__init__.py b/src/external_explainers/commons/__init__.py
new file mode 100644
index 0000000..718fc6a
--- /dev/null
+++ b/src/external_explainers/commons/__init__.py
@@ -0,0 +1 @@
+from external_explainers.commons import utils
\ No newline at end of file
diff --git a/src/external_explainers/commons/utils.py b/src/external_explainers/commons/utils.py
new file mode 100644
index 0000000..50fd8fe
--- /dev/null
+++ b/src/external_explainers/commons/utils.py
@@ -0,0 +1,25 @@
+def to_valid_latex(string, is_bold: bool = False) -> str:
+ """
+ Convert a string to a valid latex string.
+ :param string: The input string.
+ :param is_bold: Whether the string should be bold.
+ :return: The latex string.
+ """
+ latex_string = str(string) # unicode_to_latex(string)
+ # Choose the space character based on whether the string should be bold.
+ space = r'\ ' if is_bold else ' '
+ # Escape special characters and replace spaces with the chosen space character.
+ final_str = latex_string.replace("&", "\\&").replace("#", "\\#").replace(' ', space).replace("_", space)
+ return final_str
+
+
+def to_valid_latex_with_escaped_dollar_char(string, is_bold: bool = False) -> str:
+ """
+ Convert a string to a valid latex string.
+ This function adds the $ character to the list of characters that are escaped, while the function to_valid_latex
+ does not escape the $ character. Other than that, the two functions are identical.
+ :param string: The input string.
+ :param is_bold: Whether the string should be bold.
+ :return: The latex string.
+ """
+ return to_valid_latex(string, is_bold).replace("$", "\\$")
\ No newline at end of file
diff --git a/src/external_explainers/outlier_explainer/__init__.py b/src/external_explainers/outlier_explainer/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/external_explainers/outlier_explainer/outlier_explainer.py b/src/external_explainers/outlier_explainer/outlier_explainer.py
new file mode 100644
index 0000000..7e0f896
--- /dev/null
+++ b/src/external_explainers/outlier_explainer/outlier_explainer.py
@@ -0,0 +1,394 @@
+from matplotlib import pyplot as plt
+import numpy as np
+import pandas as pd
+from pandas.core.interchange.dataframe_protocol import DataFrame
+from typing import List, Tuple, Any
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+from external_explainers.commons import utils
+
+ALPHA = 0.9
+START_BOLD = r'$\bf{'
+END_BOLD = '}$'
+
+HIGH = 1
+LOW = -1
+
+
+# BETA = 0.1
+def proportion(series):
+ c = series.count()
+ return (series.count()) / (series.count().sum())
+
+
+class OutlierExplainer:
+ """
+ A class for computing the outlier measure and explaining outliers.\n
+ This is based on the article "Scorpion: Explaining Away Outliers in Aggregate Queries" by Eugene Wu, Samuel Madden.\n
+ The influence of a predicate :math:`p` on an output result :math:`o` is depends on:\n
+ .. math:: \\Delta_{agg} (o,p) = agg(g_0) - agg(g_0 - p(g_0))\n
+ Where: \n
+ - :math:`agg` is the aggregation function.\n
+ - :math:`g_0` is a set of input tuples.\n
+ In other words, it is the difference between the original result and the result after removing the tuples that satisfy the predicate.\n
+ From which we define the influence as:\n
+ .. math:: inf_{agg}(o,p) = \\frac{\\Delta_{agg}(o,p)}{\\Delta g_0} = \\frac{\\Delta_{agg}(o,p)}{|p(g_0)|}\n
+ We then include the error direction, :math:`d`, to the influence calculation:\n
+ .. math:: inf_{agg}(o,p, d) = d \\ * \\ inf_{agg}(o,p) \n
+ Next, the user may select a hold-out result, :math:`h`, that the returned predicate should not influence.\n
+ Intuitively, :math:`p` should be penalized if it influences the hold-out results in any way.\n
+ The influence with the hold-out set is defined as:\n
+ .. math:: inf_{agg}(o,p,d,h) = \\lambda \\ * \\ inf_{agg}(o,p,d) - (1 - \\lambda) \\ * \\ inf_{agg}(h,p)\n
+ Where:\n
+ - :math:`\\lambda` is a parameter that represents the importance of not changing the value of the hold-out set.
+ In this project we use :math:`\\lambda = 0.9`.\n
+ """
+
+ def __init__(self):
+ super().__init__()
+
+ def calc_influence_pred(self, df_before: DataFrame, df_after: DataFrame, target: str, dir: int) -> float:
+ """
+ Calculate the influence of a predicate on a given target attribute.
+
+ This function computes the influence of a target attribute by comparing its values
+ before and after some transformation, adjusted by a direction factor. It also
+ considers the influence of other attributes in the DataFrame.
+
+ :param df_before: DataFrame containing the data before the transformation.
+ :param df_after: DataFrame containing the data after the transformation.
+ :param target (str)) The target attribute for which the influence is being calculated.
+ :param dir: Direction factor, either 1 or -1, indicating the direction of the outlier. 1 for high, -1 for low.
+
+ :return: The calculated influence score. Returns -1 if an error occurs during calculation.
+ """
+ try:
+ # Compute target influence - the ratio between the change in the output and the number of
+ # tuples that satisfy the predicate, multiplied by the direction factor.
+ target_inf = ((df_before[target] - df_after[target]) * dir) / (df_before[target] + df_after[target])
+ except:
+ return -1
+
+ # Compute the holdout influence - the sum of the square root of the absolute difference between the
+ # values of the target attribute for each tuple in the DataFrame before and after the transformation.
+ holdout_inf = 0
+ for i in df_before.index:
+ if i != target:
+ try:
+ holdout_inf += np.sqrt(abs(df_after[i] - df_before[i]))
+ except:
+ return -1
+ # Return the final influence score, calculated as a weighted sum of the target and holdout influences.
+ return ALPHA * (target_inf) - (1 - ALPHA) * (holdout_inf / (len(df_before.index)))
+
+ def merge_preds(self, df_agg: DataFrame, df_in: DataFrame, df_in_consider: DataFrame,
+ preds: List[Tuple[str, Tuple[float, float], float, str, int | None]], g_att: str, g_agg: str,
+ agg_method: str, target, dir: int) -> tuple[
+ list[tuple[str, tuple[float, float], int | None]], float, Any | None]:
+ """
+ Merge predicates to find the most influential attributes.
+
+ This function iterates over a list of predicates, applying filters to the input DataFrame to exclude
+ certain rows based on the predicates. It calculates the influence of each predicate and keeps track
+ of the most influential ones. The final influence score and the filtered DataFrame are returned.
+
+ :param df_agg: DataFrame containing the aggregated data.
+ :param df_in: DataFrame containing the input data.
+ :param df_in_consider: DataFrame containing the data to be considered for filtering.
+ :param preds: A list of tuples containing predicates with attribute name, bin or value, score, kind, and rank.
+ :param g_att: The grouping attribute.
+ :param g_agg: The aggregation attribute.
+ :param agg_method: The aggregation method to be used.
+ :param target: The target attribute for which the influence is being calculated.
+ :param dir: Direction factor, either 1 or -1, indicating the direction of the outlier. 1 for high, -1 for low.
+
+ :return: A tuple containing a list of final predicates, the final influence score, and the final aggregated DataFrame.
+ """
+ # Initialize variables to store the final predicates, influence score, and aggregated DataFrame.
+ final_pred = []
+ final_inf = 0.001
+ final_agg_df = None
+ final_filter = False
+ final_filter_df = False
+
+ prev_attrs = []
+
+ for p in preds:
+ attr, i, score, kind, rank = p
+
+ # Avoid going over previously seen attributes
+ if attr in prev_attrs:
+ continue
+ prev_attrs.append(attr)
+
+ # If the kind is 'bin', we use the bin values to filter the DataFrame.
+ if kind == 'bin':
+ bin = i
+ final_filter_test = final_filter | ((df_in_consider[attr] < bin[0]) | (df_in_consider[attr] >= bin[1]))
+ final_filter_df_test = final_filter_df | ((df_in[attr] < bin[0]) | (df_in[attr] >= bin[1]))
+ # Otherwise, we use the attribute value to filter the DataFrame.
+ else:
+ final_filter_test = final_filter | (df_in_consider[attr] != i)
+ final_filter_df_test = final_filter_df | (df_in[attr] != i)
+
+ # Apply the filter to the input DataFrame and the aggregated DataFrame.
+ df_exc_consider = df_in_consider[final_filter_test]
+ df_exc_final = df_in[final_filter_df_test]
+
+ # Perform the aggregation operation on the filtered DataFrames.
+ agged_val_consider = df_exc_consider.groupby(g_att)[g_agg].agg(agg_method)
+ agged_val = df_exc_final.groupby(g_att)[g_agg].agg(agg_method)
+
+ # Normalize the aggregated values if the aggregation method is 'count'.
+ if agg_method == 'count':
+ agged_val_consider = agged_val_consider / agged_val_consider.sum()
+ agged_val = agged_val / agged_val.sum()
+
+ # Compute the influence score for the predicate.
+ inf = self.calc_influence_pred(df_agg, agged_val_consider, target, dir) / pow(
+ (df_in_consider.shape[0] / (df_exc_consider.shape[0] + 1)), 2)
+
+ # If the influence score is greater by a factor of at least 1.3, update the final predicates.
+ if inf / final_inf > 1.3:
+ final_pred.append((attr, i, rank))
+ final_inf = inf
+
+ final_agg_df = agged_val
+ final_filter = final_filter_test
+ final_filter_df = final_filter_df_test
+ # If the influence score is not much higher, break the loop.
+ else:
+ break
+ return final_pred, final_inf, final_agg_df
+
+
+ def compute_predicates_per_attribute(self, attr: str, df_in: DataFrame, g_att: str,
+ g_agg: str, agg_method: str, target: str, dir: int,
+ df_in_consider: DataFrame, df_agg_consider: DataFrame) -> List[Tuple[str, Any, float, str, int]]:
+ """
+ Compute predicates for a given attribute.
+
+ This function calculates the influence of various predicates on a target attribute
+ by iterating over the values or bins of the given attribute. It generates a list of
+ predicates with their corresponding influence scores.
+
+ :param attr: The attribute for which predicates are being computed.
+ :param df_in: DataFrame containing the input data.
+ :param g_att: The grouping attribute.
+ :param g_agg: The aggregation attribute.
+ :param agg_method: The aggregation method to be used.
+ :param target: The target attribute for which the influence is being calculated.
+ :param dir: Direction factor, either 1 or -1, indicating the direction of the outlier.
+ :param df_in_consider: DataFrame containing the data to be considered for filtering.
+ :param df_agg_consider: DataFrame containing the aggregated data to be considered.
+
+ :return: A list of predicates with their influence scores.
+ """
+ dtype = df_in[attr].dtype.name
+ predicates = []
+ exps = {}
+
+ # Ignore attributes with high correlation to the target attribute.
+ if dtype in ['int64', 'float64']:
+ if (df_in[g_att].dtype.name in ['int64', 'float64'] and df_in[g_att].corr(df_in[attr]) > 0.7) or (
+ df_in[g_agg].dtype.name in ['int64', 'float64'] and df_in[g_agg].corr(df_in[attr]) > 0.7):
+ return []
+
+ # Get the series for the attribute and its data type.
+ series = df_in[attr]
+ dtype = df_in[attr].dtype.name
+ flag = False
+ df_in_consider_attr = df_in_consider[[g_att, g_agg, attr]]
+
+ # If the data type is not 'float64', calculate the influence score for each value of the attribute.
+ if dtype not in ['float64']:
+ vals = series.value_counts()
+ if dtype != 'int64' or len(vals) < 20:
+ # Skip attributes with more than 50 unique values, as they are too computationally expensive.
+ if len(vals) > 50:
+ return []
+ flag = True
+
+ for i in vals.index:
+
+ # Exclude rows with the current value of the attribute.
+ df_in_target_exc = df_in_consider_attr[(df_in_consider_attr[attr] != i)]
+ # Aggregate the values for the excluded rows.
+ agged_val = df_in_target_exc.groupby(g_att)[g_agg].agg(agg_method)
+ if agg_method == 'count':
+ agged_val = agged_val / agged_val.sum()
+
+ # Calculate the influence score for the predicate.
+ inf = self.calc_influence_pred(df_agg_consider, agged_val, target, dir) / (
+ (df_in_consider.shape[0] / (df_in_target_exc.shape[0] + 0.01)))
+
+ exps[(attr, i)] = inf
+ predicates.append((attr, i, inf, 'cat', None))
+
+ n_bins = 20
+ if not flag:
+ _, bins = pd.cut(series, n_bins, retbins=True, duplicates='drop')
+ df_bins_in = pd.cut(df_in_consider_attr[attr], bins=bins).value_counts(
+ normalize=True).sort_index() # .rename('idx')
+ i = 1
+ for bin in df_bins_in.keys():
+ new_bin = (float("{:.2f}".format(bin.left)), float("{:.2f}".format(bin.right)))
+ df_in_exc = df_in_consider_attr[
+ ((df_in_consider_attr[attr] < new_bin[0]) | (df_in_consider_attr[attr] >= new_bin[1]))]
+ agged_val = df_in_exc.groupby(g_att)[g_agg].agg(agg_method)
+ if agg_method == 'count':
+ agged_val = agged_val / agged_val.sum()
+
+ # Calculate the influence score for the predicate.
+ inf = self.calc_influence_pred(df_agg_consider, agged_val, target, dir) / (
+ (df_in_consider_attr.shape[0] / df_in_exc.shape[0]) + 1)
+
+ # Store the influence score for the predicate.
+ exps[(attr, (new_bin[0], new_bin[1]))] = inf
+
+ # Add the predicate to the list of predicates.
+ predicates.append((attr, new_bin, inf, 'bin', i))
+ i += 1
+
+ return predicates
+
+ def draw_bar_plot(self, df_agg: DataFrame, final_df: DataFrame, g_att: str, g_agg: str, final_pred_by_attr: dict,
+ target: str, agg_title: str) -> None:
+ """
+ Draw a bar plot to visualize the influence of predicates on the target attribute.
+
+ This function generates a bar plot comparing the aggregated values of the target attribute
+ before and after applying the most influential predicates. It highlights the differences
+ and provides an explanation for the outlier.
+
+ :param df_agg: DataFrame containing the aggregated data.
+ :param final_df: DataFrame containing the final aggregated data after applying predicates.
+ :param g_att: The grouping attribute.
+ :param g_agg: The aggregation attribute.
+ :param final_pred_by_attr: Dictionary containing the final predicates grouped by attribute.
+ :param target: The target attribute for which the influence is being visualized.
+ :param agg_title: Title for the aggregation method used in the plot.
+
+ :return: None. Displays the bar plot.
+ """
+ fig, ax = plt.subplots(layout='constrained', figsize=(5, 5))
+ x1 = list(df_agg.index)
+ ind1 = np.arange(len(x1))
+ y1 = df_agg.values
+
+ x2 = list(final_df.index)
+ ind2 = np.arange(len(x2))
+ y2 = final_df.values
+
+ explanation = f'This outlier is not as significant when excluding rows with:\n'
+ for_wizard = ''
+ for a, bins in final_pred_by_attr.items():
+ for b in bins:
+ if type(b[0]) is tuple:
+ pred = f"{b[0][0]} < {a} < {b[0][1]}"
+ inter_exp = r'$\bf{{{}}}$'.format(utils.to_valid_latex(pred))
+ else:
+ pred = f"{a}={b[0]}"
+ inter_exp = r'$\bf{{{}}}$'.format(utils.to_valid_latex(pred))
+ if b[1] is not None:
+ if b[1] <= 5:
+ inter_exp = inter_exp + '-' + r'$\bf{low}$'
+ elif b[1] >= 25:
+ inter_exp = inter_exp + '-' + r'$\bf{high}$'
+ inter_exp += '\n'
+ for_wizard += inter_exp
+ explanation += inter_exp
+
+ bar1 = ax.bar(ind1 - 0.2, y1, 0.4, alpha=1., label='All')
+ bar2 = ax.bar(ind2 + 0.2, y2, 0.4, alpha=1., label=f'without\n{for_wizard}')
+ ax.set_ylabel(f'{g_agg} {agg_title}')
+ ax.set_xlabel(f'{g_att}')
+ ax.set_xticks(ind1)
+ ax.set_xticklabels(tuple([str(i) for i in x1]), rotation=45)
+ ax.legend(loc='best')
+ ax.set_title(explanation)
+ bar1[x1.index(target)].set_edgecolor('tab:green')
+ bar1[x1.index(target)].set_linewidth(2)
+ bar2[x2.index(target)].set_edgecolor('tab:green')
+ bar2[x2.index(target)].set_linewidth(2)
+ ax.get_xticklabels()[x1.index(target)].set_color('tab:green')
+
+ plt.show()
+
+
+ def explain(self, df_agg: DataFrame, df_in: DataFrame, g_att: str, g_agg: str, agg_method: str, target: str,
+ dir: int, control=None, hold_out: List = [], k: int = 1) -> str | None:
+ """
+ Explain the outlier in the given DataFrame.
+
+ This function identifies and explains outliers in the given DataFrame by calculating the influence of
+ various attributes on the target attribute. It iterates over the attributes, applies filters, and
+ computes the influence score for each attribute. The most influential attributes are then used to
+ generate an explanation for the outlier.
+
+ :param df_agg: DataFrame containing the aggregated data.
+ :param df_in: DataFrame containing the input data.
+ :param g_att: The grouping attribute.
+ :param g_agg: The aggregation attribute.
+ :param agg_method: The aggregation method to be used.
+ :param target: The target attribute for which the influence is being calculated.
+ :param dir: Direction factor, either 1 or -1, indicating the direction of the outlier. 1 for high, -1 for low.
+ :param control: List of control values for the grouping attribute.
+ :param hold_out: List of attributes to be held out from the analysis.
+ :param k: Number of top attributes to consider for the explanation.
+
+ :return: None. Will generate a plot with the explanation for the outlier.
+ """
+ # Get the attributes from the input DataFrame and remove the hold-out attributes.
+ attrs = df_in.columns
+ attrs = [a for a in attrs if a not in hold_out + [g_att, g_agg]]
+
+ agg_title = agg_method
+ if agg_method == 'count':
+ df_agg = df_agg / df_agg.sum()
+
+ predicates = []
+
+ # If no control values are provided, use all values for the grouping attribute.
+ if control == None:
+ control = list(df_agg.index)
+ df_in_consider = df_in
+ df_agg_consider = df_agg # [control]
+
+ # Iterate over the attributes, calculate the influence score, and generate predicates.
+ with ThreadPoolExecutor() as executor:
+ futures = [
+ executor.submit(self.compute_predicates_per_attribute, attr, df_in, g_att, g_agg, agg_method, target,
+ dir, df_in_consider, df_agg_consider) for attr in attrs
+ ]
+ for future in as_completed(futures):
+ preds = future.result()
+ predicates += preds
+
+
+ # Sort the predicates by influence score in descending order.
+ predicates.sort(key=lambda x: -x[2])
+
+ # Merge the predicates to find the most influential attributes.
+ final_pred, final_inf, final_df = self.merge_preds(df_agg_consider, df_in, df_in_consider, predicates, g_att,
+ g_agg, agg_method, target, dir)
+
+ # If the final DataFrame is empty, return an error message. Otherwise, generate the explanation plot.
+ if final_df is None:
+ return "There was no explanation."
+
+ # Create a new DataFrame with the aggregated values and the control values.
+ new_df_agg = df_agg.copy()
+ new_df_agg[control] = final_df[control]
+ new_df_agg[target] = final_df[target]
+ final_pred_by_attr = {}
+
+ # Group the final predicates by attribute.
+ for a, i, rank in final_pred:
+ if a not in final_pred_by_attr.keys():
+ final_pred_by_attr[a] = []
+ final_pred_by_attr[a].append((i, rank))
+
+ # Create a plot to display the explanation for the outlier.
+ self.draw_bar_plot(df_agg, final_df, g_att, g_agg, final_pred_by_attr, target, agg_title)
+ return None