1. Import Libraries and Dataset¶

1.1 Package imports¶

In [1]:
import pandas as pd 
import numpy as np                    
import seaborn as sns                 
import matplotlib.pyplot as plt       
%matplotlib inline 
import warnings
from scipy.stats import chi2_contingency
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.tree import DecisionTreeClassifier,export_graphviz
from sklearn.metrics import confusion_matrix,accuracy_score
from IPython.display import Image 
from pydot import graph_from_dot_data
from six import StringIO  
from sklearn.tree import export_graphviz
from imblearn.over_sampling import SMOTE
from imblearn.metrics import specificity_score
from collections import Counter
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, accuracy_score, precision_score, recall_score, f1_score
from sklearn.linear_model import LogisticRegression 

1.2 Read data from data set¶

In [2]:
pd.set_option('future.no_silent_downcasting', True)
data = pd.read_csv("./diabetes_risk_prediction_dataset.csv")

1.3 Confirm the imports¶

In [3]:
data.shape
Out[3]:
(520, 17)

1.4 Identify the features¶

In [4]:
data.columns
Out[4]:
Index(['Age', 'Gender', 'Polyuria', 'Polydipsia', 'sudden weight loss',
       'weakness', 'Polyphagia', 'Genital thrush', 'visual blurring',
       'Itching', 'Irritability', 'delayed healing', 'partial paresis',
       'muscle stiffness', 'Alopecia', 'Obesity', 'class'],
      dtype='object')

2. Data visualization and exploration¶

2.1 Sanity check¶

In [5]:
data.head(2)
Out[5]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Itching Irritability delayed healing partial paresis muscle stiffness Alopecia Obesity class
0 40 Male No Yes No Yes No No No Yes No Yes No Yes Yes Yes Positive
1 58 Male No No No Yes No No Yes No No No Yes No Yes No Positive

2.2 Check for unique data values¶

In [6]:
# Get the count of unique values in each column
unique_counts = data.nunique()

# Get the actual unique values for each column
unique_values = data.apply(lambda x: x.unique())

# Print the results
print("Unique value counts per feature:")
print(unique_counts)

print("\nUnique values per feature:")
for column, values in unique_values.items():
    print(f"{column}: {values}")
Unique value counts per feature:
Age                   51
Gender                 2
Polyuria               2
Polydipsia             2
sudden weight loss     2
weakness               2
Polyphagia             2
Genital thrush         2
visual blurring        2
Itching                2
Irritability           2
delayed healing        2
partial paresis        2
muscle stiffness       2
Alopecia               2
Obesity                2
class                  2
dtype: int64

Unique values per feature:
Age: [40 58 41 45 60 55 57 66 67 70 44 38 35 61 54 43 62 39 48 32 42 52 53 37
 49 63 30 50 46 36 51 59 65 25 47 28 68 56 31 85 90 72 69 79 34 16 33 64
 27 29 26]
Gender: ['Male' 'Female']
Polyuria: ['No' 'Yes']
Polydipsia: ['Yes' 'No']
sudden weight loss: ['No' 'Yes']
weakness: ['Yes' 'No']
Polyphagia: ['No' 'Yes']
Genital thrush: ['No' 'Yes']
visual blurring: ['No' 'Yes']
Itching: ['Yes' 'No']
Irritability: ['No' 'Yes']
delayed healing: ['Yes' 'No']
partial paresis: ['No' 'Yes']
muscle stiffness: ['Yes' 'No']
Alopecia: ['Yes' 'No']
Obesity: ['Yes' 'No']
class: ['Positive' 'Negative']

2.3 Data visualization to get insights about data¶

2.3.1 Histogram for numeric feature (Age) to visualize its distribution¶

In [7]:
plt.figure() 
sns.displot(data['Age']); 
<Figure size 640x480 with 0 Axes>
No description has been provided for this image

Observation: - Age variable is normally distributed

2.3.2 Bar cart for categorical feature (gender) to visualize frequency of counts of gender.¶

In [8]:
data['Gender'].value_counts().plot.barh(figsize=(10,5),  title= 'Gender Frequency')
Out[8]:
<Axes: title={'center': 'Gender Frequency'}, ylabel='Gender'>
No description has been provided for this image

2.3.3 Class Distribution of target varaible.¶

