0

python Matplotlib's "specgram" display of a heatmap showing frequency (y-axis) vs. time (x-axis) is useful for time series analysis, but I would like to have the y-axis displayed in terms of Period (= 1/frequency), rather than frequency. I am still asking if anyone has a complete working solution to achieve this?

The immediately following python code generates the author's original plot using "specgram" and (currently commented out) a comparison with the suggested solution that was offered using "mlab.specgram". This suggested solution succeeds with the easy conversion from frequency to period = 1/frequency, but does not generate a viable plot for the authors example.

from __future__ import division
from datetime import datetime
import numpy as np
from pandas import DataFrame, Series
import pandas.io.data as web
import pandas as pd
from pylab import plot,show,subplot,specgram
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt

################################################
# obtain data:
ticker = "SPY"
source = "google"
start_date = datetime(1999,1,1)
end_date = datetime(2012,1,1)
qt = web.DataReader(ticker, source, start_date, end_date)
qtC = qt.Close

################################################
data = qtC
fs = 1          # 1 sample / day
nfft = 128

# display the time-series data
fig = plt.figure()
ax1 = fig.add_subplot(311)
ax1.plot(range(len(data)),data)

#----------------

# Original version
##################

# specgram (NOT mlab.specgram) --> gives direct plot, but in Frequency space (want plot in Period, not freq).
ax2 = fig.add_subplot(212)
spec, freq, t = specgram(data, NFFT=nfft, Fs=fs, noverlap=0)

 #----------------
"""
# StackOverflow version (with minor changes to axis titles)
########################

# calcuate the spectrogram
spec, freq, t = mlab.specgram(data, NFFT=nfft, Fs=fs, noverlap=0)

# calculate the bin limits in time (x dir)
# note that there are n+1 fence posts
dt = t[1] - t[0]
t_edge = np.empty(len(t) + 1)
t_edge[:-1] = t - dt / 2.
# however, due to the way the spectrogram is calculates, the first and last bins 
# a bit different:
t_edge[0] = 0
t_edge[-1] = t_edge[0] + len(data) / fs

# calculate the frequency bin limits:
df = freq[1] - freq[0]
freq_edge = np.empty(len(freq) + 1)
freq_edge[:-1] = freq - df / 2.
freq_edge[-1] = freq_edge[-2] + df

# calculate the period bin limits, omit the zero frequency bin
p_edge = 1. / freq_edge[1:]

# we'll plot both
ax2 = fig.add_subplot(312)
ax2.pcolormesh(t_edge, freq_edge, spec)
ax2.set_ylim(0, fs/2)
ax2.set_ylabel('freq.[day^-1]')

ax3 = fig.add_subplot(313)
# note that the period has to be inverted both in the vector and the spectrum,
# as pcolormesh wants to have a positive difference between samples 
ax3.pcolormesh(t_edge, p_edge[::-1], spec[:0:-1])
#ax3.set_ylim(0, 100/fs)
ax3.set_ylim(0, nfft)
ax3.set_xlabel('t [days]')
ax3.set_ylabel('period [days]')
"""
TonyMorland
  • 103
  • 7

1 Answers1

0

If you are only asking how to display the spectrogram differently, then it is actually rather straightforward.

One thing to note is that there are two functions called specgram: matplotlib.pyplot.specgram and matplotlib.mlab.specgram. The difference between these two is that the former draws a spectrogram wheras the latter only calculates one (and that's what we want).

The only slightly tricky thing is to calculate the colour mesh rectangle edge positions. We get the following from the specgram:

  • t: centerpoints in time
  • freq: frequency centers of the bins

For the time dimension it is easy to calculate the bin limits by the centers:

  • t_edge[n] = t[0] + (n - .5) * dt, where dt is the time difference of two consecutive bins

It would be similarly simple for frequencies:

  • f_edge[n] = freq[0] + (n - .5) * df

but we want to use the period instead of frequency. This makes the first bin unusable, and we'll have to toss the DC component away.

A bit of code:

import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
import numpy as np

# create some data: (fs = sampling frequency)
fs = 2000.
ts = np.arange(10000) / fs
sig = np.sin(500 * np.pi * ts)
sig[5000:8000] += np.sin(200 * np.pi * (ts[5000:8000] + 0.0005 * np.random.random(3000)))

# calcuate the spectrogram
spec, freq, t = mlab.specgram(sig, Fs=fs)

# calculate the bin limits in time (x dir)
# note that there are n+1 fence posts
dt = t[1] - t[0]
t_edge = np.empty(len(t) + 1)
t_edge[:-1] = t - dt / 2.
# however, due to the way the spectrogram is calculates, the first and last bins 
# a bit different:
t_edge[0] = 0
t_edge[-1] = t_edge[0] + len(sig) / fs

# calculate the frequency bin limits:
df = freq[1] - freq[0]
freq_edge = np.empty(len(freq) + 1)
freq_edge[:-1] = freq - df / 2.
freq_edge[-1] = freq_edge[-2] + df

# calculate the period bin limits, omit the zero frequency bin
p_edge = 1. / freq_edge[1:]

# we'll plot both
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax1.pcolormesh(t_edge, freq_edge, spec)
ax1.set_ylim(0, fs/2)
ax1.set_ylabel('frequency [Hz]')

ax2 = fig.add_subplot(212)
# note that the period has to be inverted both in the vector and the spectrum,
# as pcolormesh wants to have a positive difference between samples 
ax2.pcolormesh(t_edge, p_edge[::-1], spec[:0:-1])
ax2.set_ylim(0, 100/fs)
ax2.set_xlabel('t [s]')
ax2.set_ylabel('period [s]')

This gives:

enter image description here

DrV
  • 22,637
  • 7
  • 60
  • 72
  • Conversion from frequency to period is exactly as required, but the suggested colormesh display does not work, as illustrated by the attached example now showing author's original code & figure vs. the suggested solution. – TonyMorland Jul 17 '14 at 04:02
  • @TonyMorland: I am afraid I do not quite follow you. What does "does not work" mean in this context? At least the `set_ylim` looks a bit odd, as the axis should be in time units. Or maybe the problem is that the plot should be linear but only the Y axis labels should reflect the period? – DrV Jul 17 '14 at 07:26
  • tks DrV. i can't insert a plot to show you, but if you try the commented & un-commented versions of the code now pasted below the original question, you will see the problem. – TonyMorland Jul 17 '14 at 11:23