├── .github
└── workflows
│ ├── python-package.yml
│ └── python-publish.yaml
├── .gitignore
├── LICENSE.txt
├── Makefile
├── README.md
├── auditai
├── __init__.py
├── is_newest_version.py
├── misc.py
├── simulations.py
├── stats.py
├── tests
│ ├── __init__.py
│ ├── test_crosstabs.py
│ ├── test_misc.py
│ ├── test_nothing.py
│ ├── test_simulations.py
│ ├── test_stats.py
│ ├── test_utils.py
│ └── test_viz.py
├── utils
│ ├── __init__.py
│ ├── cmh.py
│ ├── crosstabs.py
│ ├── functions.py
│ ├── general.py
│ └── validate.py
├── version.py
└── viz.py
├── data
├── GermanCreditData.csv
├── auditAI_gender_plot.png
└── student-mat.csv
├── examples
├── CMH_test_example.ipynb
├── GermanCreditData_BiasCheck.ipynb
├── StudentPerformanceData_BiasCheck.ipynb
└── implementation_suggestions.md
├── setup.cfg
├── setup.py
└── tox.ini
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | name: Python Package
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version: [2.7, 3.7, 3.8]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v1
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | make install-dev
27 | - name: Lint with flake8
28 | run: |
29 | pip install flake8
30 | # stop the build if there are Python syntax errors or undefined names
31 | flake8 auditai --count --select=E9,F63,F7,F82 --show-source --statistics
32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
33 | flake8 auditai --count --exit-zero --max-complexity=11 --max-line-length=127 --statistics
34 | - name: Test with pytest
35 | run: |
36 | make tests
37 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yaml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Set up Python
15 | uses: actions/setup-python@v1
16 | with:
17 | python-version: '3.7'
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | pip install setuptools wheel twine
22 | - name: Build and publish
23 | env:
24 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
25 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
26 | run: |
27 | python setup.py sdist bdist_wheel
28 | twine upload dist/*
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | .DS_Store
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 | Untitled.ipynb
75 |
76 | # pyenv
77 | .python-version
78 |
79 | # celery beat schedule file
80 | celerybeat-schedule
81 |
82 | # SageMath parsed files
83 | *.sage.py
84 |
85 | # dotenv
86 | .env
87 |
88 | # virtualenv
89 | .venv
90 | venv/
91 | ENV/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 pymetrics, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash
2 |
3 | build:
4 | pip install -e .
5 |
6 | install-dev: clean
7 | pip install -e ".[dev]"
8 |
9 | tests:
10 | pytest --cov-config=setup.cfg --cov=auditai --cov-fail-under=65
11 |
12 | lint:
13 | flake8 ./auditai
14 |
15 | covhtml:
16 | coverage html
17 | open ./htmlcov/index.html
18 |
19 | clean:
20 | @rm -Rf *.egg-info .eggs .tox build dist htmlcov .coverage
21 | @find ./ -depth -type d -name __pycache__ -exec rm -Rf {} \;
22 | @find ./ -type f \( -iname \*.pyc -o -iname \*.pyo -o -iname \*~ \) -delete
23 |
24 | test-all: tests lint
25 |
26 | pubdev: is_newest_version clean
27 | python setup.py bdist_wheel
28 | twine upload --repository testpypi dist/*
29 |
30 | is_newest_version:
31 | python auditai/is_newest_version.py
32 |
33 | publish: is_newest_version clean
34 | python setup.py bdist_wheel
35 | twine upload --repository pypi dist/*
36 |
37 | tox_tests:
38 | tox
39 |
40 | .PHONY: build install-dev tests lint coverage covhtml clean test-all pubdev publish
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # audit-AI
3 |
4 |
5 |
6 | Open Sourced Bias Testing for Generalized Machine Learning Applications
7 |
8 | `audit-AI` is a Python library built on top of `pandas` and `sklearn` that
9 | implements fairness-aware machine learning algorithms. `audit-AI` was developed
10 | by the Data Science team at [pymetrics](https://www.pymetrics.com/)
11 |
12 | # Bias Testing for Generalized Machine Learning Applications
13 |
14 | `audit-AI` is a tool to measure and mitigate the effects of discriminatory
15 | patterns in training data and the predictions made by machine learning
16 | algorithms trained for the purposes of socially sensitive decision processes.
17 |
18 | The overall goal of this research is to come up with a reasonable way to think
19 | about how to make machine learning algorithms more fair. While identifying
20 | potential bias in training datasets and by consequence the machine learning
21 | algorithms trained on them is not sufficient to solve the problem of
22 | discrimination, in a world where more and more decisions are being automated
23 | by Artificial Intelligence, our ability to understand and identify the degree
24 | to which an algorithm is fair or biased is a step in the right direction.
25 |
26 | # Regulatory Compliance and Checks for Practical and Statistical Bias
27 |
28 | According to the Uniform Guidelines on Employee Selection Procedures (UGESP;
29 | EEOC et al., 1978), all assessment tools should comply to fair standard of
30 | treatment for all protected groups. Audit-ai extends this to machine learning
31 | methods. Let's say we build a model that makes some prediction about people.
32 | This model could theoretically be anything -- a prediction of credit scores,
33 | the likelihood of prison recidivism, the cost of a home loan, etc. Audit-ai
34 | takes data from a known population (e.g., credit information from people of
35 | multiple genders and ethnicities), and runs them through the model in question.
36 | The proportional pass rates of the highest-passing demographic group are compared
37 | to the lowest-passing group for each demographic category (gender and
38 | ethnicity). This proportion is known as the bias ratio.
39 |
40 | Audit-ai determines whether groups are different according to a standard of
41 | statistical significance (within a statistically different margin of error) or
42 | practical significance (whether a difference is large enough to matter on a
43 | practical level). The exact threshold of statistical and practical significance
44 | depends on the field and use-case. Within the hiring space, the EEOC often
45 | uses a statistical significance of p < .05 to determine bias, and a bias ratio
46 | below the 4/5ths rule to demonstrate practical significance.
47 |
48 | The 4/5ths rule effectively states that the lowest-passing group has to be
49 | within 4/5ths of the pass rate of the highest-passing group. Consider an example
50 | with 4,000 users, 1,000 of each of the following groups: Asian, Black,
51 | Hispanic/Latino, and White, who pass at a frequency of 250, 270, 240 and 260
52 | users, respectively. The highest and lowest passing groups are Black (27%) and
53 | Hispanic/Latino (24%), respectively. The bias ratio is therefore 24/27 or .889.
54 | As this ratio is greater than .80 (4/5ths), the legal requirement enforced by
55 | the EEOC, the model would pass the check for practical significance. Likewise,
56 | a chi-squared test (a common statistical test for count data) would report that
57 | these groups are above the p = .05 threshold, and therefore pass the check for
58 | statistical significance.
59 |
60 | Audit-ai also offers tools to check for differences over time or across
61 | different regions, using the Cochran-Mantel-Hanzel test, a common test in
62 | regulatory circles. To our knowledge this is the first implementation of this
63 | measure in an open-source python format.
64 |
65 | # Features
66 |
67 | Here are a few of the bias testing and algorithm auditing techniques
68 | that this library implements.
69 |
70 | ### Classification tasks
71 |
72 | - 4/5th, fisher, z-test, bayes factor, chi squared
73 | - sim_beta_ratio, classifier_posterior_probabilities
74 |
75 | ### Regression tasks
76 |
77 | - anova
78 | - 4/5th, fisher, z-test, bayes factor, chi squared
79 | - group proportions at different thresholds
80 |
81 | # Installation
82 |
83 | The source code is currently hosted on GitHub: https://github.com/pymetrics/audit-ai
84 |
85 | You can install the latest released version with `pip`.
86 |
87 | ```
88 | # pip
89 | pip install audit-AI
90 | ```
91 |
92 | If you install with pip, you'll need to install scikit-learn, numpy, and pandas
93 | with either pip or conda. Version requirements:
94 |
95 | - numpy
96 | - scipy
97 | - pandas
98 |
99 | For vizualization:
100 | - matplotlib
101 | - seaborn
102 |
103 | # How to use this package:
104 |
105 | See our implementation paper here: https://github.com/pymetrics/audit-ai/blob/master/examples/implementation_suggestions.md
106 |
107 | ```python
108 |
109 | from auditai.misc import bias_test_check
110 |
111 | X = df.loc[:,features]
112 | y_pred = clf.predict_proba(X)
113 |
114 | # test for bias
115 | bias_test_check(labels=df['gender'], results=y_pred, category='Gender')
116 |
117 | >>> *Gender passes 4/5 test, Fisher p-value, Chi-Squared p-value, z-test p-value and Bayes Factor at 50.00*
118 |
119 | ```
120 | To get a plot of the different tests at different thresholds:
121 |
122 | ```python
123 |
124 | from auditai.viz import plot_threshold_tests
125 |
126 | X = df.loc[:,features]
127 | y_pred = clf.predict_proba(X)
128 |
129 | # test for bias
130 | plot_threshold_tests(labels=df['gender'], results=y_pred, category='Gender')
131 |
132 | ```
133 |
134 |
135 | # Example Datasets
136 |
137 | - [german-credit](https://archive.ics.uci.edu/ml/datasets/statlog+(german+credit+data))
138 | - [student-performance](https://archive.ics.uci.edu/ml/datasets/student+performance)
139 |
--------------------------------------------------------------------------------
/auditai/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pymetrics/audit-ai/4891b1d3c813a0e6ce657eaee3f1b2ab50e8f429/auditai/__init__.py
--------------------------------------------------------------------------------
/auditai/is_newest_version.py:
--------------------------------------------------------------------------------
1 | from distutils.version import StrictVersion
2 | import requests
3 |
4 |
5 | exec(open('auditai/version.py').read())
6 | PACKAGE_NAME = 'audit-AI'
7 | VERSION = StrictVersion(__version__) # noqa
8 |
9 |
10 | def versions(package_name):
11 | url = "https://pypi.python.org/pypi/{}/json".format(package_name)
12 | response = requests.get(url)
13 |
14 | data = response.json()
15 | versions = [StrictVersion(v) for v in data["releases"].keys()]
16 | versions.sort()
17 | return versions
18 |
19 |
20 | print('Version: ', VERSION)
21 | if not VERSION > versions(PACKAGE_NAME)[-1]:
22 | print("Version would not be the newest version.")
23 | exit(1)
24 |
25 | print("Version would be the newest version.")
26 |
--------------------------------------------------------------------------------
/auditai/misc.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | import numpy as np
4 | import pandas as pd
5 | from scipy.stats import chi2_contingency, fisher_exact, f_oneway
6 | from sklearn.metrics import mutual_info_score
7 |
8 | from .simulations import classifier_posterior_probabilities
9 | from .utils.crosstabs import (crosstab_bayes_factor,
10 | crosstab_ztest,
11 | top_bottom_crosstab)
12 | from .utils.validate import boolean_array, check_consistent_length
13 |
14 |
15 | def default_thresh_value(results):
16 | if len(set(results)) == 2:
17 | return np.mean(list(set(results)))
18 | return np.median(results)
19 |
20 |
21 | def anova(labels, results, subset_labels=None):
22 | """
23 | Returns one-way ANOVA f-statistic and p-value from
24 | input vectors of categorical labels and numeric results
25 |
26 | Parameters
27 | ------------
28 | labels : array_like
29 | containing categorical values like ['M', 'F']
30 | results : array_like
31 | containing real numbers
32 | subset_labels : list of strings, optional
33 | if only specific labels should be included
34 |
35 | Returns
36 | ----------
37 | F_onewayResult : scipy.stats object (essentially a 2-tuple)
38 | contains one-way f-statistic and p-value, indicating whether
39 | scores have same sample mean
40 |
41 | """
42 | check_consistent_length(labels, results)
43 |
44 | df = pd.DataFrame(list(zip(labels, results)), columns=['label', 'result'])
45 | if subset_labels is not None:
46 | df = df.loc[df['label'].isin(subset_labels)]
47 |
48 | unique_labels = df['label'].dropna().unique()
49 | score_vectors = [df.loc[df['label'] == lab, 'result']
50 | for lab in unique_labels]
51 | return f_oneway(*score_vectors)
52 |
53 |
54 | def bias_test_check(labels, results, category=None, test_thresh=None,
55 | **kwargs):
56 | """
57 | Utility function for checking if statistical tests are passed
58 | at a reference threshold
59 |
60 | Parameters
61 | --------
62 | labels : array_like
63 | containing categorical values like ['M', 'F']
64 | results : array_like
65 | containing real numbers
66 | category : string, optional
67 | the name of the category labels are in, e.g. 'Gender'
68 | test_thresh : numeric
69 | threshold value to test
70 | **kwargs : optional additional arguments for compare_groups
71 |
72 | Returns
73 | --------
74 | print statement indicating whether specific statistical tests pass or fail
75 | """
76 | if test_thresh is None:
77 | test_thresh = default_thresh_value(results)
78 |
79 | min_props, z_ps, fisher_ps, chi_ps, bfs = compare_groups(
80 | labels, results, low=test_thresh, num=1, **kwargs)
81 |
82 | # if no category is specified, concatenate strings
83 | if category is None:
84 | category = '_vs_'.join([str(i) for i in set(labels)])[:20]
85 | # test if passes at test_thresh
86 | bias_tests = {'4/5': {'results': min_props,
87 | 'check': lambda x: x < 0.8},
88 | 'Fisher exact': {'results': fisher_ps,
89 | 'check': lambda x: x < 0.05},
90 | 'Chi squared': {'results': chi_ps,
91 | 'check': lambda x: x < 0.05},
92 | 'z': {'results': z_ps,
93 | 'check': lambda x: x < 0.05},
94 | 'Bayes Factor': {'results': bfs,
95 | 'check': lambda x: x > 3.}
96 | }
97 |
98 | for name, test in bias_tests.items():
99 | stat_value = test['results'].get(test_thresh)
100 | if stat_value and not test['check'](stat_value):
101 | print('*{} passes {} test at {:.2f}*'.format(
102 | category, name, test_thresh))
103 | elif stat_value is not None:
104 | print('*{} fails {} test at {:.2f}*'.format(
105 | category, name, test_thresh))
106 | print(" - {} minimum proportion at {:.2f}: {:.3f}".format(
107 | category, test_thresh, stat_value))
108 | else:
109 | print("Unable to run {} test".format(name))
110 |
111 |
112 | def make_bias_report(clf, df, feature_names, categories, **kwargs):
113 | """
114 | Utility function for report dictionary from
115 | `classifier_posterior_probabilities`. Used for plotting
116 | bar plots in `bias_bar_plot`
117 |
118 | Parameters
119 | -----------
120 | clf : sklearn clf
121 | fitted clf with predict object
122 | df : pandas DataFrame
123 | reference dataframe containing labeled features to test for bias
124 | feature_names : list of strings
125 | names of features used in fitting clf
126 | categories : list of strings
127 | names of categories to test for bias, e.g. ['gender']
128 | ref_threshold : float (optional)
129 | cutoff value at which to generate metrics
130 | **kwargs : optional additional arguments for
131 | classifier_posterior_probabilities, specifically low, high, num
132 |
133 | Returns
134 | --------
135 | out_dict : dictionary
136 | contains category names, average probabilities and errors by category
137 | of form {'gender': {'categories':['F', 'M'],
138 | 'averages': [.5, .5],
139 | 'errors': [.1, .1]}
140 | }
141 | """
142 | ref_threshold = kwargs.pop("ref_threshold")
143 | threshes, probs = classifier_posterior_probabilities(
144 | df, clf, feature_names, categories, **kwargs)
145 |
146 | # if not specified, set ref_threshold at 80% of max(threshes)
147 | if ref_threshold is None:
148 | idx_80 = int(len(threshes)*.8)
149 | ref_threshold = sorted(threshes)[idx_80]
150 |
151 | ref_idx = list(threshes).index(ref_threshold)
152 |
153 | out_dict = {}
154 | for category in categories:
155 | cat_vals = [k.split('__')[1]
156 | for k in probs.keys() if k.split('__')[0] == category]
157 | cat_avgs = [probs[val][ref_idx][0] for val in cat_vals]
158 | cat_errors = [probs[val][ref_idx][1:] for val in cat_vals]
159 | out_dict[category] = {
160 | 'categories': cat_vals,
161 | 'averages': cat_avgs,
162 | 'errors': cat_errors}
163 |
164 | return out_dict
165 |
166 |
167 | def get_group_proportions(labels, results, **kwargs):
168 | """
169 | Returns pass proportions for each group present in labels, according to
170 | their results
171 |
172 | Parameters
173 | ------------
174 | labels : array_like
175 | contains categorical labels
176 | results : array_like
177 | contains numeric or boolean values
178 | **kwargs : optional
179 | additional values for thresholds to test:
180 | low : float
181 | if None, will default to min(results)
182 | high : float
183 | if None, will default to max(results)
184 | num : int, default 100
185 | number of thresholds to check
186 |
187 | Returns
188 | --------
189 | prop_dict: dictionary
190 | contains {group_name : [[thresholds, pass_proportions]]}
191 |
192 | """
193 | low = kwargs.get("low", min(results))
194 | high = kwargs.get("high", max(results))
195 | num = kwargs.get("num", 100)
196 | thresholds = np.linspace(low, high, num).tolist()
197 | groups = set(labels)
198 | prop_dict = defaultdict(list)
199 |
200 | for group in groups:
201 | pass_props = []
202 | for thresh in thresholds:
203 | decs = [i <= thresh for i in results]
204 | crosstab = pd.crosstab(pd.Series(labels), pd.Series(decs))
205 | row = crosstab.loc[group]
206 | pass_prop = row[True] / float(row.sum())
207 | pass_props.append(pass_prop)
208 | prop_dict[group].append(thresholds)
209 | prop_dict[group].append(pass_props)
210 | return prop_dict
211 |
212 |
213 | def compare_groups(labels, results,
214 | low=None, high=None, num=100,
215 | comp_groups=None, print_skips=False):
216 | """
217 | Function to plot proportion of largest and smallest bias groups and
218 | get relative z scores
219 |
220 | Parameters
221 | --------
222 | labels : array_like
223 | contains categorical values like ['M', 'F']
224 | results : array_like
225 | contains real numbers, e.g. threshold scores or floats in (0,1)
226 | low : float
227 | lower threshold value
228 | high : float
229 | upper threshold value
230 | num : int
231 | number of thresholds to check
232 | comp_groups : list of strings, optional
233 | subset of labels to compare, e.g. ['white', 'black']
234 | print_skips : bool
235 | whether to display thresholds skipped
236 |
237 | Returns
238 | ---------
239 | min_props : dict
240 | contains (key, value) of (threshold : max group/min group proportions)
241 | z_ps : dict
242 | contains (key, value) of (threshold : p-value of two tailed z test)
243 | fisher_ps : dict
244 | contains (key, value) of (threshold : p-value of fisher exact test)
245 | chi_ps : dict
246 | contains (key, value) of (threshold : p-value of chi squared test)
247 | bayes_facts : dict
248 | contains (key, value) of (threshold : bayes factor)
249 | """
250 |
251 | # cast labels and scores to pandas Series
252 | df = pd.DataFrame(list(zip(labels, results)), columns=['label', 'result'])
253 |
254 | min_props = {}
255 | fisher_ps = {}
256 | chi_ps = {}
257 | z_ps = {}
258 | bayes_facts = {}
259 |
260 | if comp_groups is not None:
261 | df = df[df['label'].isin(comp_groups)]
262 |
263 | # define range of values to test over if not inputted
264 | if low is None:
265 | low = min(results)
266 | if high is None:
267 | high = max(results)
268 |
269 | thresholds = np.linspace(low, high, num)
270 |
271 | skip_thresholds = []
272 | for thresh in thresholds:
273 |
274 | df['dec'] = [i >= thresh for i in results]
275 |
276 | # compare rates of passing across groups
277 | ctabs = pd.crosstab(df['label'], df['dec'])
278 |
279 | # skip any thresholds for which the crosstabs are one-dimensional
280 | if 1 in ctabs.shape:
281 | skip_thresholds.append(thresh)
282 | continue
283 |
284 | normed_ctabs = ctabs.div(ctabs.sum(axis=1), axis=0)
285 | true_val = max(set(df['dec']))
286 | max_group = normed_ctabs[true_val].max()
287 | normed_proportions = normed_ctabs[true_val] / max_group
288 | min_proportion = normed_proportions.min()
289 |
290 | # run statistical tests
291 | if ctabs.shape == (2, 2):
292 | test_results = test_multiple(df['label'].values, df['dec'].values)
293 | z_pval = test_results.get('z_score')[1]
294 | fisher_pval = test_results.get('fisher_p')[1]
295 | chi2_pval = test_results.get('chi2_p')[1]
296 | bayes_fact = test_results.get('BF')
297 |
298 | else:
299 | top_bottom_ctabs = top_bottom_crosstab(df['label'], df['dec'])
300 | z_pval = crosstab_ztest(top_bottom_ctabs)[1]
301 | fisher_pval = fisher_exact(top_bottom_ctabs)[1]
302 | chi2_pval = chi2_contingency(ctabs)[1]
303 | bayes_fact = crosstab_bayes_factor(ctabs)
304 |
305 | min_props[thresh] = min_proportion
306 | z_ps[thresh] = z_pval
307 | fisher_ps[thresh] = fisher_pval
308 | chi_ps[thresh] = chi2_pval
309 | bayes_facts[thresh] = bayes_fact
310 |
311 | if len(skip_thresholds) > 0 and print_skips:
312 | print('One-dimensional thresholds were skipped: {}'.format(
313 | skip_thresholds))
314 | return min_props, z_ps, fisher_ps, chi_ps, bayes_facts
315 |
316 |
317 | def proportion_test(labels, decisions):
318 | """
319 | Compare rates of passing across groups,
320 | relative to the highest passing group
321 |
322 | Parameters
323 | ----------
324 | labels : array_like
325 | categorical labels for each corresponding value of `decision` ie. M/F
326 |
327 | decisions : array_like
328 | binary decision values, ie. True/False, 0/1 or 'pass'/'fail'
329 | NB: the 'passing' value must evaluate to greater than the failing value
330 |
331 | Returns
332 | -------
333 | normed_proportions : pd.Series
334 | displays pass rates by `label` group
335 | relative to the highest passing group (which itself is always 1.0)
336 | """
337 | check_consistent_length(labels, decisions)
338 | decisions = boolean_array(decisions)
339 | crosstab = pd.crosstab(pd.Series(labels), pd.Series(decisions))
340 |
341 | # require crosstab not to be one-dimensional (e.g. one kind of label)
342 | if 1 in crosstab.shape:
343 | raise ValueError('One-dimensional data has no proportions')
344 |
345 | normed_ctabs = crosstab.div(crosstab.sum(axis=1), axis=0)
346 | true_val = max(set(decisions))
347 | max_group = normed_ctabs[true_val].max()
348 | normed_proportions = normed_ctabs[true_val] / max_group
349 | return normed_proportions
350 |
351 |
352 | def test_multiple(labels, decisions,
353 | tests=('ztest', 'fisher', 'chi2', 'BF', 'prop'),
354 | display=False):
355 | """
356 | Function that returns p_values for z-score, fisher exact, and chi2 test
357 | of 2x2 crosstab of passing rate by labels and decisions
358 |
359 | See docs for z_test_ctabs, fisher_exact, chi2_contingency and
360 | bf_ctabs for details of specific tests
361 |
362 | Parameters
363 | ----------
364 | labels : array_like
365 | categorical labels for each corresponding value of `decision` ie. M/F
366 |
367 | decisions : array_like
368 | binary decision values, ie. True/False or 0/1
369 |
370 | tests : list
371 | a list of strings specifying the tests to run, valid options
372 | are 'ztest', 'fisher', 'chi2' and 'bayes'. Defaults to all four.
373 | -ztest: p-value for two-sided z-score for proportions
374 | -fisher: p-value for Fisher's exact test for proportions
375 | -chi2: p-value for chi-squared test of independence for proportions
376 | -bayes: bayes factor for independence assuming uniform prior
377 | -prop: proportion of lowest to highest passing rates by group
378 |
379 | display : bool
380 | print the results of each test in addition to returning them
381 |
382 | Returns
383 | -------
384 | results : dict
385 | dictionary of values, one for each test.
386 | Valid keys are: 'z_score', 'fisher_p', 'chi2_p', 'BF', and 'prop'
387 |
388 | Examples
389 | --------
390 | >>> # no real difference between groups
391 | >>> labels = ['group1']*100 + ['group2']*100 + ['group3']*100
392 | >>> decisions = [1,0,0]*100
393 | >>> all_test_ctabs(dependent_ctabs)
394 | (0.0, 1.0, 1.0, 0.26162148804907587)
395 |
396 | >>> # massively biased ratio of hits/misses by group
397 | >>> ind_ctabs = np.array([[75,50],[25,50]])
398 | >>> all_test_ctabs(ind_ctabs)
399 | (-3.651483716701106,
400 | 0.0004203304586999487,
401 | 0.0004558800052056139,
402 | 202.95548692414306)
403 |
404 | >>> # correcting with a biased prior
405 | >>> biased_prior = np.array([[5,10],[70,10]])
406 | >>> all_test_ctabs(ind_ctabs, biased_prior)
407 | (-3.651483716701106,
408 | 0.0004203304586999487,
409 | 0.0004558800052056139,
410 | 0.00012159518854984268)
411 | """
412 |
413 | decisions = boolean_array(decisions)
414 | crosstab = pd.crosstab(pd.Series(labels), pd.Series(decisions))
415 | crosstab = crosstab.values
416 |
417 | # can only perform 2-group z-tests & fisher tests
418 | # getting crosstabs for groups with highest and lowest pass rates
419 | # as any difference between groups is considered biased
420 | tb_crosstab = top_bottom_crosstab(labels, decisions)
421 |
422 | results = {}
423 | if 'ztest' in tests:
424 | results['z_score'] = crosstab_ztest(tb_crosstab)
425 | if 'fisher' in tests:
426 | # although fisher's exact can be generalized to multiple groups
427 | # scipy is limited to shape (2, 2)
428 | # TODO make generalized fisher's exact test
429 | # returns oddsratio and p-value
430 | results['fisher_p'] = fisher_exact(tb_crosstab)[:2]
431 | if 'chi2' in tests:
432 | # returns chi2 test statistic and p-value
433 | results['chi2_p'] = chi2_contingency(crosstab)[:2]
434 | if 'BF' in tests:
435 | results['BF'] = crosstab_bayes_factor(crosstab)
436 | if 'prop' in tests:
437 | results['prop'] = min(proportion_test(labels, decisions))
438 |
439 | if display:
440 | for key in results:
441 | print("{}: {}".format(key, results[key]))
442 |
443 | return results
444 |
445 |
446 | def quick_bias_check(clf, df, feature_names, categories, thresh_pct=80,
447 | pass_ratio=.8):
448 | """
449 | Useful for generating a bias_report more quickly than make_bias_report
450 | simply uses np.percentile for checks
451 |
452 | Parameters
453 | -----------
454 | clf : sklearn clf
455 | fitted clf with predict object
456 | df : pandas DataFrame
457 | reference dataframe containing labeled features to test for bias
458 | feature_names : list of strings
459 | names of features used in fitting clf
460 | categories : list of strings
461 | names of categories to test for bias, e.g. ['gender', 'ethnicity']
462 | thresh_pct : float, default 80
463 | percentile in [0, 100] at which to check for pass rates
464 | pass_ratio : float, default .8
465 | cutoff specifying whether ratio of min/max pass rates is acceptable
466 |
467 | Returns
468 | --------
469 | passed: bool
470 | indicates whether all groups have min/max pass rates >= `pass_ratio`
471 | bias_report : dict
472 | of form {'gender': {'categories':['F', 'M'],
473 | 'averages': [.2, .22],
474 | 'errors': [[.2, .2], [.22, .22]]}
475 | }
476 | min_bias_ratio : float
477 | min of min_max_ratios across all categories
478 | if this value is less than `pass_ratio`, passed == False
479 | """
480 |
481 | bdf = df.copy()
482 | X = bdf.loc[:, feature_names].values
483 | decs = clf.decision_function(X)
484 | bdf['score'] = decs
485 |
486 | min_max_ratios = []
487 | bias_report = {}
488 | for category in categories:
489 | cat_df = bdf[bdf[category].notnull()]
490 | cat_df['pass'] = cat_df.score > np.percentile(cat_df.score, thresh_pct)
491 | cat_group = bdf.groupby(category).mean()['pass']
492 | cat_dict = cat_group.to_dict()
493 | min_max_ratios.append(cat_group.min()/float(cat_group.max()))
494 | bias_report[category] = {'averages': cat_dict.values(),
495 | 'categories': cat_dict.keys(),
496 | 'errors': [[i, i] for i in cat_dict.values()]
497 | }
498 |
499 | passed = all(np.array(min_max_ratios) >= pass_ratio)
500 | min_bias_ratio = min(min_max_ratios)
501 | return passed, bias_report, min_bias_ratio
502 |
503 |
504 | def one_way_mi(df, feature_list, group_column, y_var, bins):
505 |
506 | """
507 | Calculates one-way mutual information group variable and a
508 | target variable (y) given a feature list regarding.
509 |
510 | Parameters
511 | ----------
512 | df : pandas DataFrame
513 | df with features used to train model, plus a target variable
514 | and a group column.
515 | feature_list : list DataFrame
516 | List of strings, feature names.
517 | group_column : string
518 | name of column for testing bias, should contain numeric categories
519 | y_var : string
520 | name of target variable column
521 | bins : tuple
522 | number of bins for each dimension
523 |
524 | Returns
525 | -------
526 | mi_table : pandas DataFrame
527 | data frame with mutual information values, with one row per feature
528 | in the feature_list, columns for group and y.
529 | """
530 |
531 | group_cats = df[group_column].values
532 | y_cats = df[y_var].values
533 |
534 | c_g = [
535 | np.histogramdd([np.array(df[feature]), group_cats], bins=bins)[0]
536 | for feature in feature_list
537 | ]
538 | c_y = [
539 | np.histogramdd([np.array(df[feature]), y_cats], bins=bins)[0]
540 | for feature in feature_list
541 | ]
542 |
543 | # compute mutual information (MI) between trait and gender/eth/y
544 | mi_g = [mutual_info_score(None, None, contingency=i) for i in c_g]
545 | mi_y = [mutual_info_score(None, None, contingency=i) for i in c_y]
546 | mi_table = pd.DataFrame({'feature': feature_list,
547 | group_column: mi_g,
548 | y_var: mi_y})
549 |
550 | # NOTE: Scale group and y where the highest MI is scaled to 1 to
551 | # facilitate interpreting relative importance to bias and performance
552 | mi_table["{}_scaled".format(group_column)] = (
553 | mi_table[group_column] / mi_table[group_column].max()
554 | )
555 | mi_table["{}_scaled".format(y_var)] = (
556 | mi_table[y_var] / mi_table[y_var].max()
557 | )
558 |
559 | return mi_table
560 |
--------------------------------------------------------------------------------
/auditai/simulations.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict, OrderedDict
2 | from itertools import combinations
3 |
4 | import numpy as np
5 | import pandas as pd
6 | from scipy import stats
7 | from scipy.stats import chi2_contingency
8 |
9 | from .utils.general import get_unique_name
10 | from .utils.validate import ClassifierWrapper
11 |
12 |
13 | def classifier_posterior_probabilities(clf, df, feature_names, categories,
14 | low=None, high=None, num=100):
15 | """
16 | Function to compute posterior probabilities and 90% credible intervals
17 | for demographic categories
18 |
19 | Parameters
20 | ----------
21 | clf : sklearn clf
22 | fitted clf with predict object
23 | df : dataframe
24 | containing labeled columns to test for bias
25 | feature_names : list of strings
26 | names of features used in fitting clf
27 | categories : list of strings
28 | demographic columns in df to test for bias, e.g. ['gender', ...]
29 | low : float
30 | lower bound threshold
31 | high : float
32 | upper bound threshold
33 | num : int
34 | number of values in threshold range
35 |
36 | Returns
37 | -------
38 | thresholds_to_check : np.array
39 | np.linspace(low, high, num)
40 | range of float thresholds checked for posterior prob.
41 | post_probs : defaultdict(list)
42 | containing posterior probability at each threshold
43 |
44 | Example
45 | ---------
46 | given
47 | 1) a classifier, clf, trained on:
48 | three features ('feat1', 'feat2', 'feat3')
49 | and a target 'y', containing values ranging from 0 to 1, and
50 | 2) a df containing columns: ['feat1', 'feat2', 'feat3', 'gender']
51 |
52 | classifier_posterior_probabilities(clf,
53 | df,
54 | ['feat1', 'feat2', 'feat3'],
55 | ['gender'])
56 | >>> np.array([0.6, ..., 0.9]), defaultdict(list, {'gender__F':[.1,...]})
57 | """
58 |
59 | # get decision score for each user and sort by the score
60 |
61 | # this sort makes finding who matches at a threshold easy
62 | X = df[feature_names].values
63 |
64 | # copy dataframe and add decision column:
65 | df = df.copy()
66 | # require clf to have decision_function or predict_proba
67 | clf = ClassifierWrapper(clf)
68 | df['decision'] = clf.decision_function(X)
69 | # allow for older and newer pandas sorting schemes
70 | if hasattr(df, 'sort_values'):
71 | sorted_df = df.reindex(
72 | df.sort_values(
73 | 'decision',
74 | ascending=False).index)
75 | else:
76 | sorted_df = df.reindex(df.sort('decision', ascending=False).index)
77 |
78 | n_samples = sorted_df.shape[0]
79 |
80 | # define range of values to test over if not inputted
81 | if low is None:
82 | low = df.decision.min()
83 | if high is None:
84 | high = df.decision.max()
85 |
86 | thresholds_to_check = np.linspace(low, high, num)
87 | post_probs = defaultdict(list)
88 |
89 | for thresh in thresholds_to_check:
90 | # set the top 1-thresh proportion of sample to 1 (match) and the
91 | # rest to 0 (not match)
92 | num_matches = int(n_samples * (1 - thresh))
93 | num_not_matches = (n_samples - int(n_samples * (1 - thresh)))
94 | matched_col = get_unique_name('matched', df.columns)
95 | sorted_df[matched_col] = ([1] * num_matches) + ([0] * num_not_matches)
96 |
97 | # bias category probabilities
98 | for category in categories:
99 | cat_vals = set(sorted_df[category].dropna())
100 | for val in cat_vals:
101 | n = sum(sorted_df[category] == val)
102 | x = sum(np.logical_and(sorted_df[category] == val,
103 | sorted_df[matched_col]))
104 | a = x + 0.5
105 | b = n - x + 0.5
106 | l, u = stats.beta.interval(0.95, a, b, loc=0, scale=1)
107 | feat_sim = [
108 | round(stats.beta.mean(a, b, loc=0, scale=1), 3),
109 | round(l, 3),
110 | round(u, 3)
111 | ]
112 | post_probs[str(category) + '__' + str(val)].append(feat_sim)
113 |
114 | return thresholds_to_check, post_probs
115 |
116 |
117 | def get_bias_chi2_pvals(clf, df, feature_names, categories,
118 | low=None, high=None, num=100, **kwargs):
119 | """
120 | Get p-values across a range of decision thresholds
121 |
122 | Parameters
123 | ------------
124 | clf : sklearn clf object
125 | model classifier, must have a `decision_function` or
126 | `predict_proba` method
127 | df : pandas DataFrame
128 | contains untransformed data
129 | feature_names : list of strings
130 | features included in the classifier
131 | categories : list of strings
132 | names of demographic columns to check, e.g. ['gender', 'ethnicity']
133 | low : float
134 | lower threshold value
135 | high : float
136 | upper threshold value
137 | num : int
138 | number of thresholds to consider
139 |
140 | Returns
141 | ---------
142 | thresholds_to_check : range of floats
143 | decision thresholds obtained by np.linspace(low, high,num)
144 | post_chi2stat_pvals : defaultdict(list)
145 | containing categories' chi2 statistics and p_vals at a range
146 | of thresholds
147 |
148 | """
149 |
150 | # get decision score for each user and sort by the score
151 | # this sort makes finding who matches at a threshold easy
152 | X = df[feature_names].values
153 |
154 | # subsequent modifications on copy of the input dataframe
155 | df = df.copy()
156 | clf = ClassifierWrapper(clf)
157 | df['decision'] = clf.decision_function(X)
158 | # allow for older and newer pandas sorting schemes
159 | if hasattr(df, 'sort_values'):
160 | sorted_df = df.reindex(
161 | df.sort_values(
162 | 'decision',
163 | ascending=False).index)
164 | else:
165 | sorted_df = df.reindex(df.sort('decision', ascending=False).index)
166 |
167 | matched_col = get_unique_name('matched', df.columns)
168 |
169 | # define range of values to test over if not inputted
170 | if low is None:
171 | low = df.decision.min()
172 | if high is None:
173 | high = df.decision.max()
174 |
175 | n_samples = sorted_df.shape[0]
176 | thresholds_to_check = np.linspace(low, high, num)
177 | post_chi2stat_pvals = defaultdict(list)
178 |
179 | for threshold in thresholds_to_check:
180 | # set the top 1-threshold proportion of sample to 1 (match) and the
181 | # rest to 0 (not match)
182 | num_matches = int(n_samples * (1 - threshold))
183 | num_not_matches = (n_samples - int(n_samples * (1 - threshold)))
184 | sorted_df[matched_col] = ([1] * num_matches) + ([0] * num_not_matches)
185 |
186 | for category in categories:
187 | # get p-values for non-nan values
188 | category_vals = set(sorted_df[category].dropna())
189 | cat_df = sorted_df[sorted_df[category].isin(category_vals)]
190 | cat_ctabs = pd.crosstab(cat_df[matched_col], cat_df[category])
191 | chi2_stat, chi2_pval = chi2_contingency(cat_ctabs)[:2]
192 | post_chi2stat_pvals[category].append((chi2_stat, chi2_pval))
193 |
194 | return thresholds_to_check, post_chi2stat_pvals
195 |
196 |
197 | def generate_bayesfactors(clf, df, feature_names, categories,
198 | prior_strength='', threshold=None,
199 | hyperparam=(1, 1, 1, 1),
200 | N=1000, low=.01, high=.99, num=99, **kwargs):
201 | """
202 | Function to check demographic bias of clf with reference to
203 | dataframe containing labeled features. Decision functions for test
204 | data are sorted, and users in top thresholds, in terms of decision
205 | functions, are considered 'matched'. Proportion of matched users across
206 | demographic categories are used to estimate posterior densities of
207 | probability of being matched within demographic categories. The mean and
208 | 90% credible intervals of ratios of probabilities between categories are
209 | calculated and can be plotted using `viz.get_bias_plots`.
210 | The intervals are compared to the null hypothetical bounds of
211 | 0.8-1.2, with reference to the "4/5ths Rule" for adverse impact,
212 | across the range of thresholds. If the interval overlaps the
213 | bounds across thresholds, then the clf can be considered to be 'unbiased'.
214 |
215 | Parameters
216 | ----------
217 |
218 | clf : sklearn clf
219 | fitted clf with predict object
220 | df : pandas DataFrame
221 | reference dataframe containing labeled features to test for bias
222 | feature_names : list of strings
223 | names of features used in fitting clf
224 | categories : list of strings
225 | names of categories to test for bias, e.g. ['gender', ...]
226 | prior_strength : string from {'weak', 'strong', 'uniform'}
227 | prior distribution to be 'informative'/'noninformative'/'uniform'
228 | threshold : float
229 | single decision threshold at which to evaluate bias
230 | hyperparam : tuple (alpha1, beta1, alpha2, beta2)
231 | optional manual input of hyperparameters
232 | N : int
233 | number of posterior samples to draw for each simulation
234 | low, high, num : float, float, int
235 | range of values for thresholds
236 |
237 | Returns
238 | -------
239 | thresholds_to_check : array of floats
240 | decision thresholds obtained by np.linspace(low, high, num)
241 | ratio_probs : OrderedDict()
242 | contains ratios of matched probabilities within demographic categories
243 | N : int
244 | number of posterior samples drawn for each simulation
245 | """
246 |
247 | X = df[feature_names].values
248 |
249 | df = df.copy()
250 | # get decision score for each user and sort by the score
251 | clf = ClassifierWrapper(clf)
252 | df['decision'] = clf.decision_function(X)
253 | # this sort makes finding who matches at a threshold easy
254 | # allow for older and newer pandas sorting schemes
255 | if hasattr(df, 'sort_values'):
256 | sorted_df = df.reindex(
257 | df.sort_values(
258 | 'decision',
259 | ascending=False).index)
260 | else:
261 | sorted_df = df.reindex(df.sort('decision', ascending=False).index)
262 |
263 | # allow for testing at optional single threshold instead of range:
264 | if threshold is not None:
265 | low = high = threshold
266 | num = 1
267 |
268 | n_samples = sorted_df.shape[0]
269 | thresholds_to_check = np.linspace(low, high, num)
270 |
271 | # raise ValueError if there is a threshold where everyone would pass/fail
272 | if 0.0 in thresholds_to_check:
273 | raise ValueError('Only passing values at a thresh of 0, increase low')
274 | if 1.0 in thresholds_to_check:
275 | raise ValueError('Only failing values at thresh of 1, decrease high')
276 |
277 | ratio_probs = defaultdict(list)
278 |
279 | for thresh in thresholds_to_check:
280 | # set the top (1-thresh) of samples to 1 (match), rest to 0 (not match)
281 | num_matches = int(n_samples * (1 - thresh))
282 | num_not_matches = (n_samples - int(n_samples * (1 - thresh)))
283 | matched_col = get_unique_name('matched', df.columns)
284 | sorted_df[matched_col] = ([1] * num_matches) + ([0] * num_not_matches)
285 |
286 | # for all categories, generate contigency tables
287 | # and simulate posterior sample for ratio of matched probabilities
288 | for category in categories:
289 | feature_vals = set(df[category].dropna())
290 | for v1, v2 in combinations(feature_vals, 2):
291 |
292 | mask = np.logical_or(df[category] == v1,
293 | df[category] == v2)
294 |
295 | ct_table = pd.crosstab(
296 | sorted_df.loc[mask, category],
297 | sorted_df.loc[mask, matched_col]
298 | )
299 |
300 | ct_table = ct_table.reindex(
301 | index=[v1, v2],
302 | columns=[0, 1],
303 | fill_value=0
304 | ).values
305 |
306 | feat_sim = sim_beta_ratio(ct_table, thresh, prior_strength,
307 | hyperparam, N, return_bayes=True)
308 |
309 | # alphabetize out_string
310 | sorted_combo = sorted([str(v1), str(v2)])
311 | out_string = "{}: {} over {}".format(
312 | str(category), sorted_combo[0], sorted_combo[1])
313 | # first occurence gets an OrderedDict but others call the
314 | # existing ratio_probs
315 | ratio_probs[out_string] = ratio_probs.get(out_string,
316 | OrderedDict())
317 | ratio_probs[out_string][thresh] = feat_sim
318 |
319 | return thresholds_to_check, ratio_probs, N
320 |
321 |
322 | def sim_beta_ratio(table, threshold, prior_strength, hyperparam, N,
323 | return_bayes=False):
324 | """
325 | Calculates simulated ratios of match probabilites using a beta
326 | distribution and returns corresponding means and 95% credible
327 | intervals, posterior parameters, Bayes factor
328 |
329 | Parameters
330 | ------------
331 | table : 2x2 numpy array
332 | corresponds to contingency table,
333 | for example,
334 | False True
335 | GroupA 5 4
336 | GroupB 3 4
337 | contains frequency counts: [[5, 4], [3, 4]]
338 | threshold : float
339 | value to split continuous variable on
340 | prior_strength : string from {'weak', 'strong', 'uniform'}
341 | prior distribution to be 'informative'/'noninformative'/'uniform'
342 | N : int
343 | number of posterior samples to draw for each simulation
344 |
345 | Returns
346 | ------------
347 | list : means and 95% credible intervals, posterior parameters, Bayes factor
348 | """
349 |
350 | n_sim = N
351 | # store array of total counts in table by category
352 | category_counts = table.sum(axis=1, dtype=float)
353 | # store array of number of matches by categories
354 | match_counts = table[:, 1]
355 | # set hyperparameters according to threshold and sample size
356 | if prior_strength == 'weak':
357 | # weakly informative prior, has standard deviation
358 | # of 0.1 at alpha / (alpha + beta) = 0.5
359 | # coefficient 24 is empirically derived for best smoothing at small N
360 | alpha1, beta1 = (1 - threshold) * 24., threshold * 24.
361 | alpha2, beta2 = (1 - threshold) * 24., threshold * 24.
362 | elif prior_strength == 'strong':
363 | # observing 'idealized' dataset of size n
364 | alpha1 = round((1 - threshold) * category_counts[0])
365 | beta1 = round(threshold * category_counts[0])
366 | alpha2 = round((1 - threshold) * category_counts[1])
367 | beta2 = round(threshold * category_counts[1])
368 | elif prior_strength == 'uniform':
369 | # uniform prior
370 | alpha1, beta1 = 1, 1
371 | alpha2, beta2 = 1, 1
372 | else:
373 | # user specified, defaults to uniform
374 | alpha1, beta1, alpha2, beta2 = hyperparam
375 |
376 | # draw posterior sample of matching probabilities
377 | post_alpha1 = alpha1 + match_counts[0]
378 | post_beta1 = beta1 + category_counts[0] - match_counts[0]
379 |
380 | post_alpha2 = alpha2 + match_counts[1]
381 | post_beta2 = beta2 + category_counts[1] - match_counts[1]
382 |
383 | p1 = np.random.beta(post_alpha1, post_beta1, n_sim)
384 | p2 = np.random.beta(post_alpha2, post_beta2, n_sim)
385 |
386 | # posterior draw of ratios
387 | p1p2 = p1 / p2
388 | p2p1 = p2 / p1
389 |
390 | sim_beta_ratio_metrics = [np.mean(p1p2), np.mean(p2p1),
391 | np.std(p1p2), np.std(p2p1),
392 | np.percentile(p1p2, 2.5),
393 | np.percentile(p2p1, 2.5),
394 | np.percentile(p1p2, 97.5),
395 | np.percentile(p2p1, 97.5),
396 | (post_alpha1, post_beta1),
397 | (post_alpha2, post_beta2)]
398 |
399 | if return_bayes:
400 | # Return bayes factor for % of posterior ratios in range [.8, 1.25]
401 | post_prob_null = np.sum((p1p2 >= 0.8) & (p1p2 <= 1.25)) / float(n_sim)
402 | bayes_factor = post_prob_null / (1 - post_prob_null)
403 | sim_beta_ratio_metrics.append(bayes_factor)
404 |
405 | return sim_beta_ratio_metrics
406 |
--------------------------------------------------------------------------------
/auditai/stats.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from pandas import DataFrame
3 | from scipy.stats import chi2, fisher_exact, chi2_contingency
4 | from functools import partial
5 |
6 | from .utils.crosstabs import (crosstab,
7 | crosstab_bayes_factor,
8 | crosstab_df,
9 | crosstab_odds_ratio,
10 | crosstab_ztest,
11 | get_top_bottom_indices,
12 | top_bottom_crosstab)
13 | from .utils.validate import (check_consistent_length,
14 | boolean_array)
15 | from .utils.cmh import (parse_matrix,
16 | extract_data)
17 |
18 |
19 | def ztest(labels, results, threshold=None):
20 | """
21 | Two-tailed z score for proportions
22 |
23 | Parameters
24 | ----------
25 | labels : array_like
26 | categorical labels for each corresponding value of `result` ie. M/F
27 |
28 | results : array_like
29 | binary decision values, if continuous values are supplied then
30 | the `threshold` must also be supplied to generate binary decisions
31 |
32 | threshold : numeric
33 | value dividing scores into True/False
34 |
35 | Returns
36 | -------
37 | z-score: float
38 | test statistic of two-tailed z-test
39 | z > 1.960 is signficant at p = .05
40 | z > 2.575 is significant at p = .01
41 | z > 3.100 is significant at p = .001
42 | """
43 |
44 | check_consistent_length(labels, results)
45 | results = np.array(results)
46 |
47 | # convert the results to True/False
48 | results = boolean_array(results, threshold=threshold)
49 | # get crosstab for two groups
50 | ctab = top_bottom_crosstab(labels, results)
51 |
52 | zstat, pvalue = crosstab_ztest(ctab)
53 | return zstat, pvalue
54 |
55 |
56 | def fisher_exact_test(labels, results, threshold=None):
57 | """
58 | Returns odds ratio and p-value of Fisher's exact test
59 | Uses scipy.stats.fisher_exact, which only works for 2x2 contingency tables
60 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.fisher_exact.html
61 |
62 | Parameters
63 | ----------
64 | labels : array_like
65 | categorical labels for each corresponding value of `result` ie. M/F
66 |
67 | results : array_like
68 | binary decision values, if continuous values are supplied then
69 | the `threshold` must also be supplied to generate binary decisions
70 |
71 | threshold : numeric
72 | value dividing scores into True/False, where result>=threshold == True
73 |
74 | Returns
75 | -------
76 | oddsratio : float
77 | This is prior odds ratio and not a posterior estimate.
78 | pvalue : float
79 | P-value, the probability of obtaining a distribution at least
80 | as extreme as the one that was actually observed, assuming that
81 | the null hypothesis is true.
82 | """
83 |
84 | check_consistent_length(labels, results)
85 | results = np.array(results)
86 |
87 | # convert the results to True/False
88 | results = boolean_array(results, threshold=threshold)
89 | # get crosstab for two groups
90 | ctab = top_bottom_crosstab(labels, results)
91 |
92 | oddsratio, pvalue = fisher_exact(ctab)
93 | return oddsratio, pvalue
94 |
95 |
96 | def chi2_test(labels, results, threshold=None):
97 | """
98 | Takes list of labels and results and returns odds ratio and p-value of
99 | Chi-square test of independence. Uses scipy.stats.chi2_contingency,
100 | using an Rx2 contingency table
101 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chi2_contingency.html
102 |
103 | Parameters
104 | ----------
105 | labels : array_like
106 | categorical labels for each corresponding value of `result` ie. M/F
107 |
108 | results : array_like
109 | binary decision values, if continuous values are supplied then
110 | the `threshold` must also be supplied to generate binary decisions
111 |
112 | threshold : numeric
113 | value dividing scores into True/False, where result>=threshold == True
114 |
115 | Returns
116 | -------
117 | chi2_stat : float
118 | The test statistic.
119 | pvalue : float
120 | P-value, the probability of obtaining a distribution at least
121 | as extreme as the one that was actually observed, assuming that
122 | the null hypothesis is true.
123 | """
124 |
125 | check_consistent_length(labels, results)
126 | results = np.array(results)
127 |
128 | # convert the results to True/False
129 | results = boolean_array(results, threshold=threshold)
130 | ctab = crosstab(labels, results)
131 |
132 | chi2_stat, pvalue = chi2_contingency(ctab)[:2]
133 | return chi2_stat, pvalue
134 |
135 |
136 | def bayes_factor(labels, results, threshold=None,
137 | priors=None, top_bottom=True):
138 | """
139 | Computes the analytical Bayes factor for an nx2 contingency table.
140 | If matrix is bigger than 2x2, calculates the bayes factor for the entire
141 | dataset unless top_bottom is set to True. Will always calculate odds ratio
142 | between largest and smallest diverging groups
143 |
144 |
145 | Adapted from Albert (2007) Bayesian Computation with R,
146 | 1st ed., pg 178:184
147 |
148 | Parameters
149 | ----------
150 | labels : array_like
151 | categorical labels for each corresponding value of `result` ie. M/F
152 |
153 | results : array_like
154 | binary decision values, if continuous values are supplied then
155 | the `threshold` must also be supplied to generate binary decisions
156 |
157 | threshold : numeric, optional
158 | value dividing scores into True/False, where result>=threshold == True
159 |
160 | priors : ndarray, optional, shape (n,2)
161 | e.g. uniform_prior = np.array([[1,1],[1,1]])
162 |
163 |
164 | Returns
165 | -------
166 |
167 | BF : float,
168 | bayes factor for the hypothesis of independence
169 | BF < 1 : support for a hypothesis of dependence
170 | 1 < BF < 3 : marginal support for independence
171 | 3 < BF < 20 : moderate support for independence
172 | 20 < BF < 150 : strong support for independence
173 | BF > 150 : very strong support for independence
174 | odds_ratio : float,
175 | odds ratio for the highest and lowest groups.
176 |
177 | Examples
178 | --------
179 | TODO : update examples with odds ratios
180 |
181 | >>> from biastesting.categorical import bf_ctabs
182 | >>> ctabs = np.array([[50,50],[50,50]]) # 1:1 hit/miss ratio
183 | >>> # assuming uniformity
184 | >>> uniform_prior = np.array([[1,1],[1,1]])
185 | >>> bf_ctabs(ctabs, uniform_prior) #all same leads to low BF (dependence)
186 | 0.26162148804907587
187 |
188 | >>> biased_prior = np.array([[5,10],[70,10]])
189 | >>> # all same given strong priors lowers BF (stronger proof of dependence)
190 | >>> bf_ctabs(ctabs, biased_prior)
191 | 0.016048077654948357
192 |
193 | >>> biased_ctab = np.array([[75,50],[25,50]])
194 | >>> # biased crosstabs against prior assumption of uniformity
195 | >>> # large BF (strong proof of independence)
196 | >>> bf_ctabs(biased_ctab, biased_prior)
197 | 202.95548692414306
198 |
199 | >>> # assuming a large prior bias in one direction
200 | >>> # conteracts a large observed dataset in the opposite direction
201 | >>> bf_ctabs(biased_ctab, biased_prior)
202 | 0.00012159518854984268
203 | """
204 | check_consistent_length(labels, results)
205 | results = np.array(results)
206 |
207 | # convert the results to True/False
208 | results = boolean_array(results, threshold=threshold)
209 | ctab = crosstab_df(labels, results)
210 |
211 | if top_bottom:
212 | used_indices = list(get_top_bottom_indices(ctab))
213 | ctab = ctab.iloc[used_indices]
214 |
215 | else:
216 | used_indices = list(ctab.index)
217 |
218 | if priors:
219 | prior = DataFrame(priors)
220 | prior = prior.iloc[used_indices]
221 | prior = prior.values
222 | else:
223 | prior = np.ones(shape=ctab.shape)
224 |
225 | ctab = ctab.values
226 |
227 | BF = crosstab_bayes_factor(ctab, prior)
228 | oddsratio = crosstab_odds_ratio(ctab)
229 |
230 | return oddsratio, BF
231 |
232 |
233 | def cmh_test(dfs, pass_col=False):
234 | """
235 | The pairwise special case of the Cochran-Mantel-Haenszel test. The CMH test
236 | is a generalization of the McNemar Chi-Squared test of Homogenaity. Whereas
237 | the McNemar test examines differences over two intervals (usually before
238 | and after), the CMH test examines differences over any number of
239 | K instances.
240 |
241 | The Yates correction for continuity is not used; while some experts
242 | recommend its use even for moderately large samples (hundreds), the
243 | effect is to reduce the test statistic and increase the p-value. This
244 | is a conservative approach in experimental settings, but NOT in adverse
245 | impact monitoring; such an approach would systematically allow marginal
246 | cases of bias to go undetected.
247 |
248 | Parameters
249 | ------------
250 | dfs : pd.DataFrame, shape = (2,2) OR list of pd.DataFrame objects
251 | a K-deep list of 2x2 contingency tables
252 | representing K locations or time frames
253 | rows indexed by group (0,1)
254 | columns labelled 'pass' and 'fail'
255 | pass_col : Boolean or string, optional
256 | if true, column names assumed to be 'pass' and 'fail'
257 | if string, enter column name of passing counts
258 | if false, column index 0 interpreted as passing
259 | column name of failing counts interpreted automatically
260 |
261 | Returns
262 | ----------
263 | cmh : cmh chi-squared statistic
264 | pcmh : p-value of the cmh chi-squared statistic with 1 degree of freedom
265 |
266 | References
267 | -------
268 |
269 | McDonald, J.H. 2014. Handbook of Biological Statistics (3rd ed.). Sparky
270 | House Publishing, Baltimore, Maryland.
271 |
272 | Forumla example from Boston University School of Public Health
273 | https://tinyurl.com/k3du64x
274 | """
275 | partial_ed = partial(extract_data, pass_col=pass_col)
276 | data = list(map(partial_ed, dfs))
277 | c_nums = [val[2] for val in data]
278 | c_dens = [val[3] for val in data]
279 | c_num = sum(c_nums)**2
280 | c_den = sum(c_dens)
281 | cmh = float(c_num)/float(c_den)
282 |
283 | pcmh = 1 - chi2.cdf(cmh, 1)
284 | return cmh, pcmh
285 |
286 |
287 | def multi_odds_ratio(dfs, pass_col=False):
288 | """
289 | Common odds ratio. Designed for multiple interval odds ratio for use with
290 | the cmh test and breslow-day test, but is generalized for the 2x2 case.
291 |
292 | Parameters
293 | ------------
294 | dfs : pd.DataFrame, shape = (2,2) OR list of pd.DataFrame objects
295 | a K-deep list of 2x2 contingency tables
296 | representing K locations or time frames
297 | rows indexed by group (0,1)
298 | columns labelled 'pass' and 'fail'
299 | pass_col : Boolean or string, optional
300 | if true, column names assumed to be 'pass' and 'fail'
301 | if string, enter column name of passing counts
302 | if false, column index 0 interpreted as passing
303 | column name of failing counts interpreted automatically
304 |
305 | Returns
306 | ----------
307 | r : pooled odds ratio
308 |
309 | Example
310 | -------
311 | from https://en.wikipedia.org/wiki/Odds_ratio#Example
312 |
313 | Of a sample of 100 men and 100 women, 90 men drank wine over the past week,
314 | while only 10 women did the same.
315 |
316 | df = pd.DataFrame({'pass':[90,20], 'fail':[10,80]})
317 | multi_odds_ratio(df)
318 | > 36.0
319 |
320 | For odds ratios over multiple intervals, the use-case of the CMH test,
321 | let's presume that the next week 70 of 100 men drank wine, but now
322 | 70 of 100 women also drank.
323 |
324 | df2 = pd.DataFrame({'pass':[70,70], 'fail':[30,30]})
325 | dfs = [df,df2]
326 | multi_odds_ratio(df)
327 | > 36.0
328 |
329 | multi_odds_ratio(df2)
330 | > 1.0
331 |
332 | multi_odds_ratio(dfs)
333 | >4.043478260869565
334 |
335 | References
336 | ----------
337 | Boston University School of Public Health
338 | https://tinyurl.com/k3du64x
339 | """
340 |
341 | if isinstance(dfs, list):
342 | # if we have a list of multiple dfs
343 | partial_ed = partial(extract_data, pass_col=pass_col)
344 | data = list(map(partial_ed, dfs))
345 | r_nums = [val[0] for val in data]
346 | r_dens = [val[1] for val in data]
347 | r_num = sum(r_nums)
348 | r_den = sum(r_dens)
349 | elif np.shape(dfs) == (2, 2):
350 | data = extract_data(dfs, pass_col=pass_col)
351 | r_num = data[0]
352 | r_den = data[1]
353 | else:
354 | return('Input error. Requires 2x2 dataframe or list of dataframes')
355 | r = float(r_num)/float(r_den)
356 | return r
357 |
358 |
359 | def bres_day(df, r, pass_col=False):
360 | """
361 | Calculates the Breslow-Day test of homogeneous association for a
362 | 2 x 2 x k table. E.g., given three factors, A, B, and C, the Breslow-Day
363 | test would measure wheher pairwise effects (AB, AC, BC) have identical
364 | odds ratios.
365 |
366 | Parameters
367 | ------------
368 | df : pd.DataFrame, shape = (2,2)
369 | a 2x2 contingency table
370 | rows indexed by group (0,1)
371 | columns labelled 'pass' and 'fail'
372 | r : odds ratio; auditai.stats.multi_odds_ratio
373 | pass_col : Boolean or string, optional
374 | if true, column names assumed to be 'pass' and 'fail'
375 | if string, enter column name of passing counts
376 | if false, column index 0 interpreted as passing
377 | column name of failing counts interpreted automatically
378 |
379 | Returns
380 | ----------
381 | bd : Breslow-Day chi-squared statistic
382 | pcmh : p-value of the cmh chi-squared statistic with 1 degree of freedom
383 |
384 | References
385 | -------
386 |
387 | """
388 |
389 | pass0, fail0, pass1, fail1, total = parse_matrix(df, pass_col)
390 |
391 | coef = []
392 | coef.append(1.0-r)
393 | # coef.append(r*((a+c)+(a+b)) + (d-a))
394 | # coef.append(r*(-1*(a+c)*(a+b)))
395 | coef.append(r*((pass0+pass1)+(pass0+fail0)) + (fail1-pass0))
396 | coef.append(r*(-1*(pass0+pass1)*(pass0+fail0)))
397 |
398 | sols = np.roots(coef)
399 | if min(sols) > 0:
400 | t_a = min(sols)
401 | else:
402 | t_a = max(sols)
403 |
404 | t_b = (pass0+fail0) - t_a
405 | t_c = (pass0+pass1) - t_a
406 | t_d = (fail0+fail1) - t_b
407 |
408 | var = 1/((1/t_a) + (1/t_b) + (1/t_c) + (1/t_d))
409 | bd = (pass0-t_a)**2/var
410 | pbd = 1 - chi2.cdf(bd, len(df)-1)
411 |
412 | return bd, pbd
413 |
414 |
415 | def test_cmh_bd(dfs, pass_col=False):
416 | """
417 | Master function for Cochran-Mantel-Haenszel and associated tests
418 | Overview: Compare multiple 2x2 pass-fail contingency tables by gender
419 | or ethnicity (pairwise) to determine if there is a consistent
420 | pattern across regions, time periods, or similar groupings.
421 |
422 | Parameters
423 | ----------
424 | dfs : pd.DataFrame, 2x2xK stack of contingency tables
425 | Cell values are counts of individuals.
426 | Columns are 'pass' and 'fail'.
427 | Rows are integer 1 for focal, 0 for reference group.
428 | K regions, time periods, etc.
429 |
430 | Returns
431 | -------
432 | r : common odds ratio
433 | cmh : Cochran-Mantel-Haenszel statistic (pattern of impact)
434 | pcmh : p-value of Cochran-Mantel-Haenszel test
435 | bd : Breslow-Day statistic (sufficiency of common odds ratio)
436 | pbd : p-value of Breslow-Day test
437 |
438 | Example
439 | -------
440 | Handbook of Biological Statistics by John H. McDonald
441 | Signicant CMH test with non-significant Breslow-Day test
442 | http://www.biostathandbook.com/cmh.html
443 |
444 | "McDonald and Siebenaller (1989) surveyed allele frequencies at the Lap
445 | locus in the mussel Mytilus trossulus on the Oregon coast. At four
446 | estuaries, we collected mussels from inside the estuary and from a marine
447 | habitat outside the estuary. There were three common alleles and a couple
448 | of rare alleles; based on previous results, the biologically interesting
449 | question was whether the Lap94 allele was less common inside estuaries,
450 | so we pooled all the other alleles into a "non-94" class."
451 |
452 | "There are three nominal variables: allele (94 or non-94), habitat
453 | (marine or estuarine), and area (Tillamook, Yaquina, Alsea, or Umpqua).
454 | The null hypothesis is that at each area, there is no difference in the
455 | proportion of Lap94 alleles between the marine and estuarine habitats."
456 |
457 | tillamook = pd.DataFrame({'Marine':[56,40],'Estuarine':[69,77]})
458 | yaquina = pd.DataFrame({'Marine':[61,57],'Estuarine':[257,301]})
459 | alsea = pd.DataFrame({'Marine':[73,71],'Estuarine':[65,79]})
460 | umpqua = pd.DataFrame({'Marine':[71,55],'Estuarine':[48,48]})
461 | dfs = [tillamook,yaquina,alsea,umpqua]
462 |
463 | test_cmh_bd(dfs)
464 | > (1.3174848702393571,
465 | 5.320927767938446,
466 | 0.021070789938349432,
467 | 0.5294859090315444,
468 | 0.9123673420971026)
469 |
470 | """
471 |
472 | r = multi_odds_ratio(dfs, pass_col)
473 | cmh, pcmh = cmh_test(dfs, pass_col)
474 | part_bd = partial(bres_day, r=r, pass_col=pass_col)
475 |
476 | # sum of Breslow-Day chi-square statistics
477 | bd = DataFrame(list(map(part_bd, dfs)))[0].sum()
478 | pbd = 1 - chi2.cdf(bd, len(dfs)-1)
479 |
480 | return r, cmh, pcmh, bd, pbd
481 |
--------------------------------------------------------------------------------
/auditai/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pymetrics/audit-ai/4891b1d3c813a0e6ce657eaee3f1b2ab50e8f429/auditai/tests/__init__.py
--------------------------------------------------------------------------------
/auditai/tests/test_crosstabs.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from numpy.testing import assert_almost_equal
4 |
5 | from auditai.utils.crosstabs import top_bottom_crosstab, crosstab_ztest
6 |
7 |
8 | class TestTBCrosstab(unittest.TestCase):
9 |
10 | def test_tbcross_shape(self):
11 | T10 = [True] * 10
12 | F10 = [False] * 10
13 | labels = ['F'] * 20 + ['M'] * 20
14 | decisions = T10 + F10 + T10 + T10
15 | ctab = top_bottom_crosstab(labels, decisions)
16 | self.assertEqual(len(ctab), 2)
17 | self.assertEqual(len(ctab[0]), 2)
18 | self.assertEqual(ctab[0][1], 10)
19 | self.assertEqual(ctab[1][1], 20)
20 |
21 |
22 | class TestCtabZtest(unittest.TestCase):
23 |
24 | def test_crosstab_ztest(self):
25 | z, p = crosstab_ztest([[10, 10], [10, 10]])
26 | self.assertEqual(z, 0.0)
27 | z, p = crosstab_ztest([[10, 10], [0, 20]])
28 | assert_almost_equal(z, -3.6514837167011072)
29 | z, p = crosstab_ztest([[78, 5], [87, 12]])
30 | assert_almost_equal(z, -1.4078304151258787)
31 |
--------------------------------------------------------------------------------
/auditai/tests/test_misc.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from six import StringIO
3 | import sys
4 | import re
5 | import pandas as pd
6 |
7 | from auditai import misc
8 |
9 |
10 | def pass_fail_count(output):
11 | pass_cnt = len(re.findall('pass', output))
12 | fail_cnt = len(re.findall('fail', output))
13 | return pass_cnt, fail_cnt
14 |
15 |
16 | class TestMisc(unittest.TestCase):
17 | def setUp(self):
18 | self.labels = [0, 0, 0, 0,
19 | 1, 1, 1, 1, 1, 1]
20 | self.results = [0.25, 0.25, 0.75, 0.75,
21 | 0.25, 0.25, 0.25, 0.75, 0.75, 0.75]
22 |
23 | def test_bias_test_check_all_pass(self):
24 | """
25 | Testing unbias results.
26 | Default test_thresh = 0.50 and all tests pass.
27 | """
28 | capturedOutput = StringIO()
29 | sys.stdout = capturedOutput
30 |
31 | misc.bias_test_check(self.labels, self.results, category='test_group')
32 |
33 | sys.stdout = sys.__stdout__
34 |
35 | pass_cnt, fail_cnt = pass_fail_count(capturedOutput.getvalue())
36 | self.assertEqual(pass_cnt, 5)
37 | self.assertEqual(fail_cnt, 0)
38 |
39 | def test_bias_test_check_below_min_thresh(self):
40 | """
41 | Testing unbias results at a test_threshold below min(results).
42 | Unable to run all tests all labels are classified into one group.
43 | """
44 | capturedOutput = StringIO()
45 | sys.stdout = capturedOutput
46 |
47 | misc.bias_test_check(self.labels, self.results, category='test_group',
48 | test_thresh=0.20)
49 |
50 | sys.stdout = sys.__stdout__
51 |
52 | pass_cnt, fail_cnt = pass_fail_count(capturedOutput.getvalue())
53 | self.assertEqual(pass_cnt, 0)
54 | self.assertEqual(fail_cnt, 0)
55 |
56 | def test_bias_test_completely_bias(self):
57 | """
58 | Testing bias results at a test_threshold of 0.50. All tests will fail.
59 | """
60 | labels = [0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
61 | results = [0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
62 |
63 | capturedOutput = StringIO()
64 | sys.stdout = capturedOutput
65 |
66 | misc.bias_test_check(labels, results, category='test_group')
67 |
68 | sys.stdout = sys.__stdout__
69 |
70 | pass_cnt, fail_cnt = pass_fail_count(capturedOutput.getvalue())
71 | self.assertEqual(pass_cnt, 0)
72 | self.assertEqual(fail_cnt, 5)
73 |
74 | def test_one_way_mi(self):
75 | df = pd.DataFrame({'feat1': [1, 2, 3, 2, 1],
76 | 'feat2': [3, 2, 1, 2, 3],
77 | 'feat3': [1, 2, 3, 2, 1],
78 | 'feat4': [1, 2, 3, 2, 1],
79 | 'group': [1, 1, 3, 4, 5],
80 | 'y': [1, 2, 1, 1, 2]})
81 | expected_output = {
82 | 'feature': {0: 'feat1', 1: 'feat2', 2: 'feat3', 3: 'feat4'},
83 | 'group': {0: 0.12, 1: 0.12, 2: 0.12, 3: 0.12},
84 | 'group_scaled': {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0},
85 | 'y': {0: 0.12, 1: 0.12, 2: 0.12, 3: 0.12},
86 | 'y_scaled': {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}}
87 |
88 | features = ['feat1', 'feat2', 'feat3', 'feat4']
89 | group_column = 'group'
90 | y_var = 'y'
91 |
92 | output = misc.one_way_mi(df, features, group_column, y_var, (4, 2))
93 | for col in [group_column, y_var,
94 | group_column+'_scaled', y_var+'_scaled']:
95 | output[col] = output[col].round(2)
96 | self.assertEqual(output.to_dict(), expected_output)
97 |
--------------------------------------------------------------------------------
/auditai/tests/test_nothing.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 |
4 | class TestSimple(unittest.TestCase):
5 |
6 | def test_true(self):
7 | self.assertTrue(True)
8 |
9 | def test_print(self):
10 | print("Hello python")
11 |
--------------------------------------------------------------------------------
/auditai/tests/test_simulations.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import os
3 |
4 | from sklearn.linear_model import LogisticRegression
5 | import pandas as pd
6 | import numpy as np
7 |
8 | import auditai.simulations as sim
9 |
10 |
11 | class TestGenerateBayesFactors(unittest.TestCase):
12 | def setUp(self):
13 | data_path = os.path.join(os.path.dirname(__file__), '..', '..',
14 | 'data', 'GermanCreditData.csv')
15 | self.data = pd.read_csv(data_path)
16 | self.features = ['age',
17 | 'duration',
18 | 'amount',
19 | 'dependents',
20 | 'inst_rate',
21 | 'num_credits'
22 | ]
23 | X = self.data[self.features]
24 | y = self.data['status']
25 |
26 | self.clf = LogisticRegression()
27 | self.clf.fit(X, y)
28 |
29 | def test_use_default_values(self):
30 | thresholds, ratios, N = sim.generate_bayesfactors(self.clf, self.data,
31 | self.features,
32 | ['under_30',
33 | 'is_female'])
34 | # make sure all thresholds have ratio values
35 | for key in ratios.keys():
36 | self.assertEqual(len(ratios[key]), len(thresholds))
37 |
38 | def test_add_low_high(self):
39 | thresholds, ratios, N = sim.generate_bayesfactors(self.clf, self.data,
40 | self.features,
41 | ['under_30',
42 | 'is_female'],
43 | low=.01,
44 | high=.99,
45 | num=99)
46 | # make sure all thresholds have ratio values
47 | for key in ratios.keys():
48 | self.assertEqual(len(ratios[key]), len(thresholds))
49 |
50 | def test_bad_case_all_users_pass(self):
51 | with self.assertRaises(ValueError):
52 | thresholds, ratios, N = sim.generate_bayesfactors(self.clf,
53 | self.data,
54 | self.features,
55 | ['under_30',
56 | 'is_female'],
57 | low=.00,
58 | high=.99,
59 | num=100)
60 |
61 | def test_bad_case_all_users_fail(self):
62 | with self.assertRaises(ValueError):
63 | thresholds, ratios, N = sim.generate_bayesfactors(self.clf,
64 | self.data,
65 | self.features,
66 | ['under_30',
67 | 'is_female'],
68 | low=.01,
69 | high=1.,
70 | num=100)
71 |
72 | def test_single_threshold(self):
73 | thresholds, ratios, N = sim.generate_bayesfactors(self.clf, self.data,
74 | self.features,
75 | ['under_30',
76 | 'is_female'],
77 | threshold=.7)
78 | # make sure all thresholds have ratio values
79 | for key in ratios.keys():
80 | self.assertEqual(len(ratios[key]), len(thresholds))
81 | self.assertEqual(len(ratios[key][0.7]), 11)
82 |
83 | def test_expected_values_0_01(self):
84 | np.random.seed(42)
85 | expected_is_female = [0.9964569398234155,
86 | 1.0035990084294402,
87 | 0.006555291338954991,
88 | 0.006589967705087398,
89 | 0.9844427430302514,
90 | 0.9895322416249943,
91 | 1.0105784931410273,
92 | 1.0158031109908876,
93 | (683, 9.0),
94 | (309, 3.0),
95 | np.inf]
96 |
97 | expected_under_30 = [1.0156517754102912,
98 | 0.9846637559926464,
99 | 0.008848752073678487,
100 | 0.008532082550902103,
101 | 1.0017184895413358,
102 | 0.9656369218482502,
103 | 1.0355859215014185,
104 | 0.9982844586036186,
105 | (627, 4.0),
106 | (365, 8.0),
107 | np.inf]
108 |
109 | _, ratios, _ = sim.generate_bayesfactors(self.clf, self.data,
110 | self.features,
111 | ['under_30',
112 | 'is_female'],
113 | threshold=.01)
114 | self.assertTrue(np.all(np.isclose(
115 | ratios['is_female: 0 over 1'][0.01][:8],
116 | expected_is_female[:8])))
117 | self.assertTrue(np.all(np.isclose(
118 | ratios['under_30: 0 over 1'][0.01][:8],
119 | expected_under_30[:8])))
120 |
--------------------------------------------------------------------------------
/auditai/tests/test_stats.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from numpy.testing import assert_almost_equal
4 |
5 | from auditai.stats import ztest, fisher_exact_test, chi2_test, bayes_factor
6 |
7 |
8 | class TestZtest(unittest.TestCase):
9 |
10 | def setUp(self):
11 | T10 = [True] * 10
12 | F10 = [False] * 10
13 | self.labels = ['F'] * 20 + ['M'] * 20
14 | self.results = T10 + F10 + T10 + F10
15 | self.results_2 = T10 + F10 + T10 + T10
16 |
17 | def test_ztest(self):
18 | z, p = ztest(self.labels, self.results)
19 | self.assertEqual(z, 0.0)
20 | z, p = ztest(self.labels, self.results_2)
21 | assert_almost_equal(z, -3.6514837167011072)
22 |
23 | def test_fisher_exact_test(self):
24 | oddsratio, pvalue = fisher_exact_test(self.labels, self.results)
25 | self.assertEqual(oddsratio, 1.0)
26 | self.assertEqual(pvalue, 1.0)
27 |
28 | def test_chi2_test(self):
29 | chi2_stat, pvalue = chi2_test(self.labels, self.results)
30 | self.assertEqual(chi2_stat, 0.0)
31 | self.assertEqual(pvalue, 1.0)
32 |
33 | def test_bayes_factor(self):
34 | oddsratio, bf = bayes_factor(self.labels, self.results)
35 | self.assertEqual(oddsratio, 1.0)
36 | self.assertAlmostEqual(bf, 0.5500676358744966)
37 | with self.assertRaises(ValueError):
38 | oddsratio, bf = bayes_factor(self.labels, self.results,
39 | priors=self.results)
40 |
--------------------------------------------------------------------------------
/auditai/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from auditai.utils.functions import get_unique_name, two_tailed_ztest, dirichln
4 |
5 |
6 | class TestUtils(unittest.TestCase):
7 | def test_get_unique_name(self):
8 | new_name = 'matched'
9 | name_list = ['feat1', 'feat2', 'matched']
10 |
11 | output = get_unique_name(new_name, name_list)
12 | self.assertEqual(output, 'matched_new')
13 |
14 | name_list.append(output)
15 | output = get_unique_name(new_name, name_list)
16 | self.assertEqual(output, 'matched_new_new')
17 |
18 | def test_two_tailed_ztest(self):
19 | success1, success2, total1, total2 = 5, 10, 10, 10
20 | zstat, p_value = two_tailed_ztest(success1, success2, total1, total2)
21 | output = tuple(round(i, 2) for i in (zstat, p_value))
22 | self.assertEqual(output, (-2.58, 0.01))
23 |
24 | def test_dirichln(self):
25 | output = round(dirichln([1, 2]), 2)
26 | self.assertEqual(output, -0.69)
27 |
28 | output = round(dirichln([1]), 2)
29 | self.assertEqual(output, 0.0)
30 |
--------------------------------------------------------------------------------
/auditai/tests/test_viz.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import unittest
3 | import mock
4 | import pytest
5 | import os
6 |
7 | from sklearn.ensemble import RandomForestClassifier
8 | import pandas as pd
9 |
10 | import auditai.viz as viz
11 |
12 |
13 | @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6+")
14 | class TestBiasBarPlot(unittest.TestCase):
15 | def setUp(self):
16 | self.bias_report = {'ethnicity': {'averages': [0.291, 0.303, 0.317,
17 | 0.338, 0.301],
18 | 'categories': ['asian',
19 | 'black',
20 | 'hispanic-latino',
21 | 'two-or-more-groups',
22 | 'white'],
23 | 'errors': [[0.278, 0.304],
24 | [0.273, 0.334],
25 | [0.288, 0.347],
26 | [0.297, 0.38],
27 | [0.29, 0.313]]},
28 | 'gender': {'averages': [0.293, 0.308],
29 | 'categories': ['F', 'M'],
30 | 'errors': [[0.282, 0.304],
31 | [0.297, 0.319]]}}
32 |
33 | @mock.patch('matplotlib.pyplot.show')
34 | @mock.patch('matplotlib.pyplot.savefig')
35 | def test_bias_report_case(self, mock_plt_savefig, mock_plt_show):
36 | viz.bias_report_plot(self.bias_report)
37 |
38 | def test_bad_input(self):
39 | with self.assertRaises(TypeError):
40 | viz.bias_bar_plot(bias_report=None, clf=None)
41 |
42 |
43 | @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6+")
44 | class TestGetBiasPlots(unittest.TestCase):
45 | def setUp(self):
46 | data_path = os.path.join(os.path.dirname(__file__), '..', '..',
47 | 'data', 'GermanCreditData.csv')
48 | self.data = pd.read_csv(data_path)
49 | self.features = ['age',
50 | 'duration',
51 | 'amount',
52 | 'dependents',
53 | 'inst_rate',
54 | 'num_credits'
55 | ]
56 | X = self.data[self.features]
57 | y = self.data['status']
58 |
59 | self.clf = RandomForestClassifier()
60 | self.clf.fit(X, y)
61 |
62 | @mock.patch('matplotlib.pyplot.show')
63 | @mock.patch('matplotlib.pyplot.savefig')
64 | def test_use_default_values(self, mock_plt_savefig, mock_plt_show):
65 | viz.get_bias_plots(self.clf, self.data,
66 | self.features, ['under_30', 'is_female'])
67 |
68 | @mock.patch('matplotlib.pyplot.show')
69 | @mock.patch('matplotlib.pyplot.savefig')
70 | def test_apply_custom_formatting(self, mock_plt_savefig, mock_plt_show):
71 | viz.get_bias_plots(self.clf, self.data,
72 | self.features, ['under_30', 'is_female'], s=10)
73 |
74 | @mock.patch('matplotlib.pyplot.show')
75 | @mock.patch('matplotlib.pyplot.savefig')
76 | def test_ignore_bad_plt_kwargs(self, mock_plt_savefig, mock_plt_show):
77 | viz.get_bias_plots(self.clf, self.data,
78 | self.features, ['under_30', 'is_female'], s=10,
79 | bad_kwarg=10)
80 |
--------------------------------------------------------------------------------
/auditai/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pymetrics/audit-ai/4891b1d3c813a0e6ce657eaee3f1b2ab50e8f429/auditai/utils/__init__.py
--------------------------------------------------------------------------------
/auditai/utils/cmh.py:
--------------------------------------------------------------------------------
1 | # helper functions specifically for cmh tests
2 |
3 |
4 | def parse_matrix(df, pass_col=False):
5 | """
6 | Utility function for parsing matrix into cell counts. Used for
7 | parsing strata for the Cochran-Manzel-Haenszel and Breslow-Day tests in
8 | stats.cmh_test and stats.multi_odds_ratio
9 |
10 | Parameters
11 | ------------
12 | df : pd.DataFrame, shape (2,2)
13 | rows indexed by group (0,1)
14 | columns labelled 'pass' and 'fail'
15 | pass_col : Boolean or string, optional
16 | if true, column names assumed to be 'pass' and 'fail'
17 | if string, enter column name of passing counts
18 | if false, column index 0 interpreted as passing
19 | column name of failing counts interpreted automatically
20 |
21 | Returns
22 | ----------
23 | pass0 : counts of passing at row index 0
24 | fail0 : counts of failing at row index 1
25 | pass1 : counts of passing at row index 1
26 | fail1 : counts of failing at row index 1
27 | total : total n for matrix
28 | """
29 |
30 | # if True or string
31 | if pass_col:
32 | # pass a string for pass column
33 | if isinstance(pass_col, str):
34 | fail_col = df.columns[df.columns != pass_col][0]
35 | else:
36 | # default mapping of column headers
37 | pass_col = 'pass'
38 | fail_col = 'fail'
39 | else:
40 | pass_col = 1
41 | fail_col = 0
42 | pass0 = float(df.iloc[0][pass_col])
43 | fail0 = float(df.iloc[0][fail_col])
44 | pass1 = float(df.iloc[1][pass_col])
45 | fail1 = float(df.iloc[1][fail_col])
46 |
47 | total = pass0 + fail0 + pass1 + fail1
48 |
49 | return pass0, fail0, pass1, fail1, total
50 |
51 |
52 | def extract_data(df, pass_col=False):
53 | """
54 | Utility function for preprocessing data for stats.cmh_test and
55 | stats.multi_odds_ratio. Used for parsing strata for the
56 | Cochran-Manzel-Haenszel and Breslow-Day tests in
57 | stats.cmh_test and stats.multi_odds_ratio Returns component
58 | values for relevant tests.
59 |
60 | Parameters
61 | ------------
62 | df : pd.DataFrame, shape (2,2)
63 | rows indexed by group (0,1)
64 | columns labelled 'pass' and 'fail'
65 | pass_col : Boolean or string, optional
66 | if true, column names assumed to be 'pass' and 'fail'
67 | if string, enter column name of passing counts
68 | if false, column index 0 interpreted as passing
69 | column name of failing counts interpreted automatically
70 |
71 | Returns
72 | ----------
73 | r_num : odds ratio numerator; n group 0 pass * n group 1 fail over total
74 | r_den : odds ratio denominator; n group 0 fail * n group 1 pass over total
75 | c_num : CMH numerator
76 | c_den : CMH denominator
77 | """
78 |
79 | pass0, fail0, pass1, fail1, total = parse_matrix(df, pass_col)
80 |
81 | r_num = (pass0*fail1)/(total)
82 | r_den = (fail0*pass1)/(total)
83 |
84 | c_num = pass0 - ((pass0 + fail0)*(pass0 + pass1))/(total)
85 | c_den = (((pass0 + fail0)*(pass1 + fail1)*(pass0 + pass1)*(fail0 + fail1))
86 | / (total*total*(total - 1)))
87 |
88 | return r_num, r_den, c_num, c_den
89 |
--------------------------------------------------------------------------------
/auditai/utils/crosstabs.py:
--------------------------------------------------------------------------------
1 | """
2 | Collection of functions to create and manipulate cross tabulations
3 | """
4 | import numpy as np
5 | import pandas as pd
6 |
7 | from .general import two_tailed_ztest, dirichln
8 |
9 |
10 | def crosstab_df(labels, decisions):
11 | """
12 | Parameters
13 | ------------
14 | labels : array_like
15 | containing categorical values like ['M', 'F']
16 | decisions : array_like
17 | containing boolean / binary values
18 |
19 | Returns
20 | --------
21 | crosstab : 2x2 array
22 | in the form,
23 | False True
24 | TopGroup 5 4
25 | BottomGroup 3 4
26 | so, crosstab = array([[5, 4], [3, 4]])
27 | """
28 | labels, decisions = pd.Series(labels), pd.Series(decisions)
29 | # rows are label values (e.g. ['F', 'M'])
30 | # columns are decision values (e.g. [False, True])
31 | ctab = pd.crosstab(labels, decisions)
32 | return ctab
33 |
34 |
35 | def crosstab(labels, decisions):
36 | """
37 | Returns value of crosstab_df as numpy array
38 | """
39 | ctab = crosstab_df(labels, decisions)
40 | return ctab.values
41 |
42 |
43 | def top_bottom_crosstab(labels, decisions):
44 | """
45 | Utility function for grabbing crosstabs for groups with highest
46 | and lowest pass rates, as defined by:
47 | - a categorical comparision column
48 | - a binary decision column
49 |
50 | Parameters
51 | -------
52 | labels : array_like
53 | categorical labels for each corresponding value of `decision` ie. M/F
54 |
55 | decision : array_like
56 | binary decision values, ie. True/False or 0/1
57 |
58 | Returns
59 | ---------
60 | top_bottom_ctabs : 2x2 array
61 | contains True/False frequency counts of 2 groups with highest
62 | and lowest pass rates
63 | """
64 |
65 | ctab = crosstab_df(labels, decisions)
66 |
67 | # if already 2x2, return table
68 | if ctab.shape == (2, 2):
69 | return ctab.values
70 |
71 | top, bottom = get_top_bottom_indices(ctab)
72 |
73 | top_bottom_ctab = ctab.iloc[[top, bottom]].values
74 | return top_bottom_ctab
75 |
76 |
77 | def get_top_bottom_indices(ctab_df):
78 | """
79 | Utility function for returning row indices of groups
80 | with highest and lowest pass rates, according to input crosstab dataframe
81 |
82 | Parameters
83 | -----------
84 | ctab_df - pandas DataFrame, shape (n,2)
85 | the rows should correspond to groups, columns to binary labels
86 |
87 | Returns
88 | ---------
89 | top_group_idx - int
90 | index of group in ctab_df with highest pass rate (ratio of True/total)
91 | bottom_group_idx - int
92 | index of group in ctab_df with lowest pass rate (ratio of True/total)
93 | """
94 |
95 | if ctab_df.shape[1] != 2:
96 | ctab_df = ctab_df.T
97 |
98 | normed_ctabs = ctab_df.div(ctab_df.sum(axis=1), axis=0)
99 |
100 | # assume larger decision value corresponds to passing
101 | # e.g. max({1,0}) or max({True, False})
102 | true_val = ctab_df.columns.max()
103 |
104 | # grab top and bottom group indices
105 | top_group_idx = normed_ctabs[true_val].values.argmax()
106 | bottom_group_idx = normed_ctabs[true_val].values.argmin()
107 | return top_group_idx, bottom_group_idx
108 |
109 |
110 | def crosstab_bayes_factor(ctab, prior=None):
111 | """
112 | Computes the analytical Bayes factor for an nx2 contingency table.
113 |
114 | Adapted from Albert (2007) Bayesian Computation with R,
115 | 1st ed., pg 178:184
116 |
117 | Parameters
118 | ----------
119 | ctab : ndarray, shape (n,2)
120 | contingency table containing label x result frequency counts
121 |
122 | prior : ndarray, optional, shape (n,2)
123 | e.g. uniform_prior = np.array([[1,1],[1,1]])
124 |
125 |
126 | Returns
127 | -------
128 |
129 | BF : float,
130 | bayes factor for the hypothesis of independence
131 | BF < 1 : support for a hypothesis of dependence
132 | 1 < BF < 3 : marginal support for independence
133 | 3 < BF < 20 : moderate support for independence
134 | 20 < BF < 150 : strong support for independence
135 | BF > 150 : very strong support for independence
136 |
137 |
138 | Examples
139 | --------
140 | >>> ctab = np.array([[50,50],[50,50]]) # 1:1 hit/miss ratio
141 | >>> # assuming uniformity
142 | >>> uniform_prior = np.ones(ctab.shape)
143 | >>> bf_ctabs(ctab, uniform_prior) #all same leads to low BF (dependence)
144 | 0.26162148804907587
145 |
146 | """
147 | if isinstance(ctab, pd.DataFrame):
148 | ctab = ctab.values
149 |
150 | if prior is None:
151 | prior = np.ones(shape=ctab.shape)
152 |
153 | ac = prior.sum(axis=0) # column sums
154 | ar = prior.sum(axis=1) # row sums
155 | yc = ctab.sum(axis=0) # column sums
156 | yr = ctab.sum(axis=1) # row sums
157 |
158 | I, J = ctab.shape
159 | OR = np.ones(I) # vector of ones for rows
160 | OC = np.ones(J) # vector of ones for columns
161 |
162 | if ctab.size != prior.size:
163 | raise ValueError("ctab and priors must be of same size")
164 |
165 | lbf = dirichln(ctab + prior) + dirichln(ar - (J - 1) * OR) \
166 | + dirichln(ac - (I - 1) * OC) - dirichln(prior) \
167 | - dirichln(yr + ar - (J - 1) * OR) - dirichln(yc + ac - (I - 1) * OC)
168 |
169 | # calculating bayes factor for all groups
170 | BF = np.exp(lbf)
171 | return BF
172 |
173 |
174 | def crosstab_odds_ratio(ctab):
175 | """
176 | Calculate exact odds ratio between two groups.
177 | Designed for 2x2 matrices. When greater than 2x2,
178 | calculates OR for highest and lowest groups.
179 |
180 | Parameters
181 | ----------
182 | ctab : matrix of values, shape(2,2)
183 |
184 | Returns
185 | -------
186 | odds_ratio : exact odds ratio
187 |
188 | Examples
189 | --------
190 |
191 | TODO: add examples
192 | """
193 | ctab = np.array(ctab, dtype=np.float)
194 |
195 | if ctab.shape[1] != 2:
196 | raise ValueError("Must be of shape (n,2)")
197 |
198 | if ctab.shape[0] != 2:
199 | tab_ratios = ctab[:, 0] / ctab[:, 1]
200 | max_idx = np.argmax(tab_ratios)
201 | min_idx = np.argmin(tab_ratios)
202 | ctab = ctab[:, [min_idx, max_idx]]
203 |
204 | a_ratio = float(ctab[0, 0]) / ctab[0, 1]
205 | b_ratio = float(ctab[1, 0]) / ctab[1, 1]
206 |
207 | return a_ratio / b_ratio
208 |
209 |
210 | def crosstab_ztest(ctab):
211 | """
212 | z-scores from 2x2 cross tabs of passing rate across groups
213 |
214 | Parameters
215 | ----------
216 | ctab: array, shape=(2,2);
217 | crosstab of passing rate across groups, where each row is a group
218 | and the first and second columns count the number of unsuccessful
219 | and successful trials respectively
220 |
221 | Returns
222 | -------
223 | zstat: float
224 | test statistic for two tailed z-test
225 | """
226 | ctab = np.asarray(ctab)
227 |
228 | n1 = ctab[0].sum()
229 | n2 = ctab[1].sum()
230 | pos1 = ctab[0, 1]
231 | pos2 = ctab[1, 1]
232 | zstat, p_value = two_tailed_ztest(pos1, pos2, n1, n2)
233 | return zstat, p_value
234 |
--------------------------------------------------------------------------------
/auditai/utils/functions.py:
--------------------------------------------------------------------------------
1 | # Maintain existing API functionality
2 | from .general import * # noqa
3 |
--------------------------------------------------------------------------------
/auditai/utils/general.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from scipy.stats import norm
3 | from scipy.special import gammaln
4 |
5 |
6 | def two_tailed_ztest(success1, success2, total1, total2):
7 | """
8 | Two-tailed z score for proportions
9 |
10 | Parameters
11 | -------
12 | success1 : int
13 | the number of success in `total1` trials/observations
14 |
15 | success2 : int
16 | the number of success in `total2` trials/observations
17 |
18 | total1 : int
19 | the number of trials or observations of class 1
20 |
21 | total2 : int
22 | the number of trials or observations of class 2
23 |
24 | Returns
25 | -------
26 | zstat : float
27 | z score for two tailed z-test
28 | p_value : float
29 | p value for two tailed z-test
30 | """
31 | p1 = success1 / float(total1)
32 | p2 = success2 / float(total2)
33 | p_pooled = (success1 + success2) / float(total1 + total2)
34 |
35 | obs_ratio = (1. / total1 + 1. / total2)
36 | var = p_pooled * (1 - p_pooled) * obs_ratio
37 |
38 | # calculate z-score using foregoing values
39 | zstat = (p1 - p2) / np.sqrt(var)
40 |
41 | # calculate associated p-value for 2-tailed normal distribution
42 | p_value = norm.sf(abs(zstat)) * 2
43 |
44 | return zstat, p_value
45 |
46 |
47 | def dirichln(arr):
48 | """
49 | Dirichlet gamma function
50 | Albert (2007) Bayesian Computation with R, 1st ed., pg 178
51 |
52 | Parameters
53 | ----------
54 | arr : array or matrix of float values
55 |
56 | Returns
57 | -------
58 | val : float or array,
59 | logged Dirichlet transformed value if array or matrix
60 | """
61 | val = np.sum(gammaln(arr)) - gammaln(np.sum(arr))
62 | return val
63 |
64 |
65 | def get_unique_name(new_name, name_list, addendum='_new'):
66 | """
67 | Utility function to return a new unique name if name is in list.
68 |
69 | Parameters
70 | ----------
71 | new_name : string
72 | name to be updated
73 | name_list: list
74 | list of existing names
75 | addendum: string
76 | addendum appended to new_name if new_name is in name_list
77 |
78 | Returns
79 | -------
80 | new_name : string,
81 | updated name
82 |
83 | Example
84 | -------
85 | new_name = 'feat1'
86 | name_list = ['feat1', 'feat2']
87 |
88 | first iteration: new_name returned = 'feat1_new'
89 | now with name_list being updated to include new feature:
90 | name_list = ['feat1', 'feat2', 'feat1_new']
91 |
92 | second iteration: new_name returned = 'feat1_new_new'
93 | """
94 | # keep appending "new" until new_name is not in list
95 | while new_name in name_list:
96 | new_name += addendum
97 | return new_name
98 |
--------------------------------------------------------------------------------
/auditai/utils/validate.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 |
4 |
5 | def _num_samples(x):
6 | """
7 | Return number of samples in array_like x.
8 | """
9 | if not hasattr(x, '__len__') and not hasattr(x, 'shape'):
10 | if hasattr(x, '__array__'):
11 | x = np.asarray(x)
12 | else:
13 | raise TypeError("Expected sequence or array_like, got {}".format(
14 | type(x)))
15 | if hasattr(x, 'shape'):
16 | if len(x.shape) == 0:
17 | raise TypeError("Singleton array {} cannot be considered"
18 | " a valid collection.".format(x))
19 | return x.shape[0]
20 | else:
21 | return len(x)
22 |
23 |
24 | def check_consistent_length(*arrays):
25 | """
26 | Check that all arrays have consistent first dimensions.
27 | Checks whether all objects in arrays have the same shape or length.
28 |
29 | Parameters
30 | ----------
31 | *arrays : list or tuple of input objects.
32 | Objects that will be checked for consistent length.
33 | """
34 |
35 | lengths = [_num_samples(X) for X in arrays if X is not None]
36 | uniques = np.unique(lengths)
37 | if len(uniques) > 1:
38 | raise ValueError("Found input variables with inconsistent numbers of"
39 | " samples: {}".format([int(le) for le in lengths]))
40 |
41 |
42 | class ClassifierWrapper(object):
43 | """Simple sklearn wrapper for classifiers"""
44 |
45 | def __init__(self, clf):
46 |
47 | if not hasattr(clf, "decision_function"):
48 | if not hasattr(clf, "predict_proba"):
49 | raise ValueError("Classifier object has no decision_function"
50 | "or predict_proba attribute")
51 | self.clf = clf
52 |
53 | def decision_function(self, X):
54 | if hasattr(self.clf, 'decision_function'):
55 | return 1 / (1 + np.exp(-self.clf.decision_function(X)))
56 | elif hasattr(self.clf, 'predict_proba'):
57 | # assume positive case is maximum value
58 | positive_col = self.clf.classes_.argmax()
59 | return self.clf.predict_proba(X)[:, positive_col]
60 | else:
61 | raise ValueError(
62 | 'Classifier has no decision_function or predict_proba')
63 |
64 |
65 | def boolean_array(array, threshold=None):
66 | """
67 | Converts a numpy array of integers into True/False values.
68 | If there are more than 2 unique values, a threshold must be supplied
69 |
70 | Parameters
71 | -----------
72 | array : numpy array
73 | an array of integers
74 | threshold : integer
75 | a threshold to apply to an array of values
76 |
77 | Returns
78 | ---------
79 | array : list of boolean values
80 | """
81 | if not isinstance(array, np.ndarray):
82 | raise TypeError("Expected numpy array, got {}".format(type(array)))
83 | if threshold:
84 | return array >= threshold
85 | else:
86 | vals = np.unique(array)
87 | if vals.shape[0] != 2:
88 | raise ValueError("Expected 2 unique values when "
89 | "threshold=None, got {}".format(vals.shape[0]))
90 | max_val = np.max(vals)
91 | return array == max_val
92 |
93 |
94 | def arrays_check(labels, results, null_vals=None):
95 | """
96 | Given two input arrays of same length,
97 | returns same arrays with missing values (from either) removed from both
98 |
99 | Parameters
100 | -----------
101 | labels : array_like
102 | results : array_like
103 | null_vals : list-like, optional
104 | user-specified unwanted values, e.g. ('', 'missing')
105 |
106 | Returns
107 | ---------
108 | labels, results : two-tuple of input arrays, without missings
109 |
110 | """
111 | # require they be the same lengths
112 | assert len(labels) == len(results), 'input arrays not the same lengths!'
113 |
114 | # create dataframe and use pandas to remove rows containing nulls
115 | df = pd.DataFrame(list(zip(labels, results)), columns=['label', 'result'])
116 |
117 | # if user passes unwanted values to check for, replace these with NaNs
118 | if null_vals is not None:
119 | df.replace(null_vals, np.nan, inplace=True)
120 |
121 | # if no missing values, return original arrays
122 | if df.isnull().sum().sum() == 0:
123 | return labels, results
124 |
125 | # otherwise, get rid of rows with missing values and return remaining
126 | # arrays
127 | else:
128 | df.dropna(axis=0, how='any', inplace=True)
129 | return df.label.values, df.result.values
130 |
--------------------------------------------------------------------------------
/auditai/version.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.1.1'
2 |
--------------------------------------------------------------------------------
/auditai/viz.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import matplotlib.pyplot as plt
3 | import numpy as np
4 | import pandas as pd
5 |
6 | from .misc import (compare_groups,
7 | get_group_proportions,
8 | make_bias_report)
9 | from .simulations import (get_bias_chi2_pvals,
10 | generate_bayesfactors)
11 |
12 | plt.style.use('fivethirtyeight')
13 |
14 |
15 | def plot_bias_pvals(thresholds, pvals, category, sig_thresh=.05, outpath=None):
16 | """
17 | Plots Chi-Squared p-value test of match rate differences
18 | within demographic groups. Uses outputs of `get_bias_chi2_pvals`
19 |
20 | Parameters
21 | ----------
22 | thresholds : array_like, range of floats
23 | decision thresholds obtained by np.linspace(low,high,num)
24 | pvals : array_like, range of floats
25 | p-values from Chi-Squared test
26 | category : string
27 | demographic category to test for bias, e.g. 'Gender'
28 | sig_thresh : float
29 | y-axis value to draw threshold line
30 | outpath : string, optional
31 | location in memory to save plot
32 |
33 | Returns
34 | -------
35 | None
36 |
37 | Plots
38 | ------
39 | plot with Chi-Squared p-values
40 | """
41 |
42 | low = np.floor(min(thresholds))
43 | high = np.ceil(max(thresholds))
44 |
45 | plot_title = 'Bias Chi-Squared p-val tests for {}'.format(category)
46 |
47 | plt.figure()
48 | plt.scatter(thresholds, pvals)
49 | plt.axis([low, high, -.1, 1.1])
50 | plt.title(plot_title)
51 | plt.ylabel('p-val')
52 | plt.xlabel('Threshold')
53 | plt.axhline(y=sig_thresh, ls='dashed', color='r')
54 | if outpath is not None:
55 | plt.savefig(outpath)
56 | plt.show()
57 |
58 |
59 | def plot_bias_test(thresholds, ratios, category, outpath=None, **kwargs):
60 | """
61 | Plots prediction ratio for different groups across thresholds
62 | Checks 4/5ths test -- i.e. whether any group passes 20+% more than another
63 |
64 | Parameters
65 | ----------
66 | thresholds : array_like
67 | range of floats - thresholds obtained by np.linspace(low,high,num)
68 | ratios : dict
69 | output of `generate_bayesfactors`, contains means, lowers, uppers
70 | category : string
71 | demographic category to test for bias, e.g. 'Gender'
72 | outpath : string, optional
73 | location in memory to save plot
74 |
75 | Returns
76 | -------
77 | None
78 |
79 | Plots
80 | ------
81 | plot with prediction ratios for different groups
82 | """
83 |
84 | means = [i[0] for i in ratios.values()]
85 | lowers = [i[4] for i in ratios.values()]
86 | uppers = [i[6] for i in ratios.values()]
87 |
88 | low = np.floor(min(thresholds))
89 | high = np.ceil(max(thresholds))
90 |
91 | plot_title = 'Bias Tests for {}'.format(category)
92 | scatter_kwargs = {
93 | k: v for k, v in kwargs.items()
94 | if k in inspect.getfullargspec(plt.scatter)[0]
95 | }
96 |
97 | plt.figure()
98 | plt.scatter(thresholds, means, **scatter_kwargs)
99 | plt.plot(thresholds, lowers, '-', color='g')
100 | plt.plot(thresholds, uppers, '-', color='g')
101 | plt.axis([low, high, 0, max(2, max(uppers) + 0.1)])
102 | plt.title(plot_title)
103 | plt.ylabel('Ratio')
104 | plt.xlabel('Threshold')
105 | plt.axhline(y=0.8, ls='dashed', color='r')
106 | plt.axhline(y=1.25, ls='dashed', color='r')
107 | if outpath is not None:
108 | plt.savefig(outpath)
109 | plt.show()
110 |
111 |
112 | def get_bias_plots(clf, df, feature_names, categories, **kwargs):
113 | """
114 | Generate bias plots from a classifier
115 |
116 | Parameters
117 | ------------
118 | clf : sklearn fitted clf
119 | with predict object
120 | df : pandas DataFrame
121 | untransformed df
122 | feature_names : list of strings
123 | names of features used in fitting clf
124 | categories : list of strings
125 | demographic column names used to test for bias,
126 | e.g. ['gender', 'ethnicity']
127 | **kwargs :
128 | additional arguments for `generate_bayes_factors`,
129 | `get_bias_chi2_pvals`, and `plt.scatter`,
130 | such as `low`, `high`, `num`, `prior_strength`, `s`, `marker`, `cmap`
131 |
132 | Returns
133 | ------------
134 | None
135 |
136 | Plots
137 | -------
138 | Linear plots of simulated pass ratios and chi squared p-values
139 | at assorted thresholds
140 | """
141 |
142 | thresholds, ratios, N = generate_bayesfactors(
143 | clf, df, feature_names, categories, **kwargs
144 | )
145 | keys = sorted(ratios.keys())
146 | for key in keys:
147 | plot_bias_test(thresholds, ratios[key], key, **kwargs)
148 |
149 | chi2_thresholds, chi2stat_pvals = get_bias_chi2_pvals(
150 | clf, df, feature_names, categories, **kwargs
151 | )
152 | for category in categories:
153 | chi2_pvals = [i[1] for i in chi2stat_pvals[category]]
154 | plot_bias_pvals(chi2_thresholds, chi2_pvals, category)
155 |
156 |
157 | def bias_report_plot(bias_report):
158 | """
159 | Plot bar plots for overall recommendation by bias group
160 |
161 | Parameters
162 | ------------
163 | bias_report: dict
164 | output of make_bias_report
165 |
166 | Returns
167 | ------------
168 | ax : matplotlib subplot axes
169 |
170 | Plots
171 | --------
172 | Bar plot containing recommendations across each bias group
173 | """
174 | for key in bias_report:
175 | data = bias_report[key]
176 | errs = list(map(lambda i: (i[1] - i[0]) / 2., data['errors']))
177 | labels = data['categories']
178 | avgs = data['averages']
179 | ind = np.arange(len(labels))
180 |
181 | fig, ax = plt.subplots()
182 | ax.bar(ind, avgs, 0.8, yerr=errs)
183 | ax.set_title(key)
184 | ax.set_xticks(ind + 0.8 // 2)
185 | ax.set_xticklabels(labels)
186 | plt.show()
187 |
188 | return ax
189 |
190 |
191 | def bias_bar_plot(clf, df, feature_names, categories, **kwargs):
192 | """
193 | Plot bar plots for overall recommendation by bias group
194 |
195 | Parameters
196 | ------------
197 | clf : sklearn clf
198 | fitted clf with predict object
199 | df : pandas DataFrame
200 | reference dataframe containing labeled features to test for bias
201 | feature_names : list of strings
202 | names of features used in fitting clf
203 | categories : list of strings
204 | names of categories to test for bias, e.g. ['gender']
205 | **kwargs : additional arguments for misc.make_bias_report,
206 | e.g. low, high, num for defining threshold values
207 |
208 | Returns
209 | ------------
210 | ax : matplotlib subplot axes
211 |
212 | Plots
213 | --------
214 | Bar plot containing recommendations across each bias group
215 | """
216 | bias_report = make_bias_report(clf, df, feature_names, categories,
217 | **kwargs)
218 |
219 | ax = bias_report_plot(bias_report)
220 |
221 | return ax
222 |
223 |
224 | def plot_threshold_tests(labels, results, category=None, comp_groups=None,
225 | include_metrics=('Proportions',
226 | 'z-test p-vals',
227 | 'Fisher Exact p-vals',
228 | 'Chi Squared p-vals',
229 | 'Bayes factors'),
230 | ref_prop=0.8, ref_z=0.05, ref_fisher_p=0.05,
231 | ref_chi2_p=0.05, ref_bayes=3., **kwargs):
232 | """
233 | Plot results of following tests across thresholds:
234 | - max-vs-min pass proportions
235 | - two-tailed z-test for max-vs-min groups
236 | - Fisher exact test for max-vs-min groups
237 | - Chi Squared test of independence for all groups
238 | - Bayes factor
239 |
240 | Parameters
241 | --------
242 | labels : array_like
243 | containing categorical values like ['M', 'F']
244 | results : array_like
245 | containing real numbers
246 | category : string
247 | corresponding to category of labels, e.g. 'Gender'
248 | comp_groups : list of strings
249 | optional -- subset of 2 labels to include, e.g. ['M', 'F']
250 | include_metrics : list/tuple of strings
251 | from `{'Proportions', 'z-scores', 'Fisher Exact p-vals',
252 | 'Chi Squared p-vals', 'Bayes factors'}`
253 | NB : if only one metric plot is wanted, use a trailing comma,
254 | e.g. `include_metrics = ('Proportions', )`
255 | ref_prop, ref_z, ref_fisher_p, ref_chi2_p, ref_bayes : floats
256 | designated values to plot horizontal reference line for
257 | corresponding metric
258 | **kwargs : additional arguments for continuous.compare_groups,
259 | e.g. low, high, num for defining threshold values
260 |
261 | Returns
262 | --------
263 | axarr : numpy.ndarray
264 | contains matplotlib subplot axes
265 |
266 | Plots
267 | --------
268 | Values for desired statistical tests across threshold values
269 | """
270 |
271 | reference_vals = (ref_prop, ref_z, ref_fisher_p, ref_chi2_p, ref_bayes)
272 |
273 | # supported metrics
274 | metric_names = (
275 | 'Proportions',
276 | 'z-test p-vals',
277 | 'Fisher Exact p-vals',
278 | 'Chi Squared p-vals',
279 | 'Bayes factors')
280 |
281 | # check to make sure only allowed metrics are included:
282 | if not set(include_metrics).issubset(set(metric_names)):
283 | raise KeyError(
284 | "include_metrics must be within "
285 | "{'Proportions', 'z-test p-vals', 'Fisher Exact p-vals', "
286 | "'Chi Squared p-vals', 'Bayes factors'}")
287 | # tuple of dictionaries containing {threshold: metric_value} pairs
288 | metric_dicts = compare_groups(labels, results, **kwargs)
289 | # map those dictionaries to their names
290 | names_mets_refvals_dict = dict(
291 | zip(metric_names, zip(metric_dicts, reference_vals)))
292 |
293 | # create subplots
294 | num_subplots = len(include_metrics)
295 | labels = list(map(str, labels))
296 |
297 | fig, axarr = plt.subplots(
298 | num_subplots, sharex=True, figsize=(
299 | 12, 3 + 2 * num_subplots))
300 |
301 | if category is None:
302 | category = ' vs '.join(sorted(set(labels)))
303 |
304 | fig.suptitle('Model Statistical Bias Tests {}'.format(category),
305 | fontsize=16)
306 |
307 | plt.rc('xtick', labelsize=8)
308 | plt.rc('ytick', labelsize=8)
309 |
310 | group_name = 'Min-Max Group'
311 |
312 | if comp_groups is not None:
313 | group_name = '{} vs {}'.format(*comp_groups[:2])
314 |
315 | def _make_metric_plot(ax, input_dict, metric_name, group_name,
316 | metric_bound=.05):
317 | """internal function for making a standalone metrics plot"""
318 | x_vals, y_vals = zip(*sorted(input_dict.items()))
319 | ax.plot(x_vals, y_vals, 'o-', c='blue')
320 | ax.set_title('{} {}'.format(group_name, metric_name), fontsize=14)
321 | ax.set_ylabel(metric_name, fontsize=12)
322 | ax.axhline(metric_bound, c='r', linestyle='--')
323 | ax.tick_params(labelsize=10)
324 | ax.set_xlabel('Threshold Value', fontsize=12)
325 |
326 | # plot relative pass rates
327 | def _add_metric_plot(axarr, input_dict, metric_name, group_name,
328 | subplot_no=0, metric_bound=.05, last_plot=False):
329 | """internal function for adding a metrics subplot"""
330 | x_vals, y_vals = zip(*sorted(input_dict.items()))
331 | axarr[subplot_no].plot(x_vals, y_vals, 'o-', c='blue')
332 | axarr[subplot_no].set_title('{} {}'.format(
333 | group_name, metric_name), fontsize=14)
334 | axarr[subplot_no].set_ylabel(metric_name, fontsize=12)
335 | axarr[subplot_no].axhline(metric_bound, c='r', linestyle='--')
336 | if subplot_no == 0:
337 | axarr[subplot_no].tick_params(labelsize=10)
338 | if last_plot is True:
339 | axarr[subplot_no].set_xlabel('Threshold Value', fontsize=12)
340 |
341 | for idx, met_name in enumerate(include_metrics):
342 | if met_name in ('Chi Squared p-vals',
343 | 'Bayes factors') and comp_groups is None:
344 | group_name = 'All Groups'
345 | last_plot = idx == num_subplots - 1
346 | met_dict, ref_val = names_mets_refvals_dict[met_name]
347 | if num_subplots == 1:
348 | _make_metric_plot(axarr, met_dict, met_name, group_name,
349 | metric_bound=ref_val)
350 | else:
351 | _add_metric_plot(axarr, met_dict, met_name, group_name,
352 | idx, ref_val, last_plot)
353 |
354 | fig.tight_layout()
355 | fig.subplots_adjust(top=.90)
356 | plt.show()
357 |
358 | return axarr
359 |
360 |
361 | def plot_group_proportions(labels, results, category=None, **kwargs):
362 | """
363 | Function for plotting group pass proportions at or below various thresholds
364 | NB: A group whose curve lies on top of another passes less frequently
365 | at or below that threshold
366 |
367 | Parameters
368 | ------------
369 | labels : array_like
370 | contains categorical labels
371 | results : array_like
372 | contains numeric or boolean values
373 | category : string
374 | describes label values, e.g. 'Gender'
375 | **kwargs : optional
376 | additional values for `misc.get_group_proportions` fn
377 | specifically - low, high, num values for thresholds to test
378 |
379 | Returns
380 | --------
381 | ax : matplotlib lines object
382 |
383 | Plots
384 | -------
385 | single plot:
386 | Overlays linear plots of pass rates below range of thresholds
387 | results for the n groups found in labels
388 |
389 | """
390 | prop_dict = get_group_proportions(labels, results, **kwargs)
391 | groups = prop_dict.keys()
392 | for group in groups:
393 | x_vals, y_vals = prop_dict[group]
394 | ax = plt.plot(x_vals, y_vals, label=group)
395 | plt.legend(loc='best')
396 | plt.xlabel('Threshold')
397 | plt.ylabel('Fraction of Group Below')
398 | if not category:
399 | category = '_vs_'.join(map(str, groups))
400 | plt.title("{} Cumulative Pass Rate Below Threshold".format(category))
401 | plt.show()
402 | return ax[0]
403 |
404 |
405 | def plot_kdes(labels=None,
406 | results=None,
407 | category=None,
408 | df=None,
409 | label_col=None,
410 | result_col=None,
411 | colors=None,
412 | **kwargs):
413 | """
414 | Plots KDEs and Cumulative KDEs
415 | Requires seaborn for plotting
416 |
417 | Can either pass in arrays of labels/results or else df
418 |
419 | Parameters
420 | -----------
421 | labels : array_like
422 | categorical values
423 | results : array_like
424 | numerical values
425 | category : string, optional
426 | name of label category for plotting, e.g. 'Gender'
427 | df : pandas DataFrame, optional
428 | label_col : string, optional
429 | name of labels column in df
430 | result_col : string, optional
431 | name of results column in df
432 | colors : list of strings, optional
433 | takes xkcd hue labels, e.g. ['red', 'blue', 'mustard yellow']
434 | more here: https://xkcd.com/color/rgb/
435 |
436 | Returns
437 | --------
438 | ax : numpy array of matplotlib axes
439 |
440 | Plots
441 | -------
442 | (1,2) subplots: KDE and cumulative KDE by group in `labels`
443 | """
444 | import seaborn as sns
445 | if df is None:
446 | df = pd.DataFrame(list(zip(labels, results)),
447 | columns=['label', 'result'])
448 | else:
449 | df = df.rename(columns={label_col: 'label', result_col: 'result'})
450 | unique_labels = df.label.dropna().unique()
451 | nlabels = len(unique_labels)
452 |
453 | # Check if there is a distribution to plot in each group
454 | stds = df.groupby('label')[['result']].std()
455 | if 0 in stds.values:
456 | groups = stds.index[stds['result'] == 0].values
457 | print('No distribution of results in groups: {}'.format(
458 | ', '.join([str(i) for i in groups])))
459 | return
460 |
461 | if not colors:
462 | base_colors = ['red', 'blue']
463 | others = list(set(sns.xkcd_rgb.keys()) - set(base_colors))
464 | extra_colors = list(np.random.choice(others, nlabels, replace=False))
465 | colors = list(base_colors + extra_colors)[:nlabels]
466 | sns.set_palette(sns.xkcd_palette(colors))
467 | fig, ax = plt.subplots(1, 2, figsize=(16, 6))
468 | if not category:
469 | category = '_vs_'.join(map(str, unique_labels))
470 | ax[0].set_title("{} KDEs".format(category))
471 | ax[1].set_title("{} Cumulative KDEs".format(category))
472 | ax[0].set_ylabel('Frequency')
473 | ax[1].set_ylabel('Group Fraction Below')
474 | ax[0].set_xlabel('Threshold')
475 | ax[1].set_xlabel('Threshold')
476 | for lab in unique_labels:
477 |
478 | sns.kdeplot(df.loc[df.label == lab].result,
479 | shade=True, label=lab, ax=ax[0], **kwargs)
480 | sns.kdeplot(df.loc[df.label == lab].result,
481 | shade=False, label=lab, ax=ax[1],
482 | cumulative=True, **kwargs)
483 |
484 | ax0_max_y = max([max(i.get_data()[1]) for i in ax[0].get_lines()])
485 | ax[0].set_ylim(0, ax0_max_y*1.1)
486 | plt.show()
487 |
488 | return ax
489 |
--------------------------------------------------------------------------------
/data/auditAI_gender_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pymetrics/audit-ai/4891b1d3c813a0e6ce657eaee3f1b2ab50e8f429/data/auditAI_gender_plot.png
--------------------------------------------------------------------------------
/data/student-mat.csv:
--------------------------------------------------------------------------------
1 | school;sex;age;address;famsize;Pstatus;Medu;Fedu;Mjob;Fjob;reason;guardian;traveltime;studytime;failures;schoolsup;famsup;paid;activities;nursery;higher;internet;romantic;famrel;freetime;goout;Dalc;Walc;health;absences;G1;G2;G3
2 | "GP";"F";18;"U";"GT3";"A";4;4;"at_home";"teacher";"course";"mother";2;2;0;"yes";"no";"no";"no";"yes";"yes";"no";"no";4;3;4;1;1;3;6;"5";"6";6
3 | "GP";"F";17;"U";"GT3";"T";1;1;"at_home";"other";"course";"father";1;2;0;"no";"yes";"no";"no";"no";"yes";"yes";"no";5;3;3;1;1;3;4;"5";"5";6
4 | "GP";"F";15;"U";"LE3";"T";1;1;"at_home";"other";"other";"mother";1;2;3;"yes";"no";"yes";"no";"yes";"yes";"yes";"no";4;3;2;2;3;3;10;"7";"8";10
5 | "GP";"F";15;"U";"GT3";"T";4;2;"health";"services";"home";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";3;2;2;1;1;5;2;"15";"14";15
6 | "GP";"F";16;"U";"GT3";"T";3;3;"other";"other";"home";"father";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";4;3;2;1;2;5;4;"6";"10";10
7 | "GP";"M";16;"U";"LE3";"T";4;3;"services";"other";"reputation";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;4;2;1;2;5;10;"15";"15";15
8 | "GP";"M";16;"U";"LE3";"T";2;2;"other";"other";"home";"mother";1;2;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;4;4;1;1;3;0;"12";"12";11
9 | "GP";"F";17;"U";"GT3";"A";4;4;"other";"teacher";"home";"mother";2;2;0;"yes";"yes";"no";"no";"yes";"yes";"no";"no";4;1;4;1;1;1;6;"6";"5";6
10 | "GP";"M";15;"U";"LE3";"A";3;2;"services";"other";"home";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;2;2;1;1;1;0;"16";"18";19
11 | "GP";"M";15;"U";"GT3";"T";3;4;"other";"other";"home";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;5;1;1;1;5;0;"14";"15";15
12 | "GP";"F";15;"U";"GT3";"T";4;4;"teacher";"health";"reputation";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";3;3;3;1;2;2;0;"10";"8";9
13 | "GP";"F";15;"U";"GT3";"T";2;1;"services";"other";"reputation";"father";3;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;2;2;1;1;4;4;"10";"12";12
14 | "GP";"M";15;"U";"LE3";"T";4;4;"health";"services";"course";"father";1;1;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;3;1;3;5;2;"14";"14";14
15 | "GP";"M";15;"U";"GT3";"T";4;3;"teacher";"other";"course";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";5;4;3;1;2;3;2;"10";"10";11
16 | "GP";"M";15;"U";"GT3";"A";2;2;"other";"other";"home";"other";1;3;0;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";4;5;2;1;1;3;0;"14";"16";16
17 | "GP";"F";16;"U";"GT3";"T";4;4;"health";"other";"home";"mother";1;1;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;4;4;1;2;2;4;"14";"14";14
18 | "GP";"F";16;"U";"GT3";"T";4;4;"services";"services";"reputation";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";3;2;3;1;2;2;6;"13";"14";14
19 | "GP";"F";16;"U";"GT3";"T";3;3;"other";"other";"reputation";"mother";3;2;0;"yes";"yes";"no";"yes";"yes";"yes";"no";"no";5;3;2;1;1;4;4;"8";"10";10
20 | "GP";"M";17;"U";"GT3";"T";3;2;"services";"services";"course";"mother";1;1;3;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;5;5;2;4;5;16;"6";"5";5
21 | "GP";"M";16;"U";"LE3";"T";4;3;"health";"other";"home";"father";1;1;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";3;1;3;1;3;5;4;"8";"10";10
22 | "GP";"M";15;"U";"GT3";"T";4;3;"teacher";"other";"reputation";"mother";1;2;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;4;1;1;1;1;0;"13";"14";15
23 | "GP";"M";15;"U";"GT3";"T";4;4;"health";"health";"other";"father";1;1;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";5;4;2;1;1;5;0;"12";"15";15
24 | "GP";"M";16;"U";"LE3";"T";4;2;"teacher";"other";"course";"mother";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";4;5;1;1;3;5;2;"15";"15";16
25 | "GP";"M";16;"U";"LE3";"T";2;2;"other";"other";"reputation";"mother";2;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;4;4;2;4;5;0;"13";"13";12
26 | "GP";"F";15;"R";"GT3";"T";2;4;"services";"health";"course";"mother";1;3;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;2;1;1;5;2;"10";"9";8
27 | "GP";"F";16;"U";"GT3";"T";2;2;"services";"services";"home";"mother";1;1;2;"no";"yes";"yes";"no";"no";"yes";"yes";"no";1;2;2;1;3;5;14;"6";"9";8
28 | "GP";"M";15;"U";"GT3";"T";2;2;"other";"other";"home";"mother";1;1;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;2;2;1;2;5;2;"12";"12";11
29 | "GP";"M";15;"U";"GT3";"T";4;2;"health";"services";"other";"mother";1;1;0;"no";"no";"yes";"no";"yes";"yes";"yes";"no";2;2;4;2;4;1;4;"15";"16";15
30 | "GP";"M";16;"U";"LE3";"A";3;4;"services";"other";"home";"mother";1;2;0;"yes";"yes";"no";"yes";"yes";"yes";"yes";"no";5;3;3;1;1;5;4;"11";"11";11
31 | "GP";"M";16;"U";"GT3";"T";4;4;"teacher";"teacher";"home";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;4;5;5;5;5;16;"10";"12";11
32 | "GP";"M";15;"U";"GT3";"T";4;4;"health";"services";"home";"mother";1;2;0;"no";"yes";"yes";"no";"no";"yes";"yes";"no";5;4;2;3;4;5;0;"9";"11";12
33 | "GP";"M";15;"U";"GT3";"T";4;4;"services";"services";"reputation";"mother";2;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;1;1;1;5;0;"17";"16";17
34 | "GP";"M";15;"R";"GT3";"T";4;3;"teacher";"at_home";"course";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;5;2;1;1;5;0;"17";"16";16
35 | "GP";"M";15;"U";"LE3";"T";3;3;"other";"other";"course";"mother";1;2;0;"no";"no";"no";"yes";"no";"yes";"yes";"no";5;3;2;1;1;2;0;"8";"10";12
36 | "GP";"M";16;"U";"GT3";"T";3;2;"other";"other";"home";"mother";1;1;0;"no";"yes";"yes";"no";"no";"yes";"yes";"no";5;4;3;1;1;5;0;"12";"14";15
37 | "GP";"F";15;"U";"GT3";"T";2;3;"other";"other";"other";"father";2;1;0;"no";"yes";"no";"yes";"yes";"yes";"no";"no";3;5;1;1;1;5;0;"8";"7";6
38 | "GP";"M";15;"U";"LE3";"T";4;3;"teacher";"services";"home";"mother";1;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;4;3;1;1;4;2;"15";"16";18
39 | "GP";"M";16;"R";"GT3";"A";4;4;"other";"teacher";"reputation";"mother";2;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";2;4;3;1;1;5;7;"15";"16";15
40 | "GP";"F";15;"R";"GT3";"T";3;4;"services";"health";"course";"mother";1;3;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;2;1;1;5;2;"12";"12";11
41 | "GP";"F";15;"R";"GT3";"T";2;2;"at_home";"other";"reputation";"mother";1;1;0;"yes";"yes";"yes";"yes";"yes";"yes";"no";"no";4;3;1;1;1;2;8;"14";"13";13
42 | "GP";"F";16;"U";"LE3";"T";2;2;"other";"other";"home";"mother";2;2;1;"no";"yes";"no";"yes";"no";"yes";"yes";"yes";3;3;3;1;2;3;25;"7";"10";11
43 | "GP";"M";15;"U";"LE3";"T";4;4;"teacher";"other";"home";"other";1;1;0;"no";"yes";"no";"no";"no";"yes";"yes";"yes";5;4;3;2;4;5;8;"12";"12";12
44 | "GP";"M";15;"U";"GT3";"T";4;4;"services";"teacher";"course";"father";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;3;1;1;5;2;"19";"18";18
45 | "GP";"M";15;"U";"GT3";"T";2;2;"services";"services";"course";"father";1;1;0;"yes";"yes";"no";"no";"yes";"yes";"yes";"no";5;4;1;1;1;1;0;"8";"8";11
46 | "GP";"F";16;"U";"LE3";"T";2;2;"other";"at_home";"course";"father";2;2;1;"yes";"no";"no";"yes";"yes";"yes";"yes";"no";4;3;3;2;2;5;14;"10";"10";9
47 | "GP";"F";15;"U";"LE3";"A";4;3;"other";"other";"course";"mother";1;2;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"yes";5;2;2;1;1;5;8;"8";"8";6
48 | "GP";"F";16;"U";"LE3";"A";3;3;"other";"services";"home";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";2;3;5;1;4;3;12;"11";"12";11
49 | "GP";"M";16;"U";"GT3";"T";4;3;"health";"services";"reputation";"mother";1;4;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";4;2;2;1;1;2;4;"19";"19";20
50 | "GP";"M";15;"U";"GT3";"T";4;2;"teacher";"other";"home";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";4;3;3;2;2;5;2;"15";"15";14
51 | "GP";"F";15;"U";"GT3";"T";4;4;"services";"teacher";"other";"father";1;2;1;"yes";"yes";"no";"yes";"no";"yes";"yes";"no";4;4;4;1;1;3;2;"7";"7";7
52 | "GP";"F";16;"U";"LE3";"T";2;2;"services";"services";"course";"mother";3;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;2;3;4;2;"12";"13";13
53 | "GP";"F";15;"U";"LE3";"T";4;2;"health";"other";"other";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;1;1;5;2;"11";"13";13
54 | "GP";"M";15;"U";"LE3";"A";4;2;"health";"health";"other";"father";2;1;1;"no";"no";"no";"no";"yes";"yes";"no";"no";5;5;5;3;4;5;6;"11";"11";10
55 | "GP";"F";15;"U";"GT3";"T";4;4;"services";"services";"course";"mother";1;1;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";3;3;4;2;3;5;0;"8";"10";11
56 | "GP";"F";15;"U";"LE3";"A";3;3;"other";"other";"other";"mother";1;1;0;"no";"no";"yes";"no";"yes";"yes";"yes";"no";5;3;4;4;4;1;6;"10";"13";13
57 | "GP";"F";16;"U";"GT3";"A";2;1;"other";"other";"other";"mother";1;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";5;3;4;1;1;2;8;"8";"9";10
58 | "GP";"F";15;"U";"GT3";"A";4;3;"services";"services";"reputation";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;2;1;1;1;0;"14";"15";15
59 | "GP";"M";15;"U";"GT3";"T";4;4;"teacher";"health";"reputation";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"no";"no";3;2;2;1;1;5;4;"14";"15";15
60 | "GP";"M";15;"U";"LE3";"T";1;2;"other";"at_home";"home";"father";1;2;0;"yes";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;2;1;1;5;2;"9";"10";9
61 | "GP";"F";16;"U";"GT3";"T";4;2;"services";"other";"course";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;2;3;1;1;5;2;"15";"16";16
62 | "GP";"F";16;"R";"GT3";"T";4;4;"health";"teacher";"other";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"no";"no";2;4;4;2;3;4;6;"10";"11";11
63 | "GP";"F";16;"U";"GT3";"T";1;1;"services";"services";"course";"father";4;1;0;"yes";"yes";"no";"yes";"no";"yes";"yes";"yes";5;5;5;5;5;5;6;"10";"8";11
64 | "GP";"F";16;"U";"LE3";"T";1;2;"other";"services";"reputation";"father";1;2;0;"yes";"no";"no";"yes";"yes";"yes";"yes";"no";4;4;3;1;1;1;4;"8";"10";9
65 | "GP";"F";16;"U";"GT3";"T";4;3;"teacher";"health";"home";"mother";1;3;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"no";3;4;4;2;4;4;2;"10";"9";9
66 | "GP";"F";15;"U";"LE3";"T";4;3;"services";"services";"reputation";"father";1;2;0;"yes";"no";"no";"yes";"yes";"yes";"yes";"yes";4;4;4;2;4;2;0;"10";"10";10
67 | "GP";"F";16;"U";"LE3";"T";4;3;"teacher";"services";"course";"mother";3;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;4;3;1;2;1;2;"16";"15";15
68 | "GP";"M";15;"U";"GT3";"A";4;4;"other";"services";"reputation";"mother";1;4;0;"no";"yes";"no";"yes";"no";"yes";"yes";"yes";1;3;3;5;5;3;4;"13";"13";12
69 | "GP";"F";16;"U";"GT3";"T";3;1;"services";"other";"course";"mother";1;4;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;1;2;5;4;"7";"7";6
70 | "GP";"F";15;"R";"LE3";"T";2;2;"health";"services";"reputation";"mother";2;2;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";4;1;3;1;3;4;2;"8";"9";8
71 | "GP";"F";15;"R";"LE3";"T";3;1;"other";"other";"reputation";"father";2;4;0;"no";"yes";"no";"no";"no";"yes";"yes";"no";4;4;2;2;3;3;12;"16";"16";16
72 | "GP";"M";16;"U";"GT3";"T";3;1;"other";"other";"reputation";"father";2;4;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;2;1;1;5;0;"13";"15";15
73 | "GP";"M";15;"U";"GT3";"T";4;2;"other";"other";"course";"mother";1;4;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;3;3;1;1;3;0;"10";"10";10
74 | "GP";"F";15;"R";"GT3";"T";1;1;"other";"other";"reputation";"mother";1;2;2;"yes";"yes";"no";"no";"no";"yes";"yes";"yes";3;3;4;2;4;5;2;"8";"6";5
75 | "GP";"M";16;"U";"GT3";"T";3;1;"other";"other";"reputation";"mother";1;1;0;"no";"no";"no";"yes";"yes";"yes";"no";"no";5;3;2;2;2;5;2;"12";"12";14
76 | "GP";"F";16;"U";"GT3";"T";3;3;"other";"services";"home";"mother";1;2;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;3;2;4;5;54;"11";"12";11
77 | "GP";"M";15;"U";"GT3";"T";4;3;"teacher";"other";"home";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;3;2;3;5;6;"9";"9";10
78 | "GP";"M";15;"U";"GT3";"T";4;0;"teacher";"other";"course";"mother";2;4;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";3;4;3;1;1;1;8;"11";"11";10
79 | "GP";"F";16;"U";"GT3";"T";2;2;"other";"other";"reputation";"mother";1;4;0;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";5;2;3;1;3;3;0;"11";"11";11
80 | "GP";"M";17;"U";"GT3";"T";2;1;"other";"other";"home";"mother";2;1;3;"yes";"yes";"no";"yes";"yes";"no";"yes";"no";4;5;1;1;1;3;2;"8";"8";10
81 | "GP";"F";16;"U";"GT3";"T";3;4;"at_home";"other";"course";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";2;4;3;1;2;3;12;"5";"5";5
82 | "GP";"M";15;"U";"GT3";"T";2;3;"other";"services";"course";"father";1;1;0;"yes";"yes";"yes";"yes";"no";"yes";"yes";"yes";3;2;2;1;3;3;2;"10";"12";12
83 | "GP";"M";15;"U";"GT3";"T";2;3;"other";"other";"home";"mother";1;3;0;"yes";"no";"yes";"no";"no";"yes";"yes";"no";5;3;2;1;2;5;4;"11";"10";11
84 | "GP";"F";15;"U";"LE3";"T";3;2;"services";"other";"reputation";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;4;4;1;1;5;10;"7";"6";6
85 | "GP";"M";15;"U";"LE3";"T";2;2;"services";"services";"home";"mother";2;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";5;3;3;1;3;4;4;"15";"15";15
86 | "GP";"F";15;"U";"GT3";"T";1;1;"other";"other";"home";"father";1;2;0;"no";"yes";"no";"yes";"no";"yes";"yes";"no";4;3;2;2;3;4;2;"9";"10";10
87 | "GP";"F";15;"U";"GT3";"T";4;4;"services";"services";"reputation";"father";2;2;2;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";4;4;4;2;3;5;6;"7";"9";8
88 | "GP";"F";16;"U";"LE3";"T";2;2;"at_home";"other";"course";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"no";"no";4;3;4;1;2;2;4;"8";"7";6
89 | "GP";"F";15;"U";"GT3";"T";4;2;"other";"other";"reputation";"mother";1;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;3;3;1;3;1;4;"13";"14";14
90 | "GP";"M";16;"U";"GT3";"T";2;2;"services";"other";"reputation";"father";2;2;1;"no";"no";"yes";"yes";"no";"yes";"yes";"no";4;4;2;1;1;3;12;"11";"10";10
91 | "GP";"M";16;"U";"LE3";"A";4;4;"teacher";"health";"reputation";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"no";"no";4;1;3;3;5;5;18;"8";"6";7
92 | "GP";"F";16;"U";"GT3";"T";3;3;"other";"other";"home";"mother";1;3;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;3;3;1;3;4;0;"7";"7";8
93 | "GP";"F";15;"U";"GT3";"T";4;3;"services";"other";"reputation";"mother";1;1;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";4;5;5;1;3;1;4;"16";"17";18
94 | "GP";"F";16;"U";"LE3";"T";3;1;"other";"other";"home";"father";1;2;0;"yes";"yes";"no";"no";"yes";"yes";"no";"no";3;3;3;2;3;2;4;"7";"6";6
95 | "GP";"F";16;"U";"GT3";"T";4;2;"teacher";"services";"home";"mother";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;3;3;1;1;1;0;"11";"10";10
96 | "GP";"M";15;"U";"LE3";"T";2;2;"services";"health";"reputation";"mother";1;4;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;4;1;1;4;6;"11";"13";14
97 | "GP";"F";15;"R";"GT3";"T";1;1;"at_home";"other";"home";"mother";2;4;1;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"no";3;1;2;1;1;1;2;"7";"10";10
98 | "GP";"M";16;"R";"GT3";"T";4;3;"services";"other";"reputation";"mother";2;1;0;"yes";"yes";"no";"yes";"no";"yes";"yes";"no";3;3;3;1;1;4;2;"11";"15";15
99 | "GP";"F";16;"U";"GT3";"T";2;1;"other";"other";"course";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"no";"yes";4;3;5;1;1;5;2;"8";"9";10
100 | "GP";"F";16;"U";"GT3";"T";4;4;"other";"other";"reputation";"mother";1;1;0;"no";"no";"no";"yes";"no";"yes";"yes";"no";5;3;4;1;2;1;6;"11";"14";14
101 | "GP";"F";16;"U";"GT3";"T";4;3;"other";"at_home";"course";"mother";1;3;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";5;3;5;1;1;3;0;"7";"9";8
102 | "GP";"M";16;"U";"GT3";"T";4;4;"services";"services";"other";"mother";1;1;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;5;5;5;5;4;14;"7";"7";5
103 | "GP";"M";16;"U";"GT3";"T";4;4;"services";"teacher";"other";"father";1;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;4;3;1;1;4;0;"16";"17";17
104 | "GP";"M";15;"U";"GT3";"T";4;4;"services";"other";"course";"mother";1;1;0;"no";"yes";"no";"yes";"no";"yes";"yes";"no";5;3;3;1;1;5;4;"10";"13";14
105 | "GP";"F";15;"U";"GT3";"T";3;2;"services";"other";"home";"mother";2;2;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;5;1;1;2;26;"7";"6";6
106 | "GP";"M";15;"U";"GT3";"A";3;4;"services";"other";"course";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;4;4;1;1;1;0;"16";"18";18
107 | "GP";"F";15;"U";"GT3";"A";3;3;"other";"health";"reputation";"father";1;4;0;"yes";"no";"no";"no";"yes";"yes";"no";"no";4;3;3;1;1;4;10;"10";"11";11
108 | "GP";"F";15;"U";"GT3";"T";2;2;"other";"other";"course";"mother";1;4;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";5;1;2;1;1;3;8;"7";"8";8
109 | "GP";"M";16;"U";"GT3";"T";3;3;"services";"other";"home";"father";1;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;3;3;1;1;5;2;"16";"18";18
110 | "GP";"M";15;"R";"GT3";"T";4;4;"other";"other";"home";"father";4;4;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";1;3;5;3;5;1;6;"10";"13";13
111 | "GP";"F";16;"U";"LE3";"T";4;4;"health";"health";"other";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";5;4;5;1;1;4;4;"14";"15";16
112 | "GP";"M";15;"U";"LE3";"A";4;4;"teacher";"teacher";"course";"mother";1;1;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";5;5;3;1;1;4;6;"18";"19";19
113 | "GP";"F";16;"R";"GT3";"T";3;3;"services";"other";"reputation";"father";1;3;1;"yes";"yes";"no";"yes";"yes";"yes";"yes";"no";4;1;2;1;1;2;0;"7";"10";10
114 | "GP";"F";16;"U";"GT3";"T";2;2;"at_home";"other";"home";"mother";1;2;1;"yes";"no";"no";"yes";"yes";"yes";"yes";"no";3;1;2;1;1;5;6;"10";"13";13
115 | "GP";"M";15;"U";"LE3";"T";4;2;"teacher";"other";"course";"mother";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;5;2;1;1;3;10;"18";"19";19
116 | "GP";"M";15;"R";"GT3";"T";2;1;"health";"services";"reputation";"mother";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";5;4;2;1;1;5;8;"9";"9";9
117 | "GP";"M";16;"U";"GT3";"T";4;4;"teacher";"teacher";"course";"father";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;4;4;1;2;5;2;"15";"15";16
118 | "GP";"M";15;"U";"GT3";"T";4;4;"other";"teacher";"reputation";"father";2;2;0;"no";"yes";"no";"yes";"yes";"yes";"no";"no";4;4;3;1;1;2;2;"11";"13";14
119 | "GP";"M";16;"U";"GT3";"T";3;3;"other";"services";"home";"father";2;1;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";5;4;2;1;1;5;0;"13";"14";13
120 | "GP";"M";17;"R";"GT3";"T";1;3;"other";"other";"course";"father";3;2;1;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";5;2;4;1;4;5;20;"9";"7";8
121 | "GP";"M";15;"U";"GT3";"T";3;4;"other";"other";"reputation";"father";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;4;3;1;2;4;6;"14";"13";13
122 | "GP";"F";15;"U";"GT3";"T";1;2;"at_home";"services";"course";"mother";1;2;0;"no";"no";"no";"no";"no";"yes";"yes";"no";3;2;3;1;2;1;2;"16";"15";15
123 | "GP";"M";15;"U";"GT3";"T";2;2;"services";"services";"home";"father";1;4;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;5;4;1;2;5;6;"16";"14";15
124 | "GP";"F";16;"U";"LE3";"T";2;4;"other";"health";"course";"father";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;2;2;1;2;5;2;"13";"13";13
125 | "GP";"M";16;"U";"GT3";"T";4;4;"health";"other";"course";"mother";1;1;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";3;4;4;1;4;5;18;"14";"11";13
126 | "GP";"F";16;"U";"GT3";"T";2;2;"other";"other";"home";"mother";1;2;0;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";5;4;4;1;1;5;0;"8";"7";8
127 | "GP";"M";15;"U";"GT3";"T";3;4;"services";"services";"home";"father";1;1;0;"yes";"no";"no";"no";"yes";"yes";"yes";"no";5;5;5;3;2;5;0;"13";"13";12
128 | "GP";"F";15;"U";"LE3";"A";3;4;"other";"other";"home";"mother";1;2;0;"yes";"no";"no";"yes";"yes";"yes";"yes";"yes";5;3;2;1;1;1;0;"7";"10";11
129 | "GP";"F";19;"U";"GT3";"T";0;1;"at_home";"other";"course";"other";1;2;3;"no";"yes";"no";"no";"no";"no";"no";"no";3;4;2;1;1;5;2;"7";"8";9
130 | "GP";"M";18;"R";"GT3";"T";2;2;"services";"other";"reputation";"mother";1;1;2;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";3;3;3;1;2;4;0;"7";"4";0
131 | "GP";"M";16;"R";"GT3";"T";4;4;"teacher";"teacher";"course";"mother";1;1;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";3;5;5;2;5;4;8;"18";"18";18
132 | "GP";"F";15;"R";"GT3";"T";3;4;"services";"teacher";"course";"father";2;3;2;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";4;2;2;2;2;5;0;"12";"0";0
133 | "GP";"F";15;"U";"GT3";"T";1;1;"at_home";"other";"course";"mother";3;1;0;"no";"yes";"no";"yes";"no";"yes";"yes";"yes";4;3;3;1;2;4;0;"8";"0";0
134 | "GP";"F";17;"U";"LE3";"T";2;2;"other";"other";"course";"father";1;1;0;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";3;4;4;1;3;5;12;"10";"13";12
135 | "GP";"F";16;"U";"GT3";"A";3;4;"services";"other";"course";"father";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;2;1;1;4;5;16;"12";"11";11
136 | "GP";"M";15;"R";"GT3";"T";3;4;"at_home";"teacher";"course";"mother";4;2;0;"no";"yes";"no";"no";"yes";"yes";"no";"yes";5;3;3;1;1;5;0;"9";"0";0
137 | "GP";"F";15;"U";"GT3";"T";4;4;"services";"at_home";"course";"mother";1;3;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;3;3;1;1;5;0;"11";"0";0
138 | "GP";"M";17;"R";"GT3";"T";3;4;"at_home";"other";"course";"mother";3;2;0;"no";"no";"no";"no";"yes";"yes";"no";"no";5;4;5;2;4;5;0;"10";"0";0
139 | "GP";"F";16;"U";"GT3";"A";3;3;"other";"other";"course";"other";2;1;2;"no";"yes";"no";"yes";"no";"yes";"yes";"yes";4;3;2;1;1;5;0;"4";"0";0
140 | "GP";"M";16;"U";"LE3";"T";1;1;"services";"other";"course";"mother";1;2;1;"no";"no";"no";"no";"yes";"yes";"no";"yes";4;4;4;1;3;5;0;"14";"12";12
141 | "GP";"F";15;"U";"GT3";"T";4;4;"teacher";"teacher";"course";"mother";2;1;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";4;3;2;1;1;5;0;"16";"16";15
142 | "GP";"M";15;"U";"GT3";"T";4;3;"teacher";"services";"course";"father";2;4;0;"yes";"yes";"no";"no";"yes";"yes";"yes";"no";2;2;2;1;1;3;0;"7";"9";0
143 | "GP";"M";16;"U";"LE3";"T";2;2;"services";"services";"reputation";"father";2;1;2;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";2;3;3;2;2;2;8;"9";"9";9
144 | "GP";"F";15;"U";"GT3";"T";4;4;"teacher";"services";"course";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;2;2;1;1;5;2;"9";"11";11
145 | "GP";"F";16;"U";"LE3";"T";1;1;"at_home";"at_home";"course";"mother";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;4;4;3;3;1;2;"14";"14";13
146 | "GP";"M";17;"U";"GT3";"T";2;1;"other";"other";"home";"mother";1;1;3;"no";"yes";"no";"no";"yes";"yes";"yes";"no";5;4;5;1;2;5;0;"5";"0";0
147 | "GP";"F";15;"U";"GT3";"T";1;1;"other";"services";"course";"father";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;4;2;1;2;5;0;"8";"11";11
148 | "GP";"F";15;"U";"GT3";"T";3;2;"health";"services";"home";"father";1;2;3;"no";"yes";"no";"no";"yes";"yes";"yes";"no";3;3;2;1;1;3;0;"6";"7";0
149 | "GP";"F";15;"U";"GT3";"T";1;2;"at_home";"other";"course";"mother";1;2;0;"no";"yes";"yes";"no";"no";"yes";"yes";"no";4;3;2;1;1;5;2;"10";"11";11
150 | "GP";"M";16;"U";"GT3";"T";4;4;"teacher";"teacher";"course";"mother";1;1;0;"no";"yes";"no";"no";"yes";"no";"yes";"yes";3;3;2;2;1;5;0;"7";"6";0
151 | "GP";"M";15;"U";"LE3";"A";2;1;"services";"other";"course";"mother";4;1;3;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;5;5;2;5;5;0;"8";"9";10
152 | "GP";"M";18;"U";"LE3";"T";1;1;"other";"other";"course";"mother";1;1;3;"no";"no";"no";"no";"yes";"no";"yes";"yes";2;3;5;2;5;4;0;"6";"5";0
153 | "GP";"M";16;"U";"LE3";"T";2;1;"at_home";"other";"course";"mother";1;1;1;"no";"no";"no";"yes";"yes";"yes";"no";"yes";4;4;4;3;5;5;6;"12";"13";14
154 | "GP";"F";15;"R";"GT3";"T";3;3;"services";"services";"reputation";"other";2;3;2;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;2;1;2;3;3;8;"10";"10";10
155 | "GP";"M";19;"U";"GT3";"T";3;2;"services";"at_home";"home";"mother";1;1;3;"no";"yes";"no";"no";"yes";"no";"yes";"yes";4;5;4;1;1;4;0;"5";"0";0
156 | "GP";"F";17;"U";"GT3";"T";4;4;"other";"teacher";"course";"mother";1;1;0;"yes";"yes";"no";"no";"yes";"yes";"no";"yes";4;2;1;1;1;4;0;"11";"11";12
157 | "GP";"M";15;"R";"GT3";"T";2;3;"at_home";"services";"course";"mother";1;2;0;"yes";"no";"yes";"yes";"yes";"yes";"no";"no";4;4;4;1;1;1;2;"11";"8";8
158 | "GP";"M";17;"R";"LE3";"T";1;2;"other";"other";"reputation";"mother";1;1;0;"no";"no";"no";"no";"yes";"yes";"no";"no";2;2;2;3;3;5;8;"16";"12";13
159 | "GP";"F";18;"R";"GT3";"T";1;1;"at_home";"other";"course";"mother";3;1;3;"no";"yes";"no";"yes";"no";"yes";"no";"no";5;2;5;1;5;4;6;"9";"8";10
160 | "GP";"M";16;"R";"GT3";"T";2;2;"at_home";"other";"course";"mother";3;1;0;"no";"no";"no";"no";"no";"yes";"no";"no";4;2;2;1;2;3;2;"17";"15";15
161 | "GP";"M";16;"U";"GT3";"T";3;3;"other";"services";"course";"father";1;2;1;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;5;5;4;4;5;4;"10";"12";12
162 | "GP";"M";17;"R";"LE3";"T";2;1;"at_home";"other";"course";"mother";2;1;2;"no";"no";"no";"yes";"yes";"no";"yes";"yes";3;3;2;2;2;5;0;"7";"6";0
163 | "GP";"M";15;"R";"GT3";"T";3;2;"other";"other";"course";"mother";2;2;2;"yes";"yes";"no";"no";"yes";"yes";"yes";"yes";4;4;4;1;4;3;6;"5";"9";7
164 | "GP";"M";16;"U";"LE3";"T";1;2;"other";"other";"course";"mother";2;1;1;"no";"no";"no";"yes";"yes";"yes";"no";"no";4;4;4;2;4;5;0;"7";"0";0
165 | "GP";"M";17;"U";"GT3";"T";1;3;"at_home";"services";"course";"father";1;1;0;"no";"no";"no";"no";"yes";"no";"yes";"no";5;3;3;1;4;2;2;"10";"10";10
166 | "GP";"M";17;"R";"LE3";"T";1;1;"other";"services";"course";"mother";4;2;3;"no";"no";"no";"yes";"yes";"no";"no";"yes";5;3;5;1;5;5;0;"5";"8";7
167 | "GP";"M";16;"U";"GT3";"T";3;2;"services";"services";"course";"mother";2;1;1;"no";"yes";"no";"yes";"no";"no";"no";"no";4;5;2;1;1;2;16;"12";"11";12
168 | "GP";"M";16;"U";"GT3";"T";2;2;"other";"other";"course";"father";1;2;0;"no";"no";"no";"no";"yes";"no";"yes";"no";4;3;5;2;4;4;4;"10";"10";10
169 | "GP";"F";16;"U";"GT3";"T";4;2;"health";"services";"home";"father";1;2;0;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";4;2;3;1;1;3;0;"14";"15";16
170 | "GP";"F";16;"U";"GT3";"T";2;2;"other";"other";"home";"mother";1;2;0;"no";"yes";"yes";"no";"no";"yes";"yes";"no";5;1;5;1;1;4;0;"6";"7";0
171 | "GP";"F";16;"U";"GT3";"T";4;4;"health";"health";"reputation";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;4;2;1;1;3;0;"14";"14";14
172 | "GP";"M";16;"U";"GT3";"T";3;4;"other";"other";"course";"father";3;1;2;"no";"yes";"no";"yes";"no";"yes";"yes";"no";3;4;5;2;4;2;0;"6";"5";0
173 | "GP";"M";16;"U";"GT3";"T";1;0;"other";"other";"reputation";"mother";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;3;2;1;1;3;2;"13";"15";16
174 | "GP";"M";17;"U";"LE3";"T";4;4;"teacher";"other";"reputation";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;4;4;1;3;5;0;"13";"11";10
175 | "GP";"F";16;"U";"GT3";"T";1;3;"at_home";"services";"home";"mother";1;2;3;"no";"no";"no";"yes";"no";"yes";"yes";"yes";4;3;5;1;1;3;0;"8";"7";0
176 | "GP";"F";16;"U";"LE3";"T";3;3;"other";"other";"reputation";"mother";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;4;5;1;1;4;4;"10";"11";9
177 | "GP";"M";17;"U";"LE3";"T";4;3;"teacher";"other";"course";"mother";2;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";4;4;4;4;4;4;4;"10";"9";9
178 | "GP";"F";16;"U";"GT3";"T";2;2;"services";"other";"reputation";"mother";2;2;0;"no";"no";"yes";"yes";"no";"yes";"yes";"no";3;4;4;1;4;5;2;"13";"13";11
179 | "GP";"M";17;"U";"GT3";"T";3;3;"other";"other";"reputation";"father";1;2;0;"no";"no";"no";"yes";"no";"yes";"yes";"no";4;3;4;1;4;4;4;"6";"5";6
180 | "GP";"M";16;"R";"GT3";"T";4;2;"teacher";"services";"other";"mother";1;1;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;3;3;3;4;3;10;"10";"8";9
181 | "GP";"M";17;"U";"GT3";"T";4;3;"other";"other";"course";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";5;2;3;1;1;2;4;"10";"10";11
182 | "GP";"M";16;"U";"GT3";"T";4;3;"teacher";"other";"home";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";3;4;3;2;3;3;10;"9";"8";8
183 | "GP";"M";16;"U";"GT3";"T";3;3;"services";"other";"home";"mother";1;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";4;2;3;1;2;3;2;"12";"13";12
184 | "GP";"F";17;"U";"GT3";"T";2;4;"services";"services";"reputation";"father";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"no";"no";5;4;2;2;3;5;0;"16";"17";17
185 | "GP";"F";17;"U";"LE3";"T";3;3;"other";"other";"reputation";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";5;3;3;2;3;1;56;"9";"9";8
186 | "GP";"F";16;"U";"GT3";"T";3;2;"other";"other";"reputation";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";1;2;2;1;2;1;14;"12";"13";12
187 | "GP";"M";17;"U";"GT3";"T";3;3;"services";"services";"other";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;3;4;2;3;4;12;"12";"12";11
188 | "GP";"M";16;"U";"GT3";"T";1;2;"services";"services";"other";"mother";1;1;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";3;3;3;1;2;3;2;"11";"12";11
189 | "GP";"M";16;"U";"LE3";"T";2;1;"other";"other";"course";"mother";1;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";4;2;3;1;2;5;0;"15";"15";15
190 | "GP";"F";17;"U";"GT3";"A";3;3;"health";"other";"reputation";"mother";1;2;0;"no";"yes";"no";"no";"no";"yes";"yes";"yes";3;3;3;1;3;3;6;"8";"7";9
191 | "GP";"M";17;"R";"GT3";"T";1;2;"at_home";"other";"home";"mother";1;2;0;"no";"no";"no";"no";"yes";"yes";"no";"no";3;1;3;1;5;3;4;"8";"9";10
192 | "GP";"F";16;"U";"GT3";"T";2;3;"services";"services";"course";"mother";1;2;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;3;3;1;1;2;10;"11";"12";13
193 | "GP";"F";17;"U";"GT3";"T";1;1;"at_home";"services";"course";"mother";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";5;3;3;1;1;3;0;"8";"8";9
194 | "GP";"M";17;"U";"GT3";"T";1;2;"at_home";"services";"other";"other";2;2;0;"no";"no";"yes";"yes";"no";"yes";"yes";"no";4;4;4;4;5;5;12;"7";"8";8
195 | "GP";"M";16;"R";"GT3";"T";3;3;"services";"services";"reputation";"mother";1;1;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;2;3;4;5;8;"8";"9";10
196 | "GP";"M";16;"U";"GT3";"T";2;3;"other";"other";"home";"father";2;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";5;3;3;1;1;3;0;"13";"14";14
197 | "GP";"F";17;"U";"LE3";"T";2;4;"services";"services";"course";"father";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";4;3;2;1;1;5;0;"14";"15";15
198 | "GP";"M";17;"U";"GT3";"T";4;4;"services";"teacher";"home";"mother";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";5;2;3;1;2;5;4;"17";"15";16
199 | "GP";"M";16;"R";"LE3";"T";3;3;"teacher";"other";"home";"father";3;1;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";3;3;4;3;5;3;8;"9";"9";10
200 | "GP";"F";17;"U";"GT3";"T";4;4;"services";"teacher";"home";"mother";2;1;1;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;2;4;2;3;2;24;"18";"18";18
201 | "GP";"F";16;"U";"LE3";"T";4;4;"teacher";"teacher";"reputation";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;5;2;1;2;3;0;"9";"9";10
202 | "GP";"F";16;"U";"GT3";"T";4;3;"health";"other";"home";"mother";1;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;5;1;5;2;2;"16";"16";16
203 | "GP";"F";16;"U";"GT3";"T";2;3;"other";"other";"reputation";"mother";1;2;0;"yes";"yes";"yes";"yes";"yes";"yes";"no";"no";4;4;3;1;3;4;6;"8";"10";10
204 | "GP";"F";17;"U";"GT3";"T";1;1;"other";"other";"course";"mother";1;2;0;"no";"yes";"yes";"no";"no";"yes";"no";"no";4;4;4;1;3;1;4;"9";"9";10
205 | "GP";"F";17;"R";"GT3";"T";2;2;"other";"other";"reputation";"mother";1;1;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";5;3;2;1;2;3;18;"7";"6";6
206 | "GP";"F";16;"R";"GT3";"T";2;2;"services";"services";"reputation";"mother";2;4;0;"no";"yes";"yes";"yes";"no";"yes";"yes";"no";5;3;5;1;1;5;6;"10";"10";11
207 | "GP";"F";17;"U";"GT3";"T";3;4;"at_home";"services";"home";"mother";1;3;1;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;4;3;3;4;5;28;"10";"9";9
208 | "GP";"F";16;"U";"GT3";"A";3;1;"services";"other";"course";"mother";1;2;3;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";2;3;3;2;2;4;5;"7";"7";7
209 | "GP";"F";16;"U";"GT3";"T";4;3;"teacher";"other";"other";"mother";1;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";1;3;2;1;1;1;10;"11";"12";13
210 | "GP";"F";16;"U";"GT3";"T";1;1;"at_home";"other";"home";"mother";2;1;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";4;3;2;1;4;5;6;"9";"9";10
211 | "GP";"F";17;"R";"GT3";"T";4;3;"teacher";"other";"reputation";"mother";2;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;4;2;1;1;4;6;"7";"7";7
212 | "GP";"F";19;"U";"GT3";"T";3;3;"other";"other";"reputation";"other";1;4;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;3;1;2;3;10;"8";"8";8
213 | "GP";"M";17;"U";"LE3";"T";4;4;"services";"other";"home";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";5;3;5;4;5;3;13;"12";"12";13
214 | "GP";"F";16;"U";"GT3";"A";2;2;"other";"other";"reputation";"mother";1;2;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"no";3;3;4;1;1;4;0;"12";"13";14
215 | "GP";"M";18;"U";"GT3";"T";2;2;"services";"other";"home";"mother";1;2;1;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;4;4;2;4;5;15;"6";"7";8
216 | "GP";"F";17;"R";"LE3";"T";4;4;"services";"other";"other";"mother";1;1;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";5;2;1;1;2;3;12;"8";"10";10
217 | "GP";"F";17;"U";"LE3";"T";3;2;"other";"other";"reputation";"mother";2;2;0;"no";"no";"yes";"no";"yes";"yes";"yes";"no";4;4;4;1;3;1;2;"14";"15";15
218 | "GP";"F";17;"U";"GT3";"T";4;3;"other";"other";"reputation";"mother";1;2;2;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";3;4;5;2;4;1;22;"6";"6";4
219 | "GP";"M";18;"U";"LE3";"T";3;3;"services";"health";"home";"father";1;2;1;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";3;2;4;2;4;4;13;"6";"6";8
220 | "GP";"F";17;"U";"GT3";"T";2;3;"at_home";"other";"home";"father";2;1;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";3;3;3;1;4;3;3;"7";"7";8
221 | "GP";"F";17;"U";"GT3";"T";2;2;"at_home";"at_home";"course";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;3;1;1;4;4;"9";"10";10
222 | "GP";"F";17;"R";"GT3";"T";2;1;"at_home";"services";"reputation";"mother";2;2;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;2;5;1;2;5;2;"6";"6";6
223 | "GP";"F";17;"U";"GT3";"T";1;1;"at_home";"other";"reputation";"mother";1;3;1;"no";"yes";"no";"yes";"yes";"yes";"no";"yes";4;3;4;1;1;5;0;"6";"5";0
224 | "GP";"F";16;"U";"GT3";"T";2;3;"services";"teacher";"other";"mother";1;2;0;"yes";"no";"no";"no";"yes";"yes";"yes";"no";2;3;1;1;1;3;2;"16";"16";17
225 | "GP";"M";18;"U";"GT3";"T";2;2;"other";"other";"home";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";3;3;3;5;5;4;0;"12";"13";13
226 | "GP";"F";16;"U";"GT3";"T";4;4;"teacher";"services";"home";"mother";1;3;0;"no";"yes";"no";"yes";"no";"yes";"yes";"no";5;3;2;1;1;5;0;"13";"13";14
227 | "GP";"F";18;"R";"GT3";"T";3;1;"other";"other";"reputation";"mother";1;2;1;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";5;3;3;1;1;4;16;"9";"8";7
228 | "GP";"F";17;"U";"GT3";"T";3;2;"other";"other";"course";"mother";1;2;0;"no";"no";"no";"yes";"no";"yes";"yes";"no";5;3;4;1;3;3;10;"16";"15";15
229 | "GP";"M";17;"U";"LE3";"T";2;3;"services";"services";"reputation";"father";1;2;0;"no";"yes";"yes";"no";"no";"yes";"yes";"no";5;3;3;1;3;3;2;"12";"11";12
230 | "GP";"M";18;"U";"LE3";"T";2;1;"at_home";"other";"course";"mother";4;2;0;"yes";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;3;2;4;5;3;14;"10";"8";9
231 | "GP";"F";17;"U";"GT3";"A";2;1;"other";"other";"course";"mother";2;3;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";3;2;3;1;2;3;10;"12";"10";12
232 | "GP";"F";17;"U";"LE3";"T";4;3;"health";"other";"reputation";"father";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";3;2;3;1;2;3;14;"13";"13";14
233 | "GP";"M";17;"R";"GT3";"T";2;2;"other";"other";"course";"father";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;5;2;1;1;1;4;"11";"11";11
234 | "GP";"M";17;"U";"GT3";"T";4;4;"teacher";"teacher";"reputation";"mother";1;2;0;"yes";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;5;5;1;3;2;14;"11";"9";9
235 | "GP";"M";16;"U";"GT3";"T";4;4;"health";"other";"reputation";"father";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;2;4;2;4;1;2;"14";"13";13
236 | "GP";"M";16;"U";"LE3";"T";1;1;"other";"other";"home";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";3;4;2;1;1;5;18;"9";"7";6
237 | "GP";"M";16;"U";"GT3";"T";3;2;"at_home";"other";"reputation";"mother";2;3;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";5;3;3;1;3;2;10;"11";"9";10
238 | "GP";"M";17;"U";"LE3";"T";2;2;"other";"other";"home";"father";1;2;0;"no";"no";"yes";"yes";"no";"yes";"yes";"yes";4;4;2;5;5;4;4;"14";"13";13
239 | "GP";"F";16;"U";"GT3";"T";2;1;"other";"other";"home";"mother";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"yes";4;5;2;1;1;5;20;"13";"12";12
240 | "GP";"F";17;"R";"GT3";"T";2;1;"at_home";"services";"course";"mother";3;2;0;"no";"no";"no";"yes";"yes";"yes";"no";"no";2;1;1;1;1;3;2;"13";"11";11
241 | "GP";"M";18;"U";"GT3";"T";2;2;"other";"services";"reputation";"father";1;2;1;"no";"no";"no";"no";"yes";"no";"yes";"no";5;5;4;3;5;2;0;"7";"7";0
242 | "GP";"M";17;"U";"LE3";"T";4;3;"health";"other";"course";"mother";2;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";2;5;5;1;4;5;14;"12";"12";12
243 | "GP";"M";17;"R";"LE3";"A";4;4;"teacher";"other";"course";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";3;3;3;2;3;4;2;"10";"11";12
244 | "GP";"M";16;"U";"LE3";"T";4;3;"teacher";"other";"course";"mother";1;1;0;"no";"no";"no";"yes";"no";"yes";"yes";"no";5;4;5;1;1;3;0;"6";"0";0
245 | "GP";"M";16;"U";"GT3";"T";4;4;"services";"services";"course";"mother";1;1;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";5;3;2;1;2;5;0;"13";"12";12
246 | "GP";"F";18;"U";"GT3";"T";2;1;"other";"other";"course";"other";2;3;0;"no";"yes";"yes";"no";"no";"yes";"yes";"yes";4;4;4;1;1;3;0;"7";"0";0
247 | "GP";"M";16;"U";"GT3";"T";2;1;"other";"other";"course";"mother";3;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;3;3;1;1;4;6;"18";"18";18
248 | "GP";"M";17;"U";"GT3";"T";2;3;"other";"other";"course";"father";2;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";5;2;2;1;1;2;4;"12";"12";13
249 | "GP";"M";22;"U";"GT3";"T";3;1;"services";"services";"other";"mother";1;1;3;"no";"no";"no";"no";"no";"no";"yes";"yes";5;4;5;5;5;1;16;"6";"8";8
250 | "GP";"M";18;"R";"LE3";"T";3;3;"other";"services";"course";"mother";1;2;1;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";4;3;3;1;3;5;8;"3";"5";5
251 | "GP";"M";16;"U";"GT3";"T";0;2;"other";"other";"other";"mother";1;1;0;"no";"no";"yes";"no";"no";"yes";"yes";"no";4;3;2;2;4;5;0;"13";"15";15
252 | "GP";"M";18;"U";"GT3";"T";3;2;"services";"other";"course";"mother";2;1;1;"no";"no";"no";"no";"yes";"no";"yes";"no";4;4;5;2;4;5;0;"6";"8";8
253 | "GP";"M";16;"U";"GT3";"T";3;3;"at_home";"other";"reputation";"other";3;2;0;"yes";"yes";"no";"no";"no";"yes";"yes";"no";5;3;3;1;3;2;6;"7";"10";10
254 | "GP";"M";18;"U";"GT3";"T";2;1;"services";"services";"other";"mother";1;1;1;"no";"no";"no";"no";"no";"no";"yes";"no";3;2;5;2;5;5;4;"6";"9";8
255 | "GP";"M";16;"R";"GT3";"T";2;1;"other";"other";"course";"mother";2;1;0;"no";"no";"no";"yes";"no";"yes";"no";"no";3;3;2;1;3;3;0;"8";"9";8
256 | "GP";"M";17;"R";"GT3";"T";2;1;"other";"other";"course";"mother";1;1;0;"no";"no";"no";"no";"no";"yes";"yes";"no";4;4;2;2;4;5;0;"8";"12";12
257 | "GP";"M";17;"U";"LE3";"T";1;1;"health";"other";"course";"mother";2;1;1;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;4;4;1;2;5;2;"7";"9";8
258 | "GP";"F";17;"U";"LE3";"T";4;2;"teacher";"services";"reputation";"mother";1;4;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;2;3;1;1;4;6;"14";"12";13
259 | "GP";"M";19;"U";"LE3";"A";4;3;"services";"at_home";"reputation";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;3;1;1;1;1;12;"11";"11";11
260 | "GP";"M";18;"U";"GT3";"T";2;1;"other";"other";"home";"mother";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";5;2;4;1;2;4;8;"15";"14";14
261 | "GP";"F";17;"U";"LE3";"T";2;2;"services";"services";"course";"father";1;4;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";3;4;1;1;1;2;0;"10";"9";0
262 | "GP";"F";18;"U";"GT3";"T";4;3;"services";"other";"home";"father";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";3;1;2;1;3;2;21;"17";"18";18
263 | "GP";"M";18;"U";"GT3";"T";4;3;"teacher";"other";"course";"mother";1;2;0;"no";"yes";"yes";"no";"no";"yes";"yes";"no";4;3;2;1;1;3;2;"8";"8";8
264 | "GP";"M";18;"R";"GT3";"T";3;2;"other";"other";"course";"mother";1;3;0;"no";"no";"no";"yes";"no";"yes";"no";"no";5;3;2;1;1;3;1;"13";"12";12
265 | "GP";"F";17;"U";"GT3";"T";3;3;"other";"other";"home";"mother";1;3;0;"no";"no";"no";"yes";"no";"yes";"no";"no";3;2;3;1;1;4;4;"10";"9";9
266 | "GP";"F";18;"U";"GT3";"T";2;2;"at_home";"services";"home";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;3;3;1;1;3;0;"9";"10";0
267 | "GP";"M";18;"R";"LE3";"A";3;4;"other";"other";"reputation";"mother";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;2;5;3;4;1;13;"17";"17";17
268 | "GP";"M";17;"U";"GT3";"T";3;1;"services";"other";"other";"mother";1;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";5;4;4;3;4;5;2;"9";"9";10
269 | "GP";"F";18;"R";"GT3";"T";4;4;"teacher";"other";"reputation";"mother";2;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";4;3;4;2;2;4;8;"12";"10";11
270 | "GP";"M";18;"U";"GT3";"T";4;2;"health";"other";"reputation";"father";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";5;4;5;1;3;5;10;"10";"9";10
271 | "GP";"F";18;"R";"GT3";"T";2;1;"other";"other";"reputation";"mother";2;2;0;"no";"yes";"no";"no";"yes";"no";"yes";"yes";4;3;5;1;2;3;0;"6";"0";0
272 | "GP";"F";19;"U";"GT3";"T";3;3;"other";"services";"home";"other";1;2;2;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;5;3;3;5;15;"9";"9";9
273 | "GP";"F";18;"U";"GT3";"T";2;3;"other";"services";"reputation";"father";1;4;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;5;5;1;3;2;4;"15";"14";14
274 | "GP";"F";18;"U";"LE3";"T";1;1;"other";"other";"home";"mother";2;2;0;"no";"yes";"yes";"no";"no";"yes";"no";"no";4;4;3;1;1;3;2;"11";"11";11
275 | "GP";"M";17;"R";"GT3";"T";1;2;"at_home";"at_home";"home";"mother";1;2;0;"no";"yes";"yes";"yes";"no";"yes";"no";"yes";3;5;2;2;2;1;2;"15";"14";14
276 | "GP";"F";17;"U";"GT3";"T";2;4;"at_home";"health";"reputation";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;3;3;1;1;1;2;"10";"10";10
277 | "GP";"F";17;"U";"LE3";"T";2;2;"services";"other";"course";"mother";2;2;0;"yes";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;4;4;2;3;5;6;"12";"12";12
278 | "GP";"F";18;"R";"GT3";"A";3;2;"other";"services";"home";"mother";2;2;0;"no";"no";"no";"no";"no";"no";"yes";"yes";4;1;1;1;1;5;75;"10";"9";9
279 | "GP";"M";18;"U";"GT3";"T";4;4;"teacher";"services";"home";"mother";2;1;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";3;2;4;1;4;3;22;"9";"9";9
280 | "GP";"F";18;"U";"GT3";"T";4;4;"health";"health";"reputation";"father";1;2;1;"yes";"yes";"no";"yes";"yes";"yes";"yes";"yes";2;4;4;1;1;4;15;"9";"8";8
281 | "GP";"M";18;"U";"LE3";"T";4;3;"teacher";"services";"course";"mother";2;1;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";4;2;3;1;2;1;8;"10";"11";10
282 | "GP";"M";17;"U";"LE3";"A";4;1;"services";"other";"home";"mother";2;1;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";4;5;4;2;4;5;30;"8";"8";8
283 | "GP";"M";17;"U";"LE3";"A";3;2;"teacher";"services";"home";"mother";1;1;1;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;4;4;3;4;3;19;"11";"9";10
284 | "GP";"F";18;"R";"LE3";"T";1;1;"at_home";"other";"reputation";"mother";2;4;0;"no";"yes";"yes";"yes";"yes";"yes";"no";"no";5;2;2;1;1;3;1;"12";"12";12
285 | "GP";"F";18;"U";"GT3";"T";1;1;"other";"other";"home";"mother";2;2;0;"yes";"no";"no";"yes";"yes";"yes";"yes";"no";5;4;4;1;1;4;4;"8";"9";10
286 | "GP";"F";17;"U";"GT3";"T";2;2;"other";"other";"course";"mother";1;2;0;"no";"yes";"no";"no";"no";"yes";"yes";"no";5;4;5;1;2;5;4;"10";"9";11
287 | "GP";"M";17;"U";"GT3";"T";1;1;"other";"other";"reputation";"father";1;2;0;"no";"no";"yes";"no";"no";"yes";"yes";"no";4;3;3;1;2;4;2;"12";"10";11
288 | "GP";"F";18;"U";"GT3";"T";2;2;"at_home";"at_home";"other";"mother";1;3;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;1;2;2;5;"18";"18";19
289 | "GP";"F";17;"U";"GT3";"T";1;1;"services";"teacher";"reputation";"mother";1;3;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;1;1;3;6;"13";"12";12
290 | "GP";"M";18;"U";"GT3";"T";2;1;"services";"services";"reputation";"mother";1;3;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";4;2;4;1;3;2;6;"15";"14";14
291 | "GP";"M";18;"U";"LE3";"A";4;4;"teacher";"teacher";"reputation";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;4;3;1;1;2;9;"15";"13";15
292 | "GP";"M";18;"U";"GT3";"T";4;2;"teacher";"other";"home";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;3;2;1;4;5;11;"12";"11";11
293 | "GP";"F";17;"U";"GT3";"T";4;3;"health";"services";"reputation";"mother";1;3;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;2;2;1;2;3;0;"15";"15";15
294 | "GP";"F";18;"U";"LE3";"T";2;1;"services";"at_home";"reputation";"mother";1;2;1;"no";"no";"no";"no";"yes";"yes";"yes";"yes";5;4;3;1;1;5;12;"12";"12";13
295 | "GP";"F";17;"R";"LE3";"T";3;1;"services";"other";"reputation";"mother";2;4;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";3;1;2;1;1;3;6;"18";"18";18
296 | "GP";"M";18;"R";"LE3";"T";3;2;"services";"other";"reputation";"mother";2;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;4;2;1;1;4;8;"14";"13";14
297 | "GP";"M";17;"U";"GT3";"T";3;3;"health";"other";"home";"mother";1;1;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;4;3;1;3;5;4;"14";"12";11
298 | "GP";"F";19;"U";"GT3";"T";4;4;"health";"other";"reputation";"other";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";2;3;4;2;3;2;0;"10";"9";0
299 | "GP";"F";18;"U";"LE3";"T";4;3;"other";"other";"home";"other";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;4;5;1;2;2;10;"10";"8";8
300 | "GP";"F";18;"U";"GT3";"T";4;3;"other";"other";"reputation";"father";1;4;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;1;1;3;0;"14";"13";14
301 | "GP";"M";18;"U";"LE3";"T";4;4;"teacher";"teacher";"home";"mother";1;1;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";1;4;2;2;2;1;5;"16";"15";16
302 | "GP";"F";18;"U";"LE3";"A";4;4;"health";"other";"home";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";4;2;4;1;1;4;14;"12";"10";11
303 | "GP";"M";17;"U";"LE3";"T";4;4;"other";"teacher";"home";"father";2;1;0;"no";"no";"yes";"no";"yes";"yes";"yes";"no";4;1;1;2;2;5;0;"11";"11";10
304 | "GP";"F";17;"U";"GT3";"T";4;2;"other";"other";"reputation";"mother";2;3;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;1;1;3;0;"15";"12";14
305 | "GP";"F";17;"U";"GT3";"T";3;2;"health";"health";"reputation";"father";1;4;0;"no";"yes";"yes";"yes";"no";"yes";"yes";"no";5;2;2;1;2;5;0;"17";"17";18
306 | "GP";"M";19;"U";"GT3";"T";3;3;"other";"other";"home";"other";1;2;1;"no";"yes";"no";"yes";"yes";"yes";"yes";"yes";4;4;4;1;1;3;20;"15";"14";13
307 | "GP";"F";18;"U";"GT3";"T";2;4;"services";"at_home";"reputation";"other";1;2;1;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;4;3;1;1;3;8;"14";"12";12
308 | "GP";"M";20;"U";"GT3";"A";3;2;"services";"other";"course";"other";1;1;0;"no";"no";"no";"yes";"yes";"yes";"no";"no";5;5;3;1;1;5;0;"17";"18";18
309 | "GP";"M";19;"U";"GT3";"T";4;4;"teacher";"services";"reputation";"other";2;1;1;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;3;4;1;1;4;38;"8";"9";8
310 | "GP";"M";19;"R";"GT3";"T";3;3;"other";"services";"reputation";"father";1;2;1;"no";"no";"no";"yes";"yes";"yes";"no";"yes";4;5;3;1;2;5;0;"15";"12";12
311 | "GP";"F";19;"U";"LE3";"T";1;1;"at_home";"other";"reputation";"other";1;2;1;"yes";"yes";"no";"yes";"no";"yes";"yes";"no";4;4;3;1;3;3;18;"12";"10";10
312 | "GP";"F";19;"U";"LE3";"T";1;2;"services";"services";"home";"other";1;2;1;"no";"no";"no";"yes";"no";"yes";"no";"yes";4;2;4;2;2;3;0;"9";"9";0
313 | "GP";"F";19;"U";"GT3";"T";2;1;"at_home";"other";"other";"other";3;2;0;"no";"yes";"no";"no";"yes";"no";"yes";"yes";3;4;1;1;1;2;20;"14";"12";13
314 | "GP";"M";19;"U";"GT3";"T";1;2;"other";"services";"course";"other";1;2;1;"no";"no";"no";"no";"no";"yes";"yes";"no";4;5;2;2;2;4;3;"13";"11";11
315 | "GP";"F";19;"U";"LE3";"T";3;2;"services";"other";"reputation";"other";2;2;1;"no";"yes";"yes";"no";"no";"yes";"yes";"yes";4;2;2;1;2;1;22;"13";"10";11
316 | "GP";"F";19;"U";"GT3";"T";1;1;"at_home";"health";"home";"other";1;3;2;"no";"no";"no";"no";"no";"yes";"yes";"yes";4;1;2;1;1;3;14;"15";"13";13
317 | "GP";"F";19;"R";"GT3";"T";2;3;"other";"other";"reputation";"other";1;3;1;"no";"no";"no";"no";"yes";"yes";"yes";"yes";4;1;2;1;1;3;40;"13";"11";11
318 | "GP";"F";18;"U";"GT3";"T";2;1;"services";"other";"course";"mother";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;3;3;1;2;1;0;"8";"8";0
319 | "GP";"F";18;"U";"GT3";"T";4;3;"other";"other";"course";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;3;4;1;1;5;9;"9";"10";9
320 | "GP";"F";17;"R";"GT3";"T";3;4;"at_home";"services";"course";"father";1;3;0;"no";"yes";"yes";"yes";"no";"yes";"yes";"no";4;3;4;2;5;5;0;"11";"11";10
321 | "GP";"F";18;"U";"GT3";"T";4;4;"teacher";"other";"course";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;4;4;3;3;5;2;"11";"11";11
322 | "GP";"F";17;"U";"GT3";"A";4;3;"services";"services";"course";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";5;2;2;1;2;5;23;"13";"13";13
323 | "GP";"F";17;"U";"GT3";"T";2;2;"other";"other";"course";"mother";1;2;0;"no";"yes";"no";"no";"yes";"yes";"no";"yes";4;2;2;1;1;3;12;"11";"9";9
324 | "GP";"F";17;"R";"LE3";"T";2;2;"services";"services";"course";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";3;3;2;2;2;3;3;"11";"11";11
325 | "GP";"F";17;"U";"GT3";"T";3;1;"services";"services";"course";"father";1;3;0;"no";"yes";"no";"no";"no";"yes";"yes";"no";3;4;3;2;3;5;1;"12";"14";15
326 | "GP";"F";17;"U";"LE3";"T";0;2;"at_home";"at_home";"home";"father";2;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;3;3;2;3;2;0;"16";"15";15
327 | "GP";"M";18;"U";"GT3";"T";4;4;"other";"other";"course";"mother";1;3;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";4;3;3;2;2;3;3;"9";"12";11
328 | "GP";"M";17;"U";"GT3";"T";3;3;"other";"services";"reputation";"mother";1;1;0;"no";"no";"no";"yes";"no";"yes";"yes";"no";4;3;5;3;5;5;3;"14";"15";16
329 | "GP";"M";17;"R";"GT3";"T";2;2;"services";"other";"course";"mother";4;1;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;4;5;5;5;4;8;"11";"10";10
330 | "GP";"F";17;"U";"GT3";"T";4;4;"teacher";"services";"course";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";5;4;4;1;3;4;7;"10";"9";9
331 | "GP";"F";17;"U";"GT3";"T";4;4;"teacher";"teacher";"course";"mother";2;3;0;"no";"yes";"yes";"no";"no";"yes";"yes";"yes";4;3;3;1;2;4;4;"14";"14";14
332 | "GP";"M";18;"U";"LE3";"T";2;2;"other";"other";"course";"mother";1;4;0;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;5;5;2;4;5;2;"9";"8";8
333 | "GP";"F";17;"R";"GT3";"T";2;4;"at_home";"other";"course";"father";1;3;0;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";4;4;3;1;1;5;7;"12";"14";14
334 | "GP";"F";18;"U";"GT3";"T";3;3;"services";"services";"home";"mother";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";5;3;4;1;1;4;0;"7";"0";0
335 | "GP";"F";18;"U";"LE3";"T";2;2;"other";"other";"home";"other";1;2;0;"no";"no";"no";"yes";"no";"yes";"yes";"yes";4;3;3;1;1;2;0;"8";"8";0
336 | "GP";"F";18;"R";"GT3";"T";2;2;"at_home";"other";"course";"mother";2;4;0;"no";"no";"no";"yes";"yes";"yes";"no";"no";4;4;4;1;1;4;0;"10";"9";0
337 | "GP";"F";17;"U";"GT3";"T";3;4;"services";"other";"course";"mother";1;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;4;5;1;3;5;16;"16";"15";15
338 | "GP";"F";19;"R";"GT3";"A";3;1;"services";"at_home";"home";"other";1;3;1;"no";"no";"yes";"no";"yes";"yes";"no";"no";5;4;3;1;2;5;12;"14";"13";13
339 | "GP";"F";17;"U";"GT3";"T";3;2;"other";"other";"home";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";4;3;2;2;3;2;0;"7";"8";0
340 | "GP";"F";18;"U";"LE3";"T";3;3;"services";"services";"home";"mother";1;4;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";5;3;3;1;1;1;7;"16";"15";17
341 | "GP";"F";17;"R";"GT3";"A";3;2;"other";"other";"home";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;3;2;3;2;4;"9";"10";10
342 | "GP";"F";19;"U";"GT3";"T";2;1;"services";"services";"home";"other";1;3;1;"no";"no";"yes";"yes";"yes";"yes";"yes";"yes";4;3;4;1;3;3;4;"11";"12";11
343 | "GP";"M";18;"U";"GT3";"T";4;4;"teacher";"services";"home";"father";1;2;1;"no";"yes";"no";"yes";"yes";"yes";"yes";"no";4;3;3;2;2;2;0;"10";"10";0
344 | "GP";"M";18;"U";"LE3";"T";3;4;"services";"other";"home";"mother";1;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"yes";4;3;3;1;3;5;11;"16";"15";15
345 | "GP";"F";17;"U";"GT3";"A";2;2;"at_home";"at_home";"home";"father";1;2;1;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";3;3;1;1;2;4;0;"9";"8";0
346 | "GP";"F";18;"U";"GT3";"T";2;3;"at_home";"other";"course";"mother";1;3;0;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;3;3;1;2;3;4;"11";"10";10
347 | "GP";"F";18;"U";"GT3";"T";3;2;"other";"services";"other";"mother";1;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"yes";5;4;3;2;3;1;7;"13";"13";14
348 | "GP";"M";18;"R";"GT3";"T";4;3;"teacher";"services";"course";"mother";1;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"yes";5;3;2;1;2;4;9;"16";"15";16
349 | "GP";"M";18;"U";"GT3";"T";4;3;"teacher";"other";"course";"mother";1;3;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";5;4;5;2;3;5;0;"10";"10";9
350 | "GP";"F";17;"U";"GT3";"T";4;3;"health";"other";"reputation";"mother";1;3;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;4;3;1;3;4;0;"13";"15";15
351 | "MS";"M";18;"R";"GT3";"T";3;2;"other";"other";"course";"mother";2;1;1;"no";"yes";"no";"no";"no";"yes";"yes";"no";2;5;5;5;5;5;10;"11";"13";13
352 | "MS";"M";19;"R";"GT3";"T";1;1;"other";"services";"home";"other";3;2;3;"no";"no";"no";"no";"yes";"yes";"yes";"no";5;4;4;3;3;2;8;"8";"7";8
353 | "MS";"M";17;"U";"GT3";"T";3;3;"health";"other";"course";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;5;4;2;3;3;2;"13";"13";13
354 | "MS";"M";18;"U";"LE3";"T";1;3;"at_home";"services";"course";"mother";1;1;1;"no";"no";"no";"no";"yes";"no";"yes";"yes";4;3;3;2;3;3;7;"8";"7";8
355 | "MS";"M";19;"R";"GT3";"T";1;1;"other";"other";"home";"other";3;1;1;"no";"yes";"no";"no";"yes";"yes";"yes";"no";4;4;4;3;3;5;4;"8";"8";8
356 | "MS";"M";17;"R";"GT3";"T";4;3;"services";"other";"home";"mother";2;2;0;"no";"yes";"yes";"yes";"no";"yes";"yes";"yes";4;5;5;1;3;2;4;"13";"11";11
357 | "MS";"F";18;"U";"GT3";"T";3;3;"services";"services";"course";"father";1;2;0;"no";"yes";"no";"no";"yes";"yes";"no";"yes";5;3;4;1;1;5;0;"10";"9";9
358 | "MS";"F";17;"R";"GT3";"T";4;4;"teacher";"services";"other";"father";2;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"no";4;3;3;1;2;5;4;"12";"13";13
359 | "MS";"F";17;"U";"LE3";"A";3;2;"services";"other";"reputation";"mother";2;2;0;"no";"no";"no";"no";"yes";"yes";"no";"yes";1;2;3;1;2;5;2;"12";"12";11
360 | "MS";"M";18;"U";"LE3";"T";1;1;"other";"services";"home";"father";2;1;0;"no";"no";"no";"no";"no";"yes";"yes";"yes";3;3;2;1;2;3;4;"10";"10";10
361 | "MS";"F";18;"U";"LE3";"T";1;1;"at_home";"services";"course";"father";2;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";5;3;2;1;1;4;0;"18";"16";16
362 | "MS";"F";18;"R";"LE3";"A";1;4;"at_home";"other";"course";"mother";3;2;0;"no";"no";"no";"no";"yes";"yes";"no";"yes";4;3;4;1;4;5;0;"13";"13";13
363 | "MS";"M";18;"R";"LE3";"T";1;1;"at_home";"other";"other";"mother";2;2;1;"no";"no";"no";"yes";"no";"no";"no";"no";4;4;3;2;3;5;2;"13";"12";12
364 | "MS";"F";18;"U";"GT3";"T";3;3;"services";"services";"other";"mother";2;2;0;"no";"yes";"no";"no";"yes";"yes";"yes";"yes";4;3;2;1;3;3;0;"11";"11";10
365 | "MS";"F";17;"U";"LE3";"T";4;4;"at_home";"at_home";"course";"mother";1;2;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";2;3;4;1;1;1;0;"16";"15";15
366 | "MS";"F";17;"R";"GT3";"T";1;2;"other";"services";"course";"father";2;2;0;"no";"no";"no";"no";"no";"yes";"no";"no";3;2;2;1;2;3;0;"12";"11";12
367 | "MS";"M";18;"R";"GT3";"T";1;3;"at_home";"other";"course";"mother";2;2;0;"no";"yes";"yes";"no";"yes";"yes";"no";"no";3;3;4;2;4;3;4;"10";"10";10
368 | "MS";"M";18;"U";"LE3";"T";4;4;"teacher";"services";"other";"mother";2;3;0;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";4;2;2;2;2;5;0;"13";"13";13
369 | "MS";"F";17;"R";"GT3";"T";1;1;"other";"services";"reputation";"mother";3;1;1;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";5;2;1;1;2;1;0;"7";"6";0
370 | "MS";"F";18;"U";"GT3";"T";2;3;"at_home";"services";"course";"father";2;1;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"yes";5;2;3;1;2;4;0;"11";"10";10
371 | "MS";"F";18;"R";"GT3";"T";4;4;"other";"teacher";"other";"father";3;2;0;"no";"yes";"yes";"no";"no";"yes";"yes";"yes";3;2;2;4;2;5;10;"14";"12";11
372 | "MS";"F";19;"U";"LE3";"T";3;2;"services";"services";"home";"other";2;2;2;"no";"no";"no";"yes";"yes";"yes";"no";"yes";3;2;2;1;1;3;4;"7";"7";9
373 | "MS";"M";18;"R";"LE3";"T";1;2;"at_home";"services";"other";"father";3;1;0;"no";"yes";"yes";"yes";"yes";"no";"yes";"yes";4;3;3;2;3;3;3;"14";"12";12
374 | "MS";"F";17;"U";"GT3";"T";2;2;"other";"at_home";"home";"mother";1;3;0;"no";"no";"no";"yes";"yes";"yes";"no";"yes";3;4;3;1;1;3;8;"13";"11";11
375 | "MS";"F";17;"R";"GT3";"T";1;2;"other";"other";"course";"mother";1;1;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";3;5;5;1;3;1;14;"6";"5";5
376 | "MS";"F";18;"R";"LE3";"T";4;4;"other";"other";"reputation";"mother";2;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";5;4;4;1;1;1;0;"19";"18";19
377 | "MS";"F";18;"R";"GT3";"T";1;1;"other";"other";"home";"mother";4;3;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";4;3;2;1;2;4;2;"8";"8";10
378 | "MS";"F";20;"U";"GT3";"T";4;2;"health";"other";"course";"other";2;3;2;"no";"yes";"yes";"no";"no";"yes";"yes";"yes";5;4;3;1;1;3;4;"15";"14";15
379 | "MS";"F";18;"R";"LE3";"T";4;4;"teacher";"services";"course";"mother";1;2;0;"no";"no";"yes";"yes";"yes";"yes";"yes";"no";5;4;3;3;4;2;4;"8";"9";10
380 | "MS";"F";18;"U";"GT3";"T";3;3;"other";"other";"home";"mother";1;2;0;"no";"no";"yes";"no";"yes";"yes";"yes";"yes";4;1;3;1;2;1;0;"15";"15";15
381 | "MS";"F";17;"R";"GT3";"T";3;1;"at_home";"other";"reputation";"mother";1;2;0;"no";"yes";"yes";"yes";"no";"yes";"yes";"no";4;5;4;2;3;1;17;"10";"10";10
382 | "MS";"M";18;"U";"GT3";"T";4;4;"teacher";"teacher";"home";"father";1;2;0;"no";"no";"yes";"yes";"no";"yes";"yes";"no";3;2;4;1;4;2;4;"15";"14";14
383 | "MS";"M";18;"R";"GT3";"T";2;1;"other";"other";"other";"mother";2;1;0;"no";"no";"no";"yes";"no";"yes";"yes";"yes";4;4;3;1;3;5;5;"7";"6";7
384 | "MS";"M";17;"U";"GT3";"T";2;3;"other";"services";"home";"father";2;2;0;"no";"no";"no";"yes";"yes";"yes";"yes";"no";4;4;3;1;1;3;2;"11";"11";10
385 | "MS";"M";19;"R";"GT3";"T";1;1;"other";"services";"other";"mother";2;1;1;"no";"no";"no";"no";"yes";"yes";"no";"no";4;3;2;1;3;5;0;"6";"5";0
386 | "MS";"M";18;"R";"GT3";"T";4;2;"other";"other";"home";"father";2;1;1;"no";"no";"yes";"no";"yes";"yes";"no";"no";5;4;3;4;3;3;14;"6";"5";5
387 | "MS";"F";18;"R";"GT3";"T";2;2;"at_home";"other";"other";"mother";2;3;0;"no";"no";"yes";"no";"yes";"yes";"no";"no";5;3;3;1;3;4;2;"10";"9";10
388 | "MS";"F";18;"R";"GT3";"T";4;4;"teacher";"at_home";"reputation";"mother";3;1;0;"no";"yes";"yes";"yes";"yes";"yes";"yes";"yes";4;4;3;2;2;5;7;"6";"5";6
389 | "MS";"F";19;"R";"GT3";"T";2;3;"services";"other";"course";"mother";1;3;1;"no";"no";"no";"yes";"no";"yes";"yes";"no";5;4;2;1;2;5;0;"7";"5";0
390 | "MS";"F";18;"U";"LE3";"T";3;1;"teacher";"services";"course";"mother";1;2;0;"no";"yes";"yes";"no";"yes";"yes";"yes";"no";4;3;4;1;1;1;0;"7";"9";8
391 | "MS";"F";18;"U";"GT3";"T";1;1;"other";"other";"course";"mother";2;2;1;"no";"no";"no";"yes";"yes";"yes";"no";"no";1;1;1;1;1;5;0;"6";"5";0
392 | "MS";"M";20;"U";"LE3";"A";2;2;"services";"services";"course";"other";1;2;2;"no";"yes";"yes";"no";"yes";"yes";"no";"no";5;5;4;4;5;4;11;"9";"9";9
393 | "MS";"M";17;"U";"LE3";"T";3;1;"services";"services";"course";"mother";2;1;0;"no";"no";"no";"no";"no";"yes";"yes";"no";2;4;5;3;4;2;3;"14";"16";16
394 | "MS";"M";21;"R";"GT3";"T";1;1;"other";"other";"course";"other";1;1;3;"no";"no";"no";"no";"no";"yes";"no";"no";5;5;3;3;3;3;3;"10";"8";7
395 | "MS";"M";18;"R";"LE3";"T";3;2;"services";"other";"course";"mother";3;1;0;"no";"no";"no";"no";"no";"yes";"yes";"no";4;4;1;3;4;5;0;"11";"12";10
396 | "MS";"M";19;"U";"LE3";"T";1;1;"other";"at_home";"course";"father";1;1;0;"no";"no";"no";"no";"yes";"yes";"yes";"no";3;2;3;3;3;5;5;"8";"9";9
397 |
--------------------------------------------------------------------------------
/examples/CMH_test_example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Using Audit-AI for Testing Adverse Impact over Time: An Introduction to Cochran-Mantel-Haenzel and Breslow-Day Statistics\n",
8 | "### Contributing data scientists: Anne Thissen-Roe & Lewis Baker\n",
9 | "\n",
10 | "The Audit-AI package provides developers with simple functions that can be used to test for adverse impact in an algorithm. Many of the original functions in this repo were for testing single instances of an algorithm. In application, however, it is often necessary to test for historic trends in a decision-making pipeline. This pipeline could be any number of decisions: the college admissions rate of men vs women over the past decade, the engine failure rate of six major car manufacturers for the past 18 months, or even the hourly number of ad clicks over time in two website formats during AB testing.\n",
11 | "\n",
12 | "Here we illustrate the pairwise special case of the Cochran-Mantel-Haenzel test. The CMH test is a generalization of the McNemar Chi-Squared test of Homogenaity. Whereas the McNemar test examines differences over two intervals (usually before and after), the CMH test examines differences over any number of _k_ instances.\n",
13 | "\n",
14 | "\n",
15 | "### A Note on Significance Testing for Adverse Impact\n",
16 | "\n",
17 | "Basic researchers often use the CMH test to identify significant differences between groups caused by an experimental condition. In this case, statisticians recommend the application of Yates' correction for continuity. This acts to reduce the test statistics and increase the p-value of tests to correct for false discovery rate at moderately large samples (recommended by some experts to include N's of tens to hundreds of datapoints). Yate's correction is a conservative approach in experimental settings, but NOT in adverse impact monitoring; such an approach would systematically allow marginal cases of bias to go undetected.\n",
18 | "\n",
19 | "Users are also advised to take practical significance into account. Statistical significance may be achieved at sufficiently large sample sizes despite trivially small effect sizes. Users should consult the industry, academic and regulatory norms for practical significance in their use-case.\n",
20 | "\n",
21 | "### Example: McDonald and Siebenaller (1989) \n",
22 | "\n",
23 | "Example taken from the Handbook of Biological Statistics by John H. McDonald (http://www.biostathandbook.com/cmh.html). From the text:\n",
24 | "\n",
25 | " \"McDonald and Siebenaller (1989) surveyed allele frequencies at the Lap\n",
26 | " locus in the mussel Mytilus trossulus on the Oregon coast. At four\n",
27 | " estuaries, we collected mussels from inside the estuary and from a marine\n",
28 | " habitat outside the estuary. There were three common alleles and a couple\n",
29 | " of rare alleles; based on previous results, the biologically interesting\n",
30 | " question was whether the Lap94 allele was less common inside estuaries,\n",
31 | " so we pooled all the other alleles into a \"non-94\" class.\"\n",
32 | "\n",
33 | " \"There are three nominal variables: allele (94 or non-94), habitat\n",
34 | " (marine or estuarine), and area (Tillamook, Yaquina, Alsea, or Umpqua).\n",
35 | " The null hypothesis is that at each area, there is no difference in the\n",
36 | " proportion of Lap94 alleles between the marine and estuarine habitats.\""
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "execution_count": 1,
42 | "metadata": {},
43 | "outputs": [],
44 | "source": [
45 | "import numpy as np\n",
46 | "import pandas as pd\n",
47 | "import auditai"
48 | ]
49 | },
50 | {
51 | "cell_type": "code",
52 | "execution_count": 2,
53 | "metadata": {},
54 | "outputs": [
55 | {
56 | "data": {
57 | "text/html": [
58 | "
\n", 76 | " | Marine | \n", 77 | "Estuarine | \n", 78 | "
---|---|---|
94 | \n", 83 | "56 | \n", 84 | "69 | \n", 85 | "
non-94 | \n", 88 | "40 | \n", 89 | "77 | \n", 90 | "
The goal of the below document is to explain one way in which audit-AI can be used to de-bias a machine learning model. While this is far from the only way in which the code can be used, we hope it gives you a sense of this tool’s potential.
6 | 7 | #### Background: pymetrics models 8 |Pymetrics models are built for specific roles within specific companies. To achieve this customization, we collect data from top-performing incumbents in the target role. We then compare incumbents to a baseline sample of the over 1 million candidates who have applied to jobs through pymetrics. We also establish a special data set, which we call the debias set, which is sampled from a pool of 150,000 individuals who have voluntarily provided basic demographic information such as sex, ethnicity or age. From there, a wide variety of algorithms might be tested to create an initial machine learning model from the training data. The process itself is model agnostic. Multiple algorithms are fit in this process, and we are continuously testing new methods that might improve performance. The goal of the algorithm is to find the features that will most accurately and reliably separate the incumbent set from the baseline set. We create hundreds of possible models with slightly different model parameters to test and compare their performance before selecting the final model.
9 | 10 | #### Pre-deployment auditing 11 |Audit-ai first comes into play when we check the initial model for bias, primarily focusing on disparities across racial and gender groups. Definitions of “fairness” vary across contexts, but in the realm of employment, a tool used to evaluate job candidates must consistently recommend individuals across legally-protected groups, known as pass rates. Specifically, the Equal Employment Opportunity Commission’s (EEOC) Uniform Guidelines on Employee Selection Procedures mandates that pass rates for any one group must be within 80% of the highest-passing group, 80% (also known as “the 4/5th rule”). For example: if 200 people apply for a job,100 men and 100 women, an assessment tool is used that deems 50 of the men qualified for the role (a 50% pass rate) must also deem at least 40 women qualified (an 80% pass rate). The goal of this standard is to ensure that employment selection practices that appear neutral on the surface are not discreetly resulting in adverse impact.
12 | 13 | #### Pre-deployment de-biasing 14 |In short, by using audit-AI at this point in the process, we are able to streamline the process of testing models for compliance with the EEOC’s 4/5th rule. More importantly, audit-AI provides visibility into how we can improve the fairness of our models, without sacrificing predictive power. The package allows us to identify and adjust any traits that exhibit score discrepancies across demographic groups, perhaps due to Simpson’s Paradox or a sampling anomaly. From there, we can employ a feature selection process, such as recursive feature elimination or feature regularization on a criterion of fairness, to reduce the weighting of problematic features in the local population. This continues until we can estimate no significant differences between legally-protected groups. Prior to deployment, the overall efficacy of the newly de-biased model is approximated using five-fold stratified cross-validation, with 80% of the training data being used to train the model and 20% being held out for testing, repeated and averaged over five trials so that all data both contributes to prediction and serves as a test set.
15 | 16 | #### Other standards for fairness 17 |Depending on the precise context, it is worth noting that models may be tested for compliance with standards for fairness beyond the EEOC’s 4/5ths rule. For example, with the advent of employment selection tools that rely on automated data analysis, some U.S. courts have begun evaluating hiring assessments through the lens of statistical significance. In other words, if there are disparities in the pass rates of demographic groups, what is the likelihood that this is merely due to chance (and therefore not due to embedded systematic discrimination)? Audit-AIi is also able to streamline the process of iteratively testing models for such probabilities, reporting statistical significance calculated from z-tests, analysis of variance (ANOVA) tests, Chi-squared tests, and Fisher’s exact tests. The selection of the appropriate method to evaluate the presence of bias is typically a function of the available samples. A novel contribution of the package is the first-ever implementation of the Cochran-Mantel-Haenszel test, which the EEOC uses when testing for statistical adverse impact over time.
18 | 19 | #### Post-deployment validation 20 |Once we deploy a model to predict the hireability of a group of new job applicants, audit-AI can again be used to validate whether the fairness of our approach holds up in the “real world.” Like in the pre-deployment phase, the goal in the post-deployment phase is to test for consistency of pass rates across legally-protected demographic groups, whether defined by measures of practical or statistical significance.
21 | 22 | #### Some key takeaways 23 |The above process of iteratively testing for bias is informed by a few important principles about AI in hiring:
24 | 25 | 1. Bias checks should happen both in the initial process of building a model and after it has been deployed. 26 | 2. Bias can exist against any sub-group within a population, but the specific groups that must be tested will depend on how the algorithm is being used. 27 | 3. It is extremely important to have a large and diverse sample to robustly test for bias. 28 | 4. The law provides standards for selection procedures. If an algorithm leads to a disparity between protected groups (gender, race, age 40+), then it may not be used as part of the selection procedure. 29 | 5. Employers *should not use* an algorithm that is not rigorously tested for bias across these groups. -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # supports Python 2 and Python 3 3 | universal=1 4 | 5 | [metadata] 6 | # This includes the license file in the wheel. 7 | license_file = LICENSE.txt 8 | 9 | [run] 10 | omit = *viz* -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from codecs import open 2 | from os import path 3 | from setuptools import setup, find_packages 4 | 5 | 6 | # Get the long description from the README file 7 | here = path.abspath(path.dirname(__file__)) 8 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | # load the version 12 | exec(open('auditai/version.py').read()) 13 | 14 | setup(name='audit-AI', 15 | version=__version__, 16 | description='audit-AI detects demographic differences in the output of machine learning models or other assessments', 17 | long_description=long_description, 18 | long_description_content_type='text/markdown', 19 | keywords=[ 20 | "audit", 21 | "adverse impact", 22 | "artificial intelligence", 23 | "machine learning", 24 | "fairness", 25 | "bias", 26 | "accountability", 27 | "transparency", 28 | "discrimination", 29 | ], 30 | url='https://github.com/pymetrics/audit-ai', 31 | author='pymetrics Data Team', 32 | author_email='data@pymetrics.com', 33 | project_urls={ 34 | 'Company': 'https://www.pymetrics.com/science/', 35 | }, 36 | license='MIT', 37 | packages=find_packages(exclude=['*.tests', '*.tests.*']), 38 | install_requires=[ 39 | 'numpy', 40 | 'scipy', 41 | 'pandas', 42 | 'matplotlib', 43 | 'statsmodels', 44 | 'sklearn' 45 | ], 46 | tests_require=['pytest-cov'], 47 | extras_require={ 48 | 'dev': ['pytest-cov', 'flake8', 'detox', 'mock', 'six'] 49 | }, 50 | classifiers=[ 51 | 'Development Status :: 3 - Alpha', 52 | 'License :: OSI Approved :: MIT License', 53 | 'Programming Language :: Python :: 2', 54 | 'Programming Language :: Python :: 3', 55 | 'Intended Audience :: Developers', 56 | 'Intended Audience :: Education', 57 | 'Intended Audience :: Financial and Insurance Industry', 58 | 'Intended Audience :: Healthcare Industry', 59 | 'Intended Audience :: Legal Industry', 60 | 'Intended Audience :: Other Audience', 61 | 'Intended Audience :: Science/Research', 62 | 'Natural Language :: English', 63 | 'Topic :: Scientific/Engineering', 64 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 65 | 'Topic :: Scientific/Engineering :: Visualization', 66 | ]) 67 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py37 3 | skipsdist = true 4 | 5 | [testenv] 6 | whitelist_externals = 7 | make 8 | commands = 9 | make install-dev 10 | make tests --------------------------------------------------------------------------------