Beslutsträdsklassificering i Python

Jag kommer att implementera algoritmer för klassificering med hjälp av beslutsträd i denna handledning. Jag kommer att träna ett enkelt beslutsträd och två beslutsträdsensembler (RandomForest och XGBoost), dessa modeller kommer att jämföras med hjälp av 10-faldig korsvalidering. Jag använder Titanic-uppsättningen från kaggle, denna datauppsättning kommer att förbehandlas och visualiseras innan den används för träning.

Beslutsträdsalgoritmer var bland de första lösningarna för beslutstöd (expertsystem). Ett beslutsträd är konstruerat som ett antal om-då-regler som bygger ett hierarkiskt träd som ser mer ut som en pyramid. Ett beslutsträd skapas med rekursiv binär delning från rotnoden och ner till de slutliga förutsägelserna. Vi vill ha de viktigaste egenskaperna högst upp i trädet eftersom det innebär att vi snabbare kan nå ett tillfredsställande resultat.

Beslutsträd är lätta att förstå och förklara, de kan användas för binära klassificeringsproblem och för flerklassproblem. Beslutsträd kan vara partiska om datauppsättningen inte är balanserad och de kan vara instabila eftersom olika träd kan genereras på grund av små variationer i indata.

Beslutsträdensemblers kombinerar flera beslutsträd för att förbättra förmågan att göra förutsägelser. Metoder för beslutsträdensemblers kan implementera hopslagning eller förstärkning. Hopslagning innebär att flera träd skapas på undergrupper av indata, resultatet från en sådan modell är den genomsnittliga förutsägelsen för alla träd. Förstärkning är en teknik där träd skapas i sekventiell följd, ett träd kommer att försöka minimera förlusten/felet från det föregående trädet. Random Forest är ett exempel på en ensemble-metod som använder hopslagning och XGBoost är ett exempel på en ensemble-metod som använder förstärkning.

Datauppsättning och bibliotek

Jag kommer att använda Titanic-uppsättningen (ladda ner) från kaggle.com, du måste registrera dig för att kunna ladda ner datauppsättningen. Datauppsättningen består av en träningsuppsättning och en testuppsättning, testuppsättningen används om du vill göra en inlämning. Datauppsättningen innehåller data om passagerare på Titanic och ett booleskt målvärde som indikerar om passageraren överlevde eller inte. Jag använder följande bibliotek: pandas, joblib, numpy, matplotlib, csv, xgboost, graphviz och scikit-learning.

Förberedelse av data

Du kan öppna filen train.csv i Excel, OpenOffice Calc eller undersöka den på kaggle. Vissa kolumner i datauppsättningen innehåller många unika värden såsom PassengerId, Name, Age, Ticket, Fare och Cabin. Kolumner med många unika värden kan tas bort eller rekonstrueras. Jag bestämde mig för att ta bort PassengerId, Name och Ticket, Cabin är rekonstruerad för att ange om passageraren har en hytt eller inte. Du kanske kan förbättra noggrannheten genom att rekonstruera Age och Fare. Vissa av kolumnerna innehåller nullvärden (NaN) och strängar måste konverteras till siffror. Följande metod i en modul som heter common (common.py) används för att förbereda datauppsättningen.

# Preprocess data
def preprocess_data(ds):

    # Get passenger ids (should not be part of the dataset)
    ids = ds['PassengerId']
    
    # Set cabin to a boolean value (no, yes)
    cabins = ds['Cabin'].copy()
    for i in range(len(cabins)):
        if type(cabins.loc[i]) == float:
            cabins.loc[i] = 0
        else:
            cabins.loc[i] = 1

    # Update the cabin column in the data set
    ds['Cabin'] = cabins

    # Remove null (NaN) values from the data set
    median_fare = ds['Fare'].median()
    mean_age = ds['Age'].mean()
    ds['Fare'] = ds['Fare'].fillna(median_fare)
    ds['Age'] = ds['Age'].fillna(mean_age)
    ds['Embarked'] = ds['Embarked'].fillna('S')

    # Map string values to numbers (to be able to train and test models)
    ds['Sex'] = ds['Sex'].map({'female': 0, 'male': 1})
    ds['Embarked'] = ds['Embarked'].map({'Q': 0, 'C': 1, 'S': 2})

    # Drop columns
    ds = ds.drop(columns=['PassengerId', 'Name', 'Ticket'])

    # Return ids and data set
    return ids, ds