In [9]:
class_counts = data['class'].value_counts()
# Plot the class distribution
plt.figure(figsize=(8, 6))
class_counts.plot(kind='bar')
plt.title('Class Distribution of Target Variable', fontsize=16)
plt.xlabel('Class')
plt.ylabel('Count')
plt.xticks(rotation=0)
plt.show()
No description has been provided for this image

Observation: - 38% of records belong to the minority class(Negative) i.e dataset imbalance is negligible.

2.4 Corelational Analysis¶

In [10]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 520 entries, 0 to 519
Data columns (total 17 columns):
 #   Column              Non-Null Count  Dtype 
---  ------              --------------  ----- 
 0   Age                 520 non-null    int64 
 1   Gender              520 non-null    object
 2   Polyuria            520 non-null    object
 3   Polydipsia          520 non-null    object
 4   sudden weight loss  520 non-null    object
 5   weakness            520 non-null    object
 6   Polyphagia          520 non-null    object
 7   Genital thrush      520 non-null    object
 8   visual blurring     520 non-null    object
 9   Itching             520 non-null    object
 10  Irritability        520 non-null    object
 11  delayed healing     520 non-null    object
 12  partial paresis     520 non-null    object
 13  muscle stiffness    520 non-null    object
 14  Alopecia            520 non-null    object
 15  Obesity             520 non-null    object
 16  class               520 non-null    object
dtypes: int64(1), object(16)
memory usage: 69.2+ KB

2.4.1 Data Conversion¶

Transformed Binary attributes mentioned below from dtype object to int64 i.e 0/1. Symmetric Binary: Gender({Male: 1, Female:0}), Asymmetric Binary: Symptoms({Yes:1, No:0})

Target variable encoding: Class({Positive:1, Negative:0})

For decision tree and most other classification models, processing numerical data is more efficient than processing categorical data like Yes/No or Positive/Negative.

In [11]:
for feature in data.select_dtypes("object").columns.to_list():
    col = pd.Categorical(data[feature])
    data[feature] = col.codes
data
Out[11]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Itching Irritability delayed healing partial paresis muscle stiffness Alopecia Obesity class
0 40 1 0 1 0 1 0 0 0 1 0 1 0 1 1 1 1
1 58 1 0 0 0 1 0 0 1 0 0 0 1 0 1 0 1
2 41 1 1 0 0 1 1 0 0 1 0 1 0 1 1 0 1
3 45 1 0 0 1 1 1 1 0 1 0 1 0 0 0 0 1
4 60 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
515 39 0 1 1 1 0 1 0 0 1 0 1 1 0 0 0 1
516 48 0 1 1 1 1 1 0 0 1 1 1 1 0 0 0 1
517 58 0 1 1 1 1 1 0 1 0 0 0 1 1 0 1 1
518 32 0 0 0 0 1 0 0 1 1 0 1 0 0 1 0 0
519 42 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

520 rows × 17 columns

2.4.2 Corealtional Analysis¶

In [12]:
h_labels = [x for x in 
            list(data.select_dtypes(include=['number', 'bool']).columns.values)]

fig, ax = plt.subplots(figsize=(20,12))
_ = sns.heatmap(data.corr(), annot=True, xticklabels=h_labels, yticklabels=h_labels, cmap=sns.cubehelix_palette(as_cmap=True), ax=ax)
No description has been provided for this image
In [13]:
correlations = data.corr()['class'].sort_values(ascending=False)

# Print the correlations
print("Correlation of each feature with the target column:")
print(correlations)
Correlation of each feature with the target column:
class                 1.000000
Polyuria              0.665922
Polydipsia            0.648734
sudden weight loss    0.436568
partial paresis       0.432288
Polyphagia            0.342504
Irritability          0.299467
visual blurring       0.251300
weakness              0.243275
muscle stiffness      0.122474
Genital thrush        0.110288
Age                   0.108679
Obesity               0.072173
delayed healing       0.046980
Itching              -0.013384
Alopecia             -0.267512
Gender               -0.449233
Name: class, dtype: float64

