# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3 (ipykernel)
#     language: python
#     name: python3
# ---

# +
import pandas as pd
import numpy as np
from sklearn import tree
from sklearn.datasets import load_iris, load_breast_cancer, load_wine
from sklearn.model_selection import cross_validate, cross_val_score, train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from statistics import mean, stdev

# Todo: remove once have pip install
import sys  
sys.path.insert(0, 'C:\python_projects\ArithmeticFeatures_project\ArithmeticFeatures')
from ArithmeticFeatures import ArithmeticFeatures # todo: fix once have pip install

np.random.seed(0)


# -

# ## Methods to load data

# +
# This provides methods to test with 3 of the toy datasets provided by sklearn. These are used below
# to test the accuracy of models using RotationFeatures.

def get_iris():
    iris = load_iris()
    X, y = iris.data, iris.target
    X = pd.DataFrame(X, columns=iris['feature_names'])
    return X, y

def get_breast_cancer():
    X, y = load_breast_cancer(return_X_y=True, as_frame=True)
    return X,y

def get_wine():
    X, y = load_wine(return_X_y=True, as_frame=True)
    return X,y


# -

# ## Examples 1 to 4: Simple examples using the ArithmeticFeatures class

# +
X,y = get_iris()
arith = ArithmeticFeatures()

# Example 1: Using fit() then transform(), and get_feature_names()
arith.fit(X)
X1 = pd.DataFrame(arith.transform(X), columns=arith.get_feature_names())
print("\nExample 1:")
display(X1.head())  

# Example 2: Using fit_transform() and get_params()
X2 = pd.DataFrame(arith.fit_transform(X), columns=arith.get_feature_names())
print("\nExample 2:")
display(X2.head())
print("Calling get_params(): ", arith.get_params())

# Example 3: Using a two-column numpy array
print("\nExample 3:")
X = np.arange(6).reshape(3, 2)
print("type of X: ", type(X))
X3 = pd.DataFrame(arith.fit_transform(X), columns=arith.get_feature_names())
display(X3.head())

# Example 4: Using a two-column python matrix
print("\nExample 4:")
X = np.arange(6).reshape(3, 2).tolist()
print("type of X: ", type(X))
X4 = pd.DataFrame(arith.fit_transform(X), columns=arith.get_feature_names())
display(X4.head())

# -

# ## Example 5: Comparing the accuracy when using the arithmetic-based generated features vs using only the original features.

# +
# Test sklearn's decision tree using either the original or the original plus the generated features. In most cases there
# is an improvement in the f1 score, as well as a reduction in the variation between folds, and smaller decision trees, and 
# hence greater interpretability.

def test_classification(X, y):
    # The depth is limited to 4 in order to maintain the interpretability of the trees induced. 
    dt = tree.DecisionTreeClassifier(max_depth=4, random_state=0)
    scores = cross_validate(dt, X, y, cv=5, scoring='f1_macro', return_train_score=True, return_estimator=True)
    test_scores = scores['test_score']
    train_scores = scores['train_score']
    score_name = "f1_score: "
    avg_test_score = test_scores.mean()
    scores_std_dev = stdev(test_scores)
    avg_train_score = train_scores.mean()

    estimators = scores['estimator']    
    total_num_nodes = 0
    for est in estimators:
        total_num_nodes += est.tree_.node_count
    avg_num_nodes = total_num_nodes / len(estimators)

    print("\n Average f1 score on training data: ", round(avg_train_score,3))
    print(" Average f1 score on test data: ", round(avg_test_score,3))
    print(" Std dev of f1 scores on test data: ", round(scores_std_dev,3))
    print(" Average number of nodes: ", round(avg_num_nodes,3))

# Given a dataframe X, return an extended dataframe, having the same set of rows, but additional, generated columns
def get_extended_X(X):
    arith = ArithmeticFeatures()
    extended_X = pd.DataFrame(arith.fit_transform(X), columns=arith.get_feature_names())
    return extended_X

# Given a method to load a dataset, load the dataset and test the accuracy of a sklearn decision tree with and without
# the extended features.
def test_dataset(load_method, file_name):
    print("\n\n*********************************************")
    print("Calling for " + file_name)
    print("*********************************************")
    
    X,y = load_method()
    print("\nUsing original features only")
    test_classification(X, y)

    print("\nUsing arithmetic-based features")
    extended_X = get_extended_X(X)
    test_classification(extended_X, y)
    
test_dataset(get_iris, "Iris")
test_dataset(get_breast_cancer, "Breast Cancer")
test_dataset(get_wine, "Wine")
# -

# ## Example 6: Using an ArithmeticFeatures in a sklearn pipeline

# +
X,y = get_iris()
pipe = Pipeline([('arith', ArithmeticFeatures()), ('dt', tree.DecisionTreeClassifier())])

# Example getting the training score
pipe.fit(X,y)
sc = pipe.score(X,y)
print('Training Accuracy: %.3f' % sc)

# Example getting the cross validated accuracy
n_scores = cross_val_score(pipe, X, y, scoring='accuracy', n_jobs=-1, error_score='raise')
print('Cross validated Accuracy: %.3f (%.3f)' % (mean(n_scores), stdev(n_scores)))

# -

# ## Example 7: Using a pipeline and grid search to optimize the hyperparameters used by ArithmeticFeatures and by the Decision Tree

# +
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

parameters = {
    'arith__support_plus': (True, False),
    'arith__support_mult': (True, False),
    'arith__support_minus': (True, False),
    'arith__support_div': (True, False),
    'arith__support_min': (True, False),
    'arith__support_max': (True, False),    
    'dt__max_depth': (3,4,5)
}

gs_clf = GridSearchCV(pipe, parameters)
gs_clf.fit(X_train, y_train)
s = gs_clf.score(X_test, y_test)
print("score: ", s)
b = gs_clf.best_params_
b


StarcoderdataJupyterScriptsDedupFiltered