Skip to content

Commit d8b5058

Browse files
committed
Added tail dependency to copulas. Added seasonal decomposition to times series class. Fixed some errors in docs.
1 parent 3b902f8 commit d8b5058

23 files changed

Lines changed: 641 additions & 276 deletions

Numerics/Data/Time Series/TimeSeries.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
using Numerics.Data.Statistics;
3232
using Numerics.MachineLearning;
33+
using Numerics.Mathematics;
3334
using Numerics.Sampling;
3435
using System;
3536
using System.Collections.Generic;
@@ -1019,6 +1020,101 @@ public TimeSeries MovingSum(int period)
10191020
return timeSeries;
10201021
}
10211022

1023+
/// <summary>
1024+
/// Performs classical additive seasonal decomposition using FFT-based seasonal extraction.
1025+
/// </summary>
1026+
/// <param name="period">The seasonal period (e.g., 12 for monthly data with annual seasonality).</param>
1027+
/// <returns>
1028+
/// A tuple containing:
1029+
/// <list type="bullet">
1030+
/// <item><description>Trend: The trend component as a TimeSeries (moving average with the given period).</description></item>
1031+
/// <item><description>Seasonal: The seasonal component as a double array of length equal to the original series.</description></item>
1032+
/// <item><description>Residual: The residual component as a TimeSeries (defined where the trend is defined).</description></item>
1033+
/// </list>
1034+
/// </returns>
1035+
/// <exception cref="ArgumentException">Thrown when the period is less than 2 or the series contains fewer than 2 complete periods.</exception>
1036+
public (TimeSeries Trend, double[] Seasonal, TimeSeries Residual) SeasonalDecompose(int period)
1037+
{
1038+
if (period < 2)
1039+
throw new ArgumentException("Period must be at least 2.", nameof(period));
1040+
if (Count < 2 * period)
1041+
throw new ArgumentException("Time series must contain at least 2 complete periods.", nameof(period));
1042+
1043+
SortByTime();
1044+
int n = Count;
1045+
1046+
// Step 1: Compute trend via moving average
1047+
var trend = MovingAverage(period);
1048+
1049+
// Step 2: Build full-length arrays and detrend
1050+
double[] values = new double[n];
1051+
double[] trendFull = new double[n];
1052+
bool[] hasTrend = new bool[n];
1053+
1054+
for (int i = 0; i < n; i++)
1055+
values[i] = this[i].Value;
1056+
1057+
// Map trend values to original indices
1058+
// MovingAverage outputs (Count - period + 1) values starting at index (period - 1)
1059+
int trendStart = period - 1;
1060+
for (int i = 0; i < trend.Count; i++)
1061+
{
1062+
trendFull[trendStart + i] = trend[i].Value;
1063+
hasTrend[trendStart + i] = true;
1064+
}
1065+
1066+
// Create detrended series
1067+
double[] detrended = new double[n];
1068+
for (int i = 0; i < n; i++)
1069+
detrended[i] = hasTrend[i] ? values[i] - trendFull[i] : 0.0;
1070+
1071+
// Step 3: FFT-based seasonal extraction
1072+
// Pad to power of 2 for FFT
1073+
int fftLength = (int)Math.Pow(2, Math.Ceiling(Math.Log(n, 2)));
1074+
if (fftLength < n) fftLength *= 2;
1075+
double[] fftData = new double[fftLength];
1076+
Array.Copy(detrended, fftData, n);
1077+
1078+
// Forward FFT
1079+
Fourier.RealFFT(fftData);
1080+
1081+
// Identify harmonic bins of the seasonal frequency
1082+
// Harmonic h corresponds to frequency bin k = round(h * fftLength / period)
1083+
var harmonicBins = new HashSet<int>();
1084+
for (int h = 1; ; h++)
1085+
{
1086+
int k = (int)Math.Round((double)h * fftLength / period);
1087+
if (k <= 0 || k >= fftLength / 2) break;
1088+
harmonicBins.Add(k);
1089+
}
1090+
1091+
// Keep only harmonic frequencies
1092+
double[] filtered = new double[fftLength];
1093+
foreach (int k in harmonicBins)
1094+
{
1095+
filtered[2 * k] = fftData[2 * k];
1096+
filtered[2 * k + 1] = fftData[2 * k + 1];
1097+
}
1098+
1099+
// Inverse FFT
1100+
Fourier.RealFFT(filtered, true);
1101+
1102+
// Scale by 2/fftLength as required by the RealFFT inverse transform
1103+
double[] seasonal = new double[n];
1104+
for (int i = 0; i < n; i++)
1105+
seasonal[i] = filtered[i] * 2.0 / fftLength;
1106+
1107+
// Step 4: Compute residual
1108+
var residual = new TimeSeries(TimeInterval);
1109+
for (int i = 0; i < n; i++)
1110+
{
1111+
if (hasTrend[i])
1112+
residual.Add(new SeriesOrdinate<DateTime, double>(this[i].Index, values[i] - trendFull[i] - seasonal[i]));
1113+
}
1114+
1115+
return (trend, seasonal, residual);
1116+
}
1117+
10221118
/// <summary>
10231119
/// Shift all of the dates to match the new start date.
10241120
/// </summary>

Numerics/Distributions/Bivariate Copulas/AMHCopula.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,18 @@ public override double[] InverseCDF(double u, double v)
182182
return [u, v];
183183
}
184184

185+
/// <summary>
186+
/// Gets the upper tail dependence coefficient λ_U = 0.
187+
/// The AMH copula has no tail dependence.
188+
/// </summary>
189+
public override double UpperTailDependence => 0.0;
190+
191+
/// <summary>
192+
/// Gets the lower tail dependence coefficient λ_L = 0.
193+
/// The AMH copula has no tail dependence.
194+
/// </summary>
195+
public override double LowerTailDependence => 0.0;
196+
185197
/// <inheritdoc/>
186198
public override BivariateCopula Clone()
187199
{

Numerics/Distributions/Bivariate Copulas/Base/BivariateCopula.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ public bool ParametersValid
119119
/// <inheritdoc/>
120120
public abstract string ShortDisplayName { get; }
121121

122+
/// <inheritdoc/>
123+
public abstract double UpperTailDependence { get; }
124+
125+
/// <inheritdoc/>
126+
public abstract double LowerTailDependence { get; }
127+
122128
#endregion
123129

124130
#region Methods

Numerics/Distributions/Bivariate Copulas/Base/IBivariateCopula.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ public interface IBivariateCopula : IDistribution
142142
/// <param name="v">Probability between 0 and 1.</param>
143143
double[] InverseCDF(double u, double v);
144144

145+
/// <summary>
146+
/// Gets the upper tail dependence coefficient λ_U.
147+
/// </summary>
148+
double UpperTailDependence { get; }
149+
150+
/// <summary>
151+
/// Gets the lower tail dependence coefficient λ_L.
152+
/// </summary>
153+
double LowerTailDependence { get; }
154+
145155
/// <summary>
146156
/// Generate random values of a distribution given a sample size.
147157
/// </summary>

Numerics/Distributions/Bivariate Copulas/ClaytonCopula.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,24 @@ public override double[] InverseCDF(double u, double v)
160160
return [u, v];
161161
}
162162

163+
/// <summary>
164+
/// Gets the upper tail dependence coefficient λ_U = 0.
165+
/// The Clayton copula has no upper tail dependence.
166+
/// </summary>
167+
public override double UpperTailDependence => 0.0;
168+
169+
/// <summary>
170+
/// Gets the lower tail dependence coefficient λ_L = 2^(-1/θ).
171+
/// </summary>
172+
public override double LowerTailDependence
173+
{
174+
get
175+
{
176+
if (Theta <= 0.0) return 0.0;
177+
return Math.Pow(2.0, -1.0 / Theta);
178+
}
179+
}
180+
163181
/// <inheritdoc/>
164182
public override BivariateCopula Clone()
165183
{

Numerics/Distributions/Bivariate Copulas/FrankCopula.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,18 @@ public override double[] InverseCDF(double u, double v)
183183
return [u, v];
184184
}
185185

186+
/// <summary>
187+
/// Gets the upper tail dependence coefficient λ_U = 0.
188+
/// The Frank copula has no tail dependence.
189+
/// </summary>
190+
public override double UpperTailDependence => 0.0;
191+
192+
/// <summary>
193+
/// Gets the lower tail dependence coefficient λ_L = 0.
194+
/// The Frank copula has no tail dependence.
195+
/// </summary>
196+
public override double LowerTailDependence => 0.0;
197+
186198
/// <inheritdoc/>
187199
public override BivariateCopula Clone()
188200
{

Numerics/Distributions/Bivariate Copulas/GumbelCopula.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ public override double[] InverseCDF(double u, double v)
159159
return [u, v];
160160
}
161161

162+
/// <summary>
163+
/// Gets the upper tail dependence coefficient λ_U = 2 - 2^(1/θ).
164+
/// </summary>
165+
public override double UpperTailDependence
166+
{
167+
get
168+
{
169+
return 2.0 - Math.Pow(2.0, 1.0 / Theta);
170+
}
171+
}
172+
173+
/// <summary>
174+
/// Gets the lower tail dependence coefficient λ_L = 0.
175+
/// The Gumbel copula has no lower tail dependence.
176+
/// </summary>
177+
public override double LowerTailDependence => 0.0;
178+
162179
/// <inheritdoc/>
163180
public override BivariateCopula Clone()
164181
{

Numerics/Distributions/Bivariate Copulas/JoeCopula.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,23 @@ public override double[] InverseCDF(double u, double v)
161161
return [u, v];
162162
}
163163

164+
/// <summary>
165+
/// Gets the upper tail dependence coefficient λ_U = 2 - 2^(1/θ).
166+
/// </summary>
167+
public override double UpperTailDependence
168+
{
169+
get
170+
{
171+
return 2.0 - Math.Pow(2.0, 1.0 / Theta);
172+
}
173+
}
174+
175+
/// <summary>
176+
/// Gets the lower tail dependence coefficient λ_L = 0.
177+
/// The Joe copula has no lower tail dependence.
178+
/// </summary>
179+
public override double LowerTailDependence => 0.0;
180+
164181
/// <inheritdoc/>
165182
public override BivariateCopula Clone()
166183
{

Numerics/Distributions/Bivariate Copulas/NormalCopula.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ public override double[] InverseCDF(double u, double v)
190190
return [u, v];
191191
}
192192

193+
/// <summary>
194+
/// Gets the upper tail dependence coefficient λ_U = 0.
195+
/// The Normal copula has no upper tail dependence.
196+
/// </summary>
197+
public override double UpperTailDependence => 0.0;
198+
199+
/// <summary>
200+
/// Gets the lower tail dependence coefficient λ_L = 0.
201+
/// The Normal copula has no lower tail dependence.
202+
/// </summary>
203+
public override double LowerTailDependence => 0.0;
204+
193205
/// <inheritdoc/>
194206
public override BivariateCopula Clone()
195207
{

Numerics/Distributions/Bivariate Copulas/StudentTCopula.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,19 +365,19 @@ public override double[] InverseCDF(double u, double v)
365365
}
366366

367367
/// <summary>
368-
/// Gets the symmetric tail dependence coefficient λ = λ_L = λ_U.
368+
/// Gets the upper tail dependence coefficient λ_U.
369369
/// </summary>
370370
/// <remarks>
371371
/// <para>
372372
/// The t-copula has symmetric upper and lower tail dependence:
373373
/// <code>
374-
/// λ = 2 · t_{ν+1}(-√((ν+1)(1-ρ)/(1+ρ)))
374+
/// λ_U = λ_L = 2 · t_{ν+1}(-√((ν+1)(1-ρ)/(1+ρ)))
375375
/// </code>
376376
/// where t_{ν+1} is the CDF of the univariate Student's t with ν+1 degrees of freedom.
377377
/// For ρ = -1, λ = 0. For ρ = 1, λ = 1. As ν → ∞, λ → 0 (Normal copula limit).
378378
/// </para>
379379
/// </remarks>
380-
public double TailDependence
380+
public override double UpperTailDependence
381381
{
382382
get
383383
{
@@ -393,6 +393,12 @@ public double TailDependence
393393
}
394394
}
395395

396+
/// <summary>
397+
/// Gets the lower tail dependence coefficient λ_L.
398+
/// The t-copula has symmetric tail dependence, so λ_L = λ_U.
399+
/// </summary>
400+
public override double LowerTailDependence => UpperTailDependence;
401+
396402
/// <inheritdoc/>
397403
public override BivariateCopula Clone()
398404
{

0 commit comments

Comments
 (0)