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)
# Handle missing values immediately after loading data:
if df.isnull().sum().any():
    # Option 1: Drop rows with missing values
    df = df.dropna()
    # Option 2: Fill missing values, e.g., with the mean of the column
    # df = df.fillna(df.mean())
    
# 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
        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)
        learning_rate = 0.01
        self.weights1 -= learning_rate * d_weights1
        self.bias1 -= learning_rate * d_bias1
        self.weights2 -= learning_rate * d_weights2
        self.bias2 -= learning_rate * d_bias2
        
        
    def train(self, epochs=1000):
        for epoch in range(epochs):
            self.feedforward()
            self.backprop()
            
        # Adding error and data integrity checks
        if df.isnull().sum().any():
            raise ValueError("Data contains missing values. Handle these before proceeding.")
        

    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=1500)
    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()

can you fix errors