Given this code, I wanna make an analysis on the progression of the objective function for each point on each method. def func1(x):
    return sum(100 * (x[i+1] - x[i]**2)**2 + (1 - x[i])**2 for i in range(len(x) - 1))

def func2(x):
    return sum(x_i**4 - 16 * x_i**2 + 5 * x_i for x_i in x)

def func3(x):
    x1, x2 = x[0], x[1]
    return (x1**2 + x2 - 11)**2 + (x1 + x2**2 - 7)**2

def calcular_gradiente_hessiana_simbolico(func_symbolic, num_vars):
    # Definir as variáveis simbólicas
    vars = sp.symbols(f'x0:{num_vars}')
    
    # Calcular o gradiente
    gradiente = [sp.diff(func_symbolic(vars), var) for var in vars]
    
    # Calcular a matriz Hessiana
    hessiana = [[sp.diff(grad, var) for var in vars] for grad in gradiente]
    
    return gradiente, hessiana, vars

# Armazenar gradientes e Hessianas simbólicos
gradientes_simbolicos = {}
hessianas_simbolicas = {}
vars_simbolicas = {}

# Calcular e armazenar gradiente e Hessiana para cada função
gradientes_simbolicos['func1'], hessianas_simbolicas['func1'], vars_simbolicas['func1'] = calcular_gradiente_hessiana_simbolico(func1, 7)
gradientes_simbolicos['func2'], hessianas_simbolicas['func2'], vars_simbolicas['func2'] = calcular_gradiente_hessiana_simbolico(func2, 100)
gradientes_simbolicos['func3'], hessianas_simbolicas['func3'], vars_simbolicas['func3'] = calcular_gradiente_hessiana_simbolico(func3, 2)

gradiente_f1_lambdified = lambdify(vars_simbolicas['func1'], gradientes_simbolicos['func1'], modules='numpy')
gradiente_f2_lambdified = lambdify(vars_simbolicas['func2'], gradientes_simbolicos['func2'], modules='numpy')
gradiente_f3_lambdified = lambdify(vars_simbolicas['func3'], gradientes_simbolicos['func3'], modules='numpy')

hessiana_f1_lambdified = lambdify(vars_simbolicas['func1'], hessianas_simbolicas['func1'], modules='numpy')
hessiana_f2_lambdified = lambdify(vars_simbolicas['func2'], hessianas_simbolicas['func2'], modules='numpy')
hessiana_f3_lambdified = lambdify(vars_simbolicas['func3'], hessianas_simbolicas['func3'], modules='numpy')
def avaliar_gradiente_f1(valores):
    return np.array(gradiente_f1_lambdified(*valores))

def avaliar_gradiente_f2(valores):
    return np.array(gradiente_f2_lambdified(*valores))

def avaliar_gradiente_f3(valores):
    return np.array(gradiente_f3_lambdified(*valores))

def avaliar_hessiana_f1(valores):
        return np.array(hessiana_f1_lambdified(*valores))

def avaliar_hessiana_f2(valores):
    return np.array(hessiana_f2_lambdified(*valores))

def avaliar_hessiana_f3(valores):
    return np.array(hessiana_f3_lambdified(*valores))
# Lambdified gradient and Hessian

# Function to evaluate convexity at a point using the Hessian
def check_convexity_at_point(hessian_func, point):
    hessian_matrix = np.array(hessian_func(*point))
    eigenvalues = eigvals(hessian_matrix)
    if np.all(eigenvalues > 0):
        return "Convex at this point"
    elif np.all(eigenvalues < 0):
        return "Concave at this point"
    else:
        return "Saddle point or indefinite at this point"

# Multi-start optimization with DataFrame storage
def find_multiple_critical_points(func, func_name, hessian, num_vars, num_starts=10, tol=1e-5):
    critical_points_data = []  # List to store data for DataFrame
    for i in range(num_starts):
        initial_guess = np.random.uniform(-100, 100, num_vars)
        result = minimize(func, initial_guess, method='CG', tol=tol)

        if result.success:
            point = result.x
            is_new_point = all(np.linalg.norm(point - cp) > tol for cp in [data[0] for data in critical_points_data])  # Check against existing points
            if is_new_point:
                convexity = check_convexity_at_point(hessian, point)
                func_value = func(point)  # Calculate function value
                critical_points_data.append([point, convexity, func_name, func_value]) # Changed to add all the point attributes

    # Create DataFrame
    df = pd.DataFrame(critical_points_data, columns=['Critical Point', 'Convexity', 'Function Name', 'Function Value'])
    return df

