<jupyter_start><jupyter_text>TaskOperators like joins, selections and missing value imputaters can cause *data distribution issues*, which can heavily impact the performance of our model for specific demographic groups. Mlinspect helps with identifying such issues by offering a check that calculates histograms for sensitive groups in the data and verifying whether the histogram change is significant enough to alert the user. Thanks to our annotation propagation, we can deal with complex code involving things like nested sklearn pipelines and group memberships that are removed from the training data using projections.We want to find out if preprocessing operations in pipelines introduce bias and if so, which groups are effected.The pipeline we want to analyse in this task can be found using the path `os.path.join(str(get_project_root()), "example_pipelines", "healthcare", "healthcare.py")`. The senstive attributes we want to take a look at are `age_group` and `race`. It this task, we use a pipeline we created using synthetic data.The code of the pipeline:> ```python> """> An example pipeline> """> import os> import warnings> > import pandas as pd> from sklearn.compose import ColumnTransformer> from sklearn.impute import SimpleImputer> > from sklearn.model_selection import train_test_split> from sklearn.pipeline import Pipeline> from sklearn.preprocessing import OneHotEncoder, StandardScaler> from tensorflow.keras.wrappers.scikit_learn import KerasClassifier> from example_pipelines.healthcare.healthcare_utils import MyW2VTransformer, create_model> from mlinspect.utils import get_project_root> >  FutureWarning: Given feature/column names or counts do not match the ones for the data given during fit> warnings.filterwarnings('ignore')> > COUNTIES_OF_INTEREST = ['county2', 'county3']> >  load input data sources (data generated with https://www.mockaroo.com as a single file and then split into two)> patients = pd.read_csv(os.path.join(str(get_project_root()), "example_pipelines", "healthcare",>                                     "healthcare_patients.csv"), na_values='?')> histories = pd.read_csv(os.path.join(str(get_project_root()), "example_pipelines", "healthcare",>                                      "healthcare_histories.csv"), na_values='?')> >  combine input data into a single table> data = patients.merge(histories, on=['ssn'])> >  compute mean complications per age group, append as column> complications = data.groupby('age_group').agg(mean_complications=('complications', 'mean'))> > data = data.merge(complications, on=['age_group'])> >  target variable: people with a high number of complications> data['label'] = data['complications'] > 1.2 * data['mean_complications']> >  project data to a subset of attributes> data = data[['smoker', 'last_name', 'county', 'num_children', 'race', 'income', 'label']]> >  filter data> data = data[data['county'].isin(COUNTIES_OF_INTEREST)]> >  define the feature encoding of the data> impute_and_one_hot_encode = Pipeline([>         ('impute', SimpleImputer(strategy='most_frequent')),>         ('encode', OneHotEncoder(sparse=False, handle_unknown='ignore'))>     ])> > featurisation = ColumnTransformer(transformers=[>     ("impute_and_one_hot_encode", impute_and_one_hot_encode, ['smoker', 'county', 'race']),>     ('word2vec', MyW2VTransformer(min_count=2), ['last_name']),>     ('numeric', StandardScaler(), ['num_children', 'income'])> ])> >  define the training pipeline for the model> neural_net = KerasClassifier(build_fn=create_model, epochs=10, batch_size=1, verbose=0, input_dim=109)> pipeline = Pipeline([>     ('features', featurisation),>     ('learner', neural_net)])> >  train-test split> train_data, test_data = train_test_split(data, random_state=0)>  model trainingmodel = pipeline.fit(train_data, train_data['label'])>  model evaluation>  this is running on synthetic random data, so there is nothing meaningful to learn in this example pipeline> print(model.score(test_data, test_data['label']))> ```  Step 1/4: Add check and execute the pipelineThe central entry point of mlinspect is the `PipelineInspector`. To use mlinspect, we use it and pass it the path to the runnable version of the example pipeline. Here, we have the example pipeline in a `.py` file. First, we define the check we want mlinspect to run. In this example, we only use `NoBiasIntroducedFor(["col1", "col2", ...])` to automatically check for significant changes in the distribution of sensitive demograhpic groups and compute the histograms.Then, we execute the pipeline. Mlinspect returns a `InspectorResult`, which, among other information, contains the output of our check.<jupyter_code>import os
from mlinspect.utils import get_project_root

