import pandas as pd
import numpy as np
import sklearn as sk
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
plt.style.use('fivethirtyeight')
color_pal = sns.color_palette()
import warnings
warnings.filterwarnings('ignore')
from google.colab import drive
drive.mount('/content/drive')
#fechas
import datetime as dt
from datetime import timedelta
Pueden descargar el archivo aqui: https://www.kaggle.com/code/shreydan/spotify-top-hits-eda/data
df = pd.read_csv('/content/drive/MyDrive/Python/DQ/songs_normalize.csv') #Indicar ruta personal
df.shape
df.head(3)
Estudio columnas del dataset
df.info()
Creamos primeras reglas de calidad
Las reglas de calidad tendrán dos (o tres) opciones como resultado:
Regla 1.1: La fecha (año) no puede tener mas que 4 digitos
def regla_1_1(columna,tabla=False):
conteo = len([x for x in df[columna] if len(str(x))!=4])
if tabla==False:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/len(df)))*100,2)}% para la columna {columna}')
else:
return conteo
Al indicarle unicamente la columna, nos otorga un texto que indica el % de cumplimiento de la regla de calidad
regla_1_1('year')
En cambio, si entregamos como Verdadero al parametro "tabla", nos otorga unicamente el valor de registros en incumplimiento. En este caso, 0.
regla_1_1('year',tabla=True)
Regla 1.2: No pueden haber valores no permitidos
Un ejempo para esta base de datos puede ser que el año no sea mayor al actual, o que la "danceability" o "energy" no sea mayor a 1, como fue definida.
Confirmamos cuales son las columnas con intervalo [0;1]
numericas = df.select_dtypes( include= ['int64','float64'])
[print(f'Columna {x}: Valor maximo : {max(df[x])}, Valor minimo: {min(df[x])}') for x in numericas.columns][1]
Creamos código de la regla de calidad
def regla_1_2(columna,tabla=False):
conteo = len([x for x in df[columna] if x>1 or x<0])
if tabla==False:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/len(df)))*100,2)}% para la columna {columna}')
else:
return conteo
columnas = ['danceability','energy','mode','speechiness','acousticness','instrumentalness','valence']
Opcion 1: unicamente % cumplimiento para cada columna
for col in columnas:
regla_1_2(col)
Opcion 2: unicamente N° registros en incumplimiento para cada columna
for col in columnas:
print(regla_1_2(col,tabla=True))
Regla 2.1: No deben existir registros nulos
def regla_2_1(columna,tabla=False):
conteo = df[columna].isna().sum()
if tabla == False:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/len(df)))*100,2)}% para la columna {columna}')
else:
return conteo
Opcion 1: unicamente resumen
regla_2_1('year')
Ahora para todas las columnas
for col in df.columns:
regla_2_1(col)
Opcion 2: unicamente cantidades
for col in df.columns:
print(regla_2_1(col,tabla=True))
Regla 3.1: No deben haber datos duplicados
Previamente, hay que definir que en que columnas si tiene sentido que hayan duplicados, y descartarlas de este analisis puntual
Observo valores únicos de cada una de ellas
#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)
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
for col in df.columns:
display_side_by_side(valores_unicos(col).head(),titles = [col])
En este caso, solo tiene sentido contar duplicados para canciones repetidas.
Como las canciones pueden tener mismo nombre para distintos artistas, en este caso utilizaremos como primary key a Artist y Song
df['primary_key']=df['artist']+df['song']
Creamos codigo de regla
def regla_3_1(columna,error=False, tabla=False):
conteo = len(df) - len(df.drop_duplicates(columna))
errores = df[df.duplicated(columna)]
if error==True and tabla==True:
raise TypeError('Solo podes poner True a uno de los dos parametros')
### Por si queremos ver unicamente el numero
elif tabla == True:
return conteo
### Por si queremos ver las incidencias
elif error==True:
return pd.DataFrame(errores[columna].values,columns=[columna])
### Por si queremos ver solo la compliance
else:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/len(df)))*100,2)}% para la columna {columna}')
Opcion 1: Unicamente % de cumplimientos
regla_3_1('primary_key')
Opcion 2: N° de incumplimientos
regla_3_1('primary_key',tabla=True)
Opcion 3: Detalle de errores
regla_3_1('primary_key',error=True)
Solo podremos ver uno de los tipos
regla_3_1('primary_key',error=True,tabla=True)
Las reglas de integridad tienen como objetivo hacer controles de consistencia entre distintas columnas de un mismo objeto
Regla 4.1: Valores altos de acustica deben venir con valores bajos de instrumental
def regla_4_1(threshold_down,threshold_up,col1,col2,tabla=False):
temp = df.copy()
temp['4.1']=['error' if x > threshold_down and y > threshold_up else 'ok' for x,y in zip(df[col1],df[col2])]
conteo= len(temp[temp['4.1']=='error'])
conteo_df=len(temp)
canciones = temp[temp['4.1']=='error'].iloc[:, 1].values
if tabla==False:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/conteo_df))*100,2)}% para las columnas {col1,col2}. \nLas canciones que no cumplen la regla son {canciones}')
else:
return conteo
Si queremos ver el detalle completo:
regla_4_1(threshold_down=0.4,threshold_up=0.7,col1='acousticness',col2='instrumentalness')
Si solo queremos ver el numero de indicidencias:
regla_4_1(threshold_down=0.4,threshold_up=0.7,col1='acousticness',col2='instrumentalness',tabla=True)
Podemos crear una regla de calidad que se fije, en este caso particular, si la variacion del total de hits por año no sea mayor a un parametro que le indiquemos.
Regla 5.1: La variacion del total de registros por año/mes/dia no puede ser mayor a un parametro definido.
def regla_5_1(fecha,parametro,tabla=False):
conteo_df = df.groupby(fecha)[fecha].agg(['count'])
conteo_df['lag'] = conteo_df['count'].shift(1)
conteo_df['var'] = round(abs((conteo_df['count'] / conteo_df['lag']) - 1 )*100,2)
fallas = conteo_df[conteo_df['var'] > parametro].index.tolist()
fallados = conteo_df[conteo_df['var'] > parametro]['var'].values.tolist()
conteo = len(conteo_df[conteo_df['var'] > parametro].index.tolist())
if tabla == False:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/len(conteo_df)))*100,2)}% para la columna {fecha}. \nLas fechas que no cumplen la variacion del {parametro} son {fallas}, variando {fallados} ')
else:
return conteo
Detalle:
regla_5_1(fecha='year',parametro= 30)
Solo el numero:
regla_5_1(fecha='year',parametro= 30,tabla=True)
Regla 5.2: La métrica no puede superar un umbral determinado (por ejemplo, la acustica no puede ser mayor a 0.9)
def regla_5_2(columna,umbral,tabla=False):
conteo_df = df[df[columna]>umbral]
conteo = len(conteo_df)
errores = conteo_df['song'].values
if tabla==False:
return print(f'El porcentaje de cumplimiento de la regla de calidad es de {round((1-(conteo/len(df)))*100,2)}% para la columna {columna}. \nLos registros que sobrepasan el umbral de {umbral} son {errores} ')
else:
return conteo
Detalle
regla_5_2('acousticness',0.9)
Cantidad
regla_5_2('acousticness',0.9,tabla=True)
Tambien puede hacerse lo mismo pero segun un drilldown, cual ayude a agrupar los resultados según la poblacion que se quiera analizar.
Por ejemplo, podemos ver los saltos en las reglas de calidad según el genero de cada cancion:
Corregimos formato para que sea genero unico
df['genre']=df['genre'].apply(lambda x: x.split(',')[0].lower())
Creamos regla
def regla_5_3(columna,umbral,drilldown=False,col_drilldown = None):
conteo_df = df[df[columna]>umbral]
if drilldown==False:
conteo = len(conteo_df)
errores = conteo_df['song'].values
return round((1-(conteo/len(df)))*100,2)
else:
conteo = pd.DataFrame(conteo_df.groupby(col_drilldown)['song'].count().sort_values(ascending=False))
return conteo
regla_5_3('acousticness',0.6)
Ahora con drilldown segun el genero de la canción
regla_5_3('acousticness',0.6,drilldown=True,col_drilldown='genre')
Visualizamos los resultados de la regla de calidad
data = regla_5_3('acousticness',0.6,drilldown=True,col_drilldown='genre')
sns.barplot(data=data
,x=data.index,
y='song',
palette='Blues_r')
plt.title('Incidencias regla 5.3 según Genero musical', fontsize = 15)
plt.xticks(rotation=45)
plt.grid(False)
plt.show()
Creamos dataframes particulares con los resultados de cada regla de calidad, para las columnas utilizadas en los ejemplos enseñados previamente.
Luego, los unimos en un dataframe final llamado resultados
rdos_validez_1 = pd.DataFrame([regla_1_1.__name__,regla_1_1('year',tabla=True),'year'],index=['Regla','Errores','Variable']).T
rdos_validez_2 = pd.DataFrame([[regla_1_2.__name__,regla_1_2(col,tabla=True),col] for col in columnas]
,columns=['Regla','Errores','Variable'])
rdos_completitud = pd.DataFrame([[regla_2_1.__name__,regla_2_1(col,tabla=True),col] for col in columnas]
,columns=['Regla','Errores','Variable'])
rdos_duplicidad = pd.DataFrame([regla_3_1.__name__,regla_3_1('primary_key',tabla=True),'primary_key'],index=['Regla','Errores','Variable']).T
rdos_integridad = pd.DataFrame([regla_4_1.__name__,regla_4_1(threshold_down=0.4,threshold_up=0.7,col1='acousticness',col2='instrumentalness',tabla=True),'acousticness-instrumentalness'],index=['Regla','Errores','Variable']).T
rdos_precision_1 = pd.DataFrame([regla_5_1.__name__,regla_5_1(fecha='year',parametro= 30,tabla=True),'year'],index=['Regla','Errores','Variable']).T
rdos_precision_2 = pd.DataFrame([[regla_5_2.__name__,regla_5_2(col,0.9,tabla=True),col] for col in ['danceability','energy','speechiness','acousticness','instrumentalness','valence']]
,columns=['Regla','Errores','Variable'])
resultados = pd.concat([rdos_validez_1,rdos_validez_2,rdos_completitud,rdos_duplicidad,rdos_integridad,rdos_precision_1,rdos_precision_2]).reset_index(drop=True)
resultados
Visualizamos resultados
saltos_x_col = resultados.groupby('Variable')['Errores'].agg(['sum'])
saltos_x_regla = resultados.groupby('Regla')['Errores'].agg(['sum'])
plt.figure(figsize = (12,5))
plt.subplot(1,2,1)
ax = sns.barplot(x=saltos_x_col.index , y= 'sum',data=saltos_x_col, palette = 'coolwarm_r')
plt.title('Incidencias por variable', fontsize = 15)
plt.xticks(rotation=90,fontsize=10)
plt.ylabel('Incidencias')
plt.grid(False)
plt.subplot(1,2,2)
ax2= sns.barplot(x=saltos_x_regla.index , y= 'sum',data=saltos_x_regla, palette= 'coolwarm_r')
plt.title('Incidencias por regla de calidad', fontsize = 15)
plt.ylabel('Incidencias')
plt.xticks(rotation=90,fontsize=10)
plt.grid(False)
import plotly.io as pio
pio.renderers.default = "notebook"
px.bar(saltos_x_col, x=saltos_x_col.index , y= 'sum',title='Incidencias por variable',labels={
"sum": "Incidencias",
"Variable": "Variable"
},
color_discrete_sequence=px.colors.sequential.RdBu,width=800, height=400)
px.bar(saltos_x_regla, x=saltos_x_regla.index , y= 'sum',title='Incidencias por regla de calidad',labels={
"sum": "Incidencias",
"Regla": "Regla"
},
color_discrete_sequence=px.colors.sequential.Plasma,width=800, height=400)
%%shell
jupyter nbconvert --to html ///content/DQ.ipynb