<jupyter_start><jupyter_text>Importing necessary libraries<jupyter_code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import random
import plotly.express as px
pd.set_option("display.max_rows", None, "display.max_columns", None)
from sklearn.model_selection import train_test_split
import imblearn #Major library - Please ensure this is installed
from mpl_toolkits.mplot3d import Axes3D 
import matplotlib.pyplot as plt

#-------------------------------------------------------------------
import statsmodels #Install if not present readily
import xgboost as xgb
from sklearn.linear_model import Lasso,LogisticRegression
from sklearn.feature_selection import SelectFromModel
from sklearn.metrics import roc_auc_score
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
import warnings
warnings.filterwarnings("ignore")


random.seed(100)<jupyter_output><empty_output><jupyter_text>Importing Dataset for classification and subsequent explainability explorationLinks - https://www.kaggle.com/saurabhshahane/classification-of-malwares<jupyter_code>dataset = pd.read_csv(r"../input/classification-of-malwares/ClaMP_Integrated-5184.csv")

#------------------------------------------------------------------------------------------------
#Summary
print('Total Shape :',dataset.shape)
dataset.head()<jupyter_output>Total Shape : (5210, 70)<jupyter_text>Checking null values in the dataset (No null found)<jupyter_code>pd.DataFrame(dataset.isnull().sum()).T<jupyter_output><empty_output><jupyter_text>Checking the total Benign vs Malware Class Ratio (approximately 1:1)<jupyter_code>dataset['class'].value_counts()<jupyter_output><empty_output><jupyter_text>Checking the non-numerical features to convert into numerical format<jupyter_code>type_df = pd.DataFrame(dataset.dtypes).reset_index()
type_df.columns=['cols','type']
type_df[type_df['type']=='object']['cols'].unique()<jupyter_output><empty_output><jupyter_text>Hence, 'packer_type' feature has to be reformatted into numerical feature<jupyter_code>print('Total unique values in "packer_type":',dataset['packer_type'].nunique())<jupyter_output>Total unique values in "packer_type": 40<jupyter_text>Since this feature consists of levels which are nominal in nature (cannot be sorted in a logical way), we will use One-Hot Encoding<jupyter_code>#Extracting the required levels only, based on value counts. 
packer_unique_df = pd.DataFrame(dataset['packer_type'].value_counts()).reset_index()
packer_unique_df.columns = ['packer_type','unique_count']
catg = packer_unique_df[packer_unique_df['unique_count']>10]['packer_type'].unique()<jupyter_output><empty_output><jupyter_text>One-Hot encoding<jupyter_code>encoded = pd.get_dummies(dataset['packer_type'])
encoded = encoded[[col for col in list(encoded.columns) if col in catg]]
print('Shape of encode :',encoded.shape)
#----------------------------------------------------------------------
#Concatenating the encoded columns
if set(catg).issubset(set(dataset.columns))==False: #Conditional automation 
    dataset = pd.concat([dataset,encoded],axis=1)
    dataset.drop(columns='packer_type',inplace=True)

dataset.shape<jupyter_output>Shape of encode : (5210, 9)<jupyter_text>Splitting the data into X and Y for modelling purpose<jupyter_code>from sklearn.preprocessing import StandardScaler

# Test Train Split for modelling purpose
X = dataset.loc[:,[cols for cols in dataset.columns if ('class' not in cols)]] #Removing time since its a level column
y = dataset.loc[:,[cols for cols in dataset.columns if 'class' in cols]]

#----------------------------------------------------------------------------------------------------
#Scaling the features
scaler = StandardScaler()
X = scaler.fit_transform(X)

#Splitting data into train-test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33,random_state=100)

#----------------------------------------------------------------------------------------------------
print('Total Shape of Train X:',X_train.shape)
print('Total Shape of Train Y:',y_train.shape)
print('Total Shape of Test X:',X_test.shape)<jupyter_output>Total Shape of Train X: (3490, 77)
Total Shape of Train Y: (3490, 1)
Total Shape of Test X: (1720, 77)<jupyter_text>Using Random Forest Classifier with default hyperparameters<jupyter_code>from sklearn.ensemble import RandomForestClassifier

