Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SigmaSignificance / example structure / logistic map fix #46

Merged
merged 6 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ Manifest.toml
*.scss
*.css
*style.jl
docs/src/examples.md
docs/src/examples/*.md
11 changes: 9 additions & 2 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ using TransitionsInTimeseries, Statistics, StatsBase

using Literate

Literate.markdown("src/examples.jl", "src"; credit = false)
# process examples and add then in a sidebar column
example_files = readdir(joinpath(@__DIR__, "src", "examples"))
example_pages = String[]
for file in example_files
mkdownname = splitext(file)[1]*".md"
Literate.markdown("src/examples/$(file)", "src/examples"; credit = false)
push!(example_pages, "examples/$(mkdownname)")
end

pages = [
"index.md",
"tutorial.md",
"api.md",
"examples.md",
"Examples" => example_pages,
]

import Downloads
Expand Down
72 changes: 38 additions & 34 deletions docs/src/examples.jl → docs/src/examples/logistic.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# # Examples for TransitionsInTimeseries.jl

# ## Permutation entropy for dynamic regime changes
# # Permutation entropy for dynamic regime changes

# Permutation entropy is used frequently to detect a transition between
# one dynamic regime to another. It is useful when the mean and std. of the
Expand All @@ -12,28 +10,28 @@
# are arguably better suitable in such an application than the [Tutorial](@ref)'s default
# of significance via random Fourier surrogates.

# ### Logistic map timeseries
# ## Logistic map timeseries

# A simple example of this is transitions from periodic to weakly chaotic to chaotic
# motion in the logistic map. First, let's generate a timeseries of the logistic map

using DynamicalSystemsBase
using CairoMakie

logistic_rule(u, p, t) = @inbounds SVector(p[1]*u[1]*(1 - u[1]))
ds = DeterministicIteratedMap(logistic_rule, [0.5], [1.0])

## time-dependent logistic map, so that the `r` parameter increases with time
r1 = 3.83
r2 = 3.86
N = 2000
rs = range(r1, r2; length = N)
x = zeros(N)
for (i, r) in enumerate(rs)
set_parameter!(ds, 1, r)
step!(ds)
x[i] = current_state(ds)[1]

function logistic_drifting_rule(u, rs, n)
r = rs[n+1] # time is `n`, starting from 0
return SVector(r*u[1]*(1 - u[1]))
end

ds = DeterministicIteratedMap(logistic_drifting_rule, [0.5], rs)
x = trajectory(ds, N-1)[1][:, 1]

# Plot it, using as time the parameter value (they coincide)
fig, ax = lines(rs, x; linewidth = 0.5)
ax.xlabel = "r (time)"
Expand All @@ -45,7 +43,7 @@ fig
# to weak chaos at r ≈ 3.847. This transition is barely visible in the
# timeseries, and in fact many of the timeseries statistical properties remain identical.

# ### Using a simpler change metric
# ## Using a simpler change metric

# Now, let's compute and various indicators and their changes,
# focusing on the fourth indicator, the permutation entropy. We use
Expand Down Expand Up @@ -102,7 +100,7 @@ fig = plot_change_metrics()
# Due to its construction, permutation entropy will have a spike for periodic data at the
# start of the timeseries, so we can safely ignore the spike at r ≈ 3.83.

# ### Significance via random Fourier surrogates
# ## Significance via random Fourier surrogates

# One way to test for significance would be via the standard way as in the [Tutorial](@ref),
# utilizing surrogate timeseries and [`SurrogatesSignificance`](@ref).
Expand All @@ -117,10 +115,11 @@ fig = plot_change_metrics()
surromethod = RandomFourier()

## Define a function because we will re-use later
using Random: Xoshiro
function overplot_surrogate_significance!(fig, surromethod, color = "black")

signif = SurrogatesSignificance(;
n = 1000, tail = [:both, :both, :right], surromethod
n = 2000, tail = [:both, :both, :right], surromethod, rng = Xoshiro(42),
)
flags = significant_transitions(results, signif)

Expand Down Expand Up @@ -148,45 +147,50 @@ overplot_surrogate_significance!(fig, surromethod)

fig

# ### More appropriate surrogates
# ## Different surrogates
# %% #src
# Using random Fourier surrogates does not make much sense in our application.
# Those surrogates perserve the power spectrum of the timeseries, but the power spectrum
# Random Fourier surrogates perserve the power spectrum of the timeseries, but the power spectrum
# is a property integrated over the whole timeseries. It doesn't contain any information
# regarding a sharp transition at some point in the timeseries.
# highlighting the _local_ dynamics or information that preserves the local
# changes of dynamical behavior.

# A surrogate type that does a better job in preserving local sharp
# changes in the timeseries (and hence provides **stricter**
# surrogate-based significance) is for example `RelativePartialRandomization`.

# A much better alternative is to use block-shuffled surrogates, which
# preserve the short term local temporal correlation in the timeseries and hence
# also preserve local short term sharp changes in the dynamic behavior.

surromethod = BlockShuffle(15)
surromethod = RelativePartialRandomization(0.25)
fig = plot_change_metrics()
overplot_surrogate_significance!(fig, surromethod, "red")
overplot_surrogate_significance!(fig, surromethod, "gray")
fig

# The results are better for the variance and AR1 indicators.
# For the permutation entropy the results do not change because it already is an
# exceptionally well
# suited indicator for this application scenario. But in other cases where things
# are not as clear, or data are contaminated with noise, or we have shorter data,
# choosing a more suitable
# surrogate generator may make the difference between a false positive or not.
# Our results have improved. In the permutation entropy, we see only two
# transitions detected as significant, which is correct: only two
# real dynamical transitions exist in the data.
# In the other two indicators we also see fewer transitions, but as we
# have already discussed, no results with the other indicators should
# be taken into meaningful consideration, as these indicators
# are simply inappropriate for what we are looking for here.

# ### Simpler Significance
# ## Simpler Significance
# %% #src
# Arguably, exactly because we are using the [`difference_of_means`](@ref) as a
# change metric, we may want to be much less strict with our tests for significance.
# change metric, we may want to be less strict and more simple with our tests for significance.
# Instead of using [`SurrogatesSignificance`](@ref) we may use the simpler and much faster
# [`QuantileSignificance`](@ref), which simply claims significant time points
# whenever a change metric exceeds some pre-defined quantile of its timeseries.
# [`SigmaSignificance`](@ref), which simply claims significant time points
# whenever a change metric exceeds some pre-defined factor of its timeseries standard deviation.

fig = plot_change_metrics()
flags = significant_transitions(results, QuantileSignificance())
flags = significant_transitions(results, SigmaSignificance(factor = 5.0))

## Plot the flags
for (i, indicator) in enumerate(indicators)
vlines!(fig[i+1, 1], results.t_change[flags[:, i]];
color = Cycled(3), linestyle = :dash, linewidth = 3
)
end
content(fig[1, 1]).title = "significance from quantile"
content(fig[1, 1]).title = "significance from std"
fig
2 changes: 1 addition & 1 deletion src/TransitionsInTimeseries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export difference_of_means
# analysis
export WindowedIndicatorConfig, estimate_indicator_changes, WindowedIndicatorResults
export TransitionsSignificance, significant_transitions
export QuantileSignificance, SurrogatesSignificance
export QuantileSignificance, SigmaSignificance, SurrogatesSignificance

# timeseries
export isequispaced, equispaced_step
Expand Down
51 changes: 48 additions & 3 deletions src/analysis/quantile_significance.jl
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"""
QuantileSignificance(; p = 0.95, tail = :right) <: TransitionsSignificance
QuantileSignificance(; p = 0.95, tail = :both) <: TransitionsSignificance

A configuration struct for significance testing [`significant_transitions`](@ref).
When used with [`WindowedIndicatorResults`](@ref), significance is estimated as
When used with [`WindowedIndicatorResults`](@ref), significance is estimated
by comparing the value of each change metric with its `p`-quantile.
Values that exceed the `p`-quantile (if `tail = :right`)
or subseed the `1-p`-quantile (if `tail = :left`)
are deemed significant.
If `tail = :both` then either condition is checked.

`QuantileSignficance` guarantees that some values will be significant by
the very definition of what a quantile is.
See also [`SigmaSignificance`](@ref) that is similar but does not have this guarantee.
"""
Base.@kwdef struct QuantileSignificance{P<:Real}
p::P = 0.95
tail::Symbol = :right
tail::Symbol = :both
end

using Statistics: quantile
Expand All @@ -34,3 +38,44 @@ function significant_transitions(res::WindowedIndicatorResults, signif::Quantile
return flags
end

"""
SigmaSignificance(; factor = 3.0, tail = :both) <: TransitionsSignificance

A configuration struct for significance testing [`significant_transitions`](@ref).
When used with [`WindowedIndicatorResults`](@ref), significance is estimated
by comparing how many standard deviations (`σ`) the value exceeds the mean value (`μ`).
Values that exceed (if `tail = :right`) `μ + factor*σ`, or subseed (if `tail = :left`) `μ - factor*σ`
are deemed significant.
If `tail = :both` then either condition is checked.

`factor` can also be a vector of values,
in which case a different value is used for each change metric.

See also [`QuantileSignificance`](@ref).
"""
Base.@kwdef struct SigmaSignificance{P}
factor::P = 0.95
tail::Symbol = :both
end

using Statistics: std, mean

function significant_transitions(res::WindowedIndicatorResults, signif::SigmaSignificance)
flags = similar(res.x_change, Bool)
for (i, x) in enumerate(eachcol(res.x_change))
μ = mean(x)
σ = std(x; mean = μ)
factor = signif.factor isa AbstractVector ? signif.factor[i] : signif.factor
flag = view(flags, :, i)
if signif.tail == :right
@. flag = x > μ + factor*σ
elseif signif.tail == :left
@. flag = x < μ - factor*σ
elseif signif.tail == :both
@. flag = (x < μ - factor*σ) | (x > μ + factor*σ)
else
error("`tail` can be only `:left, :right, :both`. Got $(tail).")
end
end
return flags
end