# Example usage
num_vars_1 = 7
num_vars_2 = 100
num_vars_3 = 2

critical_points_f1_df = find_multiple_critical_points(lambda x: func1(x), 'f1', hessiana_f1_lambdified, num_vars_1)
critical_points_f2_df = find_multiple_critical_points(lambda x: func2(x), 'f2', hessiana_f2_lambdified, num_vars_2)
critical_points_f3_df = find_multiple_critical_points(lambda x: func3(x), 'f3', hessiana_f3_lambdified, num_vars_3)
def armijo_line_search(f, grad_f, x, d, gamma=0.8, eta=0.25):
    t = 1
    total_iterations = 1
    while total_iterations < 1e4 and f(x + t * d) > f(x) + eta * t * np.dot(grad_f(x), d):
        t *= gamma
        total_iterations += 1
    return t, total_iterations
def GradientSearch(f, grad_f, x, eps, max_iter, time_limit, stop):
    """
    Performs Gradient Descent optimization with Armijo line search.

    Parameters:
    - f: The objective function.
    - grad_f: The gradient of the objective function.
    - x: Initial point as a NumPy array.
    - eps: Tolerance for the gradient norm.
    - max_iter: Maximum number of iterations.
    - time_limit: Time limit for the optimization (in seconds).
    - stop: Stopping criterion ("time" or "max_iter").

    Returns:
    - x: The optimized variable.
    - k: Number of iterations performed.
    - total_armijo: Total Armijo iterations.
    - f_current: Final function value.
    - f_list: List of function values per iteration.
    - error_ratios: List of error ratios per iteration.
    - total_time: Total time taken for optimization.
    """
    k = 0
    total_armijo = 0
    gradient = grad_f(x)
    f_list = []
    error_ratios = []
    f_prev = f(x)
    start_time = time.time()

    # Initialize stop_condition correctly
    if stop == "time" or stop == "max_iter":
        stop_condition = True  # Placeholder; actual condition checked inside loop
    else:
        raise ValueError("Unsupported stopping criterion. Use 'time' or 'max_iter'.")

    while True:
        # Update stop_condition based on the chosen stopping criterion
        if stop == "time":
            stop_condition = (time.time() - start_time) < time_limit
        elif stop == "max_iter":
            stop_condition = k < max_iter

        # Check stopping conditions
        if not stop_condition or np.linalg.norm(gradient) <= eps:
            break

        # Compute search direction
        d = -gradient

        # Perform Armijo line search
        step, armijo_iter = armijo_line_search(f, grad_f, x, d)
        total_armijo += armijo_iter

        # Update position
        x = x + d * step
        k += 1

        # Compute new gradient and function value
        gradient = grad_f(x)
        f_current = f(x)
        f_list.append(f_current)

        # Calculate error ratio
        if k > 1:
            if abs(f_prev) > 1e-12:  # Avoid division by zero with a small threshold
                error_ratio = abs(f_current - f_prev) / abs(f_prev)
            else:
                error_ratio = np.nan
            error_ratios.append(error_ratio)
        else:
            error_ratios.append(np.nan)  # First iteration has no previous value to compare

        f_prev = f_current

    total_time = time.time() - start_time
    return x, k, total_armijo, f(x), f_list, error_ratios, time.time() - start_time
