# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # A2 Bias In Data

# ---
#
# __TL;DR__  
# The purpose of this exploration is to identify potential sources of bias in a corpus of human-annotated data, and describe some implications of those biases.  Specifically, this analysis attempts to answer the question, _"How consistent are the labeling behaviors among workers with different demographic profiles?"_ in regards to  two separate Wikimedia datasets:  
# 1. Toxicity
# 2. Aggression
#
#
# __RELATED LINKS:__   
# _Data Source:  https://figshare.com/projects/Wikipedia_Talk/16731_  
# _Overview of the project:  https://meta.wikimedia.org/wiki/Research:Detox_  
# _Overview of the data: https://meta.wikimedia.org/wiki/Research:Detox/Data_Release_

# ---

# ## DATA AQUISITION

# The code below will download all the data needed in this analysis, using the `%%caption` magic keyword to suppress the outputs.

# +
# %%capture  

# download toxicity data
# !wget https://ndownloader.figshare.com/files/7394539 -O Raw_Data/toxicity_annotations.tsv
# !wget https://ndownloader.figshare.com/files/7394542 -O Raw_Data/toxicity_annotated_comments.tsv
# !wget https://ndownloader.figshare.com/files/7640581 -O Raw_Data/toxicity_worker_demographics.tsv

# download aggression data
# !wget https://ndownloader.figshare.com/files/7394506 -O Raw_Data/aggression_annotations.tsv
# !wget https://ndownloader.figshare.com/files/7038038 -O Raw_Data/aggression_annotated_comments.tsv
# !wget https://ndownloader.figshare.com/files/7640644 -O Raw_Data/aggression_worker_demographics.tsv
     
# downlaod personal attacks
# !wget https://ndownloader.figshare.com/files/7554637 -O Raw_Data/attack_annotations.tsv
# !wget https://ndownloader.figshare.com/files/7554634 -O Raw_Data/attack_annotated_comments.tsv
# !wget https://ndownloader.figshare.com/files/7640752 -O Raw_Data/attack_worker_demographics.tsv

# -

# The code below will transform the downloaded data into a pandas dataframe.

# +
import pandas as pd

# toxicity data
toxicity_annotations = pd.read_csv("Raw_Data/toxicity_annotations.tsv", delimiter="\t")
toxicity_annotated_comments = pd.read_csv("Raw_Data/toxicity_annotated_comments.tsv", delimiter="\t", index_col=0)
toxicity_worker_demographics = pd.read_csv("Raw_Data/toxicity_worker_demographics.tsv", delimiter="\t")

# aggression data
aggression_annotations = pd.read_csv("Raw_Data/aggression_annotations.tsv", delimiter="\t")
aggression_annotated_comments = pd.read_csv("Raw_Data/aggression_annotated_comments.tsv", delimiter="\t", index_col=0)
aggression_worker_demographics = pd.read_csv("Raw_Data/aggression_worker_demographics.tsv", delimiter="\t")

# personal attack data
attack_annotations = pd.read_csv("Raw_Data/attack_annotations.tsv", delimiter="\t")
attack_annotated_comments = pd.read_csv("Raw_Data/attack_annotated_comments.tsv", delimiter="\t", index_col=0)
attack_worker_demographics = pd.read_csv("Raw_Data/attack_worker_demographics.tsv", delimiter="\t")
# -

# ## BIAS IN THE TOXICITY DATA SET

# ### INITIAL EDA

# From the documentation we know there are 160k labeled comments in the Toxicity dataset.  First, let's look at the demographic distribution for the labelers in the toxicity dataset by building a few simple bar plots.

# +
# import libraries for visualizations
import matplotlib.pyplot as plt
import seaborn as sns

# magic code for viewing plots using jupyter notebooks:
# %matplotlib inline 

# +
# plot education distribution
sns.catplot(y="education", 
            kind="count", 
            data=toxicity_worker_demographics,
            order=["none", "some", "hs", "bachelors", "professional", "masters", "doctorate"],
            palette=("Blues"))

