import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.io as pio
= "notebook"
pio.renderers.default
# temporal data
= pd.read_csv('../../data/processed-data/complete_merge.csv')
timedf 'Tier_2024'] = timedf['Tier_2024'].astype('category')
timedf[
# ML data
= pd.read_csv('../../data/processed-data/MLdataset.csv') df
Exploratory Data Analysis
Code
The purpose of this code is to perform exploratory data analysis (EDA) on the data through advanced visualizations and statistical testing in order to deliver insights on trends, correlations, and other informative qualities of the data. The visualizations will include a heatmap (representing a correlation matrix), line graphs (to visualize data over time), boxplots and stacked bar graphs (to compare levels of categorical variables), scatterplots (to compare numerical variables), and interactive map plots (to provide the ability to explore geospatial insights). The statistical testing methods that will be performed are the ANOVA, Kruskal-Wallis, and Chi-squared tests.
To start, necessary libraries and the two datasets must be loaded:
Interactive Maps
Tiers and Victim Nonpunishment Policy by Country
import plotly.express as px
= df
tiermap
# Make new column renaming the Tier to a more representative name for plotting
= {
tier_map 1.0: 'Tier 1',
2.0: 'Tier 2',
2.2: 'Tier 2 Watch List',
3.0: 'Tier 3'
}'Tier_Label'] = tiermap['Tier_2024'].map(tier_map)
tiermap[
# Map ISO codes to country name in dataset -- this helps choropleth identify locations
= {
iso_codes 'Albania': 'ALB', 'Andorra': 'AND', 'Armenia': 'ARM', 'Austria': 'AUT',
'Azerbaijan': 'AZE', 'Belgium': 'BEL', 'Bosnia and Herzegovina': 'BIH',
'Bulgaria': 'BGR', 'Croatia': 'HRV', 'Cyprus': 'CYP', 'Czechia': 'CZE',
'Denmark': 'DNK', 'Estonia': 'EST', 'Finland': 'FIN', 'France': 'FRA',
'Georgia': 'GEO', 'Germany': 'DEU', 'Greece': 'GRC', 'Hungary': 'HUN',
'Iceland': 'ISL', 'Ireland': 'IRL', 'Italy': 'ITA', 'Latvia': 'LVA',
'Lithuania': 'LTU', 'Luxembourg': 'LUX', 'Malta': 'MLT', 'Moldova': 'MDA',
'Monaco': 'MCO', 'Montenegro': 'MNE', 'Netherlands': 'NLD',
'North Macedonia': 'MKD', 'Norway': 'NOR', 'Poland': 'POL', 'Portugal': 'PRT',
'Romania': 'ROU', 'Russian Federation': 'RUS', 'Serbia': 'SRB',
'Slovakia': 'SVK', 'Slovenia': 'SVN', 'Spain': 'ESP', 'Sweden': 'SWE',
'Switzerland': 'CHE', 'Turkiye': 'TUR', 'Ukraine': 'UKR',
'United Kingdom': 'GBR'
}
'ISO'] = tiermap['Country'].map(iso_codes)
tiermap[
= {
color_map 'Tier 1': '#1a9850',
'Tier 2': '#fee08d',
'Tier 2 Watch List': '#f46d43',
'Tier 3': '#d73027'
}
# Create the choropleth map
= px.choropleth(
fig
tiermap,='ISO',
locations='ISO-3',
locationmode='Tier_Label',
color='europe',
scope=color_map,
color_discrete_map={'Tier_Label': ['Tier 1', 'Tier 2', 'Tier 2 Watch List', 'Tier 3']},
category_orders='Human Trafficking Tier Ratings in Europe',
title=['Country', 'Tier_Label']
custom_data
)
# Add star markers to denote countries with nonpunishment policy
= tiermap[tiermap['Nonpunishment_policy_after2021'] == 1]
marker_data
fig.add_scattergeo(=marker_data['ISO'],
locations='ISO-3',
locationmode=dict(size=10, symbol='star', color='black', line=dict(width=1, color='white')),
marker='Nonpunishment Policy After 2021',
name='text',
hoverinfo=marker_data['Country']
text
)
# specify layout
fig.update_layout(=0.5,
title_x=dict(
geo=False,
showframe=True,
showcoastlines='equirectangular',
projection_type=dict(lon=15, lat=50),
center=[35, 70],
lataxis_range=[-10, 40]
lonaxis_range
),=1000,
width=600,
height='plotly_white',
template='Tier Rating',
legend_title_text=dict(
legend="top",
yanchor=0.99,
y="left",
xanchor=0.01
x
)
)
fig.update_traces(="<b>%{customdata[0]}</b><br>%{customdata[1]}<extra></extra>"
hovertemplate
)
fig.show()
This map shows that there appears to be a geographical trend in the Tier placements. Countries in Northern and Western Europe, with a few exceptions, are mainly Tier 1, whereas countries in Southern Europe are exclusively Tier 2, as well as most of Eastern Europe. As for the countries with victim nonpunishment policy specified in national legislation, there appears to be no geographical trend, or relationship to Tier placements.
Detected Trafficking Victims over time by Country
import plotly.express as px
# map the country codes determined in the previous code
'ISO'] = timedf['Country'].map(iso_codes)
timedf[
# interactive choropleth map
= px.choropleth(
fig
timedf,='ISO',
locations='ISO-3',
locationmode='Detected_victims',
color='europe',
scope='Reds', # specify color palette
color_continuous_scale='Changes in Human Trafficking Detections per 100 People in Europe',
title='Year', # adds slider feature for years
animation_frame='Country', # hover the country name
hover_name={'Detections_per_100': 'Detected victims'},
labels=['Country', 'Detected_victims']
custom_data
)
# specify layout
fig.update_layout(=0.5,
title_x=dict(
geo=False,
showframe=True,
showcoastlines='equirectangular',
projection_type=dict(lon=15, lat=50),
center=[35, 70],
lataxis_range=[-10, 40]
lonaxis_range
),=1000,
width=600,
height='plotly_white',
template=dict(
coloraxis_colorbar="Detections per 100",
title=".2f"
tickformat
)
)
# Customize hover text
fig.update_traces(="<b>%{customdata[0]}</b><br>Detections per 100: %{customdata[1]:.2f}<extra></extra>"
hovertemplate
)
fig.show()
Detected Trafficking Victims per 100 Population over time by Country
# same code as above, just switch to detected victims per 100 population
# interactive choropleth map
= px.choropleth(
fig
timedf,='ISO',
locations='ISO-3',
locationmode='Detected_victims',
color='europe',
scope='Reds', # specify color palette
color_continuous_scale='Changes in Human Trafficking Detections per 100 People in Europe',
title='Year', # adds slider feature for years
animation_frame='Country', # hover the country name
hover_name={'Detections_per_100': 'Detected victims'},
labels=['Country', 'Detected_victims']
custom_data
)
# Layout and appearance
fig.update_layout(=0.5,
title_x=dict(
geo=False,
showframe=True,
showcoastlines='equirectangular',
projection_type=dict(lon=15, lat=50),
center=[35, 70],
lataxis_range=[-10, 40]
lonaxis_range
),=1000,
width=600,
height='plotly_white',
template=dict(
coloraxis_colorbar="Detections per 100",
title=".2f"
tickformat
)
)
# Customize hover text
fig.update_traces(="<b>%{customdata[0]}</b><br>Detections per 100: %{customdata[1]:.2f}<extra></extra>"
hovertemplate
)
# Show the map
fig.show()
This shows that over the years, Bulgaria has consistently had a high rate of detecting trafficking victims per 100 population compared to other countries. This visualization highlights which countries with smaller populations are cracking down on detecting trafficking victims.
Prostitution Policy by Country in Europe
import plotly.express as px
import pandas as pd
'ISO'] = df['Country'].map(iso_codes)
df[
# specify the color mapping for different levels of prostitution policy
= {
color_map 'prohibited': '#fa0202',
'neoabolitionism': '#f57c18',
'abolitionism': '#311a98',
'decriminalization': '#80f72a',
'legal': '#27981a'
}
= px.choropleth(df,
fig ='ISO',
locations='ISO-3',
locationmode='prostitution_policy',
color='europe',
scope=color_map,
color_discrete_map={'prostitution_policy': ['legal', 'decriminalization', 'abolitionism', 'neoabolitionism', 'prohibited']},
category_orders='Prostitution Policy by Country in Europe',
title=['Country', 'prostitution_policy']
custom_data
)
# specify layour
fig.update_layout(=0.5,
title_x=dict(
geo=False,
showframe=True,
showcoastlines='equirectangular',
projection_type=dict(lon=15, lat=50),
center=[35, 70],
lataxis_range=[-10, 40]
lonaxis_range
),=1000,
width=600,
height='plotly_white',
template='Tier Rating',
legend_title_text=dict(
legend="top",
yanchor=0.99,
y="left",
xanchor=0.01
x
)
)
fig.update_traces(="<b>%{customdata[0]}</b><br>%{customdata[1]}<extra></extra>"
hovertemplate
)
fig.show()
Heatmap correlation plot
# drop non-numerical columns
= ['Country', 'Subregion', 'prostitution_policy']
dropcols = df.drop(columns=dropcols)
numeric_df
# calculate the correlation matrix
= numeric_df.corr()
correlation_matrix
# heat map visualization
import seaborn as sns
import matplotlib.pyplot as plt
=(20, 16))
plt.figure(figsize=True, cmap='coolwarm', fmt='.2f', cbar=True)
sns.heatmap(correlation_matrix, annot"Correlation Matrix")
plt.title( plt.show()
Key observations: The country indicators do not have strong correlations with the human trafficking detection data– the human trafficking data only has relatively strong correlations with other detection indicators (i.e. the correlation coefficient between the sum of detected victims and the sum of repatriated victims is 0.62, which indicates that there is a relatively positive relationship between the two).
The highest correlation coefficients in the data between two independent variables are:
- 0.87 between the mean criminal justice score and the mean political stability over time
- -0.80 between the mean criminal justice score and the mean Tier placement over time
- 0.80 between the mean criminal justice score and Henley passport index score
- 0.77 between the mean criminal justice score and the mean GDP per capita
- 0.76 between the mean political stability over time and Henley passport index score
- 0.72 between the mean political stability and the mean GDP per capita over time
- 0.72 between the EU membership and the Henley passport index score
- -0.71 between the mean Tier placement over time and Henley passport index score
- -0.67 between the mean criminal justice score and the Tier placement in 2024
- -0.64 between the mean political stability and the mean Tier placement over time
The mean criminal justice score of a country appeared in the first five highest correlation coefficients between pairwise variables, and in six out of the ten top. This demonstrates that criminal justice score is a central quality of a country that influences and has relationships with a multitude of other variables. In terms of data that reflects government response score, the Tier placement over time appeared to be quite significantly negatively correlated (-0.80) with the mean criminal justice score, indicating that the higher of a criminal justice score, the lower the Tier placement (the more likely it is to be 1, which represents a strong government preparation to combat human trafficking). The average Tier placement over time is also moderately negatively correlated with the Henley passport index score (-0.71) (also known as the number of Visa free destinations granted by the country’s passport) and
Pairplot
In the code below, I subsetted to the variables that I deemed to be the most significant in the correlation matrix heatmap visualization (the variables that appeared in most of the high correlation coefficients) and visualized them in a pairplot format. This shows the specific datapoints’ relationships to one another. I also included GSI government response score and the sum of detected victims to this pairplot, because the first offers an insightful perspective into government efforts to address human trafficking and the second offers insight into results. Assessing these variables alongside the other strong indicators may reveal some important findngs. I decided to fill in the color of the data points with their Tier score in 2024.
= df[['Criminal_justice_mean', 'Visa_free_destinations', 'GDP_per_capita_mean', 'Political_stability_mean', 'EU_members', 'GSI_gov_response_score', 'Detected_victims_sum', 'Tier_2024']]
significants # custom palette to original orange and blue-- it started outputting the hue in shades of pink originally
= sns.color_palette(["#1f77b4", "#ff7f0e", "#aec7e8", "#ffbb78"]) # blue and orange theme
custom_palette ='Tier_2024', palette=custom_palette) sns.pairplot(significants,hue
The pairplot above reveals interesting characteristics in the data:
- Aside from showing the relationships between numerical variables, the hue representing the Tiers also show that generally, Tier 1 is associated with higher criminal justice scoring, more visa free destinations, GDP per capita, and political stability.
- The mean criminal justice scores appear to be different between countries in Tier 1 and Tier 2, as derived from the top right plot, in which the peak of the spread of Tier 1 criminal justice mean values is greater than that of Tier 2.
- GSI government response and the sum of detected victims appear to have no or very little relationship to the other indicators.* This can be inferred from the vertical line shapes of the scatter plots for in the GSI_gov_response_score column (the second from the right), which indicates that all GSI government response scores are actually very similar and do not vary over a considerable range. Thus, they are not correlated with (or influenced by) the other indicators. Furthermore, the distributions visualized in the Kernel Density Estimation plots (the non-scatterplot plots) indicates an overlap in the distribution of government response scores between Tiers, indicating that there is no significant relationship between GSI government response and Tier ranking in 2024.
- The scatterplot shapes pertaining to the sum of detected victims all have very random and sporadic shapes, indicating that none of the variables can explain or influence the sum of detected victims.
Line plots
Total Detected Victims over time
This plot visualizes the total number of detected victims over time using the temporal dataset.
# Sum of detected victims in each subregion per year
= timedf.groupby(['Year', 'Subregion'])['Detected_victims'].sum().reset_index()
data_summary
# line plot:
=(12, 6))
plt.figure(figsize=data_summary, x='Year', y='Detected_victims', hue='Subregion', marker='o')
sns.lineplot(data= range(int(data_summary['Year'].min()), int(data_summary['Year'].max()) + 1, 2)
xticks =xticks)
plt.xticks(ticks'Detected Victims Over Time by Subregion')
plt.title('Year')
plt.xlabel('Sum of Detected Victims')
plt.ylabel(='Subregion')
plt.legend(titleTrue)
plt.grid( plt.show()
This plot shows that between 2011 and 2016, human trafficking detected victims were highest in Esatern Europe. It is difficult to derive any overall trends from this visualization as the changes over time tend to be rather random. However, the number of detected victims in Southern Europe appears to be increasing rather steadily. A noticeable drop in detected victims is apparent for all regions in 2021 and 2022, likely due to the COVID-19 pandemic.
Detected Victims per 100 Citizens per Subregion over time
# Sum of detected victims in each subregion per year
= timedf.groupby(['Year', 'Subregion'])['Detections_per_100'].sum().reset_index()
per100
# line plot:
=(12, 6))
plt.figure(figsize=per100, x='Year', y='Detections_per_100', hue='Subregion', marker='o')
sns.lineplot(data= range(int(data_summary['Year'].min()), int(data_summary['Year'].max()) + 1, 2)
xticks =xticks)
plt.xticks(ticks'Detected Victims per 100 Over Time by Subregion')
plt.title('Year')
plt.xlabel('Detected Victims per 100 People')
plt.ylabel(='Subregion')
plt.legend(titleTrue)
plt.grid( plt.show()
In contrast to the previous plot, this one shows the trends in the standardized rate of detected human trafficking victims per country per year, represented as the number of detected victims per 100 population. This plot shows that in recent years, countries in Southern Europe have surpassed all others in the number of victims they detected per 100 population.
Detected Victims by Country, grouped by Subregion
import matplotlib.pyplot as plt
import seaborn as sns
# create figure with 5 subplots
= plt.subplots(3, 2, figsize=(20, 25))
fig, axes = axes.flatten()
axes
# create a plot for each of the five subregions
= timedf['Subregion'].unique()
subregions for idx, subregion in enumerate(subregions):
# filter data for the current subregion
= timedf[timedf['Subregion'] == subregion]
subregion_data
# plot line plot
= axes[idx]
ax for country in subregion_data['Country'].unique():
= subregion_data[subregion_data['Country'] == country]
country_data 'Year'], country_data['Detected_victims'],
ax.plot(country_data[=country, marker='o')
label
# Customize subplots
f'Detected Victims Over Time in {subregion}', pad=20)
ax.set_title('Year')
ax.set_xlabel('Detected Victims')
ax.set_ylabel(True, alpha=0.3)
ax.grid(
# logistics
='x', rotation=45)
ax.tick_params(axis=(1.05, 1), loc='upper left', title='Country')
ax.legend(bbox_to_anchor
if len(subregions) < 6:
5])
fig.delaxes(axes[
# adjust layout to prevent overlap
plt.tight_layout() plt.show()
No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
This plot shows that interestingly, each subregion has one country that stands out compared to the others with much higher relative counts of detected victims of human trafficking. The outlier country in Southern Europe is Italy, in Western Asia it’s Turkey, in Western Europe it’s the Netherlands, in Eastern Europe it’s Romania, and in Northern Europe it’s the UK, but the data reported by the UK stops after 2016. The Netherlands appears to be the country with the highest counts of detected victims over time.
Tier over time by Subregion
= timedf.groupby(['Year', 'Subregion'])['Tier'].mean().reset_index()
tiersub
# line plot:
=(12, 6))
plt.figure(figsize=tiersub, x='Year', y='Tier', hue='Subregion', marker='o')
sns.lineplot(data= range(int(tiersub['Year'].min()), int(tiersub['Year'].max()) + 1, 2)
xticks =xticks)
plt.xticks(ticks'Average Tier Placement per Subregion Over Time')
plt.title('Year')
plt.xlabel('Average Tier Placement')
plt.ylabel(='Subregion')
plt.legend(titleTrue)
plt.grid( plt.show()
This plot visualizes the average Tier placement score per region over time. The slopes all appear to be gradually increasing, suggesting that they are falling (getting worse) in their Tier placements. Or, this may mean that the USDS is becoming more strict and evaluative in their Tier placements over time. However, this doesn’t apply to Western Asia which remains steadily at an average of Tier 2 placement over time. An important characteristic to note is the significant change in Eastern European Tier placements over the years– in 2023, it surpassed the mean of Western Asia, and the mean actually lies under a value of 2, indicating a new weight of countries on the Tier 2 Watchlist and in Tier 3.
GDP per Capita over time by Subregion
# Group by Year and Subregion to calculate average GDP
= timedf.groupby(['Year', 'Subregion'])['GDP_per_capita'].mean().reset_index()
data_gdp
# Line plot
=(12, 6))
plt.figure(figsize=data_gdp, x='Year', y='GDP_per_capita', hue='Subregion', marker='o')
sns.lineplot(data'GDP per Capita Over Time by Subregion')
plt.title('Year')
plt.xlabel('Average GDP per Capita')
plt.ylabel(='Subregion')
plt.legend(titleTrue)
plt.grid( plt.show()
This plot shows that the the GDP per capita is substantially higher in Western and Northern Europe than the other subregions, and especially the highest in Western Europe, with a noticeable increase in recent years.
Bar charts
Tier placements in 2024
This plot visualizes the breakdown of countries in Tier placements in the USDS’s 2024 Trafficking in Persons report. As a reminder:
- Tier 1 (1): countries that fully comply with the Trafficking Victim Protection Act’s (TVPA) minimum standards
- Tier 2 (2): countries that do not fully meet the TVPA’s minimum standards but are making significant efforts to
- Tier 2 Watchlist (2.2): countries in Tier 2 that also are either:
- experiencing an increase in human trafficking prevalence that they are not proportionally responding to, OR
- failing to provide evidence of increasing efforts
- Tier 3 (3): countries that do not fully meet the TVPA’s minimum standards and are not making significant efforts to do so.
# convert Tier_2024 to categorical
'Tier_2024'] = df['Tier_2024'].astype('category')
df[
=(12, 6))
plt.figure(figsize=df, x='Tier_2024')
sns.countplot(data'Tier Placements')
plt.xlabel('Count')
plt.ylabel('Counts of Each Level of Tier_2024')
plt.title( plt.show()
This shows that the majority of countries in Europe are on Tier 2, and the second most are in Tier 1. Tier 2 Watchlist and Tier 3 both contain very few country observations in 2024.
Convictions and Prosecution totals by Subregion
This code creates a side-by-side boxplot to compare the total number of convicted and prosecuted traffickers in each region. This may help provide some perception on which regions are more strict about convicting traffickers once prosecuted, which may reflect how seriously they take the crime.
# group by Subregion to sum convictions and prosecutions
= timedf.groupby('Subregion')[['Convicted_traffickers', 'Prosecuted_traffickers']].sum().reset_index()
sub
# Melt the data for easier plotting
= sub.melt(id_vars='Subregion',
sub_melted =['Convicted_traffickers', 'Prosecuted_traffickers'],
value_vars='Metric', value_name='Count')
var_name
# Bar plot
=(12, 6))
plt.figure(figsize=sub_melted, x='Subregion', y='Count', hue='Metric')
sns.barplot(data'Convictions vs Prosecutions by Subregion')
plt.title('Subregion')
plt.xlabel('Count')
plt.ylabel(=45)
plt.xticks(rotation plt.show()
This plot shows that the ratio of convicted traffickers over prosecuted traffickers is highest in Northern Europe, where there is a smaller gap between the counts of the two values. However, it is imperative to recognize the scale of the counts here– Northern Europe has far fewer cases of prosecuted traffickers. This may reflect a poorer job of law enforcement in detecting traffickrs, or that there are simply fewer traffickers in this subregion.
Tier by Prostitution Policy
# crosstab to count occurrences
= pd.crosstab(df['Tier_2024'], df['prostitution_policy'])
crosstab
# stacked bar chart
='bar', stacked=True, figsize=(10, 6))
crosstab.plot(kind
'Prostitution policy by Tier')
plt.title('Tier')
plt.xlabel('Count')
plt.ylabel(='Prostitution policy')
plt.legend(title# set customize y axis tick intervals:
= crosstab.values.sum(axis=1).max()
max_y 0, max_y + 2, 2))
plt.yticks(np.arange( plt.show()
This stacked bar chart shows that few countries in Tier 1 have prohibited prostitution policy, whereas this policy constitutes a larger share in the lower Tiers.
Tier by Nonpunishment Policy
# crosstab to count occurrences
= pd.crosstab(df['Tier_2024'], df['Nonpunishment_policy_after2021'])
crosstab
# stacked bar chart
='bar', stacked=True, figsize=(10, 6))
crosstab.plot(kind
'Victim nonpunishment policy by Tier')
plt.title('Tier')
plt.xlabel('Count')
plt.ylabel(='Victim nonpunishment policy')
plt.legend(title= crosstab.values.sum(axis=1).max()
max_y 0, max_y + 2, 2))
plt.yticks(np.arange( plt.show()
There appears to be no noticeable difference in victim nonpunishment policy between Tiers.
Tier Placements 2024 Breakdown by Subregion
# Create a pivot table for the counts
'Tier_2024'] = pd.to_numeric(df['Tier_2024'], errors='coerce')
df[= df.dropna(subset=['Tier_2024']).copy()
df_cleaned = df_cleaned[df_cleaned['Subregion'] != "Western Asia"].copy()
df_cleaned
# order Subregion levels
= ['Northern Europe', 'Western Europe', 'Southern Europe', 'Eastern Europe']
subregion_order 'Subregion'] = pd.Categorical(df_cleaned['Subregion'], categories=subregion_order, ordered=True)
df_cleaned[
# create pivoy table
= pd.crosstab(df_cleaned['Subregion'], df_cleaned['Tier_2024'])
tier_pivot
# stacked bar chart
='bar', stacked=True, figsize=(10, 6), colormap='Set2')
tier_pivot.plot(kind
'Distribution of Tier by Subregion')
plt.title('Subregion')
plt.xlabel('Proportion')
plt.ylabel(='Tier')
plt.legend(title=45)
plt.xticks(rotation
plt.tight_layout()
plt.show()
This output demonstrates not only the counts of countries in each region, but also the breakdown of Tiers. From this plot, we see that the majority of countries in Northern and Western Europe are Tier 1. There are zero Tier 1 countries in Southern Europe and all two Tier 2 Watchlist countries (Malta and Serbia) are in Southern Europe. The only Tier 3 country in Europe (Russia) is in Eastern Europe.
Boxplots
Sum of detected victims by Tier, Nonpunishment Policy, Prostitution Policy, and EU Membership
The code below outputs a visualization with four boxplots assessing the impact of various categorical vavriables on the sum of detected victims, facilitating side-by-side comparison.
from scipy import stats
def boxplots(df):
# allow 4 subplots
= plt.subplots(2, 2, figsize=(20, 15))
fig, ((ax1, ax2), (ax3, ax4))
# 1. Detected Victims sum by Tier
=df, x='Tier_2024', y='Detected_victims_sum', ax=ax1)
sns.boxplot(data'Detected Victims per 100 Population by Tier')
ax1.set_title('Number of Detected Victims')
ax1.set_ylabel(
# 2. Detected Victims sum by Nonpunishment Policy
=df, x='Nonpunishment_policy_after2021', y='Detected_victims_sum', ax=ax2)
sns.boxplot(data'Detected Victims per 100 Population by Nonpunishment Policy')
ax2.set_title('Has Nonpunishment Policy')
ax2.set_xlabel('Number of Detected Victims')
ax2.set_ylabel(
# 3. Detected Victims sum by Prostitution Policy
=df, x='prostitution_policy', y='Detected_victims_sum', ax=ax3)
sns.boxplot(data'Detected Victims per 100 Population by Prostitution Policy')
ax3.set_title('Prostitution policy')
ax3.set_xlabel('Number of Detected Victims')
ax3.set_ylabel(
#4. Detected Victims sum by EU membership
=df, x='EU_members', y='Detected_victims_sum', ax=ax4)
sns.boxplot(data'Detected Victims per 100 Population by EU membership')
ax4.set_title('EU Member')
ax4.set_xlabel('Number of Detected Victims')
ax4.set_ylabel(
plt.tight_layout()
= boxplots(df) correlation_matrix
Among the four categorical variables assessed, none appear to have noticeable associations with the sum of detected victims. The only trends that stand out are that the sum is much lower in the Tier 2 Watchlist compared to the other tiers. Additionally, the sum of detected victims is lower and has a lower range for the prostitution policies of prohibition and neoabolitionims. There is no discernible difference in the sum of detected victims between victim nonpunishment policy and EU membership.
Mean Criminal Justice Score by Tier, Nonpunishment Policy, Prostitution Policy, and EU Membership
def boxplots(df):
# allow 4 subplots
= plt.subplots(2, 2, figsize=(20, 15))
fig, ((ax1, ax2), (ax3, ax4))
# 1. Mean Criminal Justice by Tier
=df, x='Tier_2024', y='Criminal_justice_mean', ax=ax1)
sns.boxplot(data'Average Criminal Justice Score by Tier')
ax1.set_title('Average Criminal Justice Score')
ax1.set_ylabel(
# 2. Mean Criminal Justice by Nonpunishment Policy
=df, x='Nonpunishment_policy_after2021', y='Criminal_justice_mean', ax=ax2)
sns.boxplot(data'Average Criminal Justice Score by Nonpunishment Policy')
ax2.set_title('Has Nonpunishment Policy')
ax2.set_xlabel('Average Criminal Justice Score')
ax2.set_ylabel(
# 3. Mean Criminal Justice by Prostitution Policy
=df, x='prostitution_policy', y='Criminal_justice_mean', ax=ax3)
sns.boxplot(data'Average Criminal Justice Score by Prostitution Policy')
ax3.set_title('Prostitution policy')
ax3.set_xlabel('Average Criminal Justice Score')
ax3.set_ylabel(
#4. Mean Criminal Justice by EU membership
=df, x='EU_members', y='Criminal_justice_mean', ax=ax4)
sns.boxplot(data'Average Criminal Justice Score by EU membership')
ax4.set_title('EU Member')
ax4.set_xlabel('Average Criminal Justice Score')
ax4.set_ylabel(
plt.tight_layout()
= boxplots(df) correlation_matrix
The side-by-side visualization of these boxplots conveys that criminal justice scores are higher in Tier 1, countries with neoabolitionist prostitution policy, and EU members. The criminal justice score is alarmingly low for Tier 3 countries and interestingly lower in countries with prohibited prostitution. There is not a strong apparent difference for nonpunishment policy, but the visualization suggests that the mean criminal just scores among country that do not specify victim nonpunishment policy are actually slightly higher.
Kruskal-Wallis test: Criminal justice scores by Tier
To take the boxplot results of the mean criminal justice scores by Tier one step further, the statistical significance of the difference between groups will be assessed by performing the Kruskal-Wallis test Kruskal-Wallis test along with Dunn’s pairwise analysis test. These nonparametric tests were chosen because they are more robust to violations of the assumptions of ANOVA– for example, the Kruskal-Wallis test can handle very small sample sizes and varying sample sizes across groups.
import scikit_posthocs as sp
from scipy.stats import kruskal
# A condition for the KW test is that all groups must have at least two observations,
# so the data must be filtered to only include these.
= df.groupby('Tier_2024').filter(lambda x: len(x) >= 2).copy()
df_filtered
# drop missing values
= df_filtered.dropna(subset=['Criminal_justice_mean'])
df_filtered
# Perform Kruskal-Wallis test
= [df_filtered[df_filtered['Tier_2024'] == tier]['Criminal_justice_mean'] for tier in df_filtered['Tier_2024'].unique()]
groups = kruskal(*groups)
h_stat, p_value print(f"H-statistic: {h_stat}, P-value: {p_value}")
# Pairwise comparisons using Dunn’s test
= sp.posthoc_dunn(df_filtered, val_col='Criminal_justice_mean', group_col='Tier_2024', p_adjust='bonferroni')
pairwise_results print(pairwise_results)
H-statistic: 17.21799939487991, P-value: 0.0006374006592237717
1.0 2.0 2.2 3.0
1.0 1.000000 0.000883 1.0 0.142186
2.0 0.000883 1.000000 1.0 1.000000
2.2 1.000000 1.000000 1.0 1.000000
3.0 0.142186 1.000000 1.0 1.000000
These results indicate that there is a statistically significant difference in the median of the mean criminal justice score between countries in Tier 1 and Tier 2 countries.
T-test: Criminal Justice Scores by EU Membership
To further explore the differences visualized in the boxplots above, a t-test will be performed to statistically assess if EU members are distinguished from their counterparts when it comes to criminal justice scores.
from scipy.stats import ttest_ind
= df.dropna(subset=['Criminal_justice_mean'])
ttestdf = ttest_ind(ttestdf['EU_members'], ttestdf['Criminal_justice_mean'], equal_var=False)
t_stat, p_value print(f"T-statistic: {t_stat}, P-value: {p_value}")
T-statistic: -20.595011394291824, P-value: 6.5410194981716e-24
The extremely small p-value is well below the alpha threshold of 0.05, indicating that this data provides significant evidence to suggest a difference in the true criminal justice mean scores between EU and non-EU member countries.
Criminal Justice Scores by Subregion
=(12, 6))
plt.figure(figsize=df, x='Subregion', y='Criminal_justice_mean')
sns.boxplot(data'Criminal Justice Scores by Subregion')
plt.title('Subregion')
plt.xlabel('Criminal Justice Score')
plt.ylabel(=45)
plt.xticks(rotation plt.show()
The average criminal justice scores in Western and Northern Europe appear to be consistently and considerably higher than the other regions. Eastern Europe in particular has a country that has a very low criminal justice mean score, around 20/100.
Convictions over Prosecutions Ratio by Tier
# Convictions over prosecutions by Tier
=(12, 6))
plt.figure(figsize='Tier_2024', y='Convictions_over_prosecutions_mean')
sns.boxplot(df, x'Convictions over prosecutions ratio by Tier ')
plt.title('Tier')
plt.xlabel('Convictions over prosecutions ratio')
plt.ylabel(=45)
plt.xticks(rotation plt.show()
This plot shows that, contrary to what might be expected, countries with Tier 2 Watchlist placements in 2024 actually had higher convictions over prosecutions. This signals a considerable effort by law enforcement to actually punish traffickers. However, we cannot draw any claims from this visualization because it lacks information on the sample sizes of the countries making up the different tiers.
GSI Government response score by Tier
# GSI government response score by Tier
=(12, 6))
plt.figure(figsize=df, x='Tier', y='GSI_gov_response_score')
sns.boxplot(data'GSI Government Response Score by Tier')
plt.title('Subregion')
plt.xlabel('GSI Government Response Score')
plt.ylabel(=45)
plt.xticks(rotation plt.show()
This plot shows that the mean government response score to human trafficking does not vary considerably among Tiers. This is thought-provoking, as Tier 1 countries are expected to have better government responses– that is the basis of their classification.
Sum of Repatriated Victims by Tier
=(12, 6))
plt.figure(figsize=df, x='Tier_2024', y='Number_Repatriated_Victims_sum')
sns.boxplot(data'Sum of repatriated victims by Tier')
plt.title('Tier')
plt.xlabel('Repatriated victims sum')
plt.ylabel(=45)
plt.xticks(rotation plt.show()
The sum of repatrated victims does not vary isgnificantly by Tier, except for in Malta and Serbia, the Tier 2 Watchlist countries. However, this may be explained by the fact that Malta at least has a very small population.
Summary
This EDA allowed for deeper insight into this data and into what indicators interact with each other. This is a critical phase for a project that tackles such a multifaceted problem. Specifically, this EDA highlighted how the mean criminal justice score of a country is a critical indicator related to various variables indirectly relevant to human trafficking drivers and governent response, such as Tier placement and Other indicators that were expected to be more associated with government response to human trafficking, namely the specification of victim nonpunishment policy and Walk Free’s GSI government response score, proved to be independent of Tier placement and criminal justice scores. The implications of these results are discussed in the report of this project and the indicators are further explored for their importance in predicting factors related to human trafficking in the supervised learning section.