from mlinspect import PipelineInspector
from mlinspect.checks import NoBiasIntroducedFor

HEALTHCARE_FILE_PY = os.path.join(str(get_project_root()), "example_pipelines", "healthcare", "healthcare.py")

inspector_result = PipelineInspector\
    .on_pipeline_from_py_file(HEALTHCARE_FILE_PY) \
    .add_check(NoBiasIntroducedFor(["age_group", "race"])) \
    .execute()

check_results = inspector_result.check_to_check_results<jupyter_output>Mean accuracy: 0.9178082346916199<jupyter_text>Step 2/4: Overview of the check results Did our check find issues?Let us look at the `check_result` to see our some check failed. We do this using the mlinspect utlity function `check_results_as_data_frame(...)`. We see that an issue was found, so we have to investigate it.<jupyter_code>from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

check_result_df = PipelineInspector.check_results_as_data_frame(check_results)
display(check_result_df)<jupyter_output><empty_output><jupyter_text>Step 3/4: List of operations that could change the distribution As stated before, only some operations like selections, joins and missing value imputation can change the distribution. Our check already filtered all operators that can cause data distribution issues. We can use the mlinspect utility function `get_distribution_changes_overview_as_df(...)` to get an overview. The overview already tells us that mlinspect detected a potential issue caused by a selection involving the race attribute. Note that the automatic issue detection from mlinspect is only as good as its configuration and should not be completely relied upon.<jupyter_code>no_bias_check_result = check_results[NoBiasIntroducedFor(['age_group', 'race'])]

distribution_changes_overview_df = NoBiasIntroducedFor.get_distribution_changes_overview_as_df(no_bias_check_result)
display(distribution_changes_overview_df)

dag_node_distribution_changes_list = list(no_bias_check_result.bias_distribution_change.items())<jupyter_output><empty_output><jupyter_text>Step 4/4 Detailed InvestigationNow that we know of the potential issue, we will take a look at the histograms before and after the JOIN. We can use `distribution_change.before_and_after_df` to look at the data in form of a `pandas.DataFrame` or use the mlinspect utility function `plot_distribution_change_histograms(...)` to plot the histograms.<jupyter_code># Select the DagNode we want to look at by index
dag_node, distribution_changes = dag_node_distribution_changes_list[2]

# Investige the changes
print("\033[1m{}: {}\033[0m".format(dag_node.operator_info.operator, dag_node.optional_code_info.source_code))
for column, distribution_change in distribution_changes.items():
    print("")
    print("\033[1m Column '{}'\033[0m, acceptable change: {}, min_relative_ratio_change: {}".format(column, distribution_change.acceptable_change, distribution_change.min_relative_ratio_change))
    display(distribution_change.before_and_after_df)
    NoBiasIntroducedFor.plot_distribution_change_histograms(distribution_change)<jupyter_output>[1mOperatorType.SELECTION: data[data['county'].isin(COUNTIES_OF_INTEREST)][0m

[1m Column 'age_group'[0m, acceptable change: True, min_relative_ratio_change: -0.016871921182266037
{'fraction_non_alphanumeric': 0.09201150143767971, 'fraction_numerical': 0.010376297037129641, 'mean_word_length': 6.332722273143904, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 87, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '152822', 'license': 'Apache-2.0', 'n_tokens_mistral': 2343, 'n_tokens_neox': 2220, 'n_words': 757, 'path': 'experiments/user_interviews/task-2-solution.ipynb', 'repo_name': 'adrianlut/mlinspect', 'size': '7999'}