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
Objetivos, Hipótesis y Problemáticas Iniciales
Datos
Características y limpieza de los datos
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')
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))))
# 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))))
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 |
# 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()
Análisis de los datos
db_select['race'].value_counts()
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()
# 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()
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()
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()
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()
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()
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.
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.
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.
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('')
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.
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.
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=';')
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