Cargamos datasets con información de admisiones a hospitales de enfermos de diabetes. El objetivo es, una vez limpiado el dataset, estudiarlo para extraer el máximo número de insights de los datos.
Age
Weight
%matplotlib inline
import re
import random
from collections import Counter
import pandas as pd
pd.set_option('display.max_colwidth', -1)
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import kstest
sns.set(color_codes=True)
diabetes = spark.read.csv(DATA_PATH+'diabetic_data.csv', sep=',', header=True, inferSchema=True).toPandas()
admission_source = spark.read.csv(DATA_PATH+'admission_source_id.csv', sep=',', header=True, inferSchema=True).toPandas()
admission_type = spark.read.csv(DATA_PATH+'admission_type_id.csv', sep=',', header=True, inferSchema=True).toPandas()
discharge_disposition = spark.read.csv(DATA_PATH+'discharge_disposition_id.csv', sep=',', header=True, inferSchema=True).toPandas()
Comprobamos correcta descarga y lectura de datos
diabetes.head()
admission_source.head()
admission_type.head()
discharge_disposition.head()
Left join con admission source
df = pd.merge(diabetes,admission_source,how='left',on='admission_source_id')
df=df.rename(columns={'description':'admission_source'})
Left join con admission type
df = pd.merge(df,admission_type,how='left',on='admission_type_id')
df=df.rename(columns={'description':'admission_type'})
Left join con discharge_disposition
df = pd.merge(df,discharge_disposition,how='left',on='discharge_disposition_id')
df=df.rename(columns={'description':'discharge_disposition'})
df.head()
El dataframe cuenta con 101,766 registros y 53 columnas
df.shape
Observo nombres
df.columns
1) Pasamos a minusculas los nombres de las columnas
2) Pasamos los guiones medios a guiones bajos
nuevas_col = [col.lower().replace('-','_') for col in df.columns]
df.columns = nuevas_col
df.columns
Se observan los tipos de variables en las columnas, y se nota lo siguiente:
Aunque los diag_X tengan una gran mayoria de numeros, no deberian transformarsee a INT ya que hay casos suplementarios en los cuales el codigo puede arrancar con una V o una E.
Aunque AGE y WEIGHT en la vida real son numeros INT, aqui se los trata como rangos, por lo que esta bien que sean Object
Tambien se nota la presencia de valores None o "?", pero seran atendidos mas adelante
Se han revisado los tipos de datos de cada columna, chequeandolos contra las descripciones entregadas en la base de datos. Como conclusión, los valores tomados son correctos, aunque hay que estudiar la presencia de Nulos y valores atipicos
df.dtypes
Se han revisado los valores unicos de las columnas categoricas para revisar que los tipos coincidan con las descripciones otorgadas.
df_cat = df.select_dtypes(include = ['object'])
for i in df_cat.columns:
print(df[i].value_counts())
pacientes_repetidos = df.groupby('patient_nbr')['patient_nbr'].agg(['count'])
pacientes_repetidos[pacientes_repetidos['count']>1].shape
admision_repetidos = df.groupby('encounter_id')['encounter_id'].agg(['count'])
admision_repetidos[admision_repetidos['count']>1].shape
1) Analizar la importancia de las columnas cuyos datos son las medicaciones. Estas son 23, y notamos que unicamente 7 de ellas no cuentan con "No" en 100000 o mas registros. Consideramos que estas 7 son las que pueden considerarse imporantes para el modelo.
medicamentos = df.iloc[:,24:47] #creo data frame para columnas de medicamentos
medicamentos_conteo = pd.DataFrame().reset_index() #creo data frame vacio
#loop para corroborar los conteos de No's
for columna in medicamentos.columns:
data = pd.DataFrame(medicamentos[columna].value_counts()).reset_index()
medicamentos_conteo = pd.merge(data,medicamentos_conteo,how='outer',on='index')
#Arreglo formato del dataframe y transpongo para visualizarlo mas facilmente
medicamentos_conteo.set_index('index',inplace=True)
medicamentos_conteo=medicamentos_conteo.transpose()
medicamentos_conteo
no_aporta = medicamentos_conteo[medicamentos_conteo['No']>100000]
print(no_aporta.shape)
no_aporta
2) Para el resto de las variables categoricas, se creo una funcion para devolver en formato DataFrame los value counts, y luego se ha generado un loop para que lo devuelva para cada variable categorica que no sean medicamentos, y se han seleccionado aquellas con valores repetidos en muchas ocasiones.
def valores_unicos(columna):
data_frame = pd.DataFrame(df[columna].value_counts())
data_frame['share']=data_frame[columna]/sum(data_frame[columna])*100
return data_frame
#Funcion para mostrar DataFrames lado por lado
#fuente: https://stackoverflow.com/questions/38783027/jupyter-notebook-display-two-pandas-tables-side-by-side
from IPython.display import display_html
from itertools import chain,cycle
def display_side_by_side(*args,titles=cycle([''])):
html_str=''
for df,title in zip(args, chain(titles,cycle(['</br>'])) ):
html_str+='<th style="text-align:center"><td style="vertical-align:top">'
html_str+=f'<h2>{title}</h2>'
html_str+=df.to_html().replace('table','table style="display:inline"')
html_str+='</td></th>'
display_html(html_str,raw=True)
no_medicamentos = df_cat.iloc[:,0:11] + df_cat.iloc[:,34:37]
for col in no_medicamentos.columns:
display_side_by_side(valores_unicos(col).head(),titles = [col])
Columna weight: notamos que alrededor de 100K de registros tienen dato faltante, por lo que no estaria aportando informacion al modelo
Columna max_glu_serum: notamos que alrededor de 100K de registros tienen dato None, por lo que no estaria aportando informacion al modelo
Columna a1cresult: notamos que alrededor de 85K de registros tienen dato faltante, no deberia descartarse de entrada, pero en la ejecucion del modelo podria probarse el score con y sin esta variable.
3) Para las columnas numericas, se decidio analizar los datos estadisticos a traves de la funcion describe
Se observa que las columnas number_outpatient, number_emergency, number_inpatient tienen una gran concentracion hasta el tercer (o segundo) cuartil de valores identicos, por lo que no aportaría tanta informacion al modelo.
df.select_dtypes(include = ['int32']).iloc[:,5:].describe()
No hay NAs
for col in df.columns:
if(df[col].isna().sum() >0):
print(col,"con",df[col].isna().sum(),"nulos.")
else: print(col, '-> No hay valores nulos')
Sin embargo, hay valores que deberian considerarse como Nulos
nulos = df.replace('?',np.nan)
for col in nulos.columns:
if(nulos[col].isna().sum() >0):
print(col,"->",nulos[col].isna().sum(),"nulos.")
Comprobamos correcta descarga y lectura de datos
Reemplazamos, para las columnas con pocos valores nulos, con la Moda
inputar = ['race','diag_1','diag_2','diag_3']
for col in inputar:
nulos[col].fillna(nulos[col].mode()[0], inplace=True)
for col in nulos.columns:
if(nulos[col].isna().sum() >0):
print(col,"->",nulos[col].isna().sum(),"nulos.")
Separamos las variables numericas en un data frame nuevo, sin contar los IDs
df_num = df.select_dtypes(include = ['int32'])
df_num=df_num.drop(columns=['encounter_id','patient_nbr','admission_type_id','discharge_disposition_id','admission_source_id'])
df_num.head()
Evaluo normalidad para saber que test de outlier aplicar
def is_normal(variable, alpha=0.05):
# get std, mean
mean = variable.mean()
std = variable.std()
# run kstest and get pvalue
pvalue = kstest(variable, 'norm', args=(mean, std)).pvalue
# check if pvalue is higher than alpha
return pvalue >= alpha
normal_cols = []
non_normal_cols = []
for col in df_num.columns:
normal = is_normal(df[col])
if normal:
normal_cols.append(col)
else:
non_normal_cols.append(col)
Todas las variables numericas siguen una distribucion que no es normal
print(non_normal_cols)
normal_cols
from scipy.stats import kstest
from scipy import stats
for col in df_num.columns:
mean = df_num[col].mean()
std = df_num[col].std()
mean, std
norm_data = stats.norm.rvs(loc=mean, scale=std, size=100000)
plt.figure(figsize=(6,3))
sns.distplot(df_num[col], kde=False, norm_hist=True)
sns.distplot(norm_data, kde=False, norm_hist=True)
min_thresh = mean - 3 * std
max_thresh = mean + 3 * std
plt.figure(figsize=(6, 3))
sns.distplot(df_num[col], kde=False, hist_kws={'edgecolor': 'k'})
plt.axvline(min_thresh, color='red', linewidth=2)
plt.axvline(max_thresh, color='red', linewidth=2)
Se genera una funcion para devolver un dataframe sin outliers
def remove_normal_outliers(df, col_name):
"""
Función para eliminar outliers de una distribución
de datos normal
"""
# get mean, std
mean = df[col_name].mean()
std = df[col_name].std()
# get thresholds
min_thresh = mean - 3 * std
max_thresh = mean + 3 * std
# filter dataframe
df_no_out = df[(df[col_name] >= min_thresh) &
(df[col_name] <= max_thresh)]
# return filtered dataframe
return df_no_out
Se aplica la funcion a cada columna, devolviendo el % de outliers
for col in df_num:
df_no_out = remove_normal_outliers(df, col)
nrows = len(df)
nrow_no_out = len(df_no_out)
n_outliers = nrows - nrow_no_out
perc_outliers = round(100 * n_outliers / nrows, 2)
print('{0} has {1}% outliers'.format(col, perc_outliers))
Cuando la distribución de datos es no normal, una forma muy sencilla de eliminar outliers de forma analítica es aplicando el test de Tukey. Éste dice que se considera valor atípico a todo aquel que esté fuera del siguiente rango:
def tukey_outliers(df,column,extreme=False):
q1, q3 = np.percentile(df[column],[25,75])
iqr = q3 - q1
constant = 1.5 if not extreme else 3
return df[~((df[column]>(q3+constant*iqr)) | (df[column]<(q1-constant*iqr)))] #la onda significa que no te trae eso, todo menos eso te trae.
Creamos un loop para devolver el % de outliers segun la evaluacion de cada columna
for columna in df_num.columns:
outliers = round((1 - len(tukey_outliers(df_num,columna,extreme=False))/len(df_num))*100,2)
print('Outliers en columna',columna,':',outliers,'%')
Se crea un df "limpio" sin outliers para poder contrastar las distribuciones con box plots debajo
df_limpio = df_num
for columna in df_num.columns:
sin_outliers = tukey_outliers(df_limpio,columna,extreme=False)
df_limpio=sin_outliers
df_limpio
Creamos una funcion para comparar los boxplots con y sin outliers
def plot_outliers(column):
plt.figure(figsize = (12,5))
plt.subplot(1,2,1)
ax = sns.boxplot(x=df_num[column]) #originales
plt.title('Con outliers')
temp = tukey_outliers(df_num,column,extreme=False) #sin outliers
plt.subplot(1,2,2)
ax2= sns.boxplot(x=temp[column])
plt.title('Sin outliers')
for i in df_num.columns:
plot_outliers(i)
Las variables tienen el formato correcto
nulos.info()
Previo al analisis, unimos los dataframes previamente separados para evaluar nulos y outliers
df_ids = df[['encounter_id','patient_nbr','admission_type_id','discharge_disposition_id','admission_source_id']].reset_index()
df_nulos = nulos.select_dtypes(include = ['object'])
df_nulos.reset_index(inplace=True)
df_limpio.reset_index(inplace=True)
df_final = pd.merge(df_limpio,df_ids,how='left',on='index')
df_final = pd.merge(df_final,df_nulos,how='left',on='index')
df_final
df_final.shape
Correctamente, solo hay valores nulos en las que decidimos dejar de esa manera
df_final.isna().sum()
Creamos version 2 de Age para pasar a numerica, tomando el valor medio de los rangos.
df_final['age_v2'] = df_final['age'].replace({'[70-80)':75, '[60-70)':65, '[50-60)':55,'[80-90)':85, '[40-50)':45, '[30-40)':35, '[20-30)':25,'[10-20)':15, '[0-10)':5,'[90-100)':95})
df_final['age_v2']
Primero se ha corrido la matriz de correlacion para el data frame final, sin Outliers y sin Nulos.
Sin embargo, se nota la presencia de NAs:
Esta es causada por el valor = 0 de la varianza de las columnas number_outpatient y number_emergency. Como la gran mayoria de sus datos originales eran igual a 0, al eliminar los outliers, 0 queda como el unico valor posible, causando que la varianza/desvió sea 0 y que el denominador de la matriz corr tambien lo sea, causando un error
#data frame final
df_final_num = df_final.select_dtypes(include = ['int32','int','int64']).drop(columns=['index','encounter_id','patient_nbr','admission_type_id','discharge_disposition_id','admission_source_id'])
df_final_num.corr()
Por este motivo, se deja plasmada la matriz de correlacion para los datos numericos originales.
#data frame original
df_num.corr()
1) Matrix de correlacion para df_final sin nulos ni outliers
df_numerical_corr = df_final_num.corr(method='spearman')
fig, ax = plt.subplots(figsize=(15,10))
sns.heatmap(df_numerical_corr, xticklabels=list(df_numerical_corr), yticklabels=list(df_numerical_corr),
annot=True, fmt='.1f', linewidths = 0.5, cmap="Spectral", ax=ax)
2) Matrix de correlacion para df original, con nulos y outliers
df_numerical_corr = df_num.corr(method='spearman')
mask = np.zeros_like(df_numerical_corr)
mask[np.triu_indices_from(mask)] = False
fig, ax = plt.subplots(figsize=(15,10))
sns.heatmap(df_numerical_corr, xticklabels=list(df_numerical_corr), yticklabels=list(df_numerical_corr),
annot=True, fmt='.1f', linewidths = 0.5, cmap="Spectral", ax=ax,mask=mask)
Como la matriz es simétrica, se puede observar solo la primera diagonal
def Generate_heatmap_graph(corr, chart_title, mask_uppertri=False ):
mask = np.zeros_like(corr)
mask[np.triu_indices_from(mask)] = mask_uppertri
fig,ax = plt.subplots(figsize=(12,12))
ax.set_facecolor('white')
sns.heatmap(corr
, mask = mask
, square = True
, annot = True
, annot_kws={'size': 10.5, 'weight' : 'bold'}
, cmap=plt.get_cmap("YlOrBr")
, linewidths=.1)
plt.title(chart_title, fontsize=14)
plt.show()
var_corr = round(df_num.corr(),2)
Generate_heatmap_graph(var_corr
,chart_title = 'Correlation Heatmap'
,mask_uppertri = True)
Observando la correlacion en el df orignal, vemos que tan solo 2 combinaciones de variables otorgan una correlacion mayor a 0,35, por lo que puede decirse que la correlacion es baja a lo largo de la base.
La correlacion mas alta es aquella de Numero de medicaciones con Tiempo en hospital, de 0,47.
La variable con mayor correlacion para el resto de las variables es Tiempo en hospital. Se puede notar tanto arriba como debajo que aparece en el top2 de todas las otras variables numericas del df
for col in df_num.columns:
df_numerical_corr=round(df_num.corr()[col].sort_values(ascending=False).iloc[1:3],2)
print('***********',col, 'correlacionada con:***********\n',df_numerical_corr)
df_dum=pd.get_dummies(df, prefix=['A', 'D'], columns=['race', 'gender'])
df_dum_num = df_dum.select_dtypes(include = ['int32','int','int64','uint8']).drop(columns=['encounter_id','patient_nbr','admission_type_id','discharge_disposition_id','admission_source_id'])
def Generate_heatmap_graph(corr, chart_title, mask_uppertri=False ):
mask = np.zeros_like(corr)
mask[np.triu_indices_from(mask)] = mask_uppertri
fig,ax = plt.subplots(figsize=(12,12))
ax.set_facecolor('white')
sns.heatmap(corr
, mask = mask
, square = True
, annot = True
, annot_kws={'size': 10.5, 'weight' : 'bold'}
, cmap=plt.get_cmap("YlOrBr")
, linewidths=.1)
plt.title(chart_title, fontsize=14)
plt.show()
var_corr = round(df_dum_num.corr(),2)
Generate_heatmap_graph(var_corr
,chart_title = 'Correlation Heatmap'
,mask_uppertri = True)