# plot gender dist
sns.catplot(x="gender", 
            kind="count", 
            data=toxicity_worker_demographics,
            palette=("Blues"))

# plot age group dist
sns.catplot(x="age_group", 
            kind="count", 
            data=toxicity_worker_demographics,
            order=["Under 18", "18-30", "30-45", "45-60", "Over 60"],
            palette=("Blues"))

# plot language dist
sns.catplot(x="english_first_language", 
            kind="count", 
            data=toxicity_worker_demographics,
            palette=("Blues"))

plt.tight_layout()
# -

# Immediately, it's clear there is quite a lot of skewed data in the demographics.  The following results become immediately apparent:
# * Even though this is from english wikipedia, the vast majority of labelers are non-native English speakers.
# * The majority of labelers are in the 18-30 age range.
# * The majority of labelers are male.
# * The majority of labelers hold a bachelors degree. 
#
# We can combine some of this information to look for additional potential bias:

# +
# plot education v. gender
sns.catplot(y="education", 
            kind="count", 
            hue = 'gender',
            data=toxicity_worker_demographics,
            order=["none", "some", "hs", "bachelors", "professional", "masters", "doctorate"],
            palette=("Blues"))

# plot gender v. age
sns.catplot(x="age_group", 
            kind="count",
            hue = 'gender',
            data=toxicity_worker_demographics,
            palette=("Blues"))

# plot age v. education
sns.catplot(x="age_group", 
            kind="count",
            hue = 'education',
            data=toxicity_worker_demographics,
            order=["Under 18", "18-30", "30-45", "45-60", "Over 60"],
            palette=("Blues"))

# plot language v. gender
sns.catplot(x="gender", 
            kind="count", 
            hue = 'english_first_language',
            data=toxicity_worker_demographics,
            palette=("Blues"))

# plot language v. education
sns.catplot(y="education", 
            kind="count", 
            hue = 'english_first_language',
            data=toxicity_worker_demographics,
            palette=("Blues"))
# -

# There is clearly a lot of bias towards non-native english speaking males with a bachelor's degree in the 18-30 age group.   One interesting result here is that although there are significantly more men in the overall group, the _Over 60_ age group is biased in favor of women.  Next, we will examine the types of columns that typically get labeled as toxic.   We'll consider a comment to be toxic if at least 50% of reviewers labeled it as such.

# +
# labels a comment as toxic if the majority of annotators did so
toxicity_labels = toxicity_annotations.groupby('rev_id')['toxicity'].mean() > 0.5

# join the labels and comments
toxicity_annotated_comments['labeled_toxic'] = toxicity_labels

# remove newline and tab tokens
toxicity_annotated_comments['comment'] = toxicity_annotated_comments['comment'].apply(lambda x: x.replace("NEWLINE_TOKEN", " "))
toxicity_annotated_comments['comment'] = toxicity_annotated_comments['comment'].apply(lambda x: x.replace("TAB_TOKEN", " "))
# -

# If we preview the results we can verify that these comments indeed appear to be toxic.

# preview the results
toxicity_annotated_comments.loc[toxicity_annotated_comments['labeled_toxic'] == True].head(10)

# We can quickly look for common words contained in these toxic comments.

# +
# create a list of the 200 most common words in the comments
from collections import Counter
wordlist = Counter(" ".join(toxicity_annotated_comments.loc[toxicity_annotated_comments['labeled_toxic'] == True]['comment'].str.lower()).split()).most_common(75)

# remove common stop words from the list
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
stopword_list = set(stopwords.words('english'))

# add a few extra words to the stop word list
stopword_list |= set(['==', '-', '`', "I'm", 'wikipedia', 'wiki', '====', '.', 'me', 'im', 'hi'])

# print the final result
for item in wordlist:
    if item[0] not in stopword_list:
        print(item)
# -