Corelational analysis will effect feature selection, more a feature is positively or negatively co-related to the target variable, it is more helpful in predicting the target variable. The variables should be co-related to the target variable and uncorellated among themselves, so if there is high co-relation between any 2 variables, the model only needs one as second will not add any additional value. For given field i.e. healthcare, requiring high confidence, a threshold of absolute value greater than 0 will be considered, rest will be eliminated to reduce curse of dimentionality.¶

We will dismiss Obesity, Delayed healing, Itching as their absolute value is less than 0.0.¶

Features that are weakly orleast related to target class can lead to:

  • Increase model complexity as dimentionality of data remains more.
  • Reduces model performance.
  • The model might work well in training data but might work poorly on test data because of overfitting caused by the unrelated features.
In [14]:
drop_list = ['Obesity','delayed healing','Itching']
data = data.drop(drop_list, axis='columns')

3. Data Prprocessing and Cleaning¶

3.1 Identify missing or null values.¶

In [15]:
data.isnull().sum()
Out[15]:
Age                   0
Gender                0
Polyuria              0
Polydipsia            0
sudden weight loss    0
weakness              0
Polyphagia            0
Genital thrush        0
visual blurring       0
Irritability          0
partial paresis       0
muscle stiffness      0
Alopecia              0
class                 0
dtype: int64
In [16]:
data.isna().sum()
Out[16]:
Age                   0
Gender                0
Polyuria              0
Polydipsia            0
sudden weight loss    0
weakness              0
Polyphagia            0
Genital thrush        0
visual blurring       0
Irritability          0
partial paresis       0
muscle stiffness      0
Alopecia              0
class                 0
dtype: int64
In [17]:
data
Out[17]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Irritability partial paresis muscle stiffness Alopecia class
0 40 1 0 1 0 1 0 0 0 0 0 1 1 1
1 58 1 0 0 0 1 0 0 1 0 1 0 1 1
2 41 1 1 0 0 1 1 0 0 0 0 1 1 1
3 45 1 0 0 1 1 1 1 0 0 0 0 0 1
4 60 1 1 1 1 1 1 0 1 1 1 1 1 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
515 39 0 1 1 1 0 1 0 0 0 1 0 0 1
516 48 0 1 1 1 1 1 0 0 1 1 0 0 1
517 58 0 1 1 1 1 1 0 1 0 1 1 0 1
518 32 0 0 0 0 1 0 0 1 0 0 0 1 0
519 42 1 0 0 0 0 0 0 0 0 0 0 0 0

520 rows × 14 columns

There are no missing or null values in the dataset, therefore, there is no need to apply an additional step to handle missing values.

3.2 Identify duplicates, and remove the duplicate values.¶

Duplicate records do not provide new informationt to the model and model fails to generlize patterns which fails when unseen data is presented. By removing duplicates, model can focus on unique and meaningfull patterns.

In [18]:
duplicates = data.duplicated
data[duplicates]
Out[18]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Irritability partial paresis muscle stiffness Alopecia class
47 60 0 1 1 1 1 1 0 1 0 1 1 0 1
69 50 0 1 1 1 1 1 0 1 0 1 0 0 1
84 35 0 1 1 1 1 1 0 1 0 1 1 0 1
92 40 0 1 1 1 1 0 0 1 0 1 1 0 1
159 38 0 1 1 1 1 1 0 1 1 1 1 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
496 53 1 0 0 0 1 0 0 1 0 0 1 1 0
497 47 1 0 0 0 0 0 0 0 1 1 0 0 0
498 68 0 1 1 0 1 1 0 1 0 1 0 0 1
499 64 1 0 0 0 1 1 0 1 1 0 1 1 0
504 38 1 0 0 0 0 0 0 0 0 0 0 0 0

275 rows × 14 columns

In [19]:
data.drop_duplicates(inplace=True)
data
Out[19]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Irritability partial paresis muscle stiffness Alopecia class
0 40 1 0 1 0 1 0 0 0 0 0 1 1 1
1 58 1 0 0 0 1 0 0 1 0 1 0 1 1
2 41 1 1 0 0 1 1 0 0 0 0 1 1 1
3 45 1 0 0 1 1 1 1 0 0 0 0 0 1
4 60 1 1 1 1 1 1 0 1 1 1 1 1 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
515 39 0 1 1 1 0 1 0 0 0 1 0 0 1
516 48 0 1 1 1 1 1 0 0 1 1 0 0 1
517 58 0 1 1 1 1 1 0 1 0 1 1 0 1
518 32 0 0 0 0 1 0 0 1 0 0 0 1 0
519 42 1 0 0 0 0 0 0 0 0 0 0 0 0

