CC5206 - Introducción a la Minería de Datos


Proyecto Final : Fairness & Bias (Data Science for Social Good)


Integrantes: Ignacio Bugueño, Nicolás Burgos, Alain Jélvez


Motivación


El avance de la tecnología en los últimos años ha ido de la mano en la entrega de soluciones para las diversas y múltiples problemáticas presentes en la sociedad. Desde la salud hasta la interacción social. La tecnología ha adquirido un rol transversal, fomentando la calidad del aprendizaje y del desarrollo de destrezas en la sociedad, aumentando la productividad económica, entre otros.

El producto de este conjunto de conocimiento y técnicas no solo se ve reflejado en dispositivos de fácil acceso y movilidad, sino que asimismo en sistemas de gran envergadura, caracterizados por ser procesos automáticos, con capacidad de resolver problemáticas de alta complejidad en tiempos inferiores a los tiempos de resolución humano, junto a la exploración de nuevas soluciones.

Es así, como herramientas como COMPAS (Correctional Offender Management Profiling for Alternative Sanctions) de la empresa Northpointe, que ofrece una predicción de riesgo de reincidencia de criminales en Estados Unidos, mediante la asignación de puntaje que va desde 1 (bajo riesgo de reincidencia) a 10 (alto riesgo de reincidencia), juegan un rol importante en la sociedad, pues son ampliamente utilizadas por jueces de ese país para dejar a acusados en libertad, ajustar las penas de cárcel, aceptar o rechazar a personas en los programas de rehabilitación, ajustar fianzas, aceptar o rechazar acuerdos, etc.

El problema nace cuando herramientas como COMPAS, que deberían ser imparciales en sus análisis y resultados, y que tienen gran repercusión en la vida de las personas, presentan sesgos. En particular, el programa presenta sesgos en sus resultados dada la raza a la que pertenece la persona, donde a las personas afroamericanas se les otorga un alto puntaje de riesgo y a las personas blancas un bajo puntaje por esta característica. Esta situación queda en evidencia al observar que de las personas calificadas de alto riesgo, pero que no reincidieron, el 23,5% son blancos y el 44,9% son afroamericanos, mientras que las personas calificadas de bajo riesgo, pero que sí reincidieron, el 47,7% son blancos, en comparación con el 28,0% de afroamericanos.


Objetivos, Hipótesis y Problemáticas Iniciales


Bajo el contexto descrito anteriormente, en el marco del curso CC5206 - Introducción a la Minería de Datos, se presenta el proyecto Fairness & Bias (Data Science for Social Good), el cual busca identificar y disminuir el sesgo y la discriminación de personas que se realiza en los procesos judiciales, especialmente en la conformación de la sentencia judicial. Para esto, por medio de las técnicas asociadas a Machine Learning y Data Mining, se propone la elaboración de un sistema que disminuya los sesgos existentes, a partir de la exploración y generación de datos de interés.

En concreto, el proyecto contempla los siguientes alcances y objetivos:

  • Identificar los sesgos existentes entre los puntajes de riesgo asignados por la herramienta COMPAS y los atributos de los datos.
  • Analizar el desempeño de la escala de puntajes de la herramienta COMPAS teniendo en cuenta la reincidencia efectiva de los acusados.
  • Proponer métodos para disminuir los sesgos identificados. Para esto se contempla las siguientes alternativas que pueden ser, o no, excluyentes:
    • Elminar variables sesgadas.
    • Evaluar distintos clasificadores para predecir la reincidencia de los acusados.
    • Elaborar un método de corrección del puntaje que disminuya los efectos del sesgo con ayuda de la información de reincidencia.
    • Elaborar una nueva escala de puntajes.

Datos


Se toma como referencia un análisis realizado previamente que deja en evidencia el sesgo existente en COMPAS [1]. La base de datos a considerar en este proyecto será la utilizada en el estudio mencionado. Estos datos corresponden a información obtenida por ProPublica de dos años de puntuación COMPAS de la Oficina del Sheriff del Condado de Broward en Florida, correspondientes a personas calificadas antes de un jucio en 2013 y 2014. Además, se cruza la información con perfiles criminales históricos de las personas dentro de la base de datos, antes y después de haber sido calificados. Esto implica información respecto a crímenes previos y reincidentes. Tambien se incorpora información respecto a tiempos de encarcelamiento y raza.

Características y limpieza de los datos


Parte del análisis siguiente se realiza teniendo en cuenta como referencia un tutorial disponible en [2]. El siguiente bloque de código contiene las instrucciones de inicializaciones y carga de datos.
In [1]:
import numpy as np
import pandas as pd
import scipy