rf_clf = RandomForestClassifier(random_state=100,n_jobs=-1)
rf_clf.fit(X_train,y_train)
rf_pred = rf_clf.predict(X_test)
rf_pred_proba = rf_clf.predict_proba(X_test)
#-----------------------------------------------
score = roc_auc_score(y_test, rf_pred)
print('1. ROC AUC: %.3f' % score)
print('2. Accuracy :',accuracy_score(y_test, rf_pred))
print('3. Classification Report -\n',classification_report(y_test, rf_pred))
print('4. Confusion Matrix - \n',confusion_matrix(y_test, rf_pred))
X_test_df = pd.DataFrame(X_test).reset_index(drop=True)
X_test_df.columns = [cols for cols in dataset.columns if ('class' not in cols)]<jupyter_output><empty_output><jupyter_text>Insights : 1. The predictions seem to be very accurate, but there are FN an FP cases which can be further explored2. Due to high accuracy of the classifier, we will hav to see the tendency of the model on how it predicts across some features and which features have been mostly influential in getting us such good accuracy scores3. Similar study can be performed for scenarios where the accuracy /metric i not very good and we want to check which feature is causing the classifier to not learn properly and remediate that if possible  Additional Note : 1. All the explainability techniques tried down below in no way describe actual causality between the feature and the prediction/target variable2. The patterns observed are a mirror image of what the model has learnt, rather than the actual relationship. Hence,any unclear or counter-intuitive pattern observed can be blamed on the quality of the model3. A ML model might perform very good, but there is a need to understand the prediction explainability because it should not be counter-intuitve to what a subject matter expert (SME) might interpret from raw data  Explainability of ML models:1. Since many ML models used like the one above are black-box models, i.e one cannot have an intuitive explanation of how the model arrived at that certain prediction, we use a family of techniques known as ML Explainability techniques2. These consist of the following :    - Permutation Importance    - Individual Contribution Expectation (ICE Plots)    - Partial Dependency Plots (PD Plots)    - Acumulated Local Effects (ALE Plots)    - SHApely exPlanations (SHAP analysis)   Permutation ImportanceRelevant Links: https://christophm.github.io/interpretable-ml-book/feature-importance.html<jupyter_code>import eli5
from eli5.sklearn import PermutationImportance

perm = PermutationImportance(rf_clf, scoring='accuracy',random_state=100).fit(X_test, y_test)
eli5.show_weights(perm, feature_names = [cols for cols in dataset.columns if ('class' not in cols)])<jupyter_output><empty_output><jupyter_text>Insights : We can see from above that for the estimator, "FH_char12" is the most important criteria for accuracy in the test data followed by "Subsystem". We'll explore these two features further in detail   Partial Dependency (PD) Plots & Individual Contribution Expectation (ICE) plots:1. They give a permuted representation of the effect of each value of the concerned variable on the output (prediction) of the model2. They are prone to severe bias and mis-representation if multi-collinearity is present between the features of the model3. PD plots are the averaged out representation of all ICE points for a given value of the concerned variable (Hence, ALE plots are used)3. Relevant Links: https://christophm.github.io/interpretable-ml-book/pdp.html<jupyter_code>from sklearn.inspection import plot_partial_dependence

#------------------------------------------------------------------------------------------
# specifying the fig size
fig = plt.figure(figsize=(80, 80))
plot_partial_dependence(estimator=rf_clf,X=X_test_df,features=['FH_char12','Subsystem'],n_jobs=-1)
plt.show()<jupyter_output><empty_output><jupyter_text>Insights : 1. For "FH_char12" feature, the tendency to predict higher proabability(class 1) is higher in the extreme starting points only, after it the partial dependence decreases to the next value2. For "Subsystem", similar hike in inital points is followed by a steep drop in partial dependence and then it flatlines at some average value3. The ICE plot can be plotted with the addition of an argument "kind='both'" in the above line of code (Only available in sklearn v0.24 onwards)  Accumulated Local Effects (ALE)1. Is robust to the assumptions of multi-collinearity as it permutes only in the conditional space of predictors, rather than the whole predictor space2. Relevant Links: https://christophm.github.io/interpretable-ml-book/ale.html<jupyter_code>#conda install -c conda-forge PyALE
!pip install PyALE
from PyALE import ale