245 rows × 14 columns

3.3 Identify outliers for Age and handle it.¶

In [20]:
def find_outliers(data,column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
       
    return outliers[column]
# Identify outliers for each column
outliers = find_outliers(data, 'Age')
print(outliers)

plt.figure(figsize=(10, 5 ))
plt.boxplot(data['Age'], vert=False)
plt.title('Box Plot of Age')
plt.xlabel('Age')

plt.tight_layout()
plt.show()
102    90
Name: Age, dtype: int64
No description has been provided for this image
In [21]:
def impute_outliers_with_median(data, column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    
    # Calculate median
    median = data[column].median()
    
    # Replace outliers with median
    data[column] = data[column].apply(lambda x: median if (x < lower_bound or x > upper_bound) else x)

# Apply to each column
impute_outliers_with_median(data, "Age")

plt.figure(figsize=(10, 5 ))
plt.boxplot(data['Age'], vert=False)
plt.title('Box Plot of Age')
plt.xlabel('Age')

plt.tight_layout()
plt.show()
No description has been provided for this image

Steps performed for data pre-processing

  • Identified missing and null values, no further action taken as there are no missing or null values
  • Identified the duplicates objects and data set and removed them.
  • Plotted histogram for Age to check its distribution and check if it is skewed.
  • Identified the outlier in Age using using Box plot, calculating IQR.
  • Replaced the outlier with median.

Median is used to replace the outlier in a continous valued attribute as it is NOT highly influenced by values at end of the distribution, whereas mean is highly influenced by outliers, so it is ideal to use Median over mean.

4. Exploratory Data Analysis¶

4.1 Univariate Analysis¶

4.1.1 Explore Target variable¶

Having a count of number of instances available in dataset for each class will give insight about balancing of the training dataset.

In [22]:
data['class'].value_counts()
Out[22]:
class
1    170
0     75
Name: count, dtype: int64
In [23]:
(75 / (170+75))*100
Out[23]:
30.612244897959183

30.61% of instances belong to the minority class (Non-diabetic), data is moderately imbalanced.

  • This will be handled using SMOTE Algorithm while model building.
In [24]:
data['class'].value_counts().plot.barh()
Out[24]:
<Axes: ylabel='class'>
No description has been provided for this image

4.1.2 Explore gender (Post duplicates removal)¶

In [25]:
data['Gender'].value_counts().plot.barh(figsize=(10,5),  title= 'Gender Frequency')
Out[25]:
<Axes: title={'center': 'Gender Frequency'}, ylabel='Gender'>
No description has been provided for this image

4.2 Bivariate Analysis¶

4.2.1 Comparing gender with target class.¶

In [26]:
gender_class = pd.crosstab(data['Gender'],data['class'])
gender_class.div(gender_class.sum(1).astype(float), axis=0).plot(kind="bar", stacked=True, figsize=(4,4))
Out[26]:
<Axes: xlabel='Gender'>
No description has been provided for this image

Observation: - More number of females are diabetic compared to male.

4.2.2 Comparing Age with target class.¶

Some Feature engineering.

Feature Creation: Binning Age in young(15-35), midle age(36-60) and elderly(60+). Discretization.

In context of Decision tree model, a continous attribute like Age leads to more splits. By binning it, splits are reduced and tree structure is simplified, which is easier to interpret.

In [27]:
bins = [15, 35, 60, 100]  # Age ranges: 15-35, 36-60, 61-100
labels = ['Young', 'Middle-aged', 'Elderly']
data['age_group'] = pd.cut(data['Age'], bins=bins, labels=labels, right=False)
data
Out[27]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Irritability partial paresis muscle stiffness Alopecia class age_group
0 40.0 1 0 1 0 1 0 0 0 0 0 1 1 1 Middle-aged
1 58.0 1 0 0 0 1 0 0 1 0 1 0 1 1 Middle-aged
2 41.0 1 1 0 0 1 1 0 0 0 0 1 1 1 Middle-aged
3 45.0 1 0 0 1 1 1 1 0 0 0 0 0 1 Middle-aged
4 60.0 1 1 1 1 1 1 0 1 1 1 1 1 1 Elderly
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
515 39.0 0 1 1 1 0 1 0 0 0 1 0 0 1 Middle-aged
516 48.0 0 1 1 1 1 1 0 0 1 1 0 0 1 Middle-aged
517 58.0 0 1 1 1 1 1 0 1 0 1 1 0 1 Middle-aged
518 32.0 0 0 0 0 1 0 0 1 0 0 0 1 0 Young
519 42.0 1 0 0 0 0 0 0 0 0 0 0 0 0 Middle-aged

245 rows × 15 columns

In [28]:
ageGroup_class = pd.crosstab(data['age_group'],data['class'])
ageGroup_class.div(ageGroup_class.sum(1).astype(float), axis=0).plot(kind="bar", stacked=True, figsize=(4,4))
Out[28]:
<Axes: xlabel='age_group'>
No description has been provided for this image

Observation: - Almost 50% of youth are diabetic. - Majorly middle aged people are diabetic.

4.2.3 Compare every occuring symptom with target.¶

In [29]:
target_column = 'class'  # Replace with the actual target column name
symptoms = [col for col in data.columns if (col != target_column and col!= 'Age' and col != 'Gender' and col != 'age_group')]  # Binary symptom columns

# Perform bivariate analysis for each symptom
for symptom in symptoms:
    # Calculate the proportion of target classes for each value of the symptom
    symptom_analysis = pd.crosstab(data[symptom], data[target_column], normalize='index')
    
    # Plot the data
    symptom_analysis.plot(
        kind='bar',
        stacked=True,
        figsize=(6, 4),
    )

    plt.title(f"Bivariate Analysis: {symptom} vs {target_column}")
    plt.xlabel(f"{symptom} (0=No, 1=Yes)")
    plt.ylabel("Proportion")
    plt.legend(title=target_column, loc='upper right')
    plt.tight_layout()
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Observation: - People having Polyuria or Polydipsia are majorly diabetic.

4.3 Feature Weight¶

Calculating feature weight using Chi-square test for Gender.

In [30]:
# Create a contingency table
contingency_table = pd.crosstab(data['Gender'], data['class'])

# Perform Chi-Square Test
chi2, p, dof, expected = chi2_contingency(contingency_table)

# Print results
print("Contingency Table:")
print(contingency_table)
print("\nChi-Square Test Results:")
print(f"Chi-Square Statistic: {chi2:.2f}")
print(f"Degrees of Freedom: {dof}")
print(f"P-Value: {p:.4f}")

# Interpret the results
if p < 0.05:
    print("\nThere is a significant relationship between gender and the target variable.")
else:
    print("\nThere is no significant relationship between gender and the target variable.")
Contingency Table:
class    0   1
Gender        
0       11  77
1       64  93

Chi-Square Test Results:
Chi-Square Statistic: 19.90
Degrees of Freedom: 1
P-Value: 0.0000

There is a significant relationship between gender and the target variable.

5. Model Building¶

5.1 Some feature Engineering¶

Binarzation of the newly created feature (age_group)

In [31]:
data.info()
<class 'pandas.core.frame.DataFrame'>
Index: 245 entries, 0 to 519
Data columns (total 15 columns):
 #   Column              Non-Null Count  Dtype   
---  ------              --------------  -----   
 0   Age                 245 non-null    float64 
 1   Gender              245 non-null    int8    
 2   Polyuria            245 non-null    int8    
 3   Polydipsia          245 non-null    int8    
 4   sudden weight loss  245 non-null    int8    
 5   weakness            245 non-null    int8    
 6   Polyphagia          245 non-null    int8    
 7   Genital thrush      245 non-null    int8    
 8   visual blurring     245 non-null    int8    
 9   Irritability        245 non-null    int8    
 10  partial paresis     245 non-null    int8    
 11  muscle stiffness    245 non-null    int8    
 12  Alopecia            245 non-null    int8    
 13  class               245 non-null    int8    
 14  age_group           245 non-null    category
dtypes: category(1), float64(1), int8(13)
memory usage: 7.3 KB
In [32]:
col = pd.Categorical(data['age_group'])
data['age_group'] = col.codes
In [33]:
data
Out[33]:
Age Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Irritability partial paresis muscle stiffness Alopecia class age_group
0 40.0 1 0 1 0 1 0 0 0 0 0 1 1 1 1
1 58.0 1 0 0 0 1 0 0 1 0 1 0 1 1 1
2 41.0 1 1 0 0 1 1 0 0 0 0 1 1 1 1
3 45.0 1 0 0 1 1 1 1 0 0 0 0 0 1 1
4 60.0 1 1 1 1 1 1 0 1 1 1 1 1 1 2
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
515 39.0 0 1 1 1 0 1 0 0 0 1 0 0 1 1
516 48.0 0 1 1 1 1 1 0 0 1 1 0 0 1 1
517 58.0 0 1 1 1 1 1 0 1 0 1 1 0 1 1
518 32.0 0 0 0 0 1 0 0 1 0 0 0 1 0 0
519 42.0 1 0 0 0 0 0 0 0 0 0 0 0 0 1

245 rows × 15 columns

5.2 Splitting features and class¶

In [34]:
X = data.drop(labels = ['class', 'Age'],axis="columns") 
X
Out[34]:
Gender Polyuria Polydipsia sudden weight loss weakness Polyphagia Genital thrush visual blurring Irritability partial paresis muscle stiffness Alopecia age_group
0 1 0 1 0 1 0 0 0 0 0 1 1 1
1 1 0 0 0 1 0 0 1 0 1 0 1 1
2 1 1 0 0 1 1 0 0 0 0 1 1 1
3 1 0 0 1 1 1 1 0 0 0 0 0 1
4 1 1 1 1 1 1 0 1 1 1 1 1 2
... ... ... ... ... ... ... ... ... ... ... ... ... ...
515 0 1 1 1 0 1 0 0 0 1 0 0 1
516 0 1 1 1 1 1 0 0 1 1 0 0 1
517 0 1 1 1 1 1 0 1 0 1 1 0 1
518 0 0 0 0 1 0 0 1 0 0 0 1 0
519 1 0 0 0 0 0 0 0 0 0 0 0 1

245 rows × 13 columns

In [35]:
y = data['class']
In [36]:
X.columns
Out[36]:
Index(['Gender', 'Polyuria', 'Polydipsia', 'sudden weight loss', 'weakness',
       'Polyphagia', 'Genital thrush', 'visual blurring', 'Irritability',
       'partial paresis', 'muscle stiffness', 'Alopecia', 'age_group'],
      dtype='object')

5.3 Splitting dataset into train and test set¶

In [37]:
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size =0.2, random_state=28)
In [38]:
X_train_7030, X_test_7030, y_train_7030, y_test_7030 = train_test_split(X,y, test_size =0.3, random_state=28)

Smaller amount of train data (70%) may cause underfitting of the model.

Applying SMOTE Algorithm to handle imbalanced data.¶

In [39]:
print('Before (80-20)', Counter(y_train))
print('Before (70-30)', Counter(y_train_7030))

# oversampling the train dataset using SMOTE
smt = SMOTE()
X_train_sm, y_train_sm = smt.fit_resample(X_train, y_train)
X_train_7030_sm, y_train_7030_sm = smt.fit_resample(X_train_7030, y_train_7030) 

print('After (80-20)', Counter(y_train_sm))
print('After (70-30)', Counter(y_train_7030_sm))
Before (80-20) Counter({1: 139, 0: 57})
Before (70-30) Counter({1: 122, 0: 49})
After (80-20) Counter({1: 139, 0: 139})
After (70-30) Counter({1: 122, 0: 122})
In [40]:
X_train = X_train_sm
y_train = y_train_sm
X_train_7030 = X_train_7030_sm
y_train_7030 = y_train_7030_sm
In [41]:
dt = DecisionTreeClassifier(random_state = 28)
dt.fit(X_train, y_train)
Out[41]:
DecisionTreeClassifier(random_state=28)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier(random_state=28)
In [42]:
dt_7030 = DecisionTreeClassifier(random_state=30)
dt_7030.fit(X_train_7030, y_train_7030)
Out[42]:
DecisionTreeClassifier(random_state=30)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier(random_state=30)
In [43]:
feature_names = X.columns
feature_names
Out[43]:
Index(['Gender', 'Polyuria', 'Polydipsia', 'sudden weight loss', 'weakness',
       'Polyphagia', 'Genital thrush', 'visual blurring', 'Irritability',
       'partial paresis', 'muscle stiffness', 'Alopecia', 'age_group'],
      dtype='object')
In [44]:
target_names = ['Yes','NO']
target_names
Out[44]:
['Yes', 'NO']
In [45]:
y_pred = dt.predict(X_test)
y_pred_7030 = dt_7030.predict(X_test_7030)

5.4 Visualizing feature importance for the trained model (80-20 split).¶

In [46]:
importances=pd.Series(dt.feature_importances_, index=X.columns).sort_values()
importances.plot(kind='barh', figsize=(12,8))
Out[46]:
<Axes: >
No description has been provided for this image

Polyuria and Polydipsia being important features as observed in bivariate analysis.¶

In [47]:
actuals = np.array(y_test)
predictions = np.array(y_pred)
confusion_matrix(actuals, predictions)
Out[47]:
array([[15,  3],
       [ 3, 28]])
In [48]:
actuals_7030 = np.array(y_test_7030)
predictions_7030 = np.array(y_pred_7030)
confusion_matrix(actuals_7030, predictions_7030)
Out[48]:
array([[20,  6],
       [ 3, 45]])
In [49]:
print("Accuracy for Decision Tree with 70-30 Split: ", accuracy_score(y_test_7030, y_pred_7030)*100)
print("Accuracy for Decision Tree with 80-20 Split: ", accuracy_score(y_test, y_pred)*100)
Accuracy for Decision Tree with 70-30 Split:  87.83783783783784
Accuracy for Decision Tree with 80-20 Split:  87.75510204081633

5.5 Cross Validation for hyperparameter tuning.¶

Validating Hyperparameters for 80-20 split data¶

In [50]:
param_grid = {
    'max_depth': [3, 5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'criterion': ['gini', 'entropy']
}
grid_search = GridSearchCV(estimator=dt, param_grid=param_grid, cv=5, scoring='accuracy', verbose=1)
grid_search.fit(X_train, y_train)

# Retrieve and evaluate the best model
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test)

print("Best Parameters:", grid_search.best_params_)
print("Accuracy with Tuned Model:", accuracy_score(y_test, y_pred_best)*100)
Fitting 5 folds for each of 72 candidates, totalling 360 fits
Best Parameters: {'criterion': 'entropy', 'max_depth': 10, 'min_samples_leaf': 2, 'min_samples_split': 2}
Accuracy with Tuned Model: 87.75510204081633

Validating Hyperparameters for 70-30 split data¶

In [51]:
param_grid = {
    'max_depth': [3, 5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'criterion': ['gini', 'entropy']
}
grid_search = GridSearchCV(estimator=dt_7030, param_grid=param_grid, cv=5, scoring='accuracy', verbose=1)
grid_search.fit(X_train_7030, y_train_7030)

# Retrieve and evaluate the best model
best_model = grid_search.best_estimator_
y_pred_7030_best = best_model.predict(X_test_7030)

print("Best Parameters:", grid_search.best_params_)
print("Accuracy with Tuned Model:", accuracy_score(y_test_7030, y_pred_7030_best)*100)
Fitting 5 folds for each of 72 candidates, totalling 360 fits
Best Parameters: {'criterion': 'gini', 'max_depth': 5, 'min_samples_leaf': 2, 'min_samples_split': 2}
Accuracy with Tuned Model: 90.54054054054053

Understanding Overfitting of the model.¶

When the model with 80-20 split is fit with 10 folds instead of 5 folds, the accuracy of the model changes.

In [52]:
param_grid = {
    'max_depth': [3, 5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'criterion': ['gini', 'entropy']
}
grid_search = GridSearchCV(estimator=dt, param_grid=param_grid, cv=10, scoring='accuracy', verbose=1)
grid_search.fit(X_train, y_train)

# Retrieve and evaluate the best model
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test)

print("Best Parameters:", grid_search.best_params_)
print("Accuracy with Tuned Model:", accuracy_score(y_test, y_pred_best)*100)
Fitting 10 folds for each of 72 candidates, totalling 720 fits
Best Parameters: {'criterion': 'entropy', 'max_depth': 10, 'min_samples_leaf': 1, 'min_samples_split': 2}
Accuracy with Tuned Model: 87.75510204081633

6. Performance Evaluation¶

6.1 Performance Metrics¶

In [53]:
CM = confusion_matrix(actuals, predictions)
CM_7030 = confusion_matrix(actuals_7030, predictions_7030)


disp = ConfusionMatrixDisplay(confusion_matrix=CM,display_labels=['Positive','Negative'])
disp_7030 = ConfusionMatrixDisplay(confusion_matrix=CM_7030, display_labels=['Positive','Negative'])
disp.plot()
disp_7030.plot()
plt.show()
No description has been provided for this image
No description has been provided for this image

We will consider model with 80-20 dataset split for comparsion with Logistic Regression Mode. Evaluation metrics used are

  • Accuracy
  • Precision
  • Recall
  • F1 Score
  • Specificity

6.2 Building Logistic Regression model.¶

In [54]:
model_LR = LogisticRegression(random_state=28) 
model_LR.fit(X_train, y_train)
Out[54]:
LogisticRegression(random_state=28)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
LogisticRegression(random_state=28)
In [55]:
model_LR.classes_
Out[55]:
array([0, 1], dtype=int8)
In [56]:
model_LR.coef_
Out[56]:
array([[-1.46825469,  2.25303346,  2.63408544,  1.30897724,  0.2754068 ,
         0.70164682,  1.60815608,  0.17973608,  0.95024488,  1.26590417,
        -0.41331926, -0.7221683 , -0.1584415 ]])
In [57]:
model_LR.intercept_
Out[57]:
array([-1.20548766])
In [58]:
predicted_LR = model_LR.predict(X_test)
predicted_LR
Out[58]:
array([1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1,
       1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1,
       1, 1, 1, 0, 0], dtype=int8)
In [59]:
actual_LR = y_test
In [60]:
accuracy_LR = accuracy_score(actual_LR,predicted_LR)
precision_LR = precision_score(actual_LR, predicted_LR)
recall_LR = recall_score(actual_LR, predicted_LR)
specificity_LR = specificity_score(actual_LR, predicted_LR)
f1_LR = f1_score(actual_LR, predicted_LR)


accuracy = accuracy_score(actuals,predictions)
precision = precision_score(actuals, predictions)
recall = recall_score(actuals, predictions)
specificity = specificity_score(actuals, predictions)
f1 = f1_score(actuals, predictions)

print("Accuracy of Logistic Regression model is:", accuracy_LR*100)
print("Precision of Logistic Regression model is:", precision_LR*100 )
print("Recall of Logistic Regression model is:", recall_LR*100)
print("Specificity of Logistic Regression model is:", specificity_LR*100)
print("F1 Score of Logistic Regression model is:", f1_LR*100)
print("\n_________________________________________________\n")
print("Accuracy of Decision Tree model is:", accuracy*100)
print("Precision of Decision Tree model is:", precision*100)
print("Recall of Decision Tree model is:", recall*100)
print("Specificity of Decision Tree model is:", specificity*100)
print("F1 Score of Decision Tree model is:", f1*100)
Accuracy of Logistic Regression model is: 85.71428571428571
Precision of Logistic Regression model is: 87.5
Recall of Logistic Regression model is: 90.32258064516128
Specificity of Logistic Regression model is: 77.77777777777779
F1 Score of Logistic Regression model is: 88.88888888888889

_________________________________________________

Accuracy of Decision Tree model is: 87.75510204081633
Precision of Decision Tree model is: 90.32258064516128
Recall of Decision Tree model is: 90.32258064516128
Specificity of Decision Tree model is: 83.33333333333334
F1 Score of Decision Tree model is: 90.32258064516128

Conclusion¶

From the given metrics above, it is clear that Decision Tree performs better, the accuracy and precision of Decision tree model is significantly higher than Logistic Regression. In medical diagnosis of diabetes specificity should be as high as possible to reduce domain specific cost of errors. Spcificity of Decision tree model is higher than that of Logistic Regression model.

Since the data set was balanced using SMOTE algorigthm, Accuracy is a reliable metrics which for Decision tree model is higher Logistic Regression mode.