How to do it better:
Well I would say summing up positive and negative terms of comparable size is prone to error since it leads to summing small and big numbers which is known to be problematic. But you can avoid it since exp(-30)=1/exp(30).
1/math.fsum(((30)**n/math.factorial(n) for n in range(0,100+1)))
is
9.357622968840174e-14
i.e. positve and small like you would expect. And
1/math.fsum(((30)**n/math.factorial(n) for n in range(0,100+1)))-np.exp(-30)
gives
-1.262177448353619e-29
So we get bascially the same as numpy does.
Where exactly is the error:
Here is a table of different ways to calculate exp(-30) approximately with the taylor series summing up N terms:
Naive menas I just calculated the terms as floats and sumed them up.
In the fractions column I did the same but used python's fraction instead of float division.
Then I thought it would be nice to distinguish error in division/muliplication and in summing up. So I calculated each summand with fraction but turned them into floats before summing up.
Finally I calculated the values of exp(30) nativly and then 1/exp(30) which I am calling the 1/e^30_trick.
native fractions float(fraction) 1/e^30_trick
0 1.000000e+00 1.000000e+00 1.000000e+00 1.000000e+00
10 1.212548e+08 1.212548e+08 1.212548e+08 4.187085e-09
20 8.529171e+10 8.529171e+10 8.529171e+10 2.652040e-12
30 3.848426e+11 3.848426e+11 3.848426e+11 1.706501e-13
40 6.333654e+10 6.333654e+10 6.333654e+10 9.670058e-14
50 8.782292e+08 8.782292e+08 8.782292e+08 9.360412e-14
60 1.685584e+06 1.685584e+06 1.685584e+06 9.357627e-14
70 6.225246e+02 6.225247e+02 6.225246e+02 9.357623e-14
80 5.588481e-02 5.595324e-02 5.588481e-02 9.357623e-14
90 -6.697346e-05 1.459487e-06 -6.697346e-05 9.357623e-14
100 -6.843293e-05 1.276217e-11 -6.843293e-05 9.357623e-14
110 -6.843294e-05 9.361706e-14 -6.843294e-05 9.357623e-14
120 -6.843294e-05 9.357623e-14 -6.843294e-05 9.357623e-14
I was quite happy with the result. Since it shows both the naiv and even the float(fractions) way are terrible so the error must be in summing up the negative and positive terms. Also even though both the 1/exp(30) trick and the factions do converge to the "correct" value of 9.357623e-14. The former converges much faster than the latter.
Table Code:
series = pd.Series(np.arange(0,151,10))
@np.vectorize
def naiv(N):
return math.fsum(((-30)**n/math.factorial(n) for n in range(0,N+1)))
@np.vectorize
def fractions(N):
return float(sum(Fraction((-30)**n,math.factorial(n)) for n in range(0,N+1)))
@np.vectorize
def float_fractions(N):
return math.fsum(float(Fraction((-30)**n,math.factorial(n))) for n in range(0,N+1))
@np.vectorize
def one_over_30_trick(N):
return 1/math.fsum(((30)**n/math.factorial(n) for n in range(0,N+1)))
pd.DataFrame({"native":naiv(series),"fractions":fractions(series),"float(fraction)":float_fractions(series),"1/e^30_trick":one_over_30_trick(series)},index=series)