Visualisera datauppsättningen

Följande modul används för att visualisera datauppsättningen. Resultatet från visualiseringsprocessen visas under koden.

# Import libraries
import pandas
import joblib
import math
import numpy as np
import matplotlib.pyplot as plt

import annytab.decision_trees.common as common

# Visualize data set
def visualize_dataset(ds):
    
    # Print first 10 rows in data set
    print('--- First 10 rows ---\n')
    #pandas.set_option('display.max_columns', 12)
    print(ds[0:10])

    # Print the shape
    print('\n--- Shape of data set ---\n')
    print(ds.shape)

    # Print class distribution
    print('\n--- Class distribution ---\n')
    print(ds.groupby('Survived').size())

    # Group data set
    survivors = ds[ds.Survived == True]
    non_survivors = ds[ds.Survived == False]

    # Create a figure
    figure = plt.figure(figsize = (12, 8))
    figure.suptitle('Surviviors and Non-surviviors on Titanic', fontsize=16)

    # Create a default grid
    plt.rc('axes', facecolor='#ececec', edgecolor='none', axisbelow=True, grid=True)
    plt.rc('grid', color='w', linestyle='solid')

    # Add spacing between subplots
    plt.subplots_adjust(top = 0.9, bottom=0.1, hspace=0.3, wspace=0.4)
    
    # Plot by Pclass (1)
    plt.subplot(2, 4, 1) # 2 rows and 4 columns
    survivors_data = survivors.groupby('Pclass').size().values
    non_survivors_data = non_survivors.groupby('Pclass').size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.xticks([0,1,2], [1, 2, 3])
    plt.ylabel('Count')
    plt.title('Pclass')
    plt.legend(loc='upper left')

    # Plot by Gender (2)
    plt.subplot(2, 4, 2) # 2 rows and 4 columns
    survivors_data = survivors.groupby('Sex').size().values
    non_survivors_data = non_survivors.groupby('Sex').size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.xticks([0,1], ['Female', 'Male'])
    plt.ylabel('Count')
    plt.title('Gender')
    plt.legend(loc='upper left')
    
    # Plot by Age (3)
    plt.subplot(2, 4, 3) # 2 rows and 4 columns
    survivors_data = survivors.groupby(['AgeGroup']).size().values
    non_survivors_data = non_survivors.groupby(['AgeGroup']).size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.xticks([0,1,2,3,4,5,6,7], ['0-9', '10-19', '20-29', '30-39', '40-49', '50-59', '60-69', '70-79'], rotation=40, horizontalalignment='right')
    plt.ylabel('Count')
    plt.title('Age')
    plt.legend(loc='upper left')
    
    # Plot by SibSp (4)
    plt.subplot(2, 4, 4) # 2 rows and 4 columns
    survivors_data = np.append(survivors.groupby('SibSp').size().values, np.array([0,0])) # Make sure that arrays have same length
    non_survivors_data = non_survivors.groupby('SibSp').size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.ylabel('Count')
    plt.title('Number of siblings/spouses')
    plt.legend(loc='upper left')

    # Plot by Parch (5)
    plt.subplot(2, 4, 5) # 2 rows and 4 columns
    survivors_data = np.append(survivors.groupby('Parch').size().values, np.array([0,0])) # Make sure that arrays have same length
    non_survivors_data = non_survivors.groupby('Parch').size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.ylabel('Count')
    plt.title('Number of parents/children')
    plt.legend(loc='upper left')

    # Plot by Fare (6)
    plt.subplot(2, 4, 6) # 2 rows and 4 columns
    survivors_data = survivors.groupby(['FareGroup']).size().values
    non_survivors_data = non_survivors.groupby(['FareGroup']).size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.xticks([0,1,2,3,4,5], ['0-99', '100-199', '200-299', '300-399', '400-499', '500-599'], rotation=40, horizontalalignment='right')
    plt.ylabel('Count')
    plt.title('Fare')
    plt.legend(loc='upper left')

    # Plot by Cabin (7)
    plt.subplot(2, 4, 7) # 2 rows and 4 columns
    survivors_data = survivors.groupby('Cabin').size().values
    non_survivors_data = non_survivors.groupby('Cabin').size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.xticks([0,1], ['No', 'Yes'])
    plt.ylabel('Count')
    plt.title('Cabin')
    plt.legend(loc='upper left')

    # Plot by Embarked (8)
    plt.subplot(2, 4, 8) # 2 rows and 4 columns
    survivors_data = survivors.groupby('Embarked').size().values
    non_survivors_data = non_survivors.groupby('Embarked').size().values
    plt.bar(range(len(survivors_data)), survivors_data, label='Survivors', alpha=0.5, color='g')
    plt.bar(range(len(non_survivors_data)), non_survivors_data, bottom=survivors_data, label='Non-Survivors', alpha=0.5, color='r')
    plt.xticks([0,1,2], ['Q', 'C', 'S'])
    plt.ylabel('Count')
    plt.title('Embarked')
    plt.legend(loc='upper left')
    
    # Show or save the figure
    #plt.show()
    plt.savefig('C:\\DATA\\Python-data\\titanic\\plots\\bar-charts.png')