import matplotlib.pyplot as plt
import seaborn as sns
import itertools
from sklearn.metrics import roc_curve

db = pd.read_csv("https://github.com/propublica/compas-analysis/raw/master/compas-scores-two-years.csv", header=0).set_index('id')
Primero se analiza la cantidad, el nombre y la descripción de los atributos.
In [2]:
db['length_of_stay'] = (pd.to_datetime(db['c_jail_out'])-pd.to_datetime(db['c_jail_in'])).apply(lambda x: x.days)
print("Lista con los nombres de todos los atributos:\n")
print(list(db))
print("\nCantidad de atributos: " + str(len(list(db))))
Lista con los nombres de todos los atributos:

['name', 'first', 'last', 'compas_screening_date', 'sex', 'dob', 'age', 'age_cat', 'race', 'juv_fel_count', 'decile_score', 'juv_misd_count', 'juv_other_count', 'priors_count', 'days_b_screening_arrest', 'c_jail_in', 'c_jail_out', 'c_case_number', 'c_offense_date', 'c_arrest_date', 'c_days_from_compas', 'c_charge_degree', 'c_charge_desc', 'is_recid', 'r_case_number', 'r_charge_degree', 'r_days_from_arrest', 'r_offense_date', 'r_charge_desc', 'r_jail_in', 'r_jail_out', 'violent_recid', 'is_violent_recid', 'vr_case_number', 'vr_charge_degree', 'vr_offense_date', 'vr_charge_desc', 'type_of_assessment', 'decile_score.1', 'score_text', 'screening_date', 'v_type_of_assessment', 'v_decile_score', 'v_score_text', 'v_screening_date', 'in_custody', 'out_custody', 'priors_count.1', 'start', 'end', 'event', 'two_year_recid', 'length_of_stay']

Cantidad de atributos: 53
Ahora es necesario seleccionar los atributos con cuales se trabajará, esto tomando en cuenta primeramente los más descriptivos y relevantes en cuanto a lo que se desea analizar. Otro criterio tomado en cuenta es el eliminar atributos redundantes, por ejemplo, reincidencia y número crímenes posteriores a la evaluación COMPAS. Por otro lado, fue necesario filtrar datos con campos vacíos y ruidosos.
In [3]:
# ix es el índice de los datos que queremos conservar

# Remover entradas con información inconsistente de arresto.
ix = db['days_b_screening_arrest'] <= 30
ix = (db['days_b_screening_arrest'] >= -30) & ix

# Remover entradas donde no se encuentra información de reincidencia.
ix = (db['is_recid'] != -1) & ix

# Remover entradas sin puntaje.
ix = (db['score_text'] != 'N/A') & ix

# Remover entradas con un número imposible de días encarcelado.
ix = (db['length_of_stay'] >= 0) & ix

# Se limpia la base de datos.
db_limpio = db.loc[ix,:]

# Se selecciona los atributos
db_select = db_limpio[['sex','age','age_cat','race','juv_fel_count','juv_misd_count','juv_other_count','decile_score',
                'priors_count','c_charge_degree', 'c_charge_desc', 'is_recid', 'is_violent_recid', 'length_of_stay','two_year_recid','score_text']]

# Cuantizacion del decile_score
def quantizeScore(x):
    if (x == 'High')| (x == 'Medium'):
        return 1
    else:
        return 0  
    
db_select.to_csv(path_or_buf='Data/dataset.csv', sep=',', header=True)
    
db_select['score_text'] = db_select['score_text'].apply(quantizeScore)

print("Lista con los nombres de todos los atributos a utilizar:\n")
print(list(db_select))
print("\nCantidad de atributos a utilizar: " + str(len(list(db_select))))
Lista con los nombres de todos los atributos a utilizar:

['sex', 'age', 'age_cat', 'race', 'juv_fel_count', 'juv_misd_count', 'juv_other_count', 'decile_score', 'priors_count', 'c_charge_degree', 'c_charge_desc', 'is_recid', 'is_violent_recid', 'length_of_stay', 'two_year_recid', 'score_text']