def NewtonSearch(f, grad_f, hess_f, x, eps, max_iter, time_limit, stop):
    """
    Performs Newton's method optimization with Armijo line search.

    Parameters:
    - f: The objective function.
    - grad_f: The gradient of the objective function.
    - hess_f: The Hessian of the objective function.
    - x: Initial point.
    - eps: Tolerance for the gradient norm.
    - max_iter: Maximum number of iterations.
    - time_limit: Time limit for the optimization (in seconds).
    - stop: Stopping criterion ("time" or "max_iter").

    Returns:
    - x: The optimized variable.
    - k: Number of iterations performed.
    - total_armijo: Total Armijo iterations.
    - f_current: Final function value.
    - f_list: List of function values per iteration.
    - error_ratios: List of error ratios per iteration.
    - total_time: Total time taken for optimization.
    """
    k = 0
    total_armijo = 0
    gradient = grad_f(x)
    f_list = []
    f_prev = f(x)
    error_ratios = []
    start_time = time.time()

    # Initialize stop_condition correctly
    if stop == "time":
        stop_condition = True  # Will be evaluated inside the loop
    elif stop == "max_iter":
        stop_condition = True  # Will be evaluated inside the loop
    else:
        raise ValueError("Unsupported stopping criterion. Use 'time' or 'max_iter'.")

    while True:
        # Update stop_condition based on the chosen stopping criterion
        if stop == "time":
            stop_condition = (time.time() - start_time) < time_limit
        elif stop == "max_iter":
            stop_condition = k < max_iter

        # Check stopping conditions
        if not stop_condition or np.linalg.norm(gradient) <= eps:
            break

        try:
            # Compute search direction
            hessian = hess_f(x)
            # Ensure Hessian is not singular
            d = -np.linalg.solve(hessian, gradient)
        except np.linalg.LinAlgError:
            print("Hessian is singular or ill-conditioned at iteration", k)
            break

        # Perform Armijo line search
        step, armijo_iter = armijo_line_search(f, grad_f, x, d)
        total_armijo += armijo_iter

        # Update position
        x = x + d * step
        k += 1

        # Compute new gradient and function value
        gradient = grad_f(x)
        f_current = f(x)
        f_list.append(f_current)

        # Calculate error ratio
        if k > 0:
            if abs(f_prev) > 1e-12:  # Avoid division by zero with a small threshold
                error_ratio = abs(f_current - f_prev) / abs(f_prev)
            else:
                error_ratio = np.nan
            error_ratios.append(error_ratio)

        f_prev = f_current

    total_time = time.time() - start_time
    return x, k, total_armijo, f(x), f_list, error_ratios, time.time() - start_time
def BFGS(f, grad_f, x0, tol=1e-6, max_iter=100, time_limit=100, stop="time", H0=None):
    import numpy as np
    import time

    k = 0
    n = len(x0)
    x_k = x0
    f_list = []
    f_prev = f(x0)
    error_ratios = []
    
    H_k = np.eye(n) if H0 is None else H0
    start_time = time.time()

    total_armijo = 0
    grad_k = grad_f(x0)

    while True:
        # Update stop_condition based on chosen stopping criterion
        if stop == "time":
            stop_condition = (time.time() - start_time) < time_limit
        elif stop == "max_iter":
            stop_condition = k < max_iter
        else:
            raise ValueError("Unsupported stopping criterion")

        # Check stopping conditions
        if not stop_condition or np.linalg.norm(grad_k) <= tol:
            break

        # Compute search direction
        d_k = -H_k @ grad_k

        # Perform line search
        t_k, armijo_iter = armijo_line_search(f, grad_f, x_k, d_k)
        total_armijo += armijo_iter

        # Update position
        x_next = x_k + t_k * d_k
        p_k = x_next - x_k
        grad_next = grad_f(x_next)
        q_k = grad_next - grad_k

        p_q = np.dot(p_k, q_k)
        H_q = H_k @ q_k

        if abs(p_q) > 1e-10:  # Use a small threshold instead of exact zero
            H_k = (H_k + np.outer(p_k, p_k) / p_q
                   - np.outer(H_q, H_q) / np.dot(q_k, H_q))

        x_k = x_next
        grad_k = grad_next
        k += 1

        f_current = f(x_k)
        f_list.append(f_current)
        if k > 0:
            error_ratio = abs(f_current - f_prev) / abs(f_prev) if abs(f_prev) > 0 else np.nan
            error_ratios.append(error_ratio)
        f_prev = f_current

    total_time = time.time() - start_time
    return x_k, k, total_armijo, f(x_k), f_list, error_ratios, time.time() - start_time
df_results = pd.DataFrame(columns=[
    "Função utilizada", "Ponto inicial", "Método", "# de iterações",
    "# total de iterações da busca de Armijo", "Ponto ótimo encontrado",
    "Valor da função no ponto ótimo encontrado", "Erro de aproximação",
    "Lista de f", "Razão de erros", 'Tempo de execução'])

df_armijo = pd.DataFrame(columns=[
    "Método", "Total Armijo", "Média Armijo por Iteração"
])

initial_points_f1 = []
initial_points_f2 = []
initial_points_f3 = []

for _ in range(15):
    initial_points_f1.append(np.random.uniform(-100, 100, 7))
    initial_points_f2.append(np.random.uniform(-100, 100, 100))
    initial_points_f3.append(np.random.uniform(-100, 100, 2))
for i in range(5):

    gradientX, gradientIterations, gradientArmijoIterations, gradientF, gradientFList, gradientErrors, GradientExecTime = GradientSearch(func1, avaliar_gradiente_f1, initial_points_f1[i], 1e-5, 10000, 1, "time")
    newtonX, newtonIterations, newtonArmijoIterations, newtonF, newtonFList, newtonErrors, newtonExecTime = NewtonSearch(func1, avaliar_gradiente_f1, avaliar_hessiana_f1, initial_points_f1[i], 1e-5, 10000, 1, "time")
    bfgsX, bfgsIterations, bfgsArmijoIterations, bfgsF, bfgsFList, bfgsErrors, bfgsExecTime = BFGS(func1, avaliar_gradiente_f1, initial_points_f1[i], 1e-5, 10000, 1, "time")

    gradient_armijo = {
    "Método": "Gradiente",
    "Total Armijo": gradientArmijoIterations,
    "Média Armijo por Iteração": [
        gradientArmijoIterations / gradientIterations,
    ]
}

    newton_armijo = {
    "Método": "Gradiente",
    "Total Armijo": newtonArmijoIterations,
    "Média Armijo por Iteração": [
        newtonArmijoIterations / newtonIterations,
    ]
}

    bfgs_armijo = {
    "Método": "Gradiente",
    "Total Armijo": bfgsArmijoIterations,
    "Média Armijo por Iteração": [
        bfgsArmijoIterations / bfgsIterations,
    ]
}

    df_armijo = pd.concat([df_armijo, pd.DataFrame([gradient_armijo])], ignore_index=True)
    df_armijo = pd.concat([df_armijo, pd.DataFrame([newton_armijo])], ignore_index=True)
    df_armijo = pd.concat([df_armijo, pd.DataFrame([bfgs_armijo])], ignore_index=True)

    gradient_row = {
        "Função utilizada": "func1",
        "Ponto inicial": initial_points_f1[i],
        "Método": "Gradiente",
        "# de iterações": gradientIterations,
        "# total de iterações da busca de Armijo": gradientArmijoIterations,
        "Ponto ótimo encontrado": gradientX,
        "Valor da função no ponto ótimo encontrado": gradientF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f1(gradientX)), 
        "Lista de f": gradientFList,
        "Razão de erros": gradientErrors,
        "Tempo de execução": GradientExecTime
    }

    newton_row = {
        "Função utilizada": "func1",
        "Ponto inicial": initial_points_f1[i],
        "Método": "Newton",
        "# de iterações": newtonIterations,
        "# total de iterações da busca de Armijo": newtonArmijoIterations,
        "Ponto ótimo encontrado": newtonX,
        "Valor da função no ponto ótimo encontrado": newtonF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f1(newtonX)), 
        "Lista de f": newtonFList,
        "Razão de erros": newtonErrors,
        "Tempo de execução": newtonExecTime
    }

    bfgs_row = {
        "Função utilizada": "func1",
        "Ponto inicial": initial_points_f1[i],
        "Método": "BFGS",
        "# de iterações": bfgsIterations,
        "# total de iterações da busca de Armijo": bfgsArmijoIterations,
        "Ponto ótimo encontrado": bfgsX,
        "Valor da função no ponto ótimo encontrado":bfgsF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f1(bfgsX)),  
        "Lista de f": bfgsFList,
        "Razão de erros": bfgsErrors,
        "Tempo de execução": bfgsExecTime
    }

    df_results = pd.concat([df_results, pd.DataFrame([gradient_row])], ignore_index=True)
    df_results = pd.concat([df_results, pd.DataFrame([newton_row])], ignore_index=True)
    df_results = pd.concat([df_results, pd.DataFrame([bfgs_row])], ignore_index=True)
for i in range(5):
    gradientX, gradientIterations, gradientArmijoIterations, gradientF, gradientFList, gradientErrors, GradientExecTime = GradientSearch(func2, avaliar_gradiente_f2, initial_points_f2[i], 1e-5, 10000, 1, "time")
    newtonX, newtonIterations, newtonArmijoIterations, newtonF, newtonFList, newtonErrors, newtonExecTime = NewtonSearch(func2, avaliar_gradiente_f2, avaliar_hessiana_f2, initial_points_f2[i], 1e-5, 10000, 1, "time")
    bfgsX, bfgsIterations, bfgsArmijoIterations, bfgsF, bfgsFList, bfgsErrors, bfgsExecTime = BFGS(func2, avaliar_gradiente_f2, initial_points_f2[i], 1e-5, 10000, 1, "time")


    gradient_armijo = {
    "Método": "Gradiente",
    "Total Armijo": gradientArmijoIterations,
    "Média Armijo por Iteração": [
        gradientArmijoIterations / gradientIterations,
    ]
}

    newton_armijo = {
    "Método": "Gradiente",
    "Total Armijo": newtonArmijoIterations,
    "Média Armijo por Iteração": [
        newtonArmijoIterations / newtonIterations,
    ]
}

    bfgs_armijo = {
    "Método": "Gradiente",
    "Total Armijo": bfgsArmijoIterations,
    "Média Armijo por Iteração": [
        bfgsArmijoIterations / bfgsIterations,
    ]
}

    df_armijo = pd.concat([df_armijo, pd.DataFrame([gradient_armijo])], ignore_index=True)
    df_armijo = pd.concat([df_armijo, pd.DataFrame([newton_armijo])], ignore_index=True)
    df_armijo = pd.concat([df_armijo, pd.DataFrame([bfgs_armijo])], ignore_index=True)
    
    gradient_row = {
        "Função utilizada": "func2",
        "Ponto inicial": initial_points_f2[i],
        "Método": "Gradiente",
        "# de iterações": gradientIterations,
        "# total de iterações da busca de Armijo": gradientArmijoIterations,
        "Ponto ótimo encontrado": gradientX,
        "Valor da função no ponto ótimo encontrado": gradientF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f2(gradientX)), 
        "Lista de f": gradientFList,
        "Razão de erros": gradientErrors,
        "Tempo de execução": GradientExecTime
    }

    newton_row = {
        "Função utilizada": "func2",
        "Ponto inicial": initial_points_f2[i],
        "Método": "Newton",
        "# de iterações": newtonIterations,
        "# total de iterações da busca de Armijo": newtonArmijoIterations,
        "Ponto ótimo encontrado": newtonX,
        "Valor da função no ponto ótimo encontrado": newtonF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f2(newtonX)), 
        "Lista de f": newtonFList,
        "Razão de erros": newtonErrors,
        "Tempo de execução": newtonExecTime
    }

    bfgs_row = {
        "Função utilizada": "func2",
        "Ponto inicial": initial_points_f2[i],
        "Método": "BFGS",
        "# de iterações": bfgsIterations,
        "# total de iterações da busca de Armijo": bfgsArmijoIterations,
        "Ponto ótimo encontrado": bfgsX,
        "Valor da função no ponto ótimo encontrado":bfgsF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f2(bfgsX)),  
        "Lista de f": bfgsFList,
        "Razão de erros": bfgsErrors,
        "Tempo de execução": bfgsExecTime
    }

    df_results = pd.concat([df_results, pd.DataFrame([gradient_row])], ignore_index=True)
    df_results = pd.concat([df_results, pd.DataFrame([newton_row])], ignore_index=True)
    df_results = pd.concat([df_results, pd.DataFrame([bfgs_row])], ignore_index=True)
