import os
SHOGUN_DATA_DIR=os.getenv('SHOGUN_DATA_DIR', '../../../../data')
import shogun as sg
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

def load_file(feat_file,label_file):
    feats=sg.create_features(sg.read_csv(feat_file))
    labels=sg.create_labels(sg.read_csv(label_file))
    return (feats, labels)

trainfeat_file=os.path.join(SHOGUN_DATA_DIR, 'uci/letter/train_fm_letter.dat')
trainlab_file=os.path.join(SHOGUN_DATA_DIR, 'uci/letter/train_label_letter.dat')
train_feats,train_labels=load_file(trainfeat_file,trainlab_file)

"""
Explanation: Ensemble of Decision Trees
By Parijat Mazumdar (GitHub ID: mazumdarparijat)
This notebook illustrates the use of Random Forests in Shogun for classification and regression. We will understand the functioning of Random Forests, discuss about the importance of its various parameters and appreciate the usefulness of this learning method.  
What is Random Forest?
Random Forest is an ensemble learning method in which a collection of decision trees are grown during training and the combination of the outputs of all the individual trees are considered during testing or application. The strategy for combination can be varied but generally, in case of classification, the mode of the output classes is used and, in case of regression, the mean of the outputs is used. The randomness in the method, as the method's name suggests, is infused mainly by the random subspace sampling done while training individual trees. While choosing the best split during tree growing, only a small randomly chosen subset of all the features is considered. The subset size is a user-controlled parameter and is usually the square root of the total number of available features. The purpose of the random subset sampling method is to decorrelate the individual trees in the forest, thus making the overall model more generic; i.e. decrease the variance without increasing the bias (see bias-variance trade-off). The purpose of Random Forest, in summary, is to reduce the generalization error of the model as much as possible.    
Random Forest vs Decision Tree
In this section, we will appreciate the importance of training a Random Forest over a single decision tree. In the process, we will also learn how to use Shogun's Random Forest class. For this purpose, we will use the letter recognition dataset. This dataset contains pixel information (16 features) of 20000 samples of the English alphabet. This is a 26-class classification problem where the task is to predict the alphabet given the 16 pixel features. We start by loading the training dataset.
End of explanation
"""


def setup_random_forest(num_trees,rand_subset_size,combination_rule,feature_types):
    rf=sg.create_machine("RandomForest", num_bags=num_trees,
                  combination_rule=combination_rule)
    rf.get("machine").put("m_randsubset_size", rand_subset_size)
    rf.get("machine").put("nominal", feature_types)
    
    return rf

comb_rule=sg.create_combination_rule("MajorityVote")
feat_types=np.array([False]*16)
rand_forest=setup_random_forest(10,4,comb_rule,feat_types)

"""
Explanation: Next, we decide the parameters of our Random Forest.
End of explanation
"""


# train forest
rand_forest.put('labels', train_labels)
rand_forest.train(train_feats)

# load test dataset
testfeat_file= os.path.join(SHOGUN_DATA_DIR, 'uci/letter/test_fm_letter.dat')
testlab_file= os.path.join(SHOGUN_DATA_DIR, 'uci/letter/test_label_letter.dat')
test_feats,test_labels=load_file(testfeat_file,testlab_file)

# apply forest
output_rand_forest_train=rand_forest.apply_multiclass(train_feats)
output_rand_forest_test=rand_forest.apply_multiclass(test_feats)

"""
Explanation: In the above code snippet, we decided to create a forest using 10 trees in which each split in individual trees will be using a randomly chosen subset of 4 features. Note that 4 here is the square root of the total available features (16) and is hence the usually chosen value as mentioned in the introductory paragraph. The strategy for combination chosen is Majority Vote which, as the name suggests, chooses the mode of all the individual tree outputs. The given features are all continuous in nature and hence feature types are all set false (i.e. not nominal). Next, we train our Random Forest and use it to classify letters in our test dataset.
End of explanation
"""


def train_cart(train_feats,train_labels,feature_types,problem_type):
    c=sg.create_machine("CARTree", nominal=feature_types,
                 mode=problem_type,
                 folds=2,
                 apply_cv_pruning=False,
                 labels=train_labels)
    c.train(train_feats)
    
    return c

# train CART
cart=train_cart(train_feats,train_labels,feat_types,"PT_MULTICLASS")

# apply CART model
output_cart_train=cart.apply_multiclass(train_feats)
output_cart_test=cart.apply_multiclass(test_feats)

"""
Explanation: We have with us the labels predicted by our Random Forest model. Let us also get the predictions made by a single tree. For this purpose, we train a CART-flavoured decision tree.
End of explanation
"""


accuracy=sg.create_evaluation("MulticlassAccuracy")

rf_train_accuracy=accuracy.evaluate(output_rand_forest_train,train_labels)*100
rf_test_accuracy=accuracy.evaluate(output_rand_forest_test,test_labels)*100

cart_train_accuracy=accuracy.evaluate(output_cart_train,train_labels)*100
cart_test_accuracy=accuracy.evaluate(output_cart_test,test_labels)*100

print('Random Forest training accuracy : '+str(round(rf_train_accuracy,3))+'%')
print('CART training accuracy : '+str(round(cart_train_accuracy,3))+'%')
print
print('Random Forest test accuracy : '+str(round(rf_test_accuracy,3))+'%')
print('CART test accuracy : '+str(round(cart_test_accuracy,3))+'%')

"""
Explanation: With both results at our disposal, let us find out which one is better.
End of explanation
"""


def get_rf_accuracy(num_trees,rand_subset_size):
    rf=setup_random_forest(num_trees,rand_subset_size,comb_rule,feat_types)
    rf.put('labels', train_labels)
    rf.train(train_feats)
    out_test=rf.apply_multiclass(test_feats)
    acc=sg.create_evaluation("MulticlassAccuracy")
    return acc.evaluate(out_test,test_labels)

"""
Explanation: As it is clear from the results above, we see a significant improvement in the predictions. The reason for the improvement is clear when one looks at the training accuracy. The single decision tree was over-fitting on the training dataset and hence was not generic. Random Forest on the other hand appropriately trades off training accuracy for the sake of generalization of the model. Impressed already? Let us now see what happens if we increase the number of trees in our forest.
Random Forest parameters : Number of trees and random subset size
In the last section, we trained a forest of 10 trees. What happens if we make our forest with 20 trees? Let us try to answer this question in a generic way.
End of explanation
"""


num_trees4=[5,10,20,50,100]
rf_accuracy_4=[round(get_rf_accuracy(i,4)*100,3) for i in num_trees4]

print('Random Forest accuracies (as %) :' + str(rf_accuracy_4))

# plot results

x4=[1]
y4=[86.48] # accuracy for single tree-CART
x4.extend(num_trees4)
y4.extend(rf_accuracy_4)
plt.plot(x4,y4,'--bo')
plt.xlabel('Number of trees')
plt.ylabel('Multiclass Accuracy (as %)')
plt.xlim([0,110])
plt.ylim([85,100])
plt.show()

"""
Explanation: The method above takes the number of trees and subset size as inputs and returns the evaluated accuracy as output. Let us use this method to get the accuracy for different number of trees keeping the subset size constant at 4.
End of explanation
"""


# subset size 2

num_trees2=[10,20,50,100]
rf_accuracy_2=[round(get_rf_accuracy(i,2)*100,3) for i in num_trees2]

print('Random Forest accuracies (as %) :' + str(rf_accuracy_2))

# subset size 8

num_trees8=[5,10,50,100]
rf_accuracy_8=[round(get_rf_accuracy(i,8)*100,3) for i in num_trees8]

print('Random Forest accuracies (as %) :' + str(rf_accuracy_8))

"""
Explanation: NOTE : The above code snippet takes about a minute to execute. Please wait patiently.
We see from the above plot that the accuracy of the model keeps on increasing as we increase the number of trees on our Random Forest and eventually satarates at some value. Extrapolating the above plot qualitatively, the saturation value will be somewhere around 96.5%. The jump of accuracy from 86.48% for a single tree to 96.5% for a Random Forest with about 100 trees definitely highlights the importance of the Random Forest algorithm.
The inevitable question at this point is whether it is possible to achieve higher accuracy saturation by working with lesser (or greater) random feature subset size. Let us figure this out by repeating the above procedure for random subset size as 2 and 8.
End of explanation
"""


x2=[1]
y2=[86.48]
x2.extend(num_trees2)
y2.extend(rf_accuracy_2)

x8=[1]
y8=[86.48]
x8.extend(num_trees8)
y8.extend(rf_accuracy_8)

plt.plot(x2,y2,'--bo',label='Subset Size = 2')
plt.plot(x4,y4,'--r^',label='Subset Size = 4')
plt.plot(x8,y8,'--gs',label='Subset Size = 8')
plt.xlabel('Number of trees')
plt.ylabel('Multiclass Accuracy (as %) ')
plt.legend(bbox_to_anchor=(0.92,0.4))
plt.xlim([0,110])
plt.ylim([85,100])
plt.show()

"""
Explanation: NOTE : The above code snippets take about a minute each to execute. Please wait patiently.
Let us plot all the results together and then comprehend the results.
End of explanation
"""


rf=setup_random_forest(100,2,comb_rule,feat_types)
rf.put('labels', train_labels)
rf.train(train_feats)
    
# set evaluation strategy
rf.put("oob_evaluation_metric", sg.create_evaluation("MulticlassAccuracy"))
oobe=rf.get("oob_error")

print('OOB accuracy : '+str(round(oobe*100,3))+'%')

"""
Explanation: As we can see from the above plot, the subset size does not have a major impact on the saturated accuracy obtained in this particular dataset. While this is true in many datasets, this is not a generic observation. In some datasets, the random feature sample size does have a measurable impact on the test accuracy. A simple strategy to find the optimal subset size is to use cross-validation. But with Random Forest model, there is actually no need to perform cross-validation. Let us see how in the next section.  
Out-of-bag error
The individual trees in a Random Forest are trained over data vectors randomly chosen with replacement. As a result, some of the data vectors are left out of training by each of the individual trees. These vectors form the out-of-bag (OOB) vectors of the corresponding trees. A data vector can be part of OOB classes of multiple trees. While calculating OOB error, a data vector is applied to only those trees of which it is a part of OOB class and the results are combined. This combined result averaged over similar estimate for all other vectors gives the OOB error. The OOB error is an estimate of the generalization bound of the Random Forest model. Let us see how to compute this OOB estimate in Shogun.
End of explanation
"""


trainfeat_file= os.path.join(SHOGUN_DATA_DIR, 'uci/wine/fm_wine.dat')
trainlab_file= os.path.join(SHOGUN_DATA_DIR, 'uci/wine/label_wine.dat')
train_feats,train_labels=load_file(trainfeat_file,trainlab_file)

"""
Explanation: The above OOB accuracy calculated is found to be slighly less than the test error evaluated in the previous section (see plot for num_trees=100 and rand_subset_size=2). This is because of the fact that the OOB estimate depicts the expected error for any generalized set of data vectors. It is only natural that for some set of vectors, the actual accuracy is slightly greater than the OOB estimate while in some cases the accuracy observed in a bit lower.
Let us now apply the Random Forest model to the wine dataset. This dataset is different from the previous one in the sense that this dataset is small and has no separate test dataset. Hence OOB (or equivalently cross-validation) is the only viable strategy available here. Let us read the dataset first.
End of explanation
"""


def get_oob_errors_wine(num_trees,rand_subset_size):
    feat_types=np.array([False]*13)
    rf=setup_random_forest(num_trees,rand_subset_size,sg.create_combination_rule("MajorityVote"),feat_types)
    rf.put('labels', train_labels)
    rf.train(train_feats)
    rf.put("oob_evaluation_metric", sg.create_evaluation("MulticlassAccuracy"))
    return rf.get("oob_error")   

size=[1,2,4,6,8,10,13]
oobe=[round(get_oob_errors_wine(400,i)*100,3) for i in size]

print('Out-of-box Accuracies (as %) : '+str(oobe))

plt.plot(size,oobe,'--bo')
plt.xlim([0,14])
plt.xlabel('Random subset size')
plt.ylabel('Multiclass accuracy')
plt.show()

"""
Explanation: Next let us find out the appropriate feature subset size. For this we will make use of OOB error.
End of explanation
"""


size=[50,100,200,400,600]
oobe=[round(get_oob_errors_wine(i,2)*100,3) for i in size]

print('Out-of-box Accuracies (as %) : '+str(oobe))

plt.plot(size,oobe,'--bo')
plt.xlim([40,650])
plt.ylim([90,100])
plt.xlabel('Number of trees')
plt.ylabel('Multiclass accuracy')
plt.show()

"""
Explanation: From the above plot it is clear that subset size of 2 or 3 produces maximum accuracy for wine classification. At this value of subset size, the expected classification accuracy is of the model is 98.87%. Finally, as a sanity check, let us plot the accuracy vs number of trees curve to ensure that 400 is indeed a sufficient value ie. the oob error saturates before 400.
End of explanation
"""