Cantidad de atributos a utilizar: 16
C:\Users\Ignacio\Anaconda2\lib\site-packages\ipykernel_launcher.py:32: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
En lo siguiente se describe cada uno de los atributos seleccionados para ser utilizados en el análisis que abarca este proyecto.
Atributo Descripción
sex Sexo
age Edad
age_cat Rangos de edad (menor de 25, 25-45, mayor que 45)
race Raza
juv_fel_count Cantidad de delitos
juv_misd_count Cantidad de delitos menores
juv_other_count Otros delitos
decile_score Decil de su puntaje
priors_count Delitos previos
c_charge_degree Nivel de cargo
c_charge_desc Descripción del cargos
is_recid Es reincidente previamente
length_of_stay Días en cárcel
two_year_recid Fue reincidente 2 años después de evaluación COMPAS
Las estadísticas de las variables cuantitativas se resumen en lo siguiente:
In [4]:
# Se selecciona los atributos cuantitativos
db_cuanti = db_select[['age','juv_fel_count','juv_misd_count','juv_other_count','decile_score',
                'priors_count','length_of_stay']]

#Resumen de estadísticas
db_cuanti.describe()
Out[4]:
age juv_fel_count juv_misd_count juv_other_count decile_score priors_count length_of_stay
count 5989.000000 5989.000000 5989.000000 5989.000000 5989.000000 5989.000000 5989.000000
mean 34.517950 0.060945 0.091835 0.110035 4.448322 3.285857 15.100184
std 11.718071 0.470356 0.502082 0.468530 2.840895 4.772384 47.320508
min 18.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000
25% 25.000000 0.000000 0.000000 0.000000 2.000000 0.000000 0.000000
50% 31.000000 0.000000 0.000000 0.000000 4.000000 1.000000 1.000000
75% 42.000000 0.000000 0.000000 0.000000 7.000000 4.000000 6.000000
max 96.000000 20.000000 13.000000 9.000000 10.000000 38.000000 799.000000

Análisis de los datos


Teniendo en cuenta la noción del sesgo por raza que tiene la herramienta COMPAS, un análisis inicial útil es verificar qué grupos existen dentro de la base de datos:
In [5]:
db_select['race'].value_counts()
Out[5]:
African-American    3086
Caucasian           2032
Hispanic             496
Other                333
Asian                 31
Native American       11
Name: race, dtype: int64
Ahora bien, podemos analizar la distribución de puntajes con respecto a la raza. En lo siguiente se muestra el porcentaje de personas de cada raza que pertenecen a cada valor de la escala de puntajes que asigna COMPAS.
In [6]:
table = db_select.groupby(['race','decile_score']).size().reset_index().pivot(index='decile_score',columns='race',values=0)

# Porcentaje de acusados en cada valor de la escala de puntajes según su raza.
100*table/table.sum()
Out[6]:
race African-American Asian Caucasian Hispanic Native American Other
decile_score
1 11.179520 48.387097 28.444882 31.048387 NaN 41.141141
2 10.790668 12.903226 15.108268 17.338710 18.181818 17.117117
3 9.267660 16.129032 11.220472 14.516129 9.090909 9.309309
4 10.628645 NaN 11.515748 9.475806 NaN 11.411411
5 10.304601 3.225806 9.596457 7.862903 NaN 5.705706
6 10.110175 6.451613 7.726378 5.241935 18.181818 6.006006
7 10.920285 3.225806 5.511811 5.645161 18.181818 2.702703
8 9.462087 6.451613 4.625984 2.620968 NaN 2.102102
9 10.110175 NaN 3.789370 3.225806 18.181818 2.102102
10 7.226183 3.225806 2.460630 3.024194 18.181818 2.402402
In [7]:
# Histogramas
x = db_select.loc[db_select['race']=='African-American','decile_score'].values
y = db_select.loc[db_select['race']=='Caucasian','decile_score'].values
z = db_select.loc[db_select['race']=='Hispanic','decile_score'].values
w = db_select.loc[db_select['race']=='Asian','decile_score'].values
q = db_select.loc[db_select['race']=='Other','decile_score'].values
a = db_select.loc[db_select['race']=='Native American','decile_score'].values
plt.figure(figsize=[10,8])
plt.hist([x,y,z,w,q,a],normed=True)
plt.legend(['African-American','Caucasian','Hispanic','Asian','Other','Native American'], fontsize=15)
plt.title('Distribucion de puntaje COMPAS con respecto a la raza', fontsize=20)
plt.xlabel('Puntaje', fontsize=30)
plt.ylabel('Fraccion de poblacion', fontsize=30)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()
De lo anterior, es claro que los afroamericanos tienen una distribución más uniforme de puntajes de riesgo, con respecto a las otras razas. Valdría la pena observar la distribución de otros atributos para observar si hay alguna correlación. En lo siguiente se muestra el porcentaje de reincidencia con respecto a la raza.
In [8]:
table = db_select.groupby(['race','two_year_recid']).size().reset_index().pivot(index='two_year_recid',columns='race',values=0)

