Fourier-transform for time-series : plotting complex numbers

Most of the time, people have trouble handling the Fourier Transform of a signal because of its complex form. Except for very specific cases, the Fourier transform of a time series is most of the time a complex-numbered sequence – and complex numbers are not always simple to grasp, especially when you are not used to handling those kinds of numbers.
In this post, I want to show a few ways to visualize the Fourier transform of a 1D sequence of real numbers, which is what you handle 99% of the time, especially in data analysis and time series.
All images by author.
This post is the third of my Fourier-transform for time-series. Check out the previous posts here:
- Review how the convolution relate to the Fourier transform and how fast it is:
Fourier transform for time-series: fast convolution explained with numpy
- Deepen your understanding of convolution using image examples:
Fourier-Transform for Time Series: About Image Convolution and SciPy
Before diving into the actual computing and plotting of 1D Fourier transforms, we'll review a few basic concepts of complex numbers that are crucial to what's next. As you'll see, complex numbers are actually pretty simple: just consider them as a vector of 2 numbers.
The end goal of this post is to make you more comfortable with the actual numbers behind your Fourier transforms.
Quick review of complex numbers
Any complex number can be represented with its canonical form, using 2 real values, a and b, respectively called their "real" and "imaginary" parts:

where i is the unit complex number with the well-known property that if you square it, you get -1:

We can also rewrite the above equation for Z as follows:

or again using dot product of vectors:

If we were to represent Z on an (x, y) plane with the x-axis for the real part (1) and the y-axis for the imaginary part (i), we'd plot Z as a vector from (0, 0) to (a, b).
Let's use Numpy and Matplotlib to plot an example.
%matplotlib qt
import numpy as np
import matplotlib.pyplot as plt
# We start by defininf helper functions
# one to pretify an axes
def pretty_ax(ax, b: float=8):
"""Prettify an Axes for plotting complex numbers:
- Add labels
- Set aspect to 'equal'
- Add grid
- Add unit circle with canonical vectors
- Set custom value for boundaries of axes
"""
ax.set_aspect('equal')
ax.set_xlabel("Real", color="gray")
ax.set_ylabel("Imag", color="gray")
ax.xaxis.set_label_coords(0.5,0.05)
ax.yaxis.set_label_coords(0.05,0.5)
ax.grid(ls='--')
c = plt.Circle((0,0), 1, ls='--', facecolor=None, edgecolor="gray", fill=False)
ax.add_patch(c)
ax.set_xlim(-b, b)
ax.set_ylim(-b, b)
plot_vector(1, ax, color="gray")
plot_vector(1j, ax, color="gray")
# one to plot complex numbers to a 2D ax
def plot_vector(
Z, ax,
color=None, decomp=False, txt=False,
orig=0+0J, ls=None, alpha=1,
arrowstyle=None, width=None,
headwidth=None, frac=None
):
"""Plot a complex number as vector.
"""
x = Z.real
y = Z.imag
arrow_props = dict(
color=color,
ls=ls,
edgecolor=None,
alpha=alpha,
)
if arrowstyle is None:
if width is not None:
arrow_props["width"] = width # width of the arrow in points
if headwidth is not None:
arrow_props["headwidth"] = headwidth # width of the base of the arrow head in points
arrow_props['shrink'] = 0.0 # move the tip and base some percent away from the annotated point and text
if frac is not None:
arrow_props['frac'] = frac # fraction of the arrow length occupied by the head
else:
arrow_props['arrowstyle'] = arrowstyle
ann = ax.annotate("",
xy=((Z+orig).real, (Z+orig).imag),
xytext=(orig.real, orig.imag),
arrowprops=arrow_props,
annotation_clip=False)
ann.arrow_patch.set_clip_box(ax.bbox)
if decomp:
ann = ax.annotate('', xy=(x, 0), xytext=(orig.real, orig.imag), arrowprops=dict(arrowstyle="->", color=color), annotation_clip=False)
ann.arrow_patch.set_clip_box(ax.bbox)
ann = ax.annotate('', xy=(x, y), xytext=(x,0), arrowprops=dict(arrowstyle="->", color=color), annotation_clip=False)
ann.arrow_patch.set_clip_box(ax.bbox)
if txt:
ax.text(x/2, y/2, f'Z={Z}', ha='center', va='bottom', rotation=np.angle(Z)*180/np.pi, color=color)
fig, ax = plt.subplots()
# prettify the ax for plotting complex numbers
pretty_ax(ax)
# define 2 complex numbers
Z1 = 5 + 3J
Z2 = 1 + 6J
plot_vector(Z1, ax, color="red", txt=True) # plot Z1
plot_vector(Z2, ax, color="blue", txt=True) # plot Z2
plot_vector(Z2, ax, orig=Z1, color="blue") # plot Z2 at the end of Z1
plot_vector(Z1+Z2, ax, txt=True, color="green") # plot Z1+Z2

