Spaces:
Sleeping
Sleeping
# -*- coding: utf-8 -*- | |
""" | |
Created on Thu Feb 11 01:47:20 2021 | |
utils_ml.py : utilitaires pour le cours de Data Mining | |
@author: erwann bargain | |
02/02/2021 eba Création d'un df de synthèse | |
27/01/2021 eba Rajout de "==============" dans la fonction valid_df pour mieux voir les résultats | |
11/02/2021 eba 1ère version qui va vite évoluer | |
""" | |
import numpy as np | |
import pandas as pd | |
import scipy.stats as scipy_stats | |
import datetime | |
import matplotlib.pyplot as plt | |
import seaborn as sns | |
from sklearn.metrics import confusion_matrix, roc_auc_score | |
def valid_df(df, id_var='', df_name='', edit_head = True, edit_tail = True, edit_describe = True): | |
""" | |
calcul de stats de base sur un df pour faciliter sa validation | |
df : pandas dataframe | |
df_name : nom du data frame pour l'afficher | |
optionnel : par défaut on met '' | |
id_var : indiquer le nom de la colonne qui contient l'id | |
si un nom est indiqué on vérifiera les doublons | |
defaut = '' | |
edit_head : Affichage des premières lignes | |
Indiquez False pour ne pas les voir | |
defaut : True | |
edit_tail : Affichage des dernières lignes | |
Indiquez False pour ne pas les voir | |
defaut : True | |
edit_describe : Affichage du describe | |
Indiquez False pour ne pas le voir | |
defaut : True | |
Exemples: | |
1. Exemple minimal | |
valid_df(df) | |
2. Exemple avec affichage du nom du dataframe et verif des doublons | |
mais pas d'affichage du head, tail et describe | |
valid_df(df, id_var='id_client', df_name='df_achats' | |
, edit_head = False, edit_tail = False, edit_describe = False) | |
""" | |
print("===========================") | |
print("===========================") | |
print("Synthese de la table") | |
if df_name != '': | |
print("=== DF : " + df_name) | |
print("===========================") | |
print("===========================") | |
print('============================') | |
print("nb de lignes et de colonnes") | |
print('============================') | |
print(df.shape ) #pas de () c'est un attribut | |
print("") | |
print('=======================================') | |
print("type, missing et nb de modalites <>") | |
print('=======================================') | |
info_list = [] | |
for c in df.columns: | |
df_freq = df[c].value_counts().reset_index() | |
df_freq['zzvalue'] = df_freq['index'].astype(str) + " (" \ | |
+ df_freq[c].astype(str) + " obs)" | |
#df_freq = df_freq[['zzvalue']][:20].T | |
info_list.append( ( c, df[c].dtypes, df[c].isna().sum(), df[c].isna().sum()/len(df) | |
, len(pd.unique(df[c]))) | |
+ tuple(df_freq['zzvalue'][:20])) | |
nb_zzvalues = max(map(len,info_list)) - 5 | |
#pd.unique(df[c]) renvoie la liste des valeurs unique de la colonne | |
df_stats = pd.DataFrame(info_list, columns=['column','type','missing_count','missing_rate' | |
, 'nb_labels'] | |
+ [f'value_{i}' for i in range(1,nb_zzvalues+1)]) | |
print(df_stats) | |
if id_var != '': | |
print('=======================================') | |
print('Doublon') | |
print('=======================================') | |
print("aucun doublon d'ID si la valeur la + fréquente est égale à 1") | |
print( df[id_var].value_counts()[:1] ) | |
print( 'nb duplicates :' , sum(df.duplicated(subset=[id_var], keep='first')) ) | |
print("") | |
if edit_head == True: | |
print('============================') | |
print("premieres lignes") | |
print('============================') | |
print( df.head() ) #head() c'est une fonction, une méthode d'une classe | |
print("") | |
if edit_tail == True: | |
print('============================') | |
print("dernières lignes") | |
print('============================') | |
print( df.tail() ) | |
print("") | |
if edit_describe == True: | |
print('=======================================') | |
print('résumé de chaque variable') | |
print('=======================================') | |
print( df.describe(include='all') ) | |
print("") | |
return df_stats | |
def customize_corr(df: pd.DataFrame) : | |
""" | |
Customize correlation matrix visually | |
Arguments: | |
df - dataframe with features | |
Returns: | |
""" | |
plt.figure(figsize=(16, 10)) | |
# define the mask to set the values in the upper triangle to True | |
mask = np.triu(np.ones_like(df.corr())) | |
heatmap = sns.heatmap(df.corr(), mask=mask, vmin=-1, vmax=1, annot=True, cmap='magma') | |
heatmap.set_title('Lower Correlation Matrix', fontdict={'fontsize':18}, pad=16) | |
def features_ordering(df , feature_names, target): | |
""" | |
Retourne un df avec une ligne par feature par ordre décroissant des T de Schupprow en fonction d'une variable cible Y | |
Parametres : | |
X : pandas dataframe | |
feature_names : liste des nom des colonnes dont l'on souhaite avoir les stats du chi2 | |
=> ne pas mettre d'id ou de variables numériques avec trop de modalité | |
target : nom de la variable cible (Y) | |
Exemple d'appel : | |
features_ordering = features_ordering( df = df_xy | |
, feature_names = ['gender','csp','region'] | |
, target = 'target' ) | |
""" | |
#déclaration de la liste qui contiendra les résultats | |
l_stats = [] | |
#boucle sur chaque variable | |
for i,col in enumerate(feature_names): | |
if col != target: | |
#calcul du tableau de contingence | |
cont = pd.crosstab(df[col], df[target],) | |
#calcul des stats du chi-2 pour ce tableau de contingence | |
chi2, proba, vc, ts = get_chi2_vc_ts(cont) | |
#ajouts des indicateurs | |
l_stats.append( (target,col, round(vc, 4), round(ts, 4), round(chi2, 4), round(proba, 4) )) | |
#compilation des stats démandées dans un dataframe et tri selon le critère de T de Schupprow | |
df_vc_ts = pd.DataFrame(data = l_stats, columns = [ 'target','variable', 'VC', 'TS', 'CHI2', 'p_value']) | |
df_vc_ts = df_vc_ts.sort_values(by=['TS','variable'], ascending=[False, True]) | |
return df_vc_ts | |
def get_chi2_vc_ts(crosstab): | |
""" | |
Le calcul du VC et TS doit être revu !! mais bon pour le moment on va s'en servir quand même :-) | |
FONCTION DE CALCUL DU CHI2, T DE TSCHUPROW ET V DE CRAMER | |
return 4 values : chi2, proba of chi2, V of Cramer, T of Tschuprow | |
parameters : | |
crosstab = crosstab dataframe | |
""" | |
nb_obs = crosstab.values.sum() | |
k1 = crosstab.shape[0] | |
k2 = crosstab.shape[1] | |
if min(crosstab.shape) >1: | |
khi2, proba, _,_ = scipy_stats.chi2_contingency(crosstab) | |
vc = np.sqrt(khi2 / (nb_obs*(min(crosstab.shape)-1))) | |
ts = np.sqrt(khi2 / (nb_obs*np.sqrt((k1-1)*(k2-1)))) | |
else: | |
khi2, proba, vc, ts = -1, 10000, -1, -1 | |
return khi2, proba, vc, ts | |
def corr_features(df: pd.DataFrame, threshold: float) : | |
""" | |
A function to suggest features that are highly correlated (one among two). | |
Arguments: | |
df - dataframe with features | |
treshold - lower limit to consider that features are not highly correlated | |
Returns: | |
List of features to drop | |
""" | |
correlation = df.corr().abs() | |
upper = correlation .where(np.triu(np.ones(correlation .shape), k=1).astype(np.bool)) | |
to_drop = [column for column in upper.columns if any(upper[column] > threshold)] | |
return to_drop | |
def confusio_matrix(y_test, y_predicted): | |
cm = confusion_matrix(y_test, y_predicted) | |
tn, fp, fn, tp = confusion_matrix(y_test, y_predicted).astype(int).ravel() | |
print("Accuracy:", round((tp + tn)/(tp+tn+fp+fn),2)) | |
print("Recall:", round(tp /(tp+fn),2)) | |
print("precision:", round( tp/(tp+fp),2)) | |
plt.figure(figsize=(5,5)) | |
plt.clf() | |
plt.imshow(cm, interpolation='nearest',cmap=plt.cm.Wistia) | |
classNames = ['Negative','Positive'] | |
plt.title('Matrice de confusion') | |
plt.ylabel('True label') | |
plt.xlabel('Predicted label') | |
tick_marks = np.arange(len(classNames)) | |
plt.xticks(tick_marks, classNames, rotation=45) | |
plt.yticks(tick_marks, classNames) | |
s = [['TN','FP'], ['FN', 'TP']] | |
for i in range(2): | |
for j in range(2): | |
plt.text(j,i, str(s[i][j])+" = "+str(cm[i][j])) | |
plt.show() | |
def correlation_list_to_matrix(df_corr, criterion): | |
""" | |
Transformation de la liste des corrélation en matrice des corrélations | |
afin de l'intégrer dans un heatmap ou pour l'exporter dans un rapport | |
La fonction renvoie un dataframe. | |
Si un couple de variables n'est pas trouvé => c'est une erreur | |
la valeur écrite dans la matrice sera 1000 (pour alerter l'utilisateur) | |
------------------- | |
Paramètres : | |
df_corr : dataframe issu de la fonction features_correlation | |
il doit avoir les deux variables 'variable_1' et 'variable_2' | |
+ la variable indiquée dans le paramètre criterion (par ex 'TS', 'VC') | |
criterion = une colonne numérique de la matrice df_corr | |
C'est la valeur de ce critère qui sera affiché dans la matrice | |
------------------- | |
Exemples d'appels : | |
df_corr = utils_ml.features_correlation(df_train, feature_names = ['foreign worker' | |
, 'Personal status and sex', 'Other debtors]) | |
df_corr_matrix = utils_ml.correlation_list_to_matrix(df_corr, criterion = 'TS') | |
""" | |
# Récupération de la liste des variables | |
columns = sorted(df_corr['variable_1'].unique()) | |
#Création de la matrice vide | |
corr_matrix = np.zeros(shape=(len(columns),len(columns))) | |
#Remplissage de la matrice | |
for i,c1 in enumerate(columns): | |
for j,c2 in enumerate(columns): | |
if c1 == c2: | |
corr_matrix[i,j] = 1 | |
else: | |
df_corr_temp = df_corr[ (df_corr['variable_1'] == c1) & (df_corr['variable_2'] == c2) \ | |
| (df_corr['variable_2'] == c1) & (df_corr['variable_1'] == c2) ] | |
if len(df_corr_temp)>0: | |
df_corr_temp = df_corr_temp.reset_index() | |
corr_matrix[i,j] = df_corr_temp.loc[0,criterion] | |
else: | |
corr_matrix[i,j] = 1000 | |
df_corr_matrix = pd.DataFrame(corr_matrix, columns=columns, index=columns) | |
return df_corr_matrix | |
def summary_table(train_Xy, cols_list , target): | |
""" | |
La fonction renvoie un df de synthèse des variables explicatives en fonction d'une target numérique (binaire ok) | |
On calcule pour chaque modalité de chaque variable : sa fréquence et fréquence relative et sa moyenne | |
en + 2 variables : | |
- 'nb_values' : nb de modalités de la variables | |
- 'nb_rows' : nb de lignes dans la table (=> identique sur toutes les lignes) | |
remarque : les valeurs manquantes ne sont pas considérées par défaut | |
=> si besoin utilisez df=df.fillna('') avant cette fonction | |
parametres obligatoires: | |
train_Xy : nom du df contenant les variables explicatives et la variable cible | |
cols_list : liste des variables explicatives (plutôt discrètes ou continues avec peu de moda) | |
car la table créé en sortie contiendra une ligne par valeur | |
EVITEZ la variable d'identifiant de ligne :-) | |
target : variable cible qui doit être numérique (binaire ok également) | |
""" | |
nb_rows = len(train_Xy) | |
all_synth = pd.DataFrame() | |
for c in cols_list: | |
if c != target: | |
print(c) | |
synth = train_Xy[c].value_counts().reset_index().rename(columns={'index':c, c:'freq'}) | |
mean = train_Xy.groupby([c])[target].mean().reset_index() | |
synth = pd.merge(synth,mean, on=c) | |
synth.columns = ['value', 'freq','pct_target'] | |
synth['variable'] = c | |
synth['nb_values']= len(synth) | |
synth['pct_freq'] = synth['freq'] / nb_rows | |
synth['nb_rows'] = nb_rows | |
synth['target'] = target | |
#les 3 colonnes suivantes serviront pour le recodage effectué dans excel | |
synth['code_recode']='' | |
synth['code_recode2']='' | |
synth['code_recode3']='' | |
all_synth= pd.concat([all_synth, synth]) | |
all_synth = all_synth.sort_values(by=['variable', 'pct_target'], ascending=[True, False]) | |
all_synth = all_synth[ ['nb_values', 'variable', 'value', 'freq', 'pct_freq', 'pct_target' | |
, 'nb_rows','target', 'code_recode', 'code_recode2', 'code_recode3' ]] | |
return all_synth | |
def stamp(message='', log_file='', refresh=0): | |
""" | |
Petite fonction de log | |
Elle affiche un message dans l'output (prefixé par un horodatage) | |
Optionnellement, elle écrit dans un fichier de log si le paramètre log_file est différent de '' | |
Parametres | |
---------- | |
message : str | |
message à afficher (et à écrire dans le fichier de log s'il y en a un d'indiqué) | |
log_file : str | |
chemin complet du fichier de log | |
refresh : int, default : 0 | |
Indiquez 1 pour vider le fichier de log avant d'écrire le message | |
""" | |
log_line = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ": " + str(message) | |
print(log_line) | |
if log_file != '': | |
if refresh!=1: | |
with open(log_file, "a") as myfile: | |
myfile.write(log_line+ '\n') | |
else: | |
with open(log_file, "w") as myfile: | |
myfile.write(log_line+ '\n') |