for i in range(5):
    gradientX, gradientIterations, gradientArmijoIterations, gradientF, gradientFList, gradientErrors, GradientExecTime = GradientSearch(func3, avaliar_gradiente_f3, initial_points_f3[i], 1e-5, 10000, 1, "time")
    newtonX, newtonIterations, newtonArmijoIterations, newtonF, newtonFList, newtonErrors, newtonExecTime = NewtonSearch(func3, avaliar_gradiente_f3, avaliar_hessiana_f3, initial_points_f3[i], 1e-5, 10000, 1, "time")
    bfgsX, bfgsIterations, bfgsArmijoIterations, bfgsF, bfgsFList, bfgsErrors, bfgsExecTime = BFGS(func3, avaliar_gradiente_f3, initial_points_f3[i], 1e-5, 10000, 1, "time")


    gradient_armijo = {
    "Método": "Gradiente",
    "Total Armijo": gradientArmijoIterations,
    "Média Armijo por Iteração": [
        gradientArmijoIterations / gradientIterations,
    ]
}

    newton_armijo = {
    "Método": "Gradiente",
    "Total Armijo": newtonArmijoIterations,
    "Média Armijo por Iteração": [
        newtonArmijoIterations / newtonIterations,
    ]
}

    bfgs_armijo = {
    "Método": "Gradiente",
    "Total Armijo": bfgsArmijoIterations,
    "Média Armijo por Iteração": [
        bfgsArmijoIterations / bfgsIterations,
    ]
}

    df_armijo = pd.concat([df_armijo, pd.DataFrame([gradient_armijo])], ignore_index=True)
    df_armijo = pd.concat([df_armijo, pd.DataFrame([newton_armijo])], ignore_index=True)
    df_armijo = pd.concat([df_armijo, pd.DataFrame([bfgs_armijo])], ignore_index=True)

    gradient_row = {
        "Função utilizada": "func3",
        "Ponto inicial": initial_points_f3[i],
        "Método": "Gradiente",
        "# de iterações": gradientIterations,
        "# total de iterações da busca de Armijo": gradientArmijoIterations,
        "Ponto ótimo encontrado": gradientX,
        "Valor da função no ponto ótimo encontrado": gradientF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f3(gradientX)), 
        "Lista de f": gradientFList,
        "Razão de erros": gradientErrors,
        "Tempo de execução": GradientExecTime
    }

    newton_row = {
        "Função utilizada": "func3",
        "Ponto inicial": initial_points_f3[i],
        "Método": "Newton",
        "# de iterações": newtonIterations,
        "# total de iterações da busca de Armijo": newtonArmijoIterations,
        "Ponto ótimo encontrado": newtonX,
        "Valor da função no ponto ótimo encontrado": newtonF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f3(newtonX)), 
        "Lista de f": newtonFList,
        "Razão de erros": newtonErrors,
        "Tempo de execução": newtonExecTime
    }

    bfgs_row = {
        "Função utilizada": "func3",
        "Ponto inicial": initial_points_f3[i],
        "Método": "BFGS",
        "# de iterações": bfgsIterations,
        "# total de iterações da busca de Armijo": bfgsArmijoIterations,
        "Ponto ótimo encontrado": bfgsX,
        "Valor da função no ponto ótimo encontrado":bfgsF,
        "Erro de aproximação": np.linalg.norm(avaliar_gradiente_f3(bfgsX)),  
        "Lista de f": bfgsFList,
        "Razão de erros": bfgsErrors,
        "Tempo de execução": bfgsExecTime
    }

    df_results = pd.concat([df_results, pd.DataFrame([gradient_row])], ignore_index=True)
    df_results = pd.concat([df_results, pd.DataFrame([newton_row])], ignore_index=True)
    df_results = pd.concat([df_results, pd.DataFrame([bfgs_row])], ignore_index=True)
df_results.to_csv("Last.csv")
df_armijo.to_csv("Armijo.csv")
# Aplicando formatação condicional
styled_df_results = df_results.style.applymap(
    lambda val: 'background-color: lightgreen' if isinstance(val, (int, float)) and val > 0 else '',
    subset=["# de iterações", "# total de iterações da busca de Armijo", "Erro de aproximação"]
)

# Salvando como HTML
styled_df_results.to_html("styled_results.html")

# Salvando como Excel com formatação
with pd.ExcelWriter("styled_results.xlsx", engine='openpyxl') as writer:
    df_results.to_excel(writer, sheet_name='Results', index=False)
    df_armijo.to_excel(writer, sheet_name='Armijo', index=False)

# Exemplo de gráfico de barras para iterações
plt.figure(figsize=(10, 6))
sns.barplot(x=df_results["Método"], y=df_results["# de iterações"])
plt.title("Número de Iterações por Método")
plt.show()
df = pd.read_csv('Last.csv')
df_newton = df[df['Método'] == 'Newton']
df_gradiente = df[df['Método'] == 'Gradiente']
df_bfgs = df[df['Método'] == 'BFGS']
df_newton = df_newton.reset_index(drop=True)
df_gradiente = df_gradiente.reset_index(drop=True)
df_bfgs = df_bfgs.reset_index(drop=True)

for i, df in enumerate([df_newton, df_gradiente, df_bfgs]):
    # Original number of rows
    original_row_count = len(df)

    # Filter the DataFrame
    filtered_df = df[df['Erro de aproximação'] < 1e-4]

    # Update the respective DataFrame
    if i == 0:
        df_newton = filtered_df
        print("Resultados que não convergiram com Newton:", original_row_count - len(df_newton))
    elif i == 1:
        df_gradiente = filtered_df
        print("Resultados que não convergiram com Gradiente:", original_row_count - len(df_gradiente))
    elif i == 2:
        df_bfgs = filtered_df
        print("Resultados que não convergiram com BFGS:", original_row_count - len(df_bfgs))