3.6 The Fast Fourier Transform (FFT)
The problem with the Fourier transform as it is presented above, either in its sine/cosine regression model form or in its complex exponential form, is that it requires O(n2) operations to compute all of the Fourier coefficients. There are n data points and there are n/2 frequencies for which Fourier coefficients can be computed. Each frequency coefficient requires multiplying a summing a cosine or sine with each of the n data points. For short time series this is not an issue but for very long time series this can be a prohibitively expensive computation even on today’s computers.
The Fast Fourier Transform (FFT) is a way to reduce the complexity of the Fourier transform computation from O(n2) to O(nlogn), which is a dramatic improvement. The primary version of the FFT is one due to Cooley and Tukey. The basic idea of it is easy to see.
Suppose we have a time series y1,…,yn and we want to compute the complex Fourier coefficient z1. Going by the formula in the previous section, this would require computing z0=n−1∑t=0yt, which is simply proportional to the mean of the data. If the data have been de-meaned or de-trended then this will be zero. The next Fourier coefficient is then z1=n−1∑t=0ytexp(−2πi⋅1⋅t/n)=y0exp(−2πi⋅1⋅0/n)+y1exp(−2πi⋅1⋅1/n)+y2exp(−2πi⋅1⋅2/n)+⋯. Now suppose we want to compute the next coefficient z2. This requires computing z2=y0exp(−2πi⋅2⋅0/n)+y1exp(−2πi⋅2⋅1/n)+⋯ But wait! Notice how the exponential in the second term in the sum for z2 is the same as the exponential in the third term in the sum for z1. They are both equal to exp(−2πi⋅1⋅2/n). There is no need to compute this expoential quantity twice. We can simply compute it the first time, store it in memory, and then retrieve it when it is needed to compute z2 (assuming that retrieving from memory is faster than computing it from scratch.) One can think of the FFT algorithm as an elaborate bookkeeping algorithm that keeps track of these symmetries in computing the Fourier coefficients.
3.6.1 Example: A Simple FFT
The efficiencies of the algorithm are easier to see with a simple example. Suppose we have a time series with 4 observations, y1,y2,y3,y4. First, let θ=exp(−2πi/n). Then the Fourier transform of the time series can be expressed in vector/matrix form as
z=[z0z1z2z3]=[11111θ1θ2θ31θ2θ4θ61θ3θ6θ9][y0y1y2y3]
One can see the symmetry in the matrix of θs in the various powers of θ. Taking advantage of these symmetries by storing values instead of recomputing them produces a substantial reduction in computational complexity.
If y=(y0,…,yn−1), then the Fourier transform can be written as z=Wy, where W=[111⋯11θ1θ2⋯θn−11θ2θ4⋯θ2(n−1)1θ3θ6⋯θ3(n−1)⋮⋮⋮1θ(n−1)θ2(n−1)⋯θ(n−1)(n−1)] The symmetries in the W matrix obviously remain when we have a time series of length n.
Consider the following simple time series of length 4.
Here is the Fourier transform matrix for this series.
W <- rbind(exp(-2 * pi * 0 * (0:3) * 1i/4),
exp(-2 * pi * 1 * (0:3) * 1i/4),
exp(-2 * pi * 2 * (0:3) * 1i/4),
exp(-2 * pi * 3 * (0:3) * 1i/4))
W
[,1] [,2] [,3] [,4]
[1,] 1+0i 1+0i 1+0i 1+0i
[2,] 1+0i 0-1i -1-0i 0+1i
[3,] 1+0i -1-0i 1+0i -1-0i
[4,] 1+0i 0+1i -1-0i 0-1i
We can now multiply the data in y
with the Fourier transform matrix W
and get
z <- W %*% y
drop(z)
[1] -3.1607119+0.0000000i -0.4586896+0.5282817i -2.1037024-0.0000000i
[4] -0.4586896-0.5282817i
To verify that we have the correcdt results, we can execute the FFT algorithm using the fft()
function in R, which computes a univariate FFT.
z <- fft(y)
z
[1] -3.1607119+0.0000000i -0.4586896+0.5282817i -2.1037024+0.0000000i
[4] -0.4586896-0.5282817i
For a real time series, the first coefficient will always be real and will equal the sum of the time series, so z0=-3.1607. The second and the third coefficients (z1 and z2) are the Fourier coefficients corresponding to 1 cycle per 4 observations and 2 cycles per 4 observations, respectively. You will notice that because there are only 4 observations, 2 cycles per 4 observations is the Nyquist frequency.
The final Fourier coefficient (z3) here is the complex conjugate of the second coefficient, because for a real zeries zp=ˉzn−p, so z1=ˉz3. Practically speaking, for most analyses we will ignore the coefficients after p=n/2 even though R computes all of them.
Notice also above that the imaginary component of z3 is zero, as was stated previously, because sin(2πt(n/2)/n)=0 for all integer t.
3.6.2 The Cooley-Tukey Algorithm
Apparently, John Tukey thought of the idea for the fast Fourier transform while sitting in a government meeting so I guess the lesson there is that sometimes meetings can in fact produce novel ideas.
More formally, let’s assume that the length of the time series is such that it can be factored into n=r×s. Then recall that each Fourier coefficient is zp=n−1∑t=0ytexp(−2πipt/n) for p=0,…,n−1. Now let the index t be decomposed into t=rt1+t0, where we have t0=0,…,r−1t1=0,…,s−1 You can see that as t0 and t1 vary over the respective ranges, we can reconstruct every value of t from 0 to n−1. For example
n−1=rt1+t0=r(s−1)+r−1=rs−r+r−1=rs−1=n−1 because n=rs. It might look a bit counterintuitive but you just have to believe that all the numbers from 0 to n−1 are in there!
Now, we can re-write the equation for the Fourier coefficients as
zp=n−1∑t=0ytexp(−2πipt/n)=n−1∑rt1+t0=0ytexp(−2πip(rt1+t0)/n)=n−1∑rt1+t0=0yrt1+t0exp(−2πipt0/n)exp(−2πiprt1/n)=r−1∑t0=0s−1∑t1=0yrt1+t0exp(−2πipt0/n)exp(−2πiprt1/n) In the final line, verify that there are a total of r×s=n terms and cross terms in the product of sums. If p=0,…,n−1 then we require O(n2) computations to compute all the Fourier coefficients.
Much like we did for t, we can decompose the p=sp1+p0 for p0=0,…,s−1p1=0,…,r−1
Then for a single term in the final right-hand sum above, we have
exp(−2πiprt1/n)=exp(−2πi(sp1+p0)rt1/n)=exp(−2πirsp1t1/n)exp(−2πip0rt1/n)
Note that I have bolded the product r×s in the expoential to remind you that r×s=n. Therefore, the last line translates to exp(−2πiprt1/n)=exp(−2πip1t1)exp(−2πip0rt1/n). However, we know that exp(2πip1t1)=1 for any integer value of p1 or t1. Therefore, we then have exp(−2πiprt1/n)=exp(−2πip0rt1/n) Putting things back together, we now have zp=r−1∑t0=0s−1∑t1=0yrt1+t0exp(−2πip0rt1/n)=r−1∑t0=0[exp(−2πipt0/n)(s−1∑t1=0yrt1+t0exp(−2πip0rt1/n))]
Now note that the expression in the inner set of parentheses does not depend on p1 and is only a function of p0 and t0. If we denote this constant as C(p0,t0), we can write zp=r−1∑t0=0C(p0,t0)exp(−2πipt0/n). Note that C(p0,t0) also depends on r and a subset of values of y0,…,yn−1.
If we look at the formula above (and bear in mind that p=0,…,n−1), we can do a little bookkeeping to see how many computations will be required to compute all the Fourier coefficients.
To compute zp, there are r×s values of C(p0,t0) to compute (because p0=0,…,s−1 and t0=0,…,r−1) and each value of C(p0,t0) requires s multiplications, giving us rs2=rs×s=ns computations. They key here is that once we compute C(p0,t0) for every pair p0 and t0, the do not need to be computed again (they can be stored).
Given the values of C(p0,t0), each zp requires r multiplications and there are n different zps, giving us nr computations.
Adding the two sets of computations together gives us n(r+s) total. Recall that the naive Fourier transform algorithm requires O(n(r×s))=O(n2) computations. Reducing the complexity down to O(n(r+s)) is a major reduction.
Finally, note that there is nothing in the description of the FFT algorithm that requires any special ordering of the values of yt or anything special about r or s. In particular, if the values of r and s can themselves be factored into the product of two integers, then the entire algorithm can be recursively applied until there is no more factorization possible.
In the event that n is a power of 2, say n=2K for some interger K>0, then we could take r=2 and s=2K−1. But then s itself could be factored into s=2×2K−2, etc. If the sample size n is highly composite, meaning that it can be decomposed into many factors, then the complexity of the FFT is O(nlogn). If n is in fact a power of 2, then the complexity is O(nlog2n), where log2n is the number of times n can be factored into two integers.
The benefit of having n be a power of 2 is so great that some implementations of the algorithm will pad the time series with 0s in order to make the total length a power of 2. In R, this is the default for the periodogram function spec.pgram()
, which uses the fft()
function. There is even a function nextn()
in R which will find the next value of n that is a power of 2 (or possibly a power of some other number, like 3 or 5).
So for example, if your time series has a length of n=900, the next power of 2 is
You could then add a vector of zeros of length sufficient enough to get your time series to the required length.