# Porcentaje de acusados en cada valor de la escala de puntajes según su raza.
100*table/table.sum()
Out[8]:
race African-American Asian Caucasian Hispanic Native American Other
two_year_recid
0 47.342839 74.193548 60.580709 62.701613 54.545455 63.663664
1 52.657161 25.806452 39.419291 37.298387 45.454545 36.336336
Se observa que la reincidencia es bastante alta en cada uno de los grupos por raza. Podemos hacer el mismo análisis con respecto al nivel de riesgo asignado por COMPAS, donde 0 es bajo riesgo (1 a 5) y 1 es alto riesgo (6 a 10).
In [9]:
table = db_select.groupby(['race','c_charge_degree']).size().reset_index().pivot(index='c_charge_degree',columns='race',values=0)

# Porcentaje de acusados en cada valor de la escala de puntajes según su raza.
100*table/table.sum()
Out[9]:
race African-American Asian Caucasian Hispanic Native American Other
c_charge_degree
F 69.215813 61.290323 59.448819 57.258065 63.636364 62.762763
M 30.784187 38.709677 40.551181 42.741935 36.363636 37.237237
Es claro de aquí que el porcentaje de personas Afro-Americanas que fueron calificadas como "muy riesgosas" fue mayor al que realmente reincidió. Por otro lado, se observa el efecto contrario en la categoría "Caucasian", donde hubo una mayor reincidencia mientras la fracción de personas de dicha categoría calificadas como "muy riesgosas" fue menor. Ahora estudiemos la distribución por cargo o delito.
In [10]:
table = db_select.groupby(['race','c_charge_degree']).size().reset_index().pivot(index='c_charge_degree',columns='race',values=0)

# Porcentaje de acusados en cada valor de la escala de puntajes según su raza.
100*table/table.sum()
Out[10]:
race African-American Asian Caucasian Hispanic Native American Other
c_charge_degree
F 69.215813 61.290323 59.448819 57.258065 63.636364 62.762763
M 30.784187 38.709677 40.551181 42.741935 36.363636 37.237237
En este caso, las fracciones de población de cada grupo son bastante similares entre sí. Hay entre un 30% y 40% de cargos por delitos menores y entre un 60% y 70% por de mayor grado.
Otro estudio de interés es observar la distribución de puntajes COMPAS con respecto a las variables de sexo, edad, reincidencia previa, reincidencia previa violenta y cantidad de delitos previos. Los siguientes histogramas ayudan a realizar ese análisis:
In [11]:
y = db_select.loc[(db_select['age'] >= 18) & (db_select['age'] < 20),'decile_score'].values
z = db_select.loc[(db_select['age'] >= 20) & (db_select['age'] < 30),'decile_score'].values
a = db_select.loc[(db_select['age'] >= 30) & (db_select['age'] < 40),'decile_score'].values
b = db_select.loc[(db_select['age'] >= 40) & (db_select['age'] < 50),'decile_score'].values
c = db_select.loc[(db_select['age'] >= 50) & (db_select['age'] < 60),'decile_score'].values
plt.figure(figsize=[10,8])
plt.hist([y,z,a,b,c],normed=True)
plt.legend(['18-20','20-30','30-40','40-50','50-60','60-70','70-80','80-90','90-100'],  fontsize=15)
plt.title('Distribucion de puntaje COMPAS con respecto a la edad', fontsize=20)
plt.xlabel('Puntaje', fontsize=30)
plt.ylabel('Porcentaje de poblacion',fontsize=30)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()

x = db_select.loc[db_select['sex']=='Female','decile_score'].values
y = db_select.loc[db_select['sex']=='Male','decile_score'].values
plt.figure(figsize=[10,8])
plt.hist([x,y],normed=True)
plt.legend(['Female','Male'],fontsize=15)
plt.title('Distribucion de puntaje COMPAS con respecto al sexo', fontsize=20)
plt.xlabel('Puntaje', fontsize=30)
plt.ylabel('Porcentaje de poblacion',fontsize=30)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()

x = db_select.loc[db_select['is_recid']== 1,'decile_score'].values
y = db_select.loc[db_select['is_recid']== 0 ,'decile_score'].values
plt.figure(figsize=[10,8])
plt.hist([x,y],normed=True)
plt.legend(['Es reincidente','No es reincidente'],fontsize=15)
plt.title('Distribucion de puntaje COMPAS con respecto al reincidencia previa', fontsize=20)
plt.xlabel('Puntaje', fontsize=30)
plt.ylabel('Porcentaje de poblacion',fontsize=30)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()