#-------------------------------------------------------------------------------------------------
def ALE_plot(feature_,df,est):
  df_temp = df.copy()
  df_temp[feature_] = df_temp[feature_].astype(int)
  ale_eff = ale(X=df_temp, model=est, feature=[feature_], grid_size=25, include_CI=True, C=0.95)
  del df_temp
    
#-------------------------------------------------------------------------------------------------
ALE_plot('FH_char12',X_test_df,rf_clf)
ALE_plot('Subsystem',X_test_df,rf_clf)<jupyter_output><empty_output><jupyter_text>Insights : 1. The behavior of both the features are quite similar to what was observed in PD Plots, though we can see that the ALE plots are correctly considering these features as discrete/categorical (2 & 4 unique values are present for each of the columns respectively)  Shapely Additive exPlanations (SHAP) Analysis Relevant Links:1. https://christophm.github.io/interpretable-ml-book/shap.html2. https://github.com/slundberg/shap<jupyter_code>import shap

shap.initjs()

def SHAP_generator(data_for_prediction,train_data,est):
  #-------------------------------------------------------------------------------------------
  #Passing required X data for SHAP analysis
  data_for_prediction = data_for_prediction #Change data here

  # Create object that can calculate shap values
  explainer = shap.TreeExplainer(est,data=train_data,model_output="probability") #Used for tree based model, 
                                                                                #for other models use Kernel Explainer
                                                                                #as explained in the above links

  # Calculate Shap values
  shap_values = explainer.shap_values(data_for_prediction)

  return explainer,shap_values

#----------------------------------------------------------------------------------------------
#Calculating the SHAP values by invoking the UDF
explainer,shap_values = SHAP_generator(X_test,X_train,rf_clf) #High Runtime 
#Qcing the shape of the output array with shap values - it should match with the shape of the test dataset
print(X_test.shape)
print(shap_values[0].shape)
#shap_values[0:3]<jupyter_output>(1720, 77)
(1720, 77)<jupyter_text>Overall SHAP summary (Works like feature importance as well)<jupyter_code>shap.summary_plot(shap_values[1], X_test,feature_names=[cols for cols in dataset.columns if ('class' not in cols)])<jupyter_output><empty_output><jupyter_text>Insights :1. Features FH_char12, E_file and CheckSum are the top 3 features according to their contribution to the whole predictions2. For FH_char12, lower the valus, higher the tendency to predict 1s can be inferred out from the above chart, which is waht we saw in PD plots as well. Hence the techniques are consistent here  Individual record level SHAP view (to pin-point on FPs and FNs)  Finding the FP and FN rows in the test dataset to explore which featurs led to them being mislabelled<jupyter_code>print('#------- Indexes of FNs from the test data :\n')
print(np.where((np.array(rf_pred).ravel()==0) & (np.array(y_test).ravel()==1)),'\n')

print('#------- Indexes of FPs from the test data :\n')
print(np.where((np.array(rf_pred).ravel()==1) & (np.array(y_test).ravel()==0)),'\n')
shap.initjs()

idx = 1665

print('Probability obtained :',rf_pred_proba[idx][1])
shap.force_plot(explainer.expected_value[1], shap_values[1][idx,:], X_test[idx], feature_names=[cols for cols in dataset.columns if ('class' not in cols)])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.15602557019966853, 'fraction_numerical': 0.01136453318601531, 'mean_word_length': 3.7248322147651005, 'pattern_counts': {'":': 2, '<': 60, '<?xml version=': 0, '>': 61, 'https://': 6, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '515816', 'license': 'MIT', 'n_tokens_mistral': 3722, 'n_tokens_neox': 3534, 'n_words': 1187, 'path': 'Mini Projects/ML Explainability/model-explainability-for-classification.ipynb', 'repo_name': 'srikhetramohanty/Data-Science-Portfolio', 'size': '12671'}