Implementing a Steepest Descent Algorithm
You biggest time waster appears to be this loop:

    while f(x_k + alpha * p_k) &gt; f(x_k) + (alpha * c * (gradTrans  @ p_k))[0, 0]:
        alpha = ratio * alpha


f(x_k), c, gradTrans, and p_k are all constant in the loop, so you can compute f(x_k) and c * (gradTrans @ p_k) before the loop and use these computed values in the test expression, instead of recomputing the same values over and over.

    fxk = f(x_k)
    offset = c * (gradTrans @ p_k)
    while f(x_k + alpha * p_k) &gt; fxk + (alpha * offset)[0, 0]:
        alpha = ratio * alpha


Doing so cuts the time roughly in half.

Similarly, gradient(x_k) is computed 3 times here:

while abs((gradient(x_k)[0, 0])) &gt; 0.1 or abs((gradient(x_k))[0, 1]) &gt; 0.1:
    ...
    p_k = - gradient(x_k)


Again, compute once and store the result.



You should probably use vectors instead of matrices:

    x_k = np.array([10., 5.])


Which can be unpacked using tuple assignment:

def f(x_k):
    x, y = x_k 
    return 100 * (y - x**2)**2 + (1 - x)**2


And using 10. and 5. in the above x_k initialization makes the arrays float64 instead of int32, which allows you to use in-place addition operators:

        x_k += alpha * p_k


instead of:

        x_k = x_k + alpha * p_k