x = db_select.loc[db_select['is_violent_recid']== 1,'decile_score'].values
y = db_select.loc[db_select['is_violent_recid']== 0 ,'decile_score'].values
plt.figure(figsize=[10,8])
plt.hist([x,y],normed=True)
plt.legend(['Es reincidente','No es reincidente'],fontsize=15)
plt.title('Distribucion de puntaje COMPAS con respecto al reincidencia violenta previa', fontsize=20)
plt.xlabel('Puntaje', fontsize=30)
plt.ylabel('Porcentaje de poblacion',fontsize=30)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()

a = db_select.loc[db_select['priors_count'] == 0,'decile_score'].values
b = db_select.loc[(db_select['priors_count'] >= 1) & (db_select['priors_count'] < 4),'decile_score'].values
c = db_select.loc[db_select['priors_count'] > 3,'decile_score'].values
plt.figure(figsize=[10,8])
plt.hist([a,b,c],normed=True)
plt.legend(['Sin delitos previos','1 a 3 delitos previos','4 o mas delitos previos'],  fontsize=15)
plt.title('Distribucion de puntaje COMPAS con respecto a la cantidad de delitos previos', fontsize=20)
plt.xlabel('Puntaje', fontsize=30)
plt.ylabel('Porcentaje de poblacion',fontsize=30)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()
Se observa que los puntajes COMPAS poseen una distribución muy similar tanto para sexo masculino como femenino, de todos modos, se debe tener en cuenta que en el dataset hay muchos más hombres que mujeres. Por otro lado, al analizar la distribución respecto a la edad, es posible observar que la herramienta COMPAS distribuye más uniformemente a las personas más jovenes en los puntajes de 1 a 10, mientras que para personas con mayor de edad la tendencia es marcada hacia los puntajes de menor peligro de reincidencia (bajos puntajes). En cuanto a la reincidencia y los delitos previos se logra notar lo que sería intuitivo por sentido común, personas con reincidencia y con mayor cantidad de delitos previos fueron calificadas con mayor puntaje en la escala COMPAS.
Una forma efectiva de estudiar el sesgo en una base de datos es encontrar las reglas de asociación que se construye de forma natural en ella. Para ello se aplica el algoritmo apriori en R teniendo en cuenta solo los atributos de sexo, categoría de edad, raza, delitos previos, reincidencia histórica y puntaje COMPAS, obteniendose las siguientes reglas ordenadas según el lift:

Se observa que casi todas las reglas contienen muy baja frecuencia pero tienen mucho sentido en cuanto a la relación de los itemset. Ahora se considerará los atributos en lo que se ha identificado distribuciones sesgadas en cuanto al puntaje COMPAS. Teniendo en cuenta solo la raza y el puntaje COMPAS se obtiene:

Aquí claramente se identifica una relación entre los puntajes altos de la escala COMPAS y la raza Afro-Americana. Ahora, tomando en cuenta solo la edad se obtiene:

En esta ocasión se relaciona puntajes de magnitud mayor a personas más jóvenes dentro del rango desde 25 a 45 años. En cambio, a personas de mayor edad por sobre los 45 años se les relaciona un puntaje menor en la escala COMPAS. Ahora, estudiando ambos atributos en conjunto:

Clasificación

Una vez pre procesada la base de datos proporcionada por la Oficina del Sheriff del Condado de Broward en Florida, se procede a entrenar y validar modelos de clasificación (Random Forest, SVM con kernel RBF, Árbol de Decisión, y AdaBoost), a fin de tener una noción preliminar de la validez de los criterios empleados en la normalización de la base de datos utilizada.

Generación de dataset para análisis de variables en R

Todo proceso de clasificación requiere de una correcta definición del dataset asociado, puesto que las distribuciones existentes en cada una de las variables presentes determinarán el desempeño de los clasificadores en las etapas de entrenamiento y validación.

Teniendo bajo consideración que las variables de entrada de un clasificador deben corresponder a variables cuantitativas, se realizará un análisis comparativo de estas variables. Así, será posible establecer a priori la existencia (o no) de distribuciones, que permitan elaborar un dataset adecuado al problema.

De esta manera, a partir del dataframe "db_select" se seleccionan cada una de las variables cuantitativas pertenecientes al post procesamiento del dataset original. La información de cada uno de los dataframes se guardan en el archivo "datavisualization.csv", tal como se visualiza a continuación.

In [12]:
dbcsv = db_select[['age','juv_fel_count','juv_misd_count','juv_other_count','decile_score',
                'priors_count','length_of_stay','is_recid']]

dbcsv.to_csv("Data/datavisualization.csv", sep=',')

