does the following code fullfill this task 2.3	Write a computer program using a high-level programming language of your choice (Python with NumPy or C/C++ or Java) to train and test the neural network using the Backpropagation algorithm from scratch WITHOUT using any machine learning library.  import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from sklearn.metrics import precision_score, recall_score
from sklearn.metrics import f1_score
from sklearn.metrics import roc_curve, auc

# Load the dataset
dataset_path =  'garments_worker_productivity.csv'
df = pd.read_csv(dataset_path)

# Define a threshold for satisfactory vs. unsatisfactory productivity
threshold_productivity = 0.75
df['productivity_class'] = (df['actual_productivity'] >= threshold_productivity).astype(int)
                            
# Visualise the distribution of actual productivity
sns.histplot(df['actual_productivity'], kde=True)
plt.title('Distribution of Actual Productivity')
plt.show()

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

# Convert date to datetime 
if 'date' in df.columns:
    df['date'] = pd.to_datetime(df['date'])
    df['year'] = df['date'].dt.year
    df['month'] = df['date'].dt.month
    df['day'] = df['date'].dt.day
    df.drop('date', axis=1, inplace=True)

for cat_column in ['quarter', 'department']:
    if cat_column in df.columns:
        df = pd.get_dummies(df, columns=[cat_column], drop_first=True)

# All other pre-processing steps here
imputer_mean = SimpleImputer(missing_values=np.nan, strategy='mean')

df['wip'] = imputer_mean.fit_transform(df[['wip']])

# Select features (excluding target variables and any non-features)
features = df.drop(['actual_productivity', 'productivity_class'], axis=1, errors='ignore')

# Normalise the features
scaler = StandardScaler()
features_normalized = scaler.fit_transform(features)

# Prepare the feature matrix (X) and target vector (y)
X = features_normalized
y = df['productivity_class'].values

# Split the data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"Training samples: {X_train.shape[0]}, Test samples: {X_test.shape[0]}")

# Activation functions
def sigmoid(x):
    # Clip x to avoid overflow in exp
    x_clipped = np.clip(x, -709, 709)  # np.exp(709) is the largest possible value
    return 1 / (1 + np.exp(-x_clipped))

def relu(x):
    return np.maximum(0, x)

def relu_derivative(x):
    return (x > 0).astype(x.dtype)

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

# Correct dimensions and initialisations
n_inputs = X_train.shape[1]  # Correct number of input features
n_hidden_units = 30  # Specified number of neurons in the hidden layer
n_outputs = 1  # Specified number of output neurons for binary classification

np.random.seed(42)  # For reproducibility

# Weights and biases initialisation remains unchanged
weights_input_hidden = np.random.normal(0, 0.1, (n_inputs, n_hidden_units))
weights_hidden_output = np.random.normal(0, 0.1, (n_hidden_units, n_outputs))

bias_hidden = np.zeros((1, n_hidden_units))
bias_output = np.zeros((1, n_outputs))

# Define the derivative of the ReLU function
def relu_derivative(x):
    return (x > 0).astype(x.dtype)


# Loss function and its derivative
def binary_cross_entropy(predicted, actual):
    return -np.mean(actual * np.log(predicted + 1e-15) + (1 - actual) * np.log(1 - predicted + 1e-15))

def binary_cross_entropy_derivative(predicted, actual):
    return (predicted - actual) / (predicted * (1 - predicted) + 1e-15)


# Update forward pass function to only return the last output
def forward_pass(X):
    hidden_layer_input = np.dot(X, weights_input_hidden) + bias_hidden
    hidden_layer_output = relu(hidden_layer_input)
    
    output_layer_input = np.dot(hidden_layer_output, weights_hidden_output) + bias_output
    predicted_output = sigmoid(output_layer_input)
    
    return predicted_output, hidden_layer_output  # keeping hidden_layer_output for backpropagation

# New - Training the Neural Network
epochs = 1000
learning_rate = 0.01

for epoch in range(epochs):
    # Forward pass
    predicted_output, hidden_layer_output = forward_pass(X_train)
    
    # Calculate the loss
    loss = binary_cross_entropy(predicted_output, y_train.reshape(-1, 1))
    
    # Start Backpropagation
    # Output layer error
    error = binary_cross_entropy_derivative(predicted_output, y_train.reshape(-1, 1))
    
    # Backpropagation through the output layer
    d_output = error * sigmoid_derivative(predicted_output)
    
    # Calculate error for hidden layer
    error_hidden = d_output.dot(weights_hidden_output.T)
    
    # Backpropagation through the hidden layer
    d_hidden_layer = error_hidden * relu_derivative(hidden_layer_output)
    
    # Update the weights and biases
    weights_hidden_output -= hidden_layer_output.T.dot(d_output) * learning_rate
    bias_output -= np.sum(d_output, axis=0, keepdims=True) * learning_rate
    
    weights_input_hidden -= X_train.T.dot(d_hidden_layer) * learning_rate
    bias_hidden -= np.sum(d_hidden_layer, axis=0, keepdims=True) * learning_rate

    # Optional: Print the loss every 100 epochs
    if epoch % 100 == 0:
        print(f"Epoch {epoch}, Loss: {loss}")


# Predict on training data to see how well the network learned
predicted_output, _ = forward_pass(X_train)
predicted_classes = (predicted_output > 0.5).astype(int)
train_accuracy = np.mean(predicted_classes.flatten() == y_train)
print(f"Training Accuracy: {train_accuracy * 100:.2f}%")

# validate the model on the X_test dataset to see its performance on unseen data
predicted_output_test, _ = forward_pass(X_test)
predicted_classes_test = (predicted_output_test > 0.5).astype(int)
test_accuracy = np.mean(predicted_classes_test.flatten() == y_test)
print(f"Test Accuracy: {test_accuracy * 100:.2f}%")

precision = precision_score(y_test, predicted_classes_test)
recall = recall_score(y_test, predicted_classes_test)
print(f"Precision: {precision * 100:.2f}%")
print(f"Recall: {recall * 100:.2f}%")

f1 = f1_score(y_test, predicted_classes_test)
print(f"F1 Score: {f1:.2f}")


fpr, tpr, thresholds = roc_curve(y_test, predicted_output_test)
roc_auc = auc(fpr, tpr)


plt.figure()
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='-')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC)')
plt.legend(loc="lower right")
plt.show()