# The main entry point for this module
def main():

    # Load data set (includes header values)
    ds = pandas.read_csv('C:\\DATA\\Python-data\\titanic\\train.csv')

    # Preprocess data
    ids, ds = common.preprocess_data(ds)

    # Create age groups
    ds['AgeGroup'] = pandas.cut(ds.Age, range(0, 81, 10), right=False, labels=['0-9', '10-19', '20-29', '30-39', '40-49', '50-59', '60-69', '70-79'])

    # Create fare groups
    ds['FareGroup'] = pandas.cut(ds.Fare, range(0, 601, 100), right=False, labels=['0-99', '100-199', '200-299', '300-399', '400-499', '500-599'])
    
    # Visualize data set
    visualize_dataset(ds)

# Tell python to run main method
if __name__ == "__main__": main()
--- First 10 rows ---
   Survived  Pclass  Sex        Age  ...  Cabin  Embarked  AgeGroup  FareGroup
0         0       3    1  22.000000  ...      0         2     20-29       0-99
1         1       1    0  38.000000  ...      1         1     30-39       0-99
2         1       3    0  26.000000  ...      0         2     20-29       0-99
3         1       1    0  35.000000  ...      1         2     30-39       0-99
4         0       3    1  35.000000  ...      0         2     30-39       0-99
5         0       3    1  29.699118  ...      0         0     20-29       0-99
6         0       1    1  54.000000  ...      1         2     50-59       0-99
7         0       3    1   2.000000  ...      0         2       0-9       0-99
8         1       3    0  27.000000  ...      0         2     20-29       0-99
9         1       2    0  14.000000  ...      0         1     10-19       0-99

[10 rows x 11 columns]

--- Shape of data set ---
(891, 11)

--- Class distribution ---
Survived
0    549
1    342
dtype: int64
Titanic diagram

Baslinjeprestanda

Datauppsättningen är inte perfekt balanserad eftersom det finns 549 icke-överlevande och 342 överlevande, en möjlig åtgärd för att få bättre resultat är att skapa en bättre balans i datauppsättningen. Sannolikheten för att göra en korrekt förutsägelse av en icke-överlevande är 66,67 % (549/891) och vår modell måste prestera bättre än detta.

Python-modul

Följande modul används för träning, utvärdering och inlämning. Jag använder trädmodeller, alla dessa modeller har många hyperparametrar som kan justeras. Alla projektfiler lagras i annytab/decision_trees och namnområdet för vår gemensamma modul är därför annytab.decision_trees.

# Import libraries
import pandas
import joblib
import csv
import numpy as np
import sklearn.model_selection
import sklearn.tree
import sklearn.ensemble
import sklearn.metrics
import xgboost
import graphviz
import matplotlib.pyplot as plt

import annytab.decision_trees.common as common