Generado el archivo "datavisualization.csv", este se carga en el entorno de desarrollo integrado RStudio. Posteriormente, realizando el escalamiento correspondiente, a través de K-Means se realiza un clustering comparativo de cada una de las variables, a fin de determinar la existencia de distribuciones que representen algún grado de correlación y separabilidad entre las variables. Los resultados obtenidos tras la ejecución del código implementado en R se presentan a continuación.

A partir de la ilustración generada, se establece que existe una clara separación de distribuciones al incluir en el análisis comparativo a la variable "is_recid", la cual corresponde a los registros previos de reincidencia del acusado. Por tanto, su integración debe contemplarse en el dataset asociado a los modelos de clasificación, los cuales han de predecir el riesgo de reincidencia asociado.

Clasificadores

Los clasificadores corresponden a modelos matemáticos predictivos, los cuales establecen la pertenencia de un conjunto de datos a una clase determinada. El desempeño de estos modelos depende de dos factores principales: el primero está asociado con el entrenamiento del modelo, etapa de aprendizaje en la cual cada dataset de entrada tiene un target de salida correspondiente. En general, un aumento en el número de épocas de entrenamiento del clasificador conllevará a la minimización de los errores del modelo. El segundo factor de relevancia corresponde a las distribuciones existentes en las variables de entrada, factor que determina la convergencia en el desempeño de clasificador.

En cuanto a los modelos de clasificación, tradicionalmente se menciona las redes neuronales, sin embargo, este clasificador no presenta una convergencia teórica que asegure el desempeño en la etapa de clasificación. De esta manera, para el presente trabajo se consideran cuatro modelos alternativos de clasificación: Random Forest, SVM con kernel RBF, Árbol de Decisión, y Adaboost.

Random Forest es un modelo predictivo conformado por la combinación de árboles predictores, en el cual cada árbol depende de los valores de un vector aleatorio. Support Vector Machine (SVM) es un modelo predictivo que representa a los puntos de muestra en un espacio matemático, separando las clases a una cantidad determinada de subespacios, mediante la utilización de hiperplanos separadores. Árbol de Decisión es un modelo predictivo conformado con diagramas de construcciones lógicas, estructura por medio de las cuales se representan y categorizan una serie de condiciones asociadas al conjunto de datos de entrada. Finalmente, Adaboost corresponde a un modelo predictivo que utiliza clasificadores fuertes, por medio de la combinación lineal de clasificador débiles, basado en boosting.

A partir de este contexto, la etapa de clasificación del dataset asociado considera dos etapas. La primera etapa corresponde a la identificación de las variables cuantitativas del dataset procesado, variables que serán utilizadas como entradas de cada uno de los modelos mencionados. La segunda etapa corresponde a la definición de los modelos predictivos a utilizar, los cuales fueron descritos con anterioridad.

In [13]:
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.metrics import classification_report, accuracy_score, precision_score, f1_score, recall_score
import numpy

#Seleccion X e y a partir de datos cuantitativos

X = db_select[['age','juv_fel_count','juv_misd_count','juv_other_count','decile_score',
                'priors_count','length_of_stay','is_recid']]
y = db_select[['two_year_recid']] 

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=37)

list_clf = ['Random Forest', 'SVM with RBF Kernel', 'Decision Tree', 'AdaBoost']

Definidas las variables cuantitativas que serán consideradas como entradas de los clasificadores, la partición del dataset, los target respectivos (variable "two_year_recid" corresponde a la reincidencia en un período de dos años posterior a la evaluación realizada por COMPAS), y los modelos predictivos a utilizar, se procede a inicializar cada uno de los modelos con los parámetros detallados. Inicializados, se entrenan y validan los modelos con cada los dataset respectivos, guardando en las listas indicadas los valores asociados a las métricas de desempeño de los clasificadores.

In [14]:
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.ensemble import AdaBoostClassifier

list_train_accuracy_score = []
list_train_precision_score = []
list_train_recall_score = []
list_train_f1_score = []

list_test_accuracy_score = []
list_test_precision_score = []
list_test_recall_score = []
list_test_f1_score = []