As you can see, it is easy to represent the complex numbers Z1 and Z2 as vectors on a 2D plane. Consequently, we can easily compute and plot the sum of 2 complex numbers: we just sum the vectors. And that's really all there is to remember; everything that follows is just fancy notations and plotting, along with some useful mathematical properties.
Another way to represent the same vector is using the polar notation:

where ∣Z∣ is called the "module" of Z, and θ is called the "argument" (I'll usually call this variable the "phase angle"). Geometrically, the module represents the length of the vector, and its phase angle represents the angle of the vector with respect to the Ox axis, also called the direction.
There is a direct correspondence between the canonical notation (a, b) and the polar notation (module, phase); it's just another way to write the same vector. Indeed, remember that the complex exponential is just the sum of a cosine and an imaginary sine:

so if we plot this complex number, the real part is cos(θ) and the imaginary part is sin(θ); its coordinates in the complex plane are (cos(θ),sin(θ)). Extending this idea to any complex number, we get a real part ∣Z∣cos(θ) and an imaginary part ∣Z∣sin(θ).
With NumPy, we can easily compute the real, imaginary, module, and phase components of a complex number. We can even check that both notations lead to the exact same complex number. Indeed:
Z = 1+3J
real = Z.real
imag = Z.imag
mod = np.abs(Z)
angle = np.angle(Z)
print(Z.real, Z.imag, mod, angle)
# check that both representation are equivalent
print(real+imag*1J == mod * np.exp(1J*angle))
1.0 3.0 3.1622776601683795 1.2490457723982544
True
Now that we know that complex numbers behave just like vectors, we can use what we know about vectors and apply it to complex numbers. For example, when we add 2 complex numbers, their sum is just the complex number corresponding to the sum of the underlying vectors. Let's use Python again to see an example:
fig, axes = plt.subplots(1, 2, sharex=True, sharey=True)
pretty_ax(axes[0])
pretty_ax(axes[1])
# define 2 complex numbers
Z1 = 2.5 + 2J
Z2 = 0.5 + 1.5J
# compute their sum
Z_sum = Z1 + Z2
plot_vector(Z1, axes[0], color="red") # plot Z1 alone
plot_vector(Z2, axes[0], orig=Z1, color="blue") # plot Z2 after Z1
plot_vector(Z_sum, axes[0], color="green") # plot their sum
axes[0].set_title('Sum of 2 complex numbers')
# same for the mean
Z_mean = np.mean([Z1, Z2])
plot_vector(Z1, axes[1], color="red") # plot Z1 alone
plot_vector(Z2, axes[1], color="blue") # plot Z2 alone
plot_vector(Z_mean, axes[1], color="green")# plot their mean
axes[1].set_title('Mean of 2 complex numbers')

Similarly, the mean of complex numbers is just the sum of the vectors, scaled along its direction by the number of vectors. Scaling a vector corresponds to decreasing or increasing its length, but not changing its direction. For a scale factor K:

which is a new complex number with a new length ∣Z′∣=K∣Z∣ (i.e., we scaled the module), but with the same phase angle (i.e., the vector has the same direction).
So remember:
- The same complex number Z can be represented as Z=a+ib or Z=∣Z∣e^(−iθ), where ∣Z∣ represents the length of the vector, and θ its direction.
- Summing/averaging complex numbers behaves exactly like summing/averaging vectors.
Vector approach to Fourier-transform
Let's review the formula for the Fourier transform in the context of a discrete sample sequence, called the Discrete Fourier Transform (DFT):

with

Note that several definitions exist using either none, 1/N, or 1/N**0.5 as the scaling coefficient. I usually prefer using no scaling factor, but in this post, I'll use the 1/N scaling factor as in the equation above.
Let's describe what this equation tells us :
The k-th element of the Fourier transform (X[k]) is a complex number, given by the mean of a set of complex numbers (the Zkns).
Using what we now know about complex numbers and vectors, we can obtain the k-th coefficient of the Fourier transform just by plotting all the little complex numbers Zkn, summing them (like regular vector sum), and scaling down the result by 1/N.
Let's use Python to plot an example:
from matplotlib.cm import viridis
N = 10
colors = viridis(np.linspace(0, 1, N))
x_n = np.sin(np.linspace(0, 3, N)) +0.2
ns = np.arange(0, N)
k = 1
Z_ks = x_n * np.exp(-2*1J*np.pi*k*ns/N)
fig, axes = plt.subplots(1, 2)
axes[0].scatter(ns, x_n, marker='o', c=colors)
axes[0].set_title('Input sequence, neach sample has a specific color')
for i, x in enumerate(x_n):
_Zx = 1J * x
plot_vector(_Zx, axes[0], arrowstyle="->", ls='--', color=colors[i], orig=i)
pretty_ax(axes[1], b=2)
def plot_sum_vector(Z_ks, ax, colors=colors):
cumsum = 0
for Zk, color in zip(Z_ks, colors):
plot_vector(Zk, ax=ax, color=color, arrowstyle='->', ls='--')
plot_vector(Zk, orig=cumsum, ax=ax, color=color)
cumsum += Zk
plot_vector(cumsum, ax=ax, color="red", alpha=0.5)
plot_sum_vector(Z_ks, axes[1])
axes[1].set_title('Fourier coefficient X[k=1]')
fig.tight_layout()

On the left is the input sequence for which we want to compute the Fourier transform coefficients, X[k] for k=0 to N−1. To better understand what's happening, a color is used for each sample.
On the right, all the vectors Zkn for k=1 are represented, each with a corresponding color. Notice that the length of vector Zkn is given by x[n]. These vectors are plotted twice: once starting from (0,0), and once where they are added cumulatively. The total sum corresponds to the end of the yellow arrow, which is equal to the red vector. This red vector represents the Fourier transform coefficient X[k=1].
This way, we get the vector representing the k-th coefficient of the Fourier transform: in other words, we just computed the value of X[k]. We can use the same steps to compute each coefficient, looping over k.
In the example below, we compute the little vector families for all k=0 to N−1. For each value of k, we plot all the small vectors Zks, the cumulative vector sum, as well as the final total sum in red. This way, the final red arrow represents the value of the Fourier transform for that value of k: again, it is just a complex number with a certain module and a certain phase angle. In other words, the red arrow for any k is just the vector representation of X[k].
Finally, we check that our ‘hand' computation of the Fourier coefficients leads to the same values as numpy's.
fig, axes = plt.subplots(2, 5, figsize=(17,9), sharex=True, sharey=True)
#fig.subplots_adjust(top=0.95, bottom=0.3, left=0.05, right=0.95)
#ax_X = fig.add_axes([0.17, 0.05, 0.7, 0.2])
ks = np.arange(N)
X_ks = []
for k, ax in zip(ks, axes.flat):
Z_ks = x_n * np.exp(-2*1J*np.pi*k*ns/N)
pretty_ax(ax, b=6)
plot_sum_vector(Z_ks, ax)
ax.set_title(f'k={k}')
# store the average all of the Zks for that value of k : this IS the fourier
# coefficient for index k
X_ks.append(Z_ks.mean())
X_ks = np.array(X_ks)
fig.tight_layout()
# Finaly, we can check that the Fourier-coefficients we computed are identical
# to those computed by numpy. We use norm='forward' so numpy uses the 1/N scale
# convention.
assert np.allclose(X_ks, np.fft.fft(x_n, norm='forward'))

And that's it : the Fourier coefficient X[k] is given by the red arrow, for each value of k.
Few things to notice :
- For k=0, all vectors are aligned with each other on the x-axis, hence they add up exactly, as the sum of real values.
- For k=1 and k=9, the colored vectors neatly add up giving a complex sum vector.
- For other values of k, the little vectors basically cancel each others.
Now that we know how to visually compute each element of the Fourier transform, the end result is the whole discrete Fourier transform sequence, which is another sequence of complex numbers. In the next part, we are going to plot this whole complex sequence.
Plotting the Fourier Transform Sequence
Now that we know how the Fourier-transform coefficients X[k] are computed, both mathematically and visually, the result is a sequence of complex numbers X[k], which we can plot in various ways:
- Plot the real and imaginary parts.
- Plot the module and phase angle.
- Plot the 2D vectors, along a third axis representing the index kk.
Using the numpy functions seen above, we can easily extract all these features and plot them.
fig = plt.figure()
ax = fig.add_subplot(121, projection='3d', proj_type = 'ortho')
Zs = ks
Xs = X_ks.real
Ys = X_ks.imag
ax.plot(Xs, Ys, Zs, "--", color='red')
ax.scatter(Xs, Ys, Zs, '-o', color='red')#colors)
ax.set_xlabel('Real')
ax.set_ylabel('Imag')
ax.set_zlabel('k')
ax.set_title("Fourier coefficients X[k]")
ax.quiver(np.zeros_like(Xs), np.zeros_like(Xs), ks,
Xs, Ys, np.zeros_like(Xs),
colors='red',#colors,
arrow_length_ratio = 0.3, lw=3)
zmin, zmax = ax.get_zlim()
ax.plot([0,0], [0,0], [zmin, zmax], color="r", alpha=0.5, ls="--")
ax = fig.add_subplot(322)
ax.plot(x_n, '-o')
ax.set_title("Input sequence")
ax = fig.add_subplot(324)
ax.plot(ks, X_ks.real, '--o', color="r", label="real")
ax.plot(ks, X_ks.imag, '--o', color="g", label='imag')
ax.set_title('Real and imaginary parts')
ax.set_xlabel("k")
ax.legend()
ax = fig.add_subplot(326)
ax.plot(ks, np.abs(X_ks), '--o', color="r", label="mod")
ax.plot(ks, np.angle(X_ks), '--o', color="g", label='angle')
ax.set_title('Module and phase angle')
ax.set_xlabel("k")
ax.legend()
fig.tight_layout()

- First up is the 3D plot: the z-axis represents each index of the Fourier-transform sequence k in X[k]. On each of those z=kz=k XY-planes, the vector X[k] is plotted, with the real part on the x-axis and the imaginary part on the y-axis. That being said, notice how the X[k] coefficients are almost purely real, with vectors almost aligned with the y-axis, indicating a very small imaginary part. This is because our input sequence is almost perfectly symmetric, hence its Fourier-transform is almost purely real (that's a property we'll explore in-depth another time).
- Second is the Real and Imaginary part plot: Another way to visualize a Fourier-transform sequence is to plot its real and imaginary parts. This is pretty much the same as the values you'll see if you align the 3D plot view either to the x-axis or the y-axis. Although it's easier to understand intuitively and compare with the 3D plot, it isn't used that much.
- Finally, the Module and Phase angle plot: Remember that the module is literally the length of each vector, and the phase angles represent their directions. The module values are easier to understand from the 3D plot, as it simply gives the length of the vectors, but not so much from the real/imaginary plot. Regarding the phase angles,
Wrapup
Here are the key points you should absolutely remember:
- Complex numbers are just vectors, with a real part on the x-axis and an imaginary part on the y-axis.
- They behave like vectors: you know what adding and scaling vectors mean, so you know how it acts on complex numbers as well.
- You can use various representations/decompositions of a complex number: as a 2D vector, or extract its real/imaginary part, its length with its module, and its direction with its phase angle.
- The Fourier-transform is a sequence of complex numbers: each of these complex numbers is itself a sum (or average) of a sequence of other complex numbers with modules (i.e., length) x[n]x[n] and phase angles (i.e., direction) −2πkn/N:

with

If you liked this post, check out my other posts: I usually try to explain concepts using simple numpy and maplotlib code:
One-sample t-test, visually explained
PCA/LDA/ICA : a components analysis algorithms comparison
PCA-whitening vs ZCA-whitening : a numpy 2d visual
300-times faster resolution of Finite-Difference Method using numpy