diff --git a/.gitignore b/.gitignore index a9fe1acf..b56b10b7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ Manifest.toml *.scss *.css *style.jl -docs/src/examples.md \ No newline at end of file +docs/src/examples/*.md \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index 292a48e6..a87ecf99 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -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 diff --git a/docs/src/examples.jl b/docs/src/examples/logistic.jl similarity index 74% rename from docs/src/examples.jl rename to docs/src/examples/logistic.jl index 83b54cef..bb629cc1 100644 --- a/docs/src/examples.jl +++ b/docs/src/examples/logistic.jl @@ -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 @@ -12,7 +10,7 @@ # 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 @@ -20,20 +18,20 @@ 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)" @@ -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 @@ -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). @@ -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) @@ -148,39 +147,44 @@ 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) @@ -188,5 +192,5 @@ for (i, indicator) in enumerate(indicators) 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 diff --git a/src/TransitionsInTimeseries.jl b/src/TransitionsInTimeseries.jl index 3b7ad7a9..ac2a4afe 100644 --- a/src/TransitionsInTimeseries.jl +++ b/src/TransitionsInTimeseries.jl @@ -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 diff --git a/src/analysis/quantile_significance.jl b/src/analysis/quantile_significance.jl index 0d7c927c..c571f343 100644 --- a/src/analysis/quantile_significance.jl +++ b/src/analysis/quantile_significance.jl @@ -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 @@ -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