for i in range(0, 4):
    
    if i==0:
        classifier = 'Random Forest'
        clf = RandomForestClassifier(random_state=0)
    elif i==1:
        classifier = 'SVM con RBF'
        clf = SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
                  decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
                  max_iter=-1, probability=False, random_state=None, shrinking=True,
                  tol=0.001, verbose=False)
    elif i==2:
        classifier = 'Decision Tree'
        clf = DecisionTreeClassifier()
    else:
        classifier = 'AdaBoost'
        clf = AdaBoostClassifier()
    
    clf.fit(X_train, y_train.values.ravel())     #entrenamiento con los datos X y las clases que estan en y

    y_pred = clf.predict(X_train)
       
    list_train_accuracy_score.append(numpy.mean(accuracy_score(y_train, y_pred)))
    list_train_precision_score.append(numpy.mean(precision_score(y_train, y_pred, average=None)))
    list_train_recall_score.append(numpy.mean(recall_score(y_train, y_pred, average=None)))
    list_train_f1_score.append(numpy.mean(f1_score(y_train, y_pred, average=None)))

    print('\n' + classifier + ': Estadisticos de clasificacion en entrenamiento' + '\n')
    print(classification_report(y_train, y_pred))
    
    y_pred = clf.predict(X_test)                 #validacion  
    
    list_test_accuracy_score.append(numpy.mean(accuracy_score(y_test, y_pred)))
    list_test_precision_score.append(numpy.mean(precision_score(y_test, y_pred, average=None)))
    list_test_recall_score.append(numpy.mean(recall_score(y_test, y_pred, average=None)))
    list_test_f1_score.append(numpy.mean(f1_score(y_test, y_pred, average=None)))
    
    print(classifier + ': Estadisticos de clasificacion en validacion' + '\n')
    print(classification_report(y_test, y_pred))
    print('')
Random Forest: Estadisticos de clasificacion en entrenamiento

             precision    recall  f1-score   support

          0       1.00      0.99      0.99      2158
          1       0.99      1.00      0.99      1854

avg / total       0.99      0.99      0.99      4012

Random Forest: Estadisticos de clasificacion en validacion

             precision    recall  f1-score   support

          0       0.98      0.95      0.96      1086
          1       0.94      0.98      0.96       891

avg / total       0.96      0.96      0.96      1977



SVM con RBF: Estadisticos de clasificacion en entrenamiento

             precision    recall  f1-score   support

          0       0.99      0.95      0.97      2158
          1       0.94      0.99      0.97      1854

avg / total       0.97      0.97      0.97      4012

SVM con RBF: Estadisticos de clasificacion en validacion

             precision    recall  f1-score   support

          0       0.94      0.81      0.87      1086
          1       0.81      0.94      0.87       891

avg / total       0.88      0.87      0.87      1977



Decision Tree: Estadisticos de clasificacion en entrenamiento

             precision    recall  f1-score   support

          0       0.99      1.00      1.00      2158
          1       1.00      0.99      1.00      1854

avg / total       1.00      1.00      1.00      4012

Decision Tree: Estadisticos de clasificacion en validacion

             precision    recall  f1-score   support

          0       0.93      0.95      0.94      1086
          1       0.94      0.92      0.93       891

avg / total       0.94      0.94      0.94      1977



AdaBoost: Estadisticos de clasificacion en entrenamiento

             precision    recall  f1-score   support

          0       1.00      0.95      0.97      2158
          1       0.94      1.00      0.97      1854

avg / total       0.97      0.97      0.97      4012

AdaBoost: Estadisticos de clasificacion en validacion

             precision    recall  f1-score   support

          0       1.00      0.94      0.97      1086
          1       0.94      1.00      0.97       891

avg / total       0.97      0.97      0.97      1977


A fin de visualizar los estadísticos anteriormente obtenidos, se grafican para cada métrica el desempeño asociado a cada clasificador. Esto permitirá comparar el desempeño de los modelos predictivos utilizados.

In [15]:
import pandas as pd
import matplotlib.pyplot as plt

for i in range(0, 4):
    
    if i==0:
        metric = 'Accuracy'
        list_train = list_train_accuracy_score
        list_test = list_test_accuracy_score
    elif i==1:
        metric = 'Precision'
        list_train = list_train_precision_score
        list_test = list_test_precision_score    
    elif i==2:
        metric = 'Recall'
        list_train = list_train_recall_score
        list_test = list_test_recall_score    
    else:
        metric = 'F1 Score'
        list_train = list_train_f1_score
        list_test = list_test_f1_score
    
    raw_data = {'classifier': list_clf,
            'train': list_train,
            'test': list_test_accuracy_score}
    df = pd.DataFrame(raw_data, columns = ['classifier', 'train', 'test'])

    # Setting the positions and width for the bars
    pos = list(range(len(df['train'])))
    width = 0.25 

    fig, ax = plt.subplots(figsize=(10,5))

    plt.bar(pos, df['train'], width, alpha=0.5, color='#EE3224', label=df['classifier'][0]) 
    plt.bar([p + width for p in pos], df['test'], width, alpha=0.5, color='#F78F1E', label=df['classifier'][1]) 

    ax.set_ylabel(metric)
    ax.set_title(metric+': Entrenamiento y Validacion')
    ax.set_xticks([p + 0.5 * width for p in pos])
    ax.set_xticklabels(df['classifier'])

    plt.xlim(min(pos)-width, max(pos)+width*4)
    plt.ylim([0, 1] )
    plt.legend(['Entrenamiento', 'Validacion'], loc='upper right')
    plt.show()

De los resultados obtenidos en las ilustraciones anteriores, se establece que las mejores métricas de desempeño, tanto en entrenamiento como en validación, se presentan con Random Forest y Decision Tree. Esto se debe principalmente a la integración de la reincidencia histórica del acusado en las variables de entrada, y a la estructura que presentan ambos modelos predictivos. Considerando que las mejores métricas de validación están asociadas con Random Forest (accuracy, precision, recall, f1-score), finalmente se optará por este clasificador como modelo de evaluación para la determinación de reincidencia delictual en procesos judiciales.

Cálculo de puntaje de reincidencia

Determinado el mejor modelo de clasificación para la determinación de reincidencia en procesos judiciales, se procede a calcular el puntaje de reincidencia asociado, en una escala análoga a la utilizada en COMPAS. Recordando que el mejor modelo predictivo para el presente trabajo corresponde a Random Forest, es posible utilizar la probabilidad de pertenencia a cada una de las clases (reincidente o no reincidente), y convertirla a la escala de puntaje deseada.

Random Forest determina la pertenencia a una clase de la siguient manera: un dataset pertenecerá a la clase 0 (no reincidente) si la probabilidad de la clase es mayor a 0.5, por su parte pertenecerá a la clase 1 (reincidente) si la probabilidad de la clase es mayor a 0.5. Cuantificando esta probabilidad en una probabilidad general de reincidencia, resulta claro observar que si el dataset presenta una probabilidad de pertenencia 1.0 a la clase 1, entonces la probabilidad de reincidencia es 1.0, sin embargo, si la probabilidad de pertenencia a la clase 0 es 1.0, no significa que el acusado vaya a ser reincidente, sino que tiene la menor probabilidad de reincidencia. Luego, se decide transformar la probabilidad de pertenencia de la clase 0 a una probabilidad de reincidencia de la forma P_reincidencia_clase0 = 1 - P_clase0.

In [16]:
from sklearn.tree import DecisionTreeClassifier

classifier = 'Random Forest'
clf = RandomForestClassifier(random_state=0)
clf.fit(X_train, y_train.values.ravel())     
y_pred = clf.predict_proba(X_test)  

#i[0] no es reincidente. i[1] es reincidente

y_test_pred_prob = []
for i in y_pred:
    if i[0]>=i[1]:
        y_test_pred_prob.append(int((1-i[0])*10))
    else:    
        y_test_pred_prob.append(int(i[1]*10))

test_prob_recid = X_test
test_prob_recid.loc[:,'prob_recid'] = pd.Series(y_test_pred_prob, index=test_prob_recid.index)

test_prob_recid.to_csv("Data/test_prob_recid.csv", sep=';')
C:\Users\Ignacio\Anaconda2\lib\site-packages\pandas\core\indexing.py:337: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  self.obj[key] = _infer_fill_value(value)
C:\Users\Ignacio\Anaconda2\lib\site-packages\pandas\core\indexing.py:517: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  self.obj[item] = s

Los resultados obtenidos para el cálculo del nuevo puntaje de reincidencia, en la escala de 0-10, se presenta a continuación.

Conclusiones

A partir del trabajo realizado, se logra identificar la existencia de sesgos que afectan negativamente a afroamericanos. En la misma línea, resulta factible la implementación de modelos predictivos de clasificación, con altas métricas de desempeño. Esto conlleva a utilizar Random Forest como modelo predictivo final, permitiendo la asignación de puntaje de riesgo de reincidencia, en escala de 0-10.

Así, se logra utilizar minería de datos en contexto social, a favor de la no discriminación, elaborando una herramienta de ayuda a sistema judicial, que mejora tanto los tiempos, como las sanciones.

Referencias


[1] ProPublica, “Machine Bias,” https://www.propublica.org/article/machine-bias-risk-assessments-in-criminal-sentencing, May 2016.
[2] Caitlin Kuhlman, “BPDM In the Lab Tutorial - Fairness in Data Mining,” https://github.com/caitlinkuhlman/bpdmtutorial/blob/master/tutorial.ipynb.