# The results are not particulary surprising.    This is the sort of langauge I would expect to find in toxic comments.   With few exceptions, most of these words do not have an alternate definitions. In search of bias, we can examine how these comments were treated by the labelers based on on demographic breakdowns.

# ### ANALYSIS

# The analysis below attempts to answer the question, "How consistent are the toxicity labeling behaviors among workers with different demographic profiles?  

# Since the data is divided into three separate files, it must be joined in various ways in order to analyze.

# +
# join demographics to annotations
joined_toxicity_demographics = toxicity_annotations.join(toxicity_worker_demographics, on="worker_id", rsuffix="_r")

# join comments to annotations
joined_toxicity_comments = toxicity_annotations.join(toxicity_annotated_comments, on="rev_id", rsuffix="_r")
# -

# Quickly previewing the results of each table:

display(joined_toxicity_demographics.head(3))
display(joined_toxicity_comments.head(3))

# We can calculate a mean toxicity score for each individual and add that to the demographics table.

# +
# calculate average toxicity per user
avg_worker_toxicity = joined_toxicity_demographics.groupby("worker_id")["toxicity_score"].mean()

# join the average toxicity to the demographics table
toxicity_worker_demographics = toxicity_worker_demographics.join( avg_worker_toxicity )

# preview the results
toxicity_worker_demographics.head()
# -

# There is quite a lot of bias in the toxicity score by age group.   This becomes more apparent in graph form.   There is appears to be a direct linear relationship between age and the average toxicity score.

# +
# create barplot, specify the order
sns_plot = sns.barplot(x="age_group", 
                 y="toxicity_score", 
                 data=toxicity_worker_demographics, 
                 order=["Under 18", "18-30", "30-45", "45-60", "Over 60"],
                palette=("Blues"))

# save results for README file
fig = sns_plot.get_figure()
fig.savefig('output.png') 
# -

# However, the scoring is much more uniform across the other demographic groups (education, language, and gender).   

# create barplot, specify the order
sns.barplot(y="education", 
                 x="toxicity_score", 
                 data=toxicity_worker_demographics, 
                 order=["none", "some", "hs", "bachelors", "professional", "masters", "doctorate"],
                 palette=("Blues"))

# create barplot, specify the order
sns.barplot(x="english_first_language", 
                 y="toxicity_score", 
                 data=toxicity_worker_demographics, 
                 palette=("Blues"))

# create barplot, specify the order
sns.barplot(x="gender", 
                 y="toxicity_score", 
                 data=toxicity_worker_demographics, 
                 palette=("Blues_d"))

# The most dramatic difference in scoring appears to be from individuals in the over 60 group.  Males over 60 appear to have a wide range of opinions on terms of scoring.

sns.barplot(x="toxicity_score", 
                 y="gender", 
                 data=toxicity_worker_demographics.loc[toxicity_worker_demographics['age_group'] == "Over 60"], 
                 palette=("Blues"))

# Now, we can calculate the average toxicity score per comment.   We can see which demographics differ from the mean score in the most dramatic ways.

# +
# calculate average toxicity per rev_id
avg_rev_toxicity = joined_toxicity_demographics.groupby("rev_id")["toxicity_score"].mean()

# joined the average toxicity per rev to the dataframe
joined_toxicity_demographics = joined_toxicity_demographics.join(avg_rev_toxicity, on="rev_id", rsuffix="_r")

# calculate the user's difference from the mean
joined_toxicity_demographics["dif_from_mean"] = joined_toxicity_demographics['toxicity_score'] - joined_toxicity_demographics['toxicity_score_r']

# preview results
joined_toxicity_demographics
# -

sns.barplot(x="dif_from_mean", 
                 y="gender", 
                 data=joined_toxicity_demographics, 
                 palette=("Blues"))

# In general, it appears that men have a tendency to rate comments as slightly more toxic than women.  Looking at the age groups produces an even more startling result.  Users over 60 deviate dramatically from the average toxicity score.   Clearly, in this group there appears to be much less sensitivity towards toxic comments.

sns.barplot(x="dif_from_mean", 
                 y="age_group", 
                 data=joined_toxicity_demographics, 
                 palette=("Blues"))

# ## BIAS IN THE AGGRESSION DATASET

# ### INITIAL EDA

# There are about 100k labeled comments in the `aggression` data set.  We can repeat a similar analysis here.

# +
# plot education distribution
sns.catplot(y="education", 
            kind="count", 
            data=aggression_worker_demographics,
            order=["none", "some", "hs", "bachelors", "professional", "masters", "doctorate"],
            palette=("crest"))

# plot gender dist
sns.catplot(x="gender", 
            kind="count", 
            data=aggression_worker_demographics,
            palette=("crest"))

# plot age group dist
sns.catplot(x="age_group", 
            kind="count", 
            data=aggression_worker_demographics,
            order=["Under 18", "18-30", "30-45", "45-60", "Over 60"],
            palette=("crest"))

# plot language dist
sns.catplot(x="english_first_language", 
            kind="count", 
            data=aggression_worker_demographics,
            palette=("crest"))

# -

# Here again, we see similiar issues to the toxicity dataset.   It appears as though this is a very similiar group of individuals was responsible for categorizing these comments as well.   We will again examine the types of columns that typically get labeled as aggressive.   We'll consider a comment to be toxic if at least 50% of reviewers labeled it as such.

# +
# labels a comment as toxic if the majority of annotators did so
aggression_labels = aggression_annotations.groupby('rev_id')['aggression'].mean() > 0.5

# join the labels and comments
aggression_annotated_comments['labeled_aggression'] = aggression_labels

# remove newline and tab tokens
aggression_annotated_comments['comment'] = aggression_annotated_comments['comment'].apply(lambda x: x.replace("NEWLINE_TOKEN", " "))
aggression_annotated_comments['comment'] = aggression_annotated_comments['comment'].apply(lambda x: x.replace("TAB_TOKEN", " "))
# -

# preview the results
aggression_annotated_comments.loc[aggression_annotated_comments['labeled_aggression'] == True].head(15)

# The tone of these comments appears to be very similiar to the toxic comments.   It doesn't seem as though there will be a lot of value to be gained from running a keyword analysis against this dataset for EDA as language is quite simliar.  Instead, we will move on to the analysis.

# ### AGGRESSION ANALYSIS

# The analysis below attempts to answer the question, "How consistent are the aggression labeling behaviors among workers with different demographic profiles?  

# Again, we will start by joining the data files.

# +
# join demographics to annotations
joined_aggression_demographics = aggression_annotations.join(aggression_worker_demographics, on="worker_id", rsuffix="_r")

# join comments to annotations
joined_aggression_comments = aggression_annotations.join(aggression_annotated_comments, on="rev_id", rsuffix="_r")

# preview the results of each table
display(joined_aggression_demographics.head(3))
display(joined_aggression_comments.head(3))

# +
# calculate average attack score per user
avg_worker_aggression = joined_aggression_demographics.groupby("worker_id")["aggression_score"].mean()

# join the average toxicity to the demographics table
aggression_worker_demographics = aggression_worker_demographics.join( avg_worker_aggression )

# preview the results
aggression_worker_demographics.head()
# -

# We can now look at the average aggression score, broken down by demographic.   For this dataset, the signs have switched, meaning positive numbers are friendly and negative numbers indicate an aggressive comment.

# create barplot, specify the order
sns.barplot(x="age_group", 
                 y="aggression_score", 
                 data=aggression_worker_demographics, 
                 order=["Under 18", "18-30", "30-45", "45-60", "Over 60"],
                palette=("crest"))

sns.barplot(y="education", 
                 x="aggression_score", 
                 data=aggression_worker_demographics, 
                 order=["none", "some", "hs", "bachelors", "professional", "masters", "doctorate"],
                 palette=("crest"))

# create barplot, specify the order
sns.barplot(x="english_first_language", 
                 y="aggression_score", 
                 data=aggression_worker_demographics, 
                 palette=("crest"))

# create barplot, specify the order
sns.barplot(x="gender", 
                 y="aggression_score", 
                 data=aggression_worker_demographics.loc[aggression_worker_demographics['gender'].isin(['female', 'male'])], 
                 palette=("crest"))

# These results look much more uniform than the toxicity dataset.   Most groups appear to have a similar scoring averages.  Again, let's calculate the average aggression score per comment in an attempt to see which demographic groups differ the most from the mean score.

# +
# calculate average toxicity per rev_id
avg_rev_aggression = joined_aggression_demographics.groupby("rev_id")["aggression_score"].mean()

# joined the average toxicity per rev to the dataframe
joined_aggression_demographics = joined_aggression_demographics.join(avg_rev_aggression, on="rev_id", rsuffix="_r")

# calculate the user's difference from the mean
joined_aggression_demographics["dif_from_mean"] = joined_aggression_demographics['aggression_score'] - joined_aggression_demographics['aggression_score_r']

# preview results
joined_aggression_demographics
# -

# If we break this down by education we can see a large difference in the scoring of those with no education.

sns.barplot(x="dif_from_mean", 
                 y="education", 
                 data=joined_aggression_demographics, 
                 palette=("crest"))

# When we look at the difference in scoring across age groups, the result is quite alarming.   There is very little consensus among teenagers and senior citizens when it comes to labeling an aggressive comment.  On average, labelers under 18 tend to view comments as more aggressive, while those over 60 tend to view comments as less aggressive.

sns.barplot(x="dif_from_mean", 
                 y="age_group", 
                 data=joined_aggression_demographics, 
                 order=["Under 18", "18-30", "30-45", "45-60", "Over 60"],
                 palette=("crest"))

# ## SUMMARY OF RESULTS

# For both datasets examined in this notebook, the largest demographic factor in relation to labeling behavior was age.   Users in the _Under 18_ age group consistently labeled comments in a different way than the _Over 60_ group.  If a single comment was examined by 10 users in the _Over 60_ age group, they would be significantly more likely to label it friendly and non-toxic.   The opposite would be true if the 10 labelers were replaced with teenagers.  Since flagging comments is a classification problem requiring a binary designation, this lack of consesus should be handled carefully when attempting to create machine learning models.

# ## IMPLICATIONS

# _Which, if any, of these demo applications would you expect the Perspective API—or any model trained on the Wikipedia Talk corpus—to perform well in? Why?_
#
# I believe the comment filter may be an effective application based on my findings.   Since different demographic groups clearly have a differing tolerance levels for toxic and aggressive comments, this tool would allow users to set their own personal threshold.   The potentially toxic or aggressive comments would not be removed from the site and would be available for anyone to read, but they wouldn't be immediately visible to underage users (the group that tends to find content most aggressive and toxic).

# _What are some potential unintended, negative consequences of using the Perspective API for any of these purposes? In your opinion, are these consequences likely or serious enough that you would recommend that the Perspective API not be used in these applications? Why or why not?_
#
# There are some clear differences in the labeling behavior among age groups.  If a model is simply trained based on the aggregated labeling results, one would expect that users under the age of 18 are more likely to see content that they find toxic or aggressive on the site.  Additionally, users over the age of 60 would be more likely to see content taken off the site that they feel is inoffensive.   Since there is no clear consensus on which comments are offensive, building a model on this dataset may simply exacerbate existing problems.

# _Which, if any, of these demo applications would you expect the Perspective API to perform poorly in? Why?_
#
# I would expect the toxicity timeline to be less informative than some of the other demo projects.   This tool may simply end up tracking behavior among demographic groups and share very little insight into the comments themselves.   Since different groups have differing opinions on these comments, the tool may simply reflect the different times of day that particular groups of people are active online.

StarcoderdataJupyterScriptsDedupFiltered