# Train and evaluate
def train_and_evaluate():
    
    # Load train data set (includes header values)
    ds = pandas.read_csv('C:\\DATA\\Python-data\\titanic\\train.csv')

    # Preprocess data
    ids, ds = common.preprocess_data(ds)

    # Slice data set in values and target (2D-array)
    X = ds.values[:,1:9] # Data
    Y = ds.values[:,0] # Survived

    # Create models
    models = []
    models.append(('DecisionTree', sklearn.tree.DecisionTreeClassifier(criterion='gini', splitter='best', max_depth=None, min_samples_split=5, min_samples_leaf=1, 
                                                                       min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, 
                                                                       min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort=False)))
    models.append(('RandomForest', sklearn.ensemble.RandomForestClassifier(n_estimators=100, criterion='gini', max_depth=None, min_samples_split=5, 
                                                                           min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features='auto', 
                                                                           max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, 
                                                                           bootstrap=True, oob_score=False, n_jobs=None, random_state=None, verbose=0, 
                                                                           warm_start=False, class_weight=None)))
    models.append(('XGBoost', xgboost.XGBClassifier(booster='gbtree', max_depth=6, min_child_weight=1, learning_rate=0.1, n_estimators=500, verbosity=0, objective='binary:logistic', 
                                                    gamma=0, max_delta_step=0, subsample=1, colsample_bytree=1, colsample_bylevel=1, reg_alpha=0, reg_lambda=0, 
                                                    scale_pos_weight=1, seed=0, missing=None)))
    
    # Loop models
    for name, model in models:

        # Train the model on the whole data set
        model.fit(X, Y)

        # Save the model (Make sure that the folder exists)
        joblib.dump(model, 'C:\\DATA\\Python-data\\titanic\\models\\' + name + '.jbl')

        # Evaluate on training data
        print('\n--- ' + name + ' ---')
        print('\nTraining data')
        predictions = model.predict(X)
        accuracy = sklearn.metrics.accuracy_score(Y, predictions)
        print('Accuracy: {0:.2f}'.format(accuracy * 100.0))
        print('Classification Report:')
        print(sklearn.metrics.classification_report(Y, predictions))
        print('Confusion Matrix:')
        print(sklearn.metrics.confusion_matrix(Y, predictions))
        
        # Evaluate with 10-fold CV
        print('\n10-fold CV')
        predictions = sklearn.model_selection.cross_val_predict(model, X, Y, cv=10)
        accuracy = sklearn.metrics.accuracy_score(Y, predictions)
        print('Accuracy: {0:.2f}'.format(accuracy * 100.0))
        print('Classification Report:')
        print(sklearn.metrics.classification_report(Y, predictions))
        print('Confusion Matrix:')
        print(sklearn.metrics.confusion_matrix(Y, predictions))

# Predict and submit
def predict_and_submit():

    # Load test data set (includes header values)
    ds = pandas.read_csv('C:\\DATA\\Python-data\\titanic\\test.csv')

    # Preprocess data
    ids, ds = common.preprocess_data(ds)

    # Slice data set in values (2D-array), test set does not have target values
    X = ds.values[:,0:8] # Data

    # Load the best models
    model = joblib.load('C:\\DATA\\Python-data\\titanic\\models\\RandomForest.jbl')

    # Make predictions
    predictions = model.predict(X)

    # Save predictions to a csv file
    file = open('C:\\DATA\\Python-data\\titanic\\submission.csv', 'w', newline='')
    writer = csv.writer(file, delimiter=',')
    writer.writerow(('PassengerId', 'Survived'))
    for i in range(len(predictions)):
        writer.writerow((ids[i], predictions[i].astype(int)))
    file.close()

    # Print success
    print('Successfully created submission.csv!')

# Plot models
def plot_models():

    # Load models
    decision_tree_model = joblib.load('C:\\DATA\\Python-data\\titanic\\models\\DecisionTree.jbl')
    random_forest_model = joblib.load('C:\\DATA\\Python-data\\titanic\\models\\RandomForest.jbl')
    xgboost_model = joblib.load('C:\\DATA\\Python-data\\titanic\\models\\XGBoost.jbl')

    # Names
    feature_names = ['Pclass', 'Gender', 'Age', 'SibSp', 'Parch', 'Fare', 'Cabin', 'Embarked']
    class_names = ['Died', 'Survived']

    # Save decision tree model to an image
    source = graphviz.Source(sklearn.tree.export_graphviz(decision_tree_model, out_file=None, feature_names=feature_names, class_names=class_names, filled=True))
    source.render('C:\\DATA\\Python-data\\titanic\\plots\\decision-tree',format='png', view=False)

    # Save random forest model to an image
    source = graphviz.Source(sklearn.tree.export_graphviz(random_forest_model.estimators_[8], out_file=None, filled=True))
    source.render('C:\\DATA\\Python-data\\titanic\\plots\\random-forest',format='png', view=False)

    # Save xgboost model to an image
    xgboost_model.get_booster().feature_names = feature_names
    xgboost.plot_tree(xgboost_model, num_trees=0)
    figure = plt.gcf()
    figure.set_size_inches(100, 50)
    plt.savefig('C:\\DATA\\Python-data\\titanic\\plots\\xgboost.png')

# The main entry point for this module
def main():

    # Train and evaluate
    #train_and_evaluate()

    # Predict and submit
    #predict_and_submit()

    # Plot models
    plot_models()

# Tell python to run main method
if __name__ == "__main__": main()

Träning och utvärdering

En loop används för att träna och utvärdera modeller, varje modell sparas till en fil. Resultatet från tränings- och utvärderingsprocessen visas nedan.

--- DecisionTree ---

Training data
Accuracy: 94.84
Classification Report:
              precision    recall  f1-score   support

         0.0       0.94      0.98      0.96       549
         1.0       0.96      0.90      0.93       342

    accuracy                           0.95       891
   macro avg       0.95      0.94      0.94       891
weighted avg       0.95      0.95      0.95       891

Confusion Matrix:
[[536  13]
 [ 33 309]]

10-fold CV
Accuracy: 78.90
Classification Report:
              precision    recall  f1-score   support

         0.0       0.82      0.85      0.83       549
         1.0       0.74      0.70      0.72       342

    accuracy                           0.79       891
   macro avg       0.78      0.77      0.77       891
weighted avg       0.79      0.79      0.79       891

Confusion Matrix:
[[464  85]
 [103 239]]

--- RandomForest ---

Training data
Accuracy: 94.84
Classification Report:
              precision    recall  f1-score   support

         0.0       0.94      0.98      0.96       549
         1.0       0.97      0.90      0.93       342

    accuracy                           0.95       891
   macro avg       0.95      0.94      0.94       891
weighted avg       0.95      0.95      0.95       891

Confusion Matrix:
[[538  11]
 [ 35 307]]

10-fold CV
Accuracy: 82.27
Classification Report:
              precision    recall  f1-score   support

         0.0       0.84      0.88      0.86       549
         1.0       0.79      0.73      0.76       342

    accuracy                           0.82       891
   macro avg       0.82      0.81      0.81       891
weighted avg       0.82      0.82      0.82       891

Confusion Matrix:
[[483  66]
 [ 92 250]]

--- XGBoost ---

Training data
Accuracy: 98.20
Classification Report:
              precision    recall  f1-score   support

         0.0       0.98      0.99      0.99       549
         1.0       0.99      0.97      0.98       342

    accuracy                           0.98       891
   macro avg       0.98      0.98      0.98       891
weighted avg       0.98      0.98      0.98       891

Confusion Matrix:
[[544   5]
 [ 11 331]]

10-fold CV
Accuracy: 81.37
Classification Report:
              precision    recall  f1-score   support

         0.0       0.84      0.87      0.85       549
         1.0       0.77      0.73      0.75       342

    accuracy                           0.81       891
   macro avg       0.80      0.80      0.80       891
weighted avg       0.81      0.81      0.81       891

Confusion Matrix:
[[475  74]
 [ 92 250]]

Inlämning

Jag skapade en inlämningsfil genom att använda XGBoost-modellen och laddade upp filen till kaggle. Mitt resultat var 0.73250, inte mycket bättre än baslinjeprestandan.

Rita upp träd

Du måste packa upp eller installera Graphviz för att rita upp modeller i Python. Du måste också lägga till en sökväg till bin-mappen (C:\Program Files\Graphviz\bin) i inställningarna för miljövariabler. Jag läser in alla modeller och sparar diagrammen som png-filer, du kan spara dem som pdf-filer eller i andra format.

Beslutsträd
Etiketter:

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *