pmt() function too slow - here are some ways to make it faster
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 25/100
- Tipo de issue
- Refactorización
- Claridad
- Necesita aclaración
- Estado de actividad
- Estancado
- Stack tecnológico
- numpy, python
- Área
- performance
Línea de trabajo
Comienza en el punto de entrada pmt() y reproduce los benchmarks escalares y de arrays indicados usando el código del issue. Revisa la implementación actual y determina qué enfoque de optimización es adecuado; el trabajo se considera terminado cuando haya un cambio acordado con mediciones de rendimiento que preserve el comportamiento existente.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
The pmt() function of numpy_financial is too slow and can become the main bottleneck in all those cases where it must be run thousands, if not millions of times - e.g. multiple scenarios on many loans or mortgages based on a floating rate.
I propose below a few alternative ways to write new, more optimised functions, which can be 7 to 60 times faster, depending on the circumstances:
- if you need to run this function millions of times and your code allows it, you'll be better off using one function for when the inputs, and the output, are scalar, and a separate one for when they are arrays
- unless you have reasons not to, using numba will speed things up even more
- if you cannot use numba and never run this function on scalars, you won't see much benefit
My findings are that, on my machine at least:
- For scalars: npf is slower than all the other implementations: about 30 times slower than my scalar, non-numba function, about 60 times slower than my scalar numba one but also significantly slower than my array function
- For arrays: my numba function is about 7 times faster than npf's; without numba, they are about the same
I copied below the exact code I used to test all of this:
import numpy as np
import pandas as pd
import numpy_financial as npf
import timeit
import numba
start = time.time()
def my_pmt(rate, nper, pv, fv =0, when =0):
c = (1+rate)**nper
# multipl by 1 converts an array of size 1 to a scalar
return 1 * np.where(nper == 0, np.nan,
np.where(rate ==0, -(fv + pv) /nper ,
(-pv *c - fv) * rate / ( (c - 1) *( 1 + rate * when ) ) ) )
@numba.jit
def pmt_numba_array(rate, nper, pv, fv =0, when =0):
c = (1+rate)**nper
return np.where(nper == 0, np.nan,
np.where(rate ==0, -(fv + pv) /nper ,
(-pv *c - fv) * rate / ( (c - 1) *( 1 + rate * when ) ) ) )
def my_pmt_optimised(rate,nper, pv, fv =0, when =0):
if np.isscalar(rate) and np.isscalar(nper) and np.isscalar(pv) and np.isscalar(fv):
return pmt_numba_scalar(rate, nper, pv, fv, when)
else:
return pmt_numba_array(rate, nper, pv, fv, when)
@numba.jit
def pmt_numba_scalar(rate, nper, pv, fv=0, when = 0):
# 0 = end, 1 = begin
if nper == 0:
return(np.nan)
elif rate == 0:
return ( -(fv+pv)/nper )
else:
c= (1 + rate) ** nper
return (-pv *c - fv) * rate / ( (c - 1) *( 1 + rate * when) )
def pmt_no_numba_scalar(rate, nper, pv, fv=0, when = 0):
if nper == 0:
return(np.nan)
elif rate == 0:
return ( -(fv+pv)/nper )
else:
c= (1 + rate) ** nper
return (-pv *c - fv) * rate / ( (c - 1) *( 1 + rate * when) )
def pmt_npf(rate, nper, pv, fv=0, when = 0):
#(rate, nper, pv, fv, when) = map(np.array, [rate, nper, pv, fv, when])
temp = (1 + rate)**nper
mask = (rate == 0)
masked_rate = np.where(mask, 1, rate)
fact = np.where(mask != 0, nper,
(1 + masked_rate*when)*(temp - 1)/masked_rate)
return -(fv + pv*temp) / fact
r = 4
n = int(1e4)
rate = 5e-2
nper = 120
pv = 1e6
fv = -100e3
t_my_numba = timeit.Timer("pmt_numba_scalar(rate, nper, pv, fv ) " , globals = globals() ).repeat(repeat = r, number = n)
t_my_no_numba = timeit.Timer("pmt_no_numba_scalar(rate, nper, pv, fv ) " , globals = globals() ).repeat(repeat = r, number = n)
t_npf = timeit.Timer("npf.pmt(rate, nper, pv, fv )" , globals = globals() ).repeat(repeat = r, number = n)
t_my_pmt = timeit.Timer("my_pmt(rate, nper, pv, fv )" , globals = globals() ).repeat(repeat = r, number = n)
t_my_pmt_optimised = timeit.Timer("my_pmt_optimised(rate, nper, pv, fv )" , globals = globals() ).repeat(repeat = r, number = n)
resdf_scalar = pd.DataFrame(index = ['min time'])
resdf_scalar['my scalar func, numba'] = [min(t_my_numba)]
resdf_scalar['my scalar func, no numba'] = [min(t_my_no_numba)]
resdf_scalar['npf'] = [min(t_npf)]
resdf_scalar['my array function, no numba'] = [min(t_my_pmt)]
resdf_scalar['my scalar/array function, numba'] = [min(t_my_pmt_optimised)]
# the docs explain why we should take the min and not the avg
resdf_scalar = resdf_scalar.transpose()
resdf_scalar['diff vs fastest'] = (resdf_scalar / resdf_scalar.min() )
rate =np.arange(2,12)*1e-2
nper = np.arange(200,210)
pv = np.arange(1e6,1e6+10)
fv = -100e3
t_npf_array = timeit.Timer("npf.pmt(rate, nper, pv, fv )" , globals = globals() ).repeat(repeat = r, number = n)
t_my_pmt_array = timeit.Timer("my_pmt(rate, nper, pv, fv )" , globals = globals() ).repeat(repeat = r, number = n)
t_my_pmt_optimised_array = timeit.Timer("my_pmt_optimised(rate, nper, pv, fv )" , globals = globals() ).repeat(repeat = r, number = n)
resdf_array = pd.DataFrame(index = ['min time'])
resdf_array['npf'] = [min(t_npf_array)]
resdf_array['my array function, no numba'] = [min(t_my_pmt_array)]
resdf_array['my scalar/array function, numba'] = [min(t_my_pmt_optimised_array)]
# the docs explain why we should take the min and not the avg
resdf_array = resdf_array.transpose()
resdf_array['diff vs fastest'] = (resdf_array / resdf_array.min() )
- Lenguaje dominante
- Python
- Estrellas
- 410
- Forks
- 102
- Merge medio
- 13 min
- PR fusionados (30 d)
- 10
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de numpy/numpy-financial
-
enhancement sustain-2026
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
numpy/numpy-financial#38 · 3 comentarios · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
numpy/numpy-financial#131 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
documentation sustain-2026
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
numpy/numpy-financial#130 · 7 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 52/100
numpy/numpy-financial#126 · 4 comentarios ·
Los mantenedores suelen responder en 1 día
-
BUG: Benchmarking does not work with spinQuizá libre de nuevo @Kai-Striega la tomó hace 879 días y no hay ningún pull request abierto. Abiertobench bug build sustain-2026
numpy/numpy-financial#123 · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de numpy/numpy-financial
Issues similares
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
letsencrypt/cp-cps#353 ·
-
Marble Madness II is missingAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
PedestrianDynamics/pyFDS-Evac#394 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
DOI-USGS/pywatershed#421 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
python-pillow/Pillow#10087 · 1 comentario ·
Los mantenedores suelen responder en 1 día