Function Repository Resource:

IrregularPeriodogram

Source Notebook

Compute a periodogram for data from unevenly spaced intervals

Contributed by: Daniel Lichtblau

ResourceFunction["IrregularPeriodogram"][w,times,vals]

computes the irregular periodogram at frequency w for the given set of times and vals.

ResourceFunction["IrregularPeriodogram"][w,mat]

computes the irregular periodogram for the set of time and value pairs in the two-column matrix mat.

Details and Options

ResourceFunction["IrregularPeriodogram"] accepts a Method option.
Allowed Method values are "Deeming", "LombScargle", "LaflerKinman" and "StringLength", where the last two are equivalent.
The Deeming periodogram for times {t1,,tn} and corresponding values {v1,,vn} is given by .
Unlike the classical case, the Lomb-Scargle periodogram here does not normalize by the variance of the data values.
Strictly speaking, the "LaflerKinman" setting will not give a periodogram computation. Instead of computing a value based on a given frequency, it computes directly based on a putative period p. Small values indicate a short total length when folding by p along the time axis (that is, sorting the times modulo p and approximating the total length between consecutive time-folded points).

Examples

Basic Examples (10) 

Take random times and corresponding values for the function f(t)=sin(t)+cos(3t)/2:

In[1]:=
SeedRandom[11222333344444]
data = Sort[
   Map[{#, Sin[#] + Cos[3*#]/2} &, RandomReal[{0, 20}, 500]]];

Subtract the mean of the data values and plot the resulting time series:

In[2]:=
times = data[[All, 1]];
vals = data[[All, 2]] - Mean[data[[All, 2]]];
ListPlot[Transpose[{times, vals}]]
Out[2]=

Plot the periodogram computed from this unevenly spaced set of measurements:

In[3]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals], {w, .1,
   5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[3]=

Compute the larger frequency (the location of the larger spike):

In[4]:=
freq = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals], .7 <= w <= 1.3}, w]
Out[4]=

Compute the period from this frequency:

In[5]:=
per = 2 Pi/freq
Out[5]=

Plot the time series "folded" by this period (times are computed modulo the computed period) to see the approximately sinusoidal variation in the light curve:

In[6]:=
foldTimes[times_, period_] := period*FractionalPart[times/period]
In[7]:=
ListPlot[Transpose[{foldTimes[times, per], vals}]]
Out[7]=

The string-length approach also shows this periodicity:

In[8]:=
Plot[ResourceFunction["IrregularPeriodogram"][p, times, vals, Method -> "LaflerKinman"], {p, 1, 8}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"p", "\!\(\*SubscriptBox[\(P\), \(LK\)]\)(p)"}, ImageSize -> 400]
Out[8]=

One can get a fairly good estimate of the frequencies present using far fewer samples, so long as they are irregularly spaced in time. Take 20 random points from the preceding original set:

In[9]:=
SeedRandom[11222333344444]
data2 = Sort[RandomSample[data, 20]];
times2 = data2[[All, 1]];
vals2 = data2[[All, 2]] - Mean[data2[[All, 2]]];
ListPlot[Transpose[{times2, vals2}]]
Out[9]=

Plot the periodogram from this subset:

In[10]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times2, vals2], {w, .1, 5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[10]=

Estimate the period for the larger frequency:

In[11]:=
freq = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times2, vals2], .7 <= w <= 1.3}, w]
Out[11]=

Scope (2) 

Use a light curve time series from the Wolfram Demonstration Cepheid Variable Star Light Curve Analysis:

In[12]:=
data = CompressedData["
1:eJxVW3d4lNXTDT0UIQSQDu8KSmwQFKQIculNIYEAIQEJvQoJQUjPllQgPVlS
IQWkiFKVIuVSBCkKKD+pSlWa0qQktHx35u6ePB//hH2S3b1l5sw5Z+Y1TZo3
YmplFxeX0iouLvRz095/lvfYOVm+WPvg18+KDVkv/8PirTmTZZfE3gF/nTSk
m2+3rT99P07Sj4V7DLmxyo2lHqHjZEu/KR9fOmHI7cJ69fAjfxk9a9eCjLOG
nJH2b0SvTf7y24FXwhrONeSjl6dDbn3rJ0tHXluVF2PIwF8Sfv3T00/6qJcH
Cw35j+zWZsi9sTL2jvf2Vj8a8r/kkQvabhgrm3f4o231fYYcXGB+a3/3MVJ9
2/CkZYZMH/eq77Oy0fLtcSn7H9805IIBmU+qHxgtI3t1/nxnoiF71jpTEhjq
eF3TJF3Xt/Cb0mGULGymPjDOkGs6//zxsDIf+b+QW291/MmQBfVX7d63xQf7
u9Fy9c1BUT5SvXtwHashYyNObhnb0Qf7M7V+NGf0gxFykNret/GGbLR0yO2q
Id7SJo8W9X9iSLVqS1awt/z+P9q42v+c0ZHfPvGSvfgDTbK4v/pNiJc+j6om
ObJaSXr3B8MlLfPjS4b87qvQw2/fHiZD1fJc1xvyh3b1an/35FN9rl8asvlr
l99PvT9Yv/+gIc+4P+x94tUgae+7ZM6N54acnbsov3h2b3x+fnGbHS/KheTj
+9aQk+/69fnELKQ6PY9x6v7eogXc7io78MIN2bV0/5R1Lu3l+5sL7q2ubpLq
dCdkr39f0rH2rGWS0/93Y9a7T96V7dTqWuQaUqy8tKfui3b6vPYbcgC/oZ2k
X/utNOSSl1OfXq/0FvazdMfsmmWFb8rDQ1WEXTbkIK8XTYKftpHqdJtU/d2Q
5m0qoAa64u9XDRMNJvatLt9Uq+u7RMXXG3NCwt6sru8vwZC/uvxZuebi+3vX
HFIfuM9xHtPu9eLwvWrIl/GPLpyxlPf67bNJbr6h6nwbPT92d3Al4ccBZej4
+KGGcO53ndV7cIFwE0cW7lnRbJ4hy00vn3xZ6CaW31t9aOgix3rPNBAjtreK
vVPfJN1ubdtQ5UZzQae/9oEhpb/R+0FJC3Ff/XkXFX9dp/ebOe9+a9G06ui6
72825N07FPCGoPD1yDekV9Kyue5t3hK5OfTPkJu81A228BB0nJXvGHKMv8rQ
Vh6Cdj/nhiFfn/WZSpH2guNtoSGDKODvdxDOeK87fEPm1CaegrZzqLZJHvqi
k3mbS0cxpE7ld+PthqRVFLTuKLLnutvkYke8v/pAOM9vnYKBScaHwhkvLY/8
1Wtlvc5Cvfv1c78Y8rdd1xq2fLuzcMYf/768s+D4XurIj5Yficbnfnk6ssSQ
753O/bvl066CP9fNJLedogvrLpz5+r+a6kZLe+r1HlP3vXtf47fChWDcUftb
rdKh+2IhDqhwTVH5uKVO0Poltftgf0XTt9wOzuojnPkav3rM0Q/q9BXOeOXz
MfcTHD/HHPf9vD/Oj+PVdYCg435X5XPx1i6l+yMGiGBKuHJDvv3va3s80wYI
xrO7Kn4vzHCZVzZAn5fFcT/VB4o2F8qiZkUY8tyEhcNbJQ8U4Q3VO54Z8sdX
m8MfhgwSfP1/GBoX4wYJJ14sK0+Nev5skHil0OKtTYa8Nah9uem1wYKOf266
Az/rDhbz3Okd6r5UlP2aNFhw/p133FfpYMHhfMaQaVHPuwdWHypUNo2u+68h
W1HAvP6pPk/1fdU61f+ox8vPdHzfM+Q0+rdwGM5f/90wwXC4woG3rw0XTrz4
ZuafQYsXDkd83lRosKy3l2C8rmSSfz1s/eu8oV6C8/+6ITvmvNMkK8JLn2e0
ITlqzF64L/6eJV6IBxfawAsvHS+phlToWOmzqt74/ft9W9y5+ok3vm8nwUm4
t+C43mDIPp/cPLd+5wj9ep4jXg6MEE684Px7NkJw3lUxaVzpNRKff9YSPCBz
wEisl2EoYiTiKWHy+IsDjo3E+s83j5j4QW8ffd7fKLzenlT9/h4fwTh/2pDt
ChM3Xinz0XiRZsiY5T+vO1FplDjxlBLIkAc3tZ9b0nOU4Lr10pDjv6CIGSUY
jm2GpLLTZNco4M11m/qAKqOFs57Oux97xKfnaOQDx0/kaMH1Z60hW6jljRJj
BOPfDkf8ho4RK9Vuzlwz5IY31SfuGCPoOGxmQzIOlY3R8fzCkB6q/HzVzRfx
zX8nfIVC1zcuKDwdG9f19x9DfYE/vE6Lrz5vV5NkXNzhKxjfFf4MrBJSu630
1Xir6jHX8VJf8ZUKi40PVf38LibiZLkv8PP0u1ObXu06FnjEMNRrrHDyh9pU
MEPH6vs5qs7DUvXHlLKxwlnPEgj++/ppvL1oyC9UOf5+j59gPqDw3vfoBwc3
PfMTtJ2yKEMOp4Sp4i8G8AEasulV38bbuvlr/Ktr0nU90l98QYmY7ri/l/7I
zx67R9hN3ccJF/onHXi5Y5zOnxRDvrO2wcl+VccjP6gc3wwdj/vj/Ng+Xn9f
suJXqros7fq5zrevDUnlv7D0c8HntsaQF9V1WrpOEERHBl4x5O2qKsFDJgjm
U7GG5h3bJ4i7Eep/zxz5vGOC4OVWM8nmyz9/+1jpBJ1PCs/4x/gA/D3XDXOA
eEjwnW3oulsYoO9D4WG+urb5ewL0fR1R8b4/bvWYiwGoZ5fzFcF6HiCYvzw2
5PzFydcf5k3U8afqw/pvzk5YuHaijv9IxT/pQo9MRPxonjdR18/5huR1vJgo
jhWpBNut+KEK95euk1C/FwYphtR8kqjDB+iIh7cnCSdfaesRtXSH/yQxTKVD
t0eKT6jy8V3zyfo+fzWkOtyOf3tMRj72owD+eLKOh+0OPuM/WTAdUfyE6HKX
iMmoL8wv8iajvu5vrBjW4CmaPzw15LZJ0ed6jp+i70vFS909njnvzJiC/X+k
6Omp8CmCbjPstON13hTBOKDqrd/jHecX756CeAv76OtPxvScivxlejZ4qmD+
U+zA34SpOh7V7zkf86bi/jicmk/T56Xui+hfwIxpguvmRvX5q97r2yJ+msar
Cw5+ljdNOPlvjQnZnzb6aprOP7XfXlQQd00TTv7J53ZzGvKJ99FiusaPHc7z
nY77/F6l/xb/6Xp/Kp5JLlzOmy4+pH//Kb3QQTHiVdPFjyrsZpxSrynMf5gu
1CsVYQ48/HE68IHj/fl04eT/jBdTZwgnH+fzj5khmI+qv49qqRjbqhniFMHB
bUPrhuozEa81KWFqzgT/YX0SMxP4zPiycqZw8lXO87azNJ4fUPFfVu/Wtr6z
BOeNyt8QOuAps5C/np9+VWtA7Cydf2EOvlJlNvL5lQq/idVmC4LBpxmGPH73
yIE+fWdr/NzswNNJszVfUny8mbeqULbZmu/+bEhOi/2zBZ+bqt/8PVdnC6Zb
Cv8Izr+oPAf8he/HNAf1jvXW4DmaL9Uwafzv/QXqA+dV4Tzkj4kuZt88xCuF
q/+1eXo/Cn9UtX42ziUQ8aRhKFBw3VX8mODybIdAjT9Kfy0kgP4wEJ+/y55a
L6xfIPjtXEV3hVeg2Kfo1fdbVD4qOjclI1DHc6Aht45VCbQiUHC6v2aSfSgQ
NwXivvgc9gXqevW6SXaYW1Jp18lArH+k3ZT85emK9XK+XHH8vdIvU46PH/rT
/UB9fzsNyXlfPwh8jXl86yDxhK5H6bnn41TB7xCk+aTiM3w/I4Nw/jUXt558
d2IQ8jfJQxFSWxDqQ8Nehy5+lx6E+vZgslrAxiCdH2kOPrk3SOdbmSFJPSy4
GiTounYp/PuSiMu9IPBrvi+X+cgP5vfGfI13/6j46/vs3zsfzgd//in7B3tq
r/maT25w4F/f+eAXrGsfzNf4/50hh1LA/Ddf8xG1Hq7vr+aDjyp291EzEaz5
U0OTpO3FTAgWXPcVftbJWGv1NgcLXrfSI4z7tmDEA8mPyNRgjb+KD8S8pgRR
RjD0F9fZFcHgBw83KsFVEiyeqeNY8JshVfR3Mm8IFsyL/zbk372UwN0bjPrI
uHIpGPyL6P/ya8HAX+anD4J1PBEfozjuuwj8YZaiz8nDFwmn30J0ITlokeBz
V/yRlxm9CHjE15C6SHzKhozCs2PRg/YWLhKUZlsVn630Ge1wkXDq/3ebZJ2u
uXWRcPoVXB9OLEI9ZXy6vEjXa8V3VXRXc28QAr3D+NshBHyVcd87RPMFhT8E
90vnhQinnuZ7zghB/dq3ZuUwcSpE82uFd0Sv2t8LQXx/PvSnGj3LQ3T8LTDk
CALQqqG4j711VUWtF6r52CtD8r5HhIKv871MDMV5jaIvDAzV9XWb0g9U8NND
cd8cb8dCUY/Jbql0MhT5R3D75qWK76Nwv3QtFPhA5WDmvVDRmgBruSG7NFMK
vzwU9Z7lf5UwjQcKD2sPePPLyvXChNMf4/rZ2vFa8b/Ik1vGPv4wTHD1UPX1
8h614egw6B3232xhWg8GOfAhOQx80ZsMnowwfb9NTJrfF4UBnxqQ/VESBj3D
97shTOu/7w258/zil1P3hun9/eyoF7+EoT7yefwWpvnUIcU/VRh4XwrT9bvU
kd91w8HfiX49aRWuz1Pxu6u3sjbX6RCOeknwWa1XOPgM8+O+4Yg3tkeGhev6
vcuQi943tX40Nxz5yD5CcTj0ueUtRTBPhYufj6t/Cp+Ibh69Ei44PdT5Ff3e
6V7X++G6vt50vL9SBPZ/9ECfb2bWjcDfM/9sFSEU+nT447qjvrSPEPS2a1MN
7XN6RuA8mT/2igB/Zj3lFQH+odTwHxnREcBX/vu0CNQz/vsVEVrf/+qoR99G
gK9ynG+OQPwyL9gfgXgbdipt3KufI3DeH9CB/RoBvUl2Rc6lCPgTc4hg3KtY
L9Hv1i6RuO8GEw/89m69SPgzBB8zWkeCD3L+mCKBF0+rKwToEAn+mKHkeYx6
zetV+u3wTiWARST4Metx70jsl+87IBJ6LYoMwaBI1F+y91ZFR0IPtidCnhqJ
/bCeK4zEfpgXXooEP+DzuhcJ/kPl0+9VxX44/40o4BnfV68ojS82h14bFqX5
ksr/H1M6Lnp/QhTwkPcdHSWYF611fH5KFOKV8XN5FPKf8/dgFM6D/csTUdDP
eX+3XH3zehT4Gde/x1HwK94+5nZ4Z91o4fS/+b4bRYOvsV/SIRr6PvuPjPjY
FdHga+wX/BkNvsp+bXm0cPrNxNd7VTZDjxxQdManhhl8g9dRy6z5ptof+bIJ
7mbh9Ofp2j9obMZ69uxTgqedWetZdZ/XGyoG0dEMvS3/Wd7jve5mzW8Uv6Vy
crSXGfqG9G6dIWboTfqcAX5mzVeV/lXst2j0fLM+7wxdv5stMsN/WNB2WXnq
MjPOv/EgrxfZuWbwGcr3J0VmjZ/q89kvXW2G31aJ7OR1ZsQz48MGs8bjB6r+
qQsZuNOs9cgWzXd37Hd8v9K/7Pcertg/+evuZ8y4P67X583g98yHr5o1PrQw
sV895oZZ+8mbtP/f+bYZfI/57cOK/XI9flixP+rHDHxq1vXqT81fPqlnQfyy
X+BuAV/j+2xmEcyDlb4ju7Zda4u+jxgHf+xgEVwHlH5jv+QDC+KD2EfDLhaN
HycNSekU392i8VXhCdtmAy3gO1zGh1ugr+i8a42xaH9jtgPfPrfoel3LpP2o
SRYdP4cc9W+WRfOzr3S/5/5cC/gN+w8Wi9YDir/bN9cJWm+1gG8eVPD/Z5wF
+pb1WJpF8yn1/lpkR+dYoB8533ZYdD34y+Gf77TAD2X/eZ8Ffh3h2IEjFuA/
0e2VRy3C2a+in1+esgjWdeEOfL5i0fidaeh+0C2LYN/3b+1Xltyu2B/Vv3YP
HL//ytEvKrPoenhc19tYd6u+71MOPGlqBV8jPyK2mRX9D9a/JqvGy8omybD0
hhV+cWZ8rN9vHlbNt5Md/O8dK/gT6ayPOlrBz6h8vOFt1Xxd8TFVXaY2HWtF
fJF+jPOzar76g8M/mGgFfpAemDTTivxj/yPQCj507aNm8oJ67fTrXj758nJ+
qBX+GX2Nf5hVx6eLSfsFFivqi2JnuQlWK/gn+7EJ1or42NR+bpsiK/gIl4G1
VuQL48kmK/gM8aG5m63C2S/9dd6EGsN2WMFXJilCMm+nFfWfeJTPvorvG0+C
6pQV93snWH3iWSvy3bVn3qjWt6zwC4hOzH5o1fF7X+Gh2n79MqvO/+Nafw56
ZoXe4ntxsaGeEC8bU9kG/2UoGYY1bcA3uu+RtWyo16paxw6ub4O+p/bXmGY2
jQeKD7bd8SL+kWFDPHOcfGDD/fDnDna83qP7vUe9bKh3Sg3kjRrheL+Kh4tn
LMGHfG3AH39FIMb62cCHyA/wnWRDvSc/rNZMG+op8doRs2zwS37783LtBvNt
OG+ya6qH2qCfiI5Us9h0/L1y+E2pNuhp5qOFNvjpf6ry6rfWpv1NxdemJUwe
P26TDfdN+mb3dhv0DvevdtrQj4l+3j1w2Ekb6hvhdNezNvRT2F86bwO/ZT/h
sg38/mNVQJJu2uAvsH54YIPfzPymzKb14iqD87HYJQZ+Am/LNQZ6lfOySQz4
0KYrisB7xgiCuXs3HHqzdwz8XY6XQTE6ntX5sMz1ioEeJju10YgY8G3WSxNj
kG+MV0Ex4N9thoTbNoTEAH8Jj7usjUE/kf2FIzHAVyrvUScd61f5zfs6FSO4
zjRW+ksR7jpnYxAvHG/nYtBv5d9fiYHfljdqxdfvl8bAf6hx//VBJ8tioJ95
XqE8BvFIdGx7jVj4Q1Qfi9xiwY/XKvmbUT8W8UD9zK+axqKf0Wr1zUHtPWJx
voS79bxiEZ983t6xul5eNXS/yTcW+kLfWyz4N/GA7gmxyCe+v9RY+NXcj8yO
RXyxv14Yi/jn9a2J1furapLDqd2+MRb5zraujNV6McnRD/8pFvlC9XHFyVj0
I8h/XnguFvWd+lkf3IwVBEedlX4O9V3T+bPSWPBt7p9XitP7e6H7r31c48AP
Of7d4jTfa2CSvO0JcTjvS5drZ7wdFAd9zXqmMA7+Ptfrw3HwR8kfSTwVBz+J
rjfgbBz4BfX/Gt6Pw/6r1Fzc+lr9ePjbHLee8cgH9ju84+Ef3Fby90FAvMZP
d5M0qCDMjEf92a3kxZGgePA15pHmePjfzOcT47EexvPsePRPKE9NRfHIV/aT
1sajn8x+lIyHX8F+xNF41EfGj7Px2q/ZrevLilvxqC9cb10SoH+Y3yUkgD9S
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
IC3B
"];

Subtract the mean of the data values and plot the resulting time series:

In[13]:=
times = data[[All, 1]];
vals = data[[All, 2]] - Mean[data[[All, 2]]];
ListPlot[Transpose[{times, vals}]]
Out[13]=

Plot the periodogram computed from this unevenly spaced set of measurements:

In[14]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals], {w, .1,
   5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/days)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[14]=

Compute the main frequency as the location of the larger spike:

In[15]:=
freq = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals], 2. <= w <= 3}, w]
Out[15]=

Compute the period from this frequency:

In[16]:=
per = 2 Pi/freq
Out[16]=

Plot the time series "folded" by this period (times are computed modulo the computed period) to see the approximately sinusoidal variation in the light curve:

In[17]:=
ListPlot[Transpose[{foldTimes[times, per], vals}]]
Out[17]=

Plot the Lomb-Scargle periodogram for the same data:

In[18]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals, Method -> "LombScargle"], {w, .1, 5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/days)", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[18]=

The computed main frequency and period agree with the values from the Deeming periodogram:

In[19]:=
freqLS = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals, Method -> "LombScargle"], 2.5 <= w <= 3}, w];
perLS = 2 Pi/freqLS;
{freqLS, perLS}
Out[19]=

Plot the string-length values for the same data:

In[20]:=
Plot[ResourceFunction["IrregularPeriodogram"][p, times, vals, Method -> "LaflerKinman"], {p, .1, 5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"p (days)", "\!\(\*SubscriptBox[\(L\), \(LK\)]\)(p)"}, ImageSize -> 400]
Out[20]=

Compute the best estimate for the period based on the minimum value attained by the Lafler-Kinman computation for this data:

In[21]:=
perLK = NArgMin[{ResourceFunction["IrregularPeriodogram"][p, times, vals, Method -> "LaflerKinman"], 2. <= p <= 2.5}, p]
Out[21]=

A periodogram can often detect multiple frequencies provided they are well separated, even if they are not commensurate (that is, there is no actual “period” for the data). Extract values from such a function with frequencies at :

In[22]:=
SeedRandom[11222333344444]
data = Sort[
   Map[{#, Sin[#] - 2*Sin[E*#] Cos[Sqrt[2]*#] + Cos[Pi*#]/2} &, RandomReal[{0, 20}, 800]]];
times = data[[All, 1]];
vals = data[[All, 2]] - Mean[data[[All, 2]]];
ListPlot[Transpose[{times, vals}]]
Out[22]=

Plot the periodogram for this data:

In[23]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals], {w, .1,
   5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[23]=

There is one estimated frequency around midway between 1 and :

In[24]:=
NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals], .7 <= w <= 1.4}, w]
Out[24]=

Another is approximately π:

In[25]:=
NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals], 3 <= w <= 3.3}, w]
Out[25]=

A third is close to :

In[26]:=
NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals], 3.9 <= w <= 4.6}, w]
Out[26]=

Rough estimates can be made with far fewer sampled values:

In[27]:=
SeedRandom[11222333344444]
data = Sort[
   Map[{#, Sin[#] - 2*Sin[E*#] Cos[Sqrt[2]*#] + Cos[Pi*#]/2} &, RandomReal[{0, 20}, 20]]];
times = data[[All, 1]];
vals = data[[All, 2]] - Mean[data[[All, 2]]];
ListPlot[Transpose[{times, vals}]]
Out[27]=

Peaks of the periodogram give estimates of the frequencies:

In[28]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals], {w, .1,
   5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[28]=

Properties & Relations (2) 

When the data is equally spaced on the horizontal axis, the irregular periodogram is essentially the square of the Fourier transform. Modify a previous example to demonstrate this:

In[29]:=
SeedRandom[11222333344444]
times = Range[2, 20, .1];
data = Table[Sin[j] + Cos[3*j]/2 + .01*RandomReal[], {j, times}];

Subtract the mean of the data values and plot the resulting time series:

In[30]:=
vals = data - Mean[data];
ListPlot[vals]
Out[30]=

Plot the periodogram computed from this (evenly-) spaced set of measurements:

In[31]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals], {w, .1,
   5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[31]=

The square of the Fourier transform gives a similar result once frequencies are correctly rescaled on the horizontal axis:

In[32]:=
ftsqr = Abs[Fourier[vals]]^2;
ListPlot[ftsqr[[1 ;; 20]], DataRange -> {0, 2 Pi}, PlotRange -> All]
Out[32]=

In the Wolfram Language, this can also be computed using PeriodogramArray:

In[33]:=
ListPlot[PeriodogramArray[vals][[1 ;; 20]], DataRange -> {0, 2 Pi}, PlotRange -> All]
Out[33]=

Yet another way to obtain these absolute values is with the Fourier transform of the set of values convolved with itself:

In[34]:=
ftconvolved = Fourier[ListConvolve[vals, vals, {1, 1}, vals]]/Sqrt[Length[vals]];
ListPlot[Abs@ftconvolved[[1 ;; 20]], DataRange -> {0, 2 Pi}, PlotRange -> All]
Out[34]=

The Lafler-Kinman string-lengths-based measure can be made to more closely resemble an ordinary periodogram using reciprocals. Use a basic example:

In[35]:=
SeedRandom[11222333344444]
data = Sort[Map[{#, Sin[#] + Cos[3*#]/2} &, RandomReal[{0, 20}, 500]]];
times = data[[All, 1]];
vals = data[[All, 2]] - Mean[data[[All, 2]]];
Plot[irregPG[w, times, vals], {w, .1, 5}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}]
Out[35]=

Now again show the Lafler-Kinman plot using this data:

In[36]:=
Plot[irregPG[p, times, vals, Method -> "LaflerKinman"], {p, 1, 8}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"p", "\!\(\*SubscriptBox[\(P\), \(LK\)]\)(p)"}]
Out[36]=

It is possible to go from time to frequency using the formula f=2π/t, and likewise go from small to large on the vertical axis by taking reciprocals:

In[37]:=
FrequencyAmplitude[{time_, len_}] := {2*Pi/time, 1/len}

Generate and plot a list of {foldtime,magnitude} pairs:

In[38]:=
pairs = Table[{p, irregPG[p, times, vals, Method -> "LaflerKinman"]}, {p, 1., 15, 1/400}];
ListPlot[pairs]
Out[38]=

Now do the transformation and replot:

In[39]:=
ListPlot[Map[FrequencyAmplitude, pairs], PlotRange -> All]
Out[39]=

The main spike coincides with that of the Deeming periodogram. There is a small bump in the Deeming periodogram near frequency 3. For the Lafler-Kinman version, this corresponds to a fold time near 2.1:

In[40]:=
NSolve[2. Pi/t == 3]
Out[40]=

Compute a lot of values in the vicinity of this folding time:

In[41]:=
pairsNearby = Table[{p, irregPG[p, times, vals, Method -> "LaflerKinman"]}, {p, 1.85, 2.3, 1/10000}];
ListPlot[Map[FrequencyAmplitude, pairsNearby], PlotRange -> All]
Out[41]=

Applications (43) 

Sunspot activity cycles (10) 

Sunspot activity follows an approximately 11-year cycle. Download a century of monthly averaged values from the Wolfram Data Repository:

In[42]:=
monthsunspots = ResourceFunction["SpaceWeatherData"][{DateObject[{1918}], DateObject[{2018}]}, "MonthlySunspotNumber"];

Show the plot for this time series:

In[43]:=
DateListPlot[monthsunspots]
Out[43]=

Obtain the times and mean-centered values:

In[44]:=
tvpairs = monthsunspots["Path"];
tlist = tvpairs[[All, 1]];
vals = tvpairs[[All, 2]] - Mean[tvpairs[[All, 2]]];
times = N[(tlist - First[tlist])/(tlist[[2]] - First[tlist])];

Plot the Deeming periodogram for this time series:

In[45]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals, Method -> "Deeming"], {w, .001, .1}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/months)", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[45]=

Compute the maximum frequency and the corresponding estimated period in months:

In[46]:=
freqD = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals, Method -> "Deeming"], .045 <= w <= .06}, w];
perD = 2*Pi/freqD;
{freqD, perD}
Out[46]=

Plot the Lomb-Scargle periodogram for this time series:

In[47]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times, vals, Method -> "LombScargle"], {w, .001, .1}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/months)", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[47]=

Compute the maximum frequency and the corresponding estimated period in months:

In[48]:=
freqLS = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times, vals, Method -> "LombScargle"], .04 <= w <= .06}, w];
perLS = 2*Pi/freqLS;
{freqLS, perLS}
Out[48]=

Plot a list of string lengths versus folding period for this time series:

In[49]:=
ListPlot[lkvals = Table[ResourceFunction["IrregularPeriodogram"][p, times, vals, Method -> "LaflerKinman"], {p, 1, 300, .1}]]
Out[49]=

Compute the estimated period from the low point position:

In[50]:=
perLK = 1 + (FirstPosition[lkvals, Min[lkvals]][[1]] - 1)*.1
Out[50]=

All three estimates are in the ballpark of 10.5 years. Plot the time series epoch-folded by the second and third period estimates, as well as by the conventional 11 years:

In[51]:=
GraphicsRow[{ListPlot[Transpose[{foldTimes[times, perLS], vals}]], ListPlot[Transpose[{foldTimes[times, perLK], vals}]], ListPlot[Transpose[{foldTimes[times, 12*11], vals}]]}]
Out[51]=

Another cepheid variable star analysis (6) 

Download and process times and values of the light curve for the variable star known as cep2308:

In[52]:=
cep2308Idata = Import["http://ogledb.astrouw.edu.pl/~ogle/CVS/data/I/08/OGLE-LMC-CEP-2308.dat", "Table"][[All, 1 ;; 2]]; {times2308, ovalues2308} = Transpose[cep2308Idata]; times2308 = times2308 - times2308[[1]]; mean2308 = Mean[ovalues2308]; values2308 = ovalues2308 - mean2308;

Show a plot of measured amplitudes over time:

In[53]:=
ListPlot[Transpose[{times2308, values2308}], AxesLabel -> {"time (days)", "amplitude fluctuation"}, ImageSize -> 400]
Out[53]=

Plot the Deeming periodogram and compute the corresponding estimated period:

In[54]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times2308, values2308, Method -> "Deeming"], {w, .001, 3}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/days)", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[54]=
In[55]:=
freqD = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times2308, values2308], .8 <= w <= 1.3}, w];
perD = 2 Pi/freqD;
{freqD, perD}
Out[55]=

Plot the Lomb-Scargle periodogram and compute the corresponding estimated period:

In[56]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times2308, values2308, Method -> "LombScargle"], {w, .001, 3}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/days)", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[56]=
In[57]:=
freqLS = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, times2308, values2308, Method -> "LombScargle"], .8 <= w <= 1.3},
    w];
perLS = 2 Pi/freqLS;
{freqLS, perLS}
Out[57]=

Plot the Lafler-Kinman folded epoch total string lengths and compute the corresponding estimated period:

In[58]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, times2308, values2308, Method -> "LaflerKinman"], {w, .001, 20}, PlotPoints -> 300, PlotRange -> All, AxesLabel -> {"\[Omega] (radians/months)", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[58]=
In[59]:=
perLK = NArgMin[{ResourceFunction["IrregularPeriodogram"][p, times2308, values2308, Method -> "LaflerKinman"], 5.4 <= p <= 6}, p]
Out[59]=

The period estimates all agree to several places. Plot the time series folded by epoch using the second one:

In[60]:=
ListPlot[Transpose[{foldTimes[times2308, perLS] , values2308}], AxesLabel -> {"time (days)", "amplitude fluctuation"}, ImageSize -> 400]
Out[60]=

Searching for periodicity in a chromosome (12) 

Coding sections of genes are well known to exhibit a periodicity of three base pairs (bp). Often there are other periodicities. There is a method for finding them, using chromosome 12 from Saccharomyces cerevisiae (baker’s yeast) as an example:

In[61]:=
seq = Import[
   "ftp://ftp.ensembl.org/pub/release-81/fasta/saccharomyces_cerevisiae/dna/Saccharomyces_cerevisiae.R64-1-1.dna_rm.chromosome.XII.fa.gz"];
chromo = seq[[1]];

Literature on the subject states that it has a periodicity in the 10–11 range when looking at tetramers of the form A*T*, where the star here denotes zero or more occurrences of the preceding nucleotide (some specific values reported are 10, 10.2, 10.4 and 10.5). Split the chromosome into segments of 1000 bp and locate positions of the 4-mers "AAAA", "AAAT", "AATT", "ATTT" and "TTTT":

In[62]:=
chromos = StringSplit[chromo, "N"] /. "" :> Nothing;
sublengths = 1000;
subchromos = Flatten[Map[StringPartition[#, sublengths] &, chromos]];
at4 = "AAAA" | "AAAT" | "AATT" | "ATTT" | "TTTT";
posnlists = Map[StringPosition[#, at4][[All, 1]] &, subchromos];

While there are various ways to turn a gene sequence into a numerical sequence, here the irregular periodogram is instead employed on common gaps between the positions found:

In[63]:=
pdiffs = Flatten[
   Table[Outer[Subtract, posnsj, posnsj], {posnsj, posnlists}]];
pdiffs = DeleteCases[pdiffs, aa_ /; aa <= 0];
tallies = Tally[pdiffs];
talliesS = Sort[tallies];

Smaller counts are discarded if there are more than 500. They are further decimated by removing all gap counts that are smaller than at least one of their 19 subsequent neighbors:

In[64]:=
grouped = Partition[talliesS, 20, 1];
upper = 500;
groupedS = DeleteDuplicates[
   Map[First[SortBy[#, -#[[2]] &]] &, grouped[[2 ;; UpTo[upper]]]]];

Plot the number of gaps as a function of gap size:

In[65]:=
ListPlot[groupedS, AxesLabel -> {"gap", "bin size"}, ImageSize -> 400]
Out[65]=

Treat gap lengths as the "time" variable and gap counts as the dependent variable:

In[66]:=
{timesC, valsC} = Transpose[groupedS];
valsC = valsC - Mean[N@valsC];

A periodicity of 3 is already known and larger ones are sought, so plot up to a frequency range that will only give periods of at least 4:

In[67]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, timesC, valsC, Method -> "LombScargle"], {w, .3, 2*Pi/4}, PlotPoints -> 500, PlotRange -> All, AxesLabel -> {"\[Omega]", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[67]=

Isolate the large frequency near 0.6 to find a plausible periodicity value:

In[68]:=
freqLS = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, timesC, valsC, Method -> "LombScargle"], .5 <= w <= .7}, w];
perLS = 2*Pi/freqLS;
{freqLS, perLS}
Out[68]=

Homing in near 0.6 in the plot will show that there is a double peak, with the slightly larger one giving rise to a periodicity less than 10. This may be due to an interaction between the periodicity of 3 and the larger one. An attempt can be made to curtail this effect by repeating the periodogram computations on data that has had gaps that are multiples of 3 removed:

In[69]:=
{timesC, valsC} = Transpose[Select[groupedS, (! IntegerQ[#[[1]]/3]) &]];
valsC = valsC - Mean[N@valsC];
Plot[ResourceFunction["IrregularPeriodogram"][w, timesC, valsC, Method -> "LombScargle"], {w, .3, 2*Pi/4}, PlotPoints -> 500, PlotRange -> All, AxesLabel -> {"\[Omega]", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[69]=

Home in near 0.6:

In[70]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, timesC, valsC, Method -> "LombScargle"], {w, .4, .8}, PlotPoints -> 500, PlotRange -> All, AxesLabel -> {"\[Omega]", "\!\(\*SubscriptBox[\(P\), \(LS\)]\)(\[Omega])"}, ImageSize -> 400]
Out[70]=

Recompute the period estimate:

In[71]:=
freqLS = NArgMax[{ResourceFunction["IrregularPeriodogram"][w, timesC, valsC, Method -> "LombScargle"], .4 <= w <= .8}, w];
perLS = 2*Pi/freqLS;
{freqLS, perLS}
Out[71]=

This is in line with values reported in the literature. A string-length plot and optimization gives a value in the same vicinity:

In[72]:=
Plot[ResourceFunction["IrregularPeriodogram"][p, timesC, valsC, Method -> "LaflerKinman"], {p, 8, 11}, PlotRange -> All]
Out[72]=
In[73]:=
NArgMin[{ResourceFunction["IrregularPeriodogram"][p, timesC, valsC, Method -> "LaflerKinman"], 10 <= p <= 11}, p]
Out[73]=

Different settings will give slightly different estimates; for example, with sublengths set to 5000 and upper set to 2000, the estimate is around 10.28. So the method here is viable but only to low precision.

Breaking a Vigenère cipher (15) 

The Vigenère cipher is attacked from the Rosetta Code text example:

In[74]:=
text = StringDelete[
   "MOMUD EKAPV TQEFM OEVHP AJMII CDCTI FGYAG JSPXY ALUYM NSMYH
  VUXJE LEPXJ FXGCM JHKDZ RYICU HYPUS PGIGM OIYHF WHTCQ KMLRD
  ITLXZ LJFVQ GHOLW CUHLO MDSOE KTALU VYLNZ RFGBX PHVGA LWQIS
  FGRPH JOOFW GUBYI LAPLA LCAFA AMKLG CETDW VOELJ IKGJB XPHVG
  ALWQC SNWBU BYHCU HKOCE XJEYK BQKVY KIIEH GRLGH XEOLW AWFOJ
  ILOVV RHPKD WIHKN ATUHN VRYAQ DIVHX FHRZV QWMWV LGSHN NLVZS
  JLAKI FHXUF XJLXM TBLQV RXXHR FZXGV LRAJI EXPRV OSMNP KEPDT
  LPRWM JAZPK LQUZA ALGZX GVLKL GJTUI ITDSU REZXJ ERXZS HMPST
  MTEOE PAPJH SMFNB YVQUZ AALGA YDNMP AQOWT UHDBV TSMUE UIMVH
  QGVRW AEFSP EMPVE PKXZY WLKJA GWALT VYYOB YIXOK IHPDS EVLEV
  RVSGB JOGYW FHKBL GLXYA MVKIS KIEHY IMAPX UOISK PVAGN MZHPW
  TTZPV XFCCD TUHJH WLAPF YULTB UXJLN SIJVV YOVDJ SOLXG TGRVO
  SFRII CTMKO JFCQF KTINQ BWVHG TENLH HOGCS PSFPV GJOKM SIFPR
  ZPAAS ATPTZ FTPPD PORRF TAXZP KALQA WMIUD BWNCT LEFKO ZQDLX
  BUXJL ASIMR PNMBF ZCYLV WAPVF QRHZV ZGZEF KBYIO OFXYE VOWGB
  BXVCB XBAWG LQKCM ICRRX MACUO IKHQU AJEGL OIJHH XPVZW JEWBA
  FWAML ZZRXJ EKAHV FASMU LVVUT TGK", Whitespace];

Split the text into trigrams and find the positions of each, retaining all that appear at least twice:

In[75]:=
triads = StringPartition[text, 3, 1];
unique = Union[triads];
mposns = Map[Flatten, DeleteCases[Map[Position[triads, #] &, unique], aa_ /; Length[aa] < 2]]
Out[75]=

Find distances separating each given triad, and also retain the number of times a given distance appears (these lists will be used to assess the length of the key phrase):

In[76]:=
{permults, lens} = Transpose[
  Tally[Flatten[
    Map[Select[Flatten[Outer[Subtract, #, #]], # > 0 &] &, mposns]]]]
Out[76]=

Use a periodogram to guess the length of the key phrase:

In[77]:=
Plot[ResourceFunction["IrregularPeriodogram"][w, permults, lens], {w, 0, 1.5}, PlotPoints -> 400, PlotRange -> All,
 AxesLabel -> {"\[Omega]", "\!\(\*SubscriptBox[\(P\), \(D\)]\)(\[Omega])"}, ImageSize -> 400]
Out[77]=

Find the period corresponding to the highest peak:

In[78]:=
2 Pi/NArgMax[{ResourceFunction["IrregularPeriodogram"][w, permults, lens], .5 <= w <= 1.}, w]
Out[78]=

Find the period corresponding to the highest peak at lower frequency:

In[79]:=
2*Pi/NArgMax[{ResourceFunction["IrregularPeriodogram"][w, permults, lens], .1 <= w <= .7}, w]
Out[79]=

The key length guess of 14 is used because, in the worst-case scenario, it simply means a key phrase that is repeated is derived. First, list the most frequent letters in English in order of descending frequencies:

In[80]:=
topchars = ToCharacterCode[{"ETARIONSH"}][[1]];

A "zero" value is also needed for character codes, so as to translate "A" to have the value 1:

In[81]:=
anumminus = ToCharacterCode["A"][[1]] - 1;

A scoring heuristic is required to guess what the Caesar cipher shift is for each of the 14 letter subsets. It is based on numbers and positions of the most frequent letters in each subset that would, given a particular shift, map to letters that actually have the highest frequencies in English:

In[82]:=
score[startchar_, l1_, letters_] := Module[
  {diff = startchar - ToCharacterCode[l1][[1]], newletters, s1, s2, badposns},
  newletters = anumminus + Mod[Flatten[ToCharacterCode[letters]] - anumminus + diff, 26, 1];
  s2 = Map[Position[Complement[topchars, {startchar}], #] &, newletters];
  s1 = Range[Length[s2]] . Flatten[(s2 /. {} -> 0)];
  badposns = Flatten[Position[s2, {}]];
  10*Total[15 - badposns] + s1
  ]
scores[l1_, letters_] := Map[score[#, l1, letters] &, topchars]

Code is also required to translate (decode) characters given a shift value:

In[83]:=
diff[{posn_, letter_}] := ToCharacterCode[letter][[1]] - topchars[[posn]]
translate[nums_, diff_] := FromCharacterCode[anumminus + Mod[nums - diff - anumminus, 26, 1]]

Break the coded text into 14 distinct subsets; each will be treated as a Caesar cipher encoding. Find the most commonly appearing letters in each subset in order of descending frequency:

In[84]:=
caesars = Transpose[Partition[Characters[text], 14]];
caesarnums = Map[ToCharacterCode[#] &, caesars];
tops = Map[Take[ReverseSortBy[Tally[#], Last], 8][[All, 1]] &, caesars]
Out[84]=

Find the top-scoring shift translation for each of the 14 cases:

In[85]:=
caesarscores = Map[scores[First[#], Rest[#]] &, tops];
posns = Flatten[Map[TakeSmallest[# -> "Index", 1] &, caesarscores]]
Out[85]=

Compute the corresponding shifts:

In[86]:=
caesardiffs = Map[diff, Transpose[{posns, tops[[All, 1]]}]]
Out[86]=

Use the shifts found to translate the key (this is not actually necessary for translating the encoded message):

In[87]:=
FromCharacterCode[Mod[caesardiffs + 1, 26, 1] + anumminus]
Out[87]=

Translate the encoded message:

In[88]:=
StringJoin @@ Flatten[Transpose[
   Map[translate[#[[1]], #[[2]]] &, Transpose[{caesarnums, caesardiffs}]]]]
Out[88]=

Version History

  • 3.0.0 – 07 August 2020
  • 2.0.0 – 11 November 2019
  • 1.0.0 – 22 October 2019

Related Resources

License Information