import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
np.seterr(divide='ignore')

dataset = pd.read_csv('/kaggle/input/students-performance-in-exams/StudentsPerformance.csv')
dataset.head()
print("possible value of 'parental level of education'")
dataset['parental level of education'].unique()
"""
## Preprocessing
"""
from sklearn.preprocessing import MinMaxScaler

dataset['gender'] = dataset['gender'].apply(lambda x: [0, 1][x == 'male'])
dataset['parental level of education'] = dataset['parental level of education'].apply(
    lambda x: [0, 1][x in ['high school', 'some high school', 'some college']]
)
dataset['lunch'] = dataset['lunch'].apply(lambda x: [0, 1][x == 'standard'])
dataset['test preparation course'] = dataset['test preparation course'].apply(lambda x: [0, 1][x == 'none'])

# for key in ['group A', 'group B', 'group C', 'group D', 'group E']:
#     dataset[key] = dataset['race/ethnicity'].apply(lambda x: [0, 1][x == key])
dataset = dataset.drop(['race/ethnicity'], axis=1)

dataset['passed math'] = dataset['math score'].apply(lambda x: [0, 1][x >= 60])
dataset = dataset.drop(['math score'], axis=1)

scaler = MinMaxScaler()
scaler.fit(dataset[['reading score']])
dataset['reading score'] = scaler.transform(dataset[['reading score']])

scaler = MinMaxScaler()
scaler.fit(dataset[['writing score']])
dataset['writing score'] = scaler.transform(dataset[['writing score']])
"""
## Predict student will pass or fail on Math
"""
y = dataset['passed math']
X = dataset.drop(['passed math'], axis=1)

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=10)
X_train
from sklearn.neighbors import KDTree
from sklearn.metrics import accuracy_score, precision_recall_fscore_support

class KNNClassifier():

    def __init__(
        self,
        n_neighbors=5,
        weights='uniform',
        algorithm='brute',
        leaf_size=30,
    ):
        if int(n_neighbors) <= 0:
            raise Exception('n_neighbors should be a positive integer')
        if weights not in ['uniform', 'distance'] and not callable(weights):
            raise Exception("weights should be 'uniform' or 'distance' or a callable")
        if algorithm not in ['brute', 'kd_tree']:
            raise Exception("algorithm should be either 'brute' or 'kd_tree'")
        if int(leaf_size) <= 0:
            raise Exception('leaf_size should be a positive integer')
        self.K = int(n_neighbors)
        self.weights = weights
        self.algorithm = algorithm
        self.leaf_size = leaf_size

    def fit(self, X, y):
        self.X = np.array(X)
        self.y = np.array(y)
        # build a k-d tree
        if self.algorithm == 'kd_tree':
            self.kd_tree = KDTree(self.X, leaf_size=self.leaf_size)

    def compute_k_nearest(self, x_predict):
        if self.algorithm == 'brute':
            # Euclidean metric
            dists = np.array([np.linalg.norm(x_predict - x) for x in self.X])
            k_nearest_indices = np.argsort(dists)[:self.K]
            k_nearest_dists = dists[k_nearest_indices]
            return k_nearest_dists, k_nearest_indices
        else:
            k_nearest_dists, k_nearest_indices = self.kd_tree.query(np.array([x_predict]), self.K)
            return k_nearest_dists[0], k_nearest_indices[0]

    def compute_weights(self, dists):
        if self.weights == 'uniform':
            return np.ones(dists.shape[0])
        elif self.weights == 'distance':
            return np.reciprocal(dists.astype(np.float32))
        else:
            return self.weights(dists)

    def predict(self, X):
        X_predict = np.array(X)
        y_predict = np.empty(X_predict.shape[0])
        for i in range(X_predict.shape[0]):
            k_nearest_dists, k_nearest_indices = self.compute_k_nearest(X_predict[i])
            y_k_nearest = self.y[k_nearest_indices]
            weights = self.compute_weights(k_nearest_dists)
            y_predict[i] = np.argmax(np.bincount(y_k_nearest, weights=weights))
        return y_predict


knn = KNNClassifier(n_neighbors=5, weights='distance', algorithm='kd_tree')
knn.fit(X_train, y_train)
y_predict = knn.predict(X_test)
accuracy = accuracy_score(y_test, y_predict)
precision, recall, fscore, support = precision_recall_fscore_support(y_test, y_predict, average='weighted')
print('Accuracy:', accuracy)
print('Precision:', precision)
print('Recall:', recall)
print('F score:', fscore)
# print('Support:', support)
"""
使用 Sklean 驗算
"""
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neighbors import KDTree
from sklearn.metrics import accuracy_score, precision_recall_fscore_support

knn = KNeighborsClassifier(n_neighbors=5, weights='distance', algorithm='kd_tree')
knn.fit(X_train, y_train)
y_predict = knn.predict(X_test)
accuracy = accuracy_score(y_test, y_predict)
precision, recall, fscore, support = precision_recall_fscore_support(y_test, y_predict, average='weighted')
print('Accuracy:', accuracy)
print('Precision:', precision)
print('Recall:', recall)
print('F score:', fscore)
# print('Support:', support)

import matplotlib.pyplot as plt

x = []
uniform_y = []
distance_y = []
for k in range(1, 50):
    knn = KNNClassifier(n_neighbors=k, weights='uniform', algorithm='kd_tree')
    knn.fit(X_train, y_train)
    y_predict = knn.predict(X_test)
    accuracy = accuracy_score(y_test, y_predict)
    x.append(k)
    uniform_y.append(accuracy)
    knn = KNNClassifier(n_neighbors=k, weights='distance', algorithm='kd_tree')
    knn.fit(X_train, y_train)
    y_predict = knn.predict(X_test)
    accuracy = accuracy_score(y_test, y_predict)
    distance_y.append(accuracy)

fig, axes = plt.subplots(1, 2, figsize=(16, 5), sharey=True)
axes[0].set_xlabel('k')
axes[1].set_xlabel('k')
axes[0].set_ylabel('accuracy')
axes[0].plot(x, uniform_y)
axes[0].axvline(x=5, color='red')
axes[1].plot(x, distance_y)
axes[1].axvline(x=5, color='red')
fig.show()