it seems that the logistic regression plots some wrong thetas and loss. can you help me find error?

logistic_regression file:
import numpy as np

np.random.seed(123)

class LogisticRegression:
    def __init__(self, learning_rate=1e-2, n_steps=2000, n_features=1):
        self.learning_rate = learning_rate
        self.n_steps = n_steps
        if n_features is not None:
            self.theta = np.random.rand(n_features)

    @staticmethod
    def sigmoid(z):
        return 1 / (1 + np.exp(-z))

    def fit_full_batch(self, X, y):
        mt = X.shape[0]
        cost_history = np.zeros(self.n_steps)
        theta_history = np.zeros((self.n_steps, self.theta.shape[0]))

        for step in range(self.n_steps):
            z = np.dot(X, self.theta)
            prediction = self.sigmoid(z)
            error = prediction - y

            self.theta = self.theta - self.learning_rate / mt * np.dot(X.T, error)
            cost_history[step] = -(1 / mt) * (np.dot(y, np.log(prediction) + np.dot(1-y, np.log(1-prediction))))
            theta_history[step, :] = self.theta

        return cost_history, theta_history

    def predict(self, X, threshold=0.5):
        z = np.dot(X, self.theta)
        predictions = self.sigmoid(z)
        return predictions > threshold


    def fit_mini_batch(self, X, y, batch_size=4):
        # Full Batch Gradient Descent
        # - Computes gradient using the entire training dataset at each iteration.
        # - Updates model parameters using the average error over the entire dataset.
        # - Suitable for smaller datasets.
        # - Generally more stable convergence compared to other methods.
        # - Computational intensity may limit its use for large datasets.
        mt = len(X)
        cost_history = np.zeros(self.n_steps)
        theta_history = np.zeros((self.n_steps, self.theta.shape[0]))

        for step in range(0, self.n_steps):
            for index in range(0, mt, batch_size):
                x_batch = X[index:index+batch_size]
                y_batch = y[index:index + batch_size]
                z_batch = np.dot(x_batch, self.theta)
                predictions_batch = self.sigmoid(z_batch)
                error_batch = predictions_batch - y_batch
                self.theta = self.theta - self.learning_rate / batch_size * np.dot(x_batch.T, error_batch)

            theta_history[step, :] = self.theta.T
            z = np.dot(X, self.theta)
            predictions = self.sigmoid(z)

            # Cross-entropy loss
            cost = -1 / mt * (np.dot(y, np.log(predictions)) + np.dot(1 - y, np.log(1 - predictions)))
            cost_history[step] = cost

        return cost_history, theta_history

    def fit_sgd(self, X, y):
        # Stochastic Gradient Descent
        # - Updates model parameters using a single randomly selected training example at each iteration.
        # - Fastest to converge but may have high variance in parameter updates.
        # - Suitable for large datasets as it processes one data point at a time, saving memory.
        # - May require more iterations to converge and may exhibit oscillations during training.
        m = len(X)
        cost_history = np.zeros(self.n_steps)
        theta_history = np.zeros((self.n_steps, self.theta.shape[0]))

        for step in range(self.n_steps):
            random_index = np.random.randint(m)
            xi = X[random_index]
            yi = y[random_index]
            z = np.dot(xi, self.theta)
            prediction = self.sigmoid(z)
            error = prediction - yi
            self.theta = self.theta - self.learning_rate * xi.T.dot(error)

            theta_history[step, :] = self.theta.T
            z = np.dot(X, self.theta)
            predictions = self.sigmoid(z)
            cost = -(np.dot(y, np.log(predictions)) + np.dot(1 - y, np.log(1 - predictions)))
            cost_history[step] = cost

        return cost_history, theta_history


main file:

import numpy as np
import pandas as pd
from logistic_regression import LogisticRegression
from utilities import plot_theta_gd

df = pd.read_csv('datasets/diabetes.csv')
print(df.describe())

features_names, label_name = df.columns[:-1], df.columns[-1]

X = df[features_names].values
y = df[label_name].values

train_index = round(len(X) * 0.8)

x_train, y_train = X[:train_index], y[:train_index]
x_test, y_test = X[train_index:], y[train_index:]

mean = x_train.mean(axis=0)
std = x_train.mean(axis=0)

x_train = (x_train - mean) / std
x_test = (x_test - mean) / std

x_train = np.c_[np.ones(x_train.shape[0]), x_train]
x_test = np.c_[np.ones(x_test.shape[0]), x_test]

logistic_reg = LogisticRegression(learning_rate=0.005, n_features=x_train.shape[1], n_steps=1000)
cost_history, theta_history = logistic_reg.fit_full_batch(x_train, y_train)

plot_theta_gd(x_train, y_train, logistic_reg, cost_history, theta_history, 0, 1)

del logistic_reg, cost_history, theta_history
logistic_reg = LogisticRegression(learning_rate=0.005, n_features=x_train.shape[1], n_steps=1000)
cost_history, theta_history = logistic_reg.fit_mini_batch(x_train, y_train, batch_size=32)

plot_theta_gd(x_train, y_train, logistic_reg, cost_history, theta_history, 0, 1)

del logistic_reg, cost_history, theta_history
logistic_reg = LogisticRegression(learning_rate=0.005, n_features=x_train.shape[1], n_steps=1000)
cost_history, theta_history = logistic_reg.fit_sgd(x_train, y_train)

plot_theta_gd(x_train, y_train, logistic_reg, cost_history, theta_history, 0, 1)