import numpy as np
import seaborn as sns
import pandas as pd
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

# Load the dataset
csv_file_path = "garments_worker_productivity.csv"
df = pd.read_csv(csv_file_path)

# Inspect the distribution
print(df["actual_productivity"].describe())

# Transform ‘actual_productivity’ to a binary variable
threshold = 0.5
df["productivity_class"] = (df["actual_productivity"] >= threshold).astype(int)

# Check the transformation
print(df["productivity_class"].value_counts())

# Visualize the distribution of classes
sns.countplot(x="productivity_class", data=df)
plt.title("Distribution of Productivity Classes")
plt.show()

# Define features and target
X = df.drop(["actual_productivity", "productivity_class"], axis=1)
y = df["productivity_class"]

# Convert categorical variables into dummies
X = pd.get_dummies(X)

# Split the data into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Normalize - Fit StandardScaler on X_train and transform both X_train and X_test 
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Display normalized data
print(pd.DataFrame(X_train_scaled, columns=X_train.columns).head())

# Confirm the splitting
print(f"Training Set Size: {len(X_train)}")
print(f"Test Set Size: {len(X_test)}")

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return sigmoid(x) * (1 - sigmoid(x))

class NeuralNetwork:
    def __init__(self, x, y, hidden_neurons=20):
        # Initialize parameters
        self.input = x
        self.weights1 = np.random.rand(self.input.shape[1], hidden_neurons)
        self.bias1 = np.random.rand(1, hidden_neurons)
        self.weights2 = np.random.rand(hidden_neurons, 1)
        self.bias2 = np.random.rand(1, 1)
        self.y = y
        self.output = np.zeros(self.y.shape)

    def feedforward(self):
        # Perform feedforward operation
        self.layer1 = sigmoid(np.dot(self.input, self.weights1) + self.bias1)
        self.output = sigmoid(np.dot(self.layer1, self.weights2) + self.bias2)

    def backprop(self):
        # Perform backpropagation
        error = self.y - self.output
        
        # Calculate gradients for weights and biases based on error
        d_weights2 = np.dot(self.layer1.T, (2 * error * sigmoid_derivative(self.output)))
        d_bias2 = np.sum(2 * error * sigmoid_derivative(self.output), axis=0, keepdims=True)

        d_layer1 = np.dot(2 * error * sigmoid_derivative(self.output), self.weights2.T)
        d_weights1 = np.dot(self.input.T, (d_layer1 * sigmoid_derivative(self.layer1)))
        d_bias1 = np.sum(d_layer1 * sigmoid_derivative(self.layer1), axis=0, keepdims=True)
        
        # Update parameters
        self.weights1 += d_weights1
        self.bias1 += d_bias1
        self.weights2 += d_weights2
        self.bias2 += d_bias2

    def train(self, epochs=1000):
        # Train the neural network for a given number of epochs
        for epoch in range(epochs):
            self.feedforward()
            self.backprop()

    def predict(self, X):
        # Function to perform prediction using the neural network
        self.input = X
        self.feedforward()
        return self.output

# Function to create and train the network
def create_and_train_nn(X_train, y_train, hidden_neurons, epochs=1000):
    y_train_np = y_train.to_numpy().reshape(-1, 1)  # Convert Series to numpy array and reshape
    nn = NeuralNetwork(X_train, y_train_np, hidden_neurons=hidden_neurons)
    nn.train(epochs=epochs)
    return nn

# Function to evaluate network performance
def evaluate_nn(nn, X_test, y_test):
    y_test_np = y_test.to_numpy().reshape(-1, 1)  # Convert Series to numpy array and reshape
    predictions = (nn.predict(X_test) > 0.5).astype(int)
    accuracy = accuracy_score(y_test_np, predictions.flatten())
    confusion_mat = confusion_matrix(y_test_np, predictions.flatten())
    classification_rep = classification_report(y_test_np, predictions.flatten(), zero_division=0)
    return accuracy, confusion_mat, classification_rep

# Initialize network configurations
hidden_neurons_configs = [10, 20, 30]
results = {}

for neurons in hidden_neurons_configs:
    nn = create_and_train_nn(X_train_scaled, y_train, neurons, epochs=1000)
    accuracy, confusion_mat, class_report = evaluate_nn(nn, X_test_scaled, y_test)
    results[neurons] = {
        "Neural Network": nn,
        "Accuracy": accuracy,
        "Confusion Matrix": confusion_mat,
        "Classification Report": class_report
    }

# Print results for each configuration
for config, result in results.items():
    print(f"Results for {config} Hidden Neurons:")
    print("Accuracy:", result["Accuracy"])
    print("Confusion Matrix:\n", result["Confusion Matrix"])
    print("Classification Report:\n", result["Classification Report"])
    print()

does this code accomplish this task: D.	Performance Comparison
2.5	Train a new neural network classifier with 10 hidden neurons and evaluate its performance. Repeat the process for training and testing neural network classifier with 30 hidden neurons.
2.6	Contrast the differences and compare among the performance of three trained neural network classifiers (i.e., with 20, 10 and 30 hidden neurons). Justify your observations.