<jupyter_start><jupyter_text>Analyzing Bias in Detox Dataset  Step 1: Download datasets<jupyter_code>import urllib
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
import numpy as np
import os
import warnings
warnings.filterwarnings('ignore')
if os.path.isdir('data')==False:
    os.mkdir('data')
if os.path.isdir('images')==False:
    os.mkdir('images')
def download_file(url, fname):
    urllib.request.urlretrieve(url, fname)
TOXICITY_ANNOTATED_COMMENTS_URL = 'https://ndownloader.figshare.com/files/7394542' 
TOXICITY_ANNOTATIONS_URL = 'https://ndownloader.figshare.com/files/7394539'
TOXICITY_WORKER_DEMOGRAPHICS_IRL = 'https://ndownloader.figshare.com/files/7640581'

download_file(TOXICITY_ANNOTATED_COMMENTS_URL, 'data/toxicity_annotated_comments.tsv')
download_file(TOXICITY_ANNOTATIONS_URL, 'data/toxicity_annotations.tsv')
download_file(TOXICITY_WORKER_DEMOGRAPHICS_IRL, 'data/toxicity_worker_demographics.tsv')
ATTACK_ANNOTATED_COMMENTS_URL = 'https://ndownloader.figshare.com/files/7554634' 
ATTACK_ANNOTATIONS_URL = 'https://ndownloader.figshare.com/files/7554637'
ATTACK_WORKER_DEMOGRAPHICS_IRL = 'https://ndownloader.figshare.com/files/7640752'

download_file(ATTACK_ANNOTATED_COMMENTS_URL, 'data/attack_annotated_comments.tsv')
download_file(ATTACK_ANNOTATIONS_URL, 'data/attack_annotations.tsv')
download_file(ATTACK_WORKER_DEMOGRAPHICS_IRL, 'data/attack_worker_demographics.tsv')<jupyter_output><empty_output><jupyter_text>Step 2: Load tables using pandas<jupyter_code>toxicity_annotations = pd.read_csv("data/toxicity_annotations.tsv", delimiter="\t")
toxicity_annotated_comments = pd.read_csv("data/toxicity_annotated_comments.tsv", delimiter="\t")
toxicity_worker_demographics = pd.read_csv("data/toxicity_worker_demographics.tsv", delimiter="\t")

toxicity_worker_demographics = toxicity_worker_demographics.set_index("worker_id")
attack_annotations = pd.read_csv("data/attack_annotations.tsv", delimiter="\t")
attack_annotated_comments = pd.read_csv("data/attack_annotated_comments.tsv", delimiter="\t")
attack_worker_demographics = pd.read_csv("data/attack_worker_demographics.tsv", delimiter="\t")

attack_worker_demographics = attack_worker_demographics.set_index("worker_id")
toxicity_annotations = pd.read_csv("data/toxicity_annotations.tsv", delimiter="\t")
toxicity_annotated_comments = pd.read_csv("data/toxicity_annotated_comments.tsv", delimiter="\t")
toxicity_worker_demographics = pd.read_csv("data/toxicity_worker_demographics.tsv", delimiter="\t")

toxicity_worker_demographics = toxicity_worker_demographics.set_index("worker_id")<jupyter_output><empty_output><jupyter_text>Analysis Questions  Question 1: Analyze the level of disagreement among crowdworkers around certain labels  How much do labelers tend to agree while labelling hostile speech? Do people disagree more to a comment more likely to be labeled as hostile? Are some kinds of hostile speech harder for people to agree on than others? For example, do labelers tend to disagree more about “personal attacks” vs. “toxicity”?    For this analysis, we use "entropy" to measure the level of disagreement between different workers on the labeling of each comment. Entropy is a measure of randomness in the data which makes it harder to draw any conclusions from. There are several ways to measure entropy. In this analysis, we use [Shannon's entropy](https://arxiv.org/ftp/arxiv/papers/1405/1405.2061.pdf:~:text=Meaning%20of%20Entropy,of%20information%20in%20that%20variable)  which returns a value between 0 to 1. 0 indicates no entropy i.e. complete agreement while 1 indicates maximum disagreement i.e. 50-50 split between labels.  Step 1: We first compute the entropy in the toxicity and personal attacks annotations respectively.  We also find the majority vote (is_toxic/is_attack) for each comment (e.g. is_toxic = 1 if more than half workers label the comment as toxic)  Toxicity<jupyter_code>toxicity_mean = toxicity_annotations.groupby("rev_id")["toxicity_score"].mean().to_frame().head().reset_index()
toxic_count = pd.DataFrame({"toxic_count": toxicity_annotations.groupby("rev_id")["toxicity"].apply(lambda c : c.sum())}).reset_index()
nontoxic_count = pd.DataFrame({"nontoxic_count": toxicity_annotations.groupby("rev_id")["toxicity"].apply(lambda c : (c == 0).sum())}).reset_index()
joined_toxicity_count = toxic_count.set_index("rev_id").join(nontoxic_count.set_index("rev_id")).reset_index()
joined_toxicity_count['total_count'] = joined_toxicity_count["toxic_count"] + joined_toxicity_count["nontoxic_count"]
joined_toxicity_count['p_toxic'] = joined_toxicity_count['toxic_count']/joined_toxicity_count['total_count']
joined_toxicity_count['p_nontoxic'] = joined_toxicity_count['nontoxic_count']/joined_toxicity_count['total_count']
joined_toxicity_count["entropy"] = -(np.log2(joined_toxicity_count['p_toxic'])*joined_toxicity_count['p_toxic']) - (np.log2(joined_toxicity_count['p_nontoxic'])*joined_toxicity_count['p_nontoxic'])
joined_toxicity_count["entropy"] = joined_toxicity_count["entropy"].fillna(0)
joined_toxicity_count['is_toxic'] = joined_toxicity_count['toxic_count']> joined_toxicity_count['nontoxic_count']
joined_toxicity_count.head()<jupyter_output><empty_output><jupyter_text>Personal Attacks<jupyter_code>attack_count = pd.DataFrame({"attack_count": attack_annotations.groupby("rev_id")["attack"].apply(lambda c : c.sum())}).reset_index()
nonattack_count = pd.DataFrame({"nonattack_count": attack_annotations.groupby("rev_id")["attack"].apply(lambda c : (c == 0).sum())}).reset_index()
joined_attack_count = attack_count.set_index("rev_id").join(nonattack_count.set_index("rev_id")).reset_index()
joined_attack_count['total_count'] = joined_attack_count["attack_count"] + joined_attack_count["nonattack_count"]
joined_attack_count['p_attack'] = joined_attack_count['attack_count']/joined_attack_count['total_count']
joined_attack_count['p_nonattack'] = joined_attack_count['nonattack_count']/joined_attack_count['total_count']
joined_attack_count["entropy"] = -(np.log2(joined_attack_count['p_attack'])*joined_attack_count['p_attack']) - (np.log2(joined_attack_count['p_nonattack'])*joined_attack_count['p_nonattack'])
joined_attack_count["entropy"] = joined_attack_count["entropy"].fillna(0)
joined_attack_count['is_attack'] = joined_attack_count['attack_count']> joined_attack_count['nonattack_count']
joined_attack_count.head()<jupyter_output><empty_output><jupyter_text>Step 2: Next we compare the distribution of entropy (level of disagreement) between the comment labels for toxicity and personal attacks.<jupyter_code>toxicity_vs_attack_entropy = joined_attack_count[['rev_id', 'entropy']].merge(joined_toxicity_count[['rev_id', 'entropy']], on = 'rev_id')[['entropy_x', 'entropy_y']]
toxicity_vs_attack_entropy.columns = ['attack', 'toxicity']
pd.melt(toxicity_vs_attack_entropy)
fig, ax = plt.subplots(figsize=(15,8))
ax.set_title("Distribution of Level of Disagreement in Different Kinds of Hostile speech")
sns.violinplot( x="variable", y="value", data=pd.melt(toxicity_vs_attack_entropy), ax=ax, palette = 'magma' )
plt.savefig("images/label_disagreement_toxic_vs_attack.png")<jupyter_output><empty_output><jupyter_text>We observe that both the distributions and means of the entropy for comments on toxicity and personal attacks are quite similar. This means that people tend to have a similar level of agreement while identifying different types of hostile speech.  Step 3: We compare the entropy between comments which were labelled as hostile by a majority of the labellers vs comments that were labelled as not hostile We first compare the number of hostile to non-hostile comments. We observe that in both datasets (toxicity and personal attacks), there is higher number of comments that are marked as hostile.<jupyter_code>joined_toxicity_count.groupby(['is_toxic'])['rev_id'].count().reset_index()
joined_attack_count.groupby(['is_attack'])['rev_id'].count().reset_index()<jupyter_output><empty_output><jupyter_text>Toxicity<jupyter_code>joined_toxicity_count.groupby('is_toxic')['entropy'].mean().reset_index()
fig, ax = plt.subplots(figsize=(15,8))
ax.set_title("Distribution of Level of Disagreement in Toxicity annotations, by majority vote")
sns.violinplot( x="is_toxic", y="entropy", data=joined_toxicity_count, ax=ax , palette = 'cubehelix')
plt.savefig("images/label_disagreement_toxicity.png")<jupyter_output><empty_output><jupyter_text>We see that the mean entropy for comments labeled as not-toxic is lower (0.24) compared to the mean entropy when comments are labeled as toxic (0.53). This means that labelers tend to agree less that a comment is toxic although majority labelers do tend to label it as toxic.  Personal Attacks<jupyter_code>joined_attack_count.groupby('is_attack')['entropy'].mean().reset_index()
fig, ax = plt.subplots(figsize=(15,8))
ax.set_title("Distribution of Level of Disagreement in Personal Attack annotations, by majority vote")
sns.violinplot( x="is_attack", y="entropy", data=joined_attack_count, ax=ax, palette = 'cubehelix' )
plt.savefig("images/label_disagreement_attack.png")<jupyter_output><empty_output><jupyter_text>We see that similar to the toxicity data, the mean entropy for comments labeled as not-attacks is lower (0.28) compared to the mean entropy when comments are labeled as attacks (0.58). This means that labelers tend to agree less that a comment is an attack although majority labelers do tend to label it as attack.  Results  Labelers tend to agree and disagree similarly for the different kinds of hostile speech (toxicity and personal attack).  A higher mean entropy indicates that there is more disagreement between the labelling of comments that are voted as having hostile speech by a majority of the annotators. This indicates that there is more ambiguity between the decision to label comments as toxic/attack. Thus any model trained on this data might not do a very good job in detecting comments with toxicity or personal attacks.  Question 2: Explore relationships between worker demographics and labeling behavior  How consistent are labelling behaviors among workers with different demographic profiles? For example, are female-identified labelers more or less likely to label comments as aggressive than male-identified labelers?For this analysis, we compare the proportion of comments that are marked as toxic between different groups in demographic profiles such as gender, age, education and language. If the data is unbiased we should observe similar proportions between the groups.  Step 1: Join the annotation data with the worker demographics data<jupyter_code>toxicity_worker_demographics = toxicity_worker_demographics.reset_index()
attack_worker_demographics = attack_worker_demographics.reset_index()
toxicity_label_demographics = toxicity_annotations.set_index('worker_id').join(toxicity_worker_demographics.set_index('worker_id'))
toxicity_label_demographics.head()
attack_label_demographics = attack_annotations.set_index('worker_id').join(attack_worker_demographics.set_index('worker_id'))
attack_label_demographics.head()<jupyter_output><empty_output><jupyter_text>Step 2: Find proportion of comments marked as hostile for different demographic profile groups  Gender<jupyter_code>toxicity_gender_labels = toxicity_label_demographics[toxicity_label_demographics['toxicity'] == 1].groupby('gender')['rev_id'].count().reset_index()
toxicity_gender_totals = toxicity_label_demographics.groupby('gender')['rev_id'].count().reset_index()
toxicity_gender_labels['proportion'] = toxicity_gender_labels['rev_id']/toxicity_gender_totals['rev_id']
toxicity_gender_labels.head()
attack_gender_labels = attack_label_demographics[attack_label_demographics['attack'] == 1].groupby('gender')['rev_id'].count().reset_index()
attack_gender_totals = attack_label_demographics.groupby('gender')['rev_id'].count().reset_index()
attack_gender_labels['proportion'] = attack_gender_labels['rev_id']/attack_gender_totals['rev_id']
attack_gender_labels
toxicity_vs_attack_gender = toxicity_gender_labels.merge(attack_gender_labels, on = 'gender')
toxicity_vs_attack_gender = pd.DataFrame({'gender': toxicity_vs_attack_gender['gender'], 'toxicity':  toxicity_vs_attack_gender['proportion_x'], 'attack': toxicity_vs_attack_gender['proportion_y']})
toxicity_vs_attack_gender = toxicity_vs_attack_gender.set_index('gender').stack().reset_index()
toxicity_vs_attack_gender.columns = ['gender', 'type', 'comments_proportion']
fig, ax = plt.subplots(figsize=(10,5))
ax.set_title("Proportion of Hostile Comments by Gender")
sns.barplot(x = 'gender', y = 'comments_proportion', hue = 'type', palette = 'magma', data = toxicity_vs_attack_gender)
plt.savefig("images/hostile_comments_by_gender.png")<jupyter_output><empty_output><jupyter_text>Age Group<jupyter_code>toxicity_age_labels = toxicity_label_demographics[toxicity_label_demographics['toxicity'] == 1].groupby('age_group')['rev_id'].count().reset_index()
toxicity_age_totals = toxicity_label_demographics.groupby('age_group')['rev_id'].count().reset_index()
toxicity_age_labels['proportion'] = toxicity_age_labels['rev_id']/toxicity_age_totals['rev_id']
toxicity_age_labels
attack_age_labels = attack_label_demographics[attack_label_demographics['attack'] == 1].groupby('age_group')['rev_id'].count().reset_index()
attack_age_totals = attack_label_demographics.groupby('age_group')['rev_id'].count().reset_index()
attack_age_labels['proportion'] = attack_age_labels['rev_id']/attack_age_totals['rev_id']
attack_age_labels
toxicity_vs_attack_age = toxicity_age_labels.merge(attack_age_labels, on = 'age_group')
toxicity_vs_attack_age = pd.DataFrame({'age_group': toxicity_vs_attack_age['age_group'], 'toxicity':  toxicity_vs_attack_age['proportion_x'], 'attack': toxicity_vs_attack_age['proportion_y']})
toxicity_vs_attack_age = toxicity_vs_attack_age.set_index('age_group').stack().reset_index()
fig, ax = plt.subplots(figsize=(10,5))
ax.set_title("Proportion of Hostile Comments by Age Group")
toxicity_vs_attack_age.columns = ['age_group', 'type', 'comments_proportion']
sns.barplot(x = 'age_group', y = 'comments_proportion', hue = 'type', palette = 'magma', data = toxicity_vs_attack_age)
plt.savefig("images/hostile_comments_by_age.png")<jupyter_output><empty_output><jupyter_text>Education<jupyter_code>toxicity_education_labels = toxicity_label_demographics[toxicity_label_demographics['toxicity'] == 1].groupby('education')['rev_id'].count().reset_index()
toxicity_education_totals = toxicity_label_demographics.groupby('education')['rev_id'].count().reset_index()
toxicity_education_labels['proportion'] = toxicity_education_labels['rev_id']/toxicity_education_totals['rev_id']
toxicity_education_labels
attack_education_labels = attack_label_demographics[attack_label_demographics['attack'] == 1].groupby('education')['rev_id'].count().reset_index()
attack_education_totals = attack_label_demographics.groupby('education')['rev_id'].count().reset_index()
attack_education_labels['proportion'] = attack_education_labels['rev_id']/attack_education_totals['rev_id']
attack_education_labels
toxicity_vs_attack_education = toxicity_education_labels.merge(attack_education_labels, on = 'education')
toxicity_vs_attack_education = pd.DataFrame({'education': toxicity_vs_attack_education['education'], 'toxicity':  toxicity_vs_attack_education['proportion_x'], 'attack': toxicity_vs_attack_education['proportion_y']})
toxicity_vs_attack_education = toxicity_vs_attack_education.set_index('education').stack().reset_index()
fig, ax = plt.subplots(figsize=(10,5))
ax.set_title("Proportion of Hostile Comments by Education")
toxicity_vs_attack_education.columns = ['education', 'type', 'comments_proportion']
sns.barplot(x = 'education', y = 'comments_proportion', hue = 'type', palette = 'magma', data = toxicity_vs_attack_education)
plt.savefig("images/hostile_comments_by_education.png")<jupyter_output><empty_output><jupyter_text>First language<jupyter_code>toxicity_language_labels = toxicity_label_demographics[toxicity_label_demographics['toxicity'] == 1].groupby('english_first_language')['rev_id'].count().reset_index()
toxicity_language_totals = toxicity_label_demographics.groupby('english_first_language')['rev_id'].count().reset_index()
toxicity_language_labels['proportion'] = toxicity_language_labels['rev_id']/toxicity_language_totals['rev_id']
toxicity_language_labels
attack_language_labels = attack_label_demographics[attack_label_demographics['attack'] == 1].groupby('english_first_language')['rev_id'].count().reset_index()
attack_language_totals = attack_label_demographics.groupby('english_first_language')['rev_id'].count().reset_index()
attack_language_labels['proportion'] = attack_language_labels['rev_id']/attack_language_totals['rev_id']
attack_language_labels
toxicity_vs_attack_language = toxicity_language_labels.merge(attack_language_labels, on = 'english_first_language')
toxicity_vs_attack_language = pd.DataFrame({'english_first_language': toxicity_vs_attack_language['english_first_language'], 'toxicity':  toxicity_vs_attack_language['proportion_x'], 'attack': toxicity_vs_attack_language['proportion_y']})
toxicity_vs_attack_language = toxicity_vs_attack_language.set_index('english_first_language').stack().reset_index()
toxicity_vs_attack_language.columns = ['english_first_language', 'type', 'comments_proportion']
sns.barplot(x = 'english_first_language', y = 'comments_proportion', hue = 'type', palette = 'magma', data = toxicity_vs_attack_language)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11332382310984308, 'fraction_numerical': 0.007417974322396576, 'mean_word_length': 10.227418321588726, 'pattern_counts': {'":': 4, '<': 53, '<?xml version=': 0, '>': 55, 'https://': 7, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '388969', 'license': 'MIT', 'n_tokens_mistral': 5999, 'n_tokens_neox': 5387, 'n_words': 1041, 'path': 'data-512-a2/data512-a2-bias-in-data.ipynb', 'repo_name': 'chavi-g/data-512', 'size': '17525'}