From 535045d1a26ad20d78e59bb0c448eb9c23289b20 Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Fri, 4 Mar 2022 19:07:51 -0500 Subject: [PATCH 01/13] compare numbers with isapprox --- Project.toml | 2 +- src/workspaces.jl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 47b3b17..90ff6be 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TimeSeriesEcon" uuid = "8b6756d2-c55c-11ea-2998-5f67ea17da60" authors = ["Atai Akunov ", "Boyan Bejanov ", "Nicholas Labelle St-Pierre "] -version = "0.4.0" +version = "0.4.1" [deps] MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" diff --git a/src/workspaces.jl b/src/workspaces.jl index 730682f..bca3633 100644 --- a/src/workspaces.jl +++ b/src/workspaces.jl @@ -154,6 +154,7 @@ export compare, @compare @inline compare_equal(x, y; kwargs...) = isequal(x, y) +@inline compare_equal(x::Number, y::Number; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), kwargs...) = isapprox(x, y; atol, rtol) @inline compare_equal(x::AbstractVector, y::AbstractVector; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), kwargs...) = isapprox(x, y; atol, rtol) function compare_equal(x::TSeries, y::TSeries; trange = nothing, atol = 0, rtol = atol > 0 ? 0.0 : √eps(), kwargs...) if trange === nothing || !(frequencyof(x) == frequencyof(y) == frequencyof(trange)) From 2c13269922e0c6f97a165c8b981bf57d0f367df6 Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Mon, 7 Mar 2022 14:08:49 -0500 Subject: [PATCH 02/13] compare() passes through option nans=true/false to isapprox() --- src/workspaces.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/workspaces.jl b/src/workspaces.jl index bca3633..12ce25a 100644 --- a/src/workspaces.jl +++ b/src/workspaces.jl @@ -154,13 +154,13 @@ export compare, @compare @inline compare_equal(x, y; kwargs...) = isequal(x, y) -@inline compare_equal(x::Number, y::Number; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), kwargs...) = isapprox(x, y; atol, rtol) -@inline compare_equal(x::AbstractVector, y::AbstractVector; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), kwargs...) = isapprox(x, y; atol, rtol) -function compare_equal(x::TSeries, y::TSeries; trange = nothing, atol = 0, rtol = atol > 0 ? 0.0 : √eps(), kwargs...) +@inline compare_equal(x::Number, y::Number; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) = isapprox(x, y; atol, rtol, nans) +@inline compare_equal(x::AbstractVector, y::AbstractVector; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) = isapprox(x, y; atol, rtol, nans) +function compare_equal(x::TSeries, y::TSeries; trange = nothing, atol = 0, rtol = atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) if trange === nothing || !(frequencyof(x) == frequencyof(y) == frequencyof(trange)) trange = intersect(rangeof(x), rangeof(y)) end - isapprox(x[trange], y[trange]; atol, rtol) + isapprox(x[trange], y[trange]; atol, rtol, nans) end function compare_equal(x::LikeWorkspace, y::LikeWorkspace; kwargs...) From 8a2fa84ad963d36edd46bd71ad55292b510c006d Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Mon, 7 Mar 2022 16:45:25 -0500 Subject: [PATCH 03/13] assignment x[2020Q1] = y for TSeries --- src/tseries.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tseries.jl b/src/tseries.jl index 8b1ae8c..363581f 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -251,6 +251,8 @@ function Base.setindex!(t::TSeries{F}, v::Number, m::MIT{F}) where {F<:Frequency setindex!(t.values, v, fi + oftype(fi, m - firstdate(t))) end +Base.setindex!(t::TSeries, from::TSeries, m::MIT) = setindex!(t, from[m], m) + Base.setindex!(t::TSeries, ::AbstractVector{<:Number}, rng::AbstractRange{<:MIT}) = mixed_freq_error(t, rng) function Base.setindex!(t::TSeries{F}, vec::AbstractVector{<:Number}, rng::AbstractRange{MIT{F}}) where {F<:Frequency} if !issubset(rng, rangeof(t)) From f79b1e3184f2aa9540f1b5ca52d9b99e37aae8b2 Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Fri, 11 Mar 2022 19:43:16 -0500 Subject: [PATCH 04/13] added serialization (for parallel computing comms) --- Project.toml | 2 + src/TimeSeriesEcon.jl | 9 ++- src/momentintime.jl | 9 +-- src/mvtseries.jl | 149 ++++++++++++++++++++--------------------- src/serialize.jl | 55 +++++++++++++++ src/tseries.jl | 104 ++++++---------------------- test/runtests.jl | 1 + test/test_serialize.jl | 50 ++++++++++++++ 8 files changed, 210 insertions(+), 169 deletions(-) create mode 100644 src/serialize.jl create mode 100644 test/test_serialize.jl diff --git a/Project.toml b/Project.toml index 90ff6be..7f0b61e 100644 --- a/Project.toml +++ b/Project.toml @@ -4,9 +4,11 @@ authors = ["Atai Akunov ", "Boyan Bejanov q[begin:begin+1] .= 1; @rec rangeof(q; drop=2) q[t] = q[t-1] + q[t-2]; q 21Q4 : 21.0 ``` """ -@inline rangeof(x::Union{TSeries, MVTSeries}; drop::Integer) = - (rng = rangeof(x); - drop > 0 ? (first(rng) + drop:last(rng)) : (first(rng):last(rng)+drop)) +@inline function rangeof(x::Union{TSeries,MVTSeries}; drop::Integer) + rng = rangeof(x) + return drop > 0 ? (first(rng)+drop:last(rng)) : (first(rng):last(rng)+drop) +end include("workspaces.jl") +include("serialize.jl") + end diff --git a/src/momentintime.jl b/src/momentintime.jl index 9d111f3..c3771df 100644 --- a/src/momentintime.jl +++ b/src/momentintime.jl @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2021, Bank of Canada +# Copyright (c) 2020-2022, Bank of Canada # All rights reserved. # ---------------------------------------- @@ -47,8 +47,8 @@ Yearly, Quarterly, Monthly # 2. MIT (moment in time) and Duration # ---------------------------------------- -primitive type MIT{F <: Frequency} <: Signed 64 end -primitive type Duration{F <: Frequency} <: Signed 64 end +primitive type MIT{F<:Frequency} <: Signed 64 end +primitive type Duration{F<:Frequency} <: Signed 64 end """ MIT{F <: Frequency}, Duration{F <: Frequency} @@ -295,7 +295,8 @@ Base.promote_rule(::Type{<:MIT}, ::Type{T}) where T <: AbstractFloat = T # ---------------------------------------- # added so MIT can be used as dictionary keys -Base.hash(x::MIT{T}) where T <: Frequency = hash(("$T", Int(x))) +Base.hash(x::MIT{T}, h::UInt) where T <: Frequency = hash(("$T", Int(x)), h) +Base.hash(x::Duration{T}, h::UInt) where T <: Frequency = hash(("$T", Int(x)), h) # # added for sorting Vector{MIT{T}} where T <: Frequency # Base.sub_with_overflow(x::MIT{T}, y::MIT{T}) where T <: Frequency = begin diff --git a/src/mvtseries.jl b/src/mvtseries.jl index e70bf00..4c8fe65 100644 --- a/src/mvtseries.jl +++ b/src/mvtseries.jl @@ -7,21 +7,13 @@ using OrderedCollections # MVTSeries -- multivariate TSeries # ------------------------------------------------------------------------------- -mutable struct MVTSeries{ - F<:Frequency, - T<:Number, - C<:AbstractMatrix{T} -} <: AbstractMatrix{T} - +mutable struct MVTSeries{F<:Frequency,T<:Number,C<:AbstractMatrix{T}} <: AbstractMatrix{T} firstdate::MIT{F} columns::OrderedDict{Symbol,TSeries{F,T}} values::C # inner constructor enforces constraints - function MVTSeries(firstdate::MIT{F}, - names::NTuple{N,Symbol}, - values::AbstractMatrix - ) where {F<:Frequency,N} + function MVTSeries(firstdate::MIT{F}, names::NTuple{N,Symbol}, values::AbstractMatrix) where {F<:Frequency,N} if N != size(values, 2) ArgumentError("Number of names and columns don't match:" * " $N ≠ $(size(values, 2)).") |> throw @@ -30,12 +22,11 @@ mutable struct MVTSeries{ for (nm, ind) in zip(names, axes(values, 2))) new{F,eltype(values),typeof(values)}(firstdate, columns, values) end - end -@inline _names_as_tuple(names::Symbol) = (names,) -@inline _names_as_tuple(names::AbstractString) = (Symbol(names),) -@inline _names_as_tuple(names) = tuple((Symbol(n) for n in names)...) +_names_as_tuple(names::Symbol) = (names,) +_names_as_tuple(names::AbstractString) = (Symbol(names),) +_names_as_tuple(names) = tuple((Symbol(n) for n in names)...) # standard constructor with default empty values @@ -54,8 +45,8 @@ end # see more constructors below # easy access to internals. -@inline _vals(x::MVTSeries) = getfield(x, :values) -@inline _cols(x::MVTSeries) = getfield(x, :columns) +_vals(x::MVTSeries) = getfield(x, :values) +_cols(x::MVTSeries) = getfield(x, :columns) function _col(x::MVTSeries, col::Symbol) ret = get(getfield(x, :columns), col, nothing) if ret === nothing @@ -66,49 +57,53 @@ end columns(x::MVTSeries) = getfield(x, :columns) -@inline colnames(x::MVTSeries) = keys(_cols(x)) -@inline rawdata(x::MVTSeries) = _vals(x) +colnames(x::MVTSeries) = keys(_cols(x)) +rawdata(x::MVTSeries) = _vals(x) # some methods to make MVTSeries function like a Dict (collection of named of columns) -@inline Base.pairs(x::MVTSeries) = pairs(_cols(x)) -@inline Base.keys(x::MVTSeries) = keys(_cols(x)) -@inline Base.haskey(x::MVTSeries, sym::Symbol) = haskey(_cols(x), sym) -@inline Base.get(x::MVTSeries, sym::Symbol, default) = get(_cols(x), sym, default) -@inline Base.get(f::Function, x::MVTSeries, sym::Symbol) = get(f, _cols(x), sym) +Base.pairs(x::MVTSeries) = pairs(_cols(x)) +Base.keys(x::MVTSeries) = keys(_cols(x)) +Base.haskey(x::MVTSeries, sym::Symbol) = haskey(_cols(x), sym) +Base.get(x::MVTSeries, sym::Symbol, default) = get(_cols(x), sym, default) +Base.get(f::Function, x::MVTSeries, sym::Symbol) = get(f, _cols(x), sym) # no get!() - can't add columns like this!! # methods related to TSeries -@inline firstdate(x::MVTSeries) = getfield(x, :firstdate) -@inline lastdate(x::MVTSeries) = firstdate(x) + size(_vals(x), 1) - one(firstdate(x)) -@inline frequencyof(::Type{<:MVTSeries{F}}) where {F<:Frequency} = F -@inline rangeof(x::MVTSeries) = firstdate(x) .+ (0:size(_vals(x), 1)-1) +firstdate(x::MVTSeries) = getfield(x, :firstdate) +lastdate(x::MVTSeries) = firstdate(x) + size(_vals(x), 1) - one(firstdate(x)) +frequencyof(::Type{<:MVTSeries{F}}) where {F<:Frequency} = F +rangeof(x::MVTSeries) = firstdate(x) .+ (0:size(_vals(x), 1)-1) # ------------------------------------------------------------------------------- # Make MVTSeries work properly as an AbstractArray -@inline Base.size(x::MVTSeries) = size(_vals(x)) -@inline Base.axes(x::MVTSeries) = (rangeof(x), tuple(colnames(x)...)) -@inline Base.axes1(x::MVTSeries) = rangeof(x) +Base.size(x::MVTSeries) = size(_vals(x)) +Base.axes(x::MVTSeries) = (rangeof(x), tuple(colnames(x)...)) +Base.axes1(x::MVTSeries) = rangeof(x) # the following are needed for copy() and copyto!() (and a bunch of Julia internals that use them) -@inline Base.IndexStyle(x::MVTSeries) = IndexStyle(_vals(x)) -@inline Base.dataids(x::MVTSeries) = Base.dataids(_vals(x)) +Base.IndexStyle(x::MVTSeries) = IndexStyle(_vals(x)) +Base.dataids(x::MVTSeries) = Base.dataids(_vals(x)) -@inline Base.eachindex(x::MVTSeries) = eachindex(_vals(x)) +Base.eachindex(x::MVTSeries) = eachindex(_vals(x)) # normally only the first of the following is sufficient. # we add few other versions of similar below -@inline Base.similar(x::MVTSeries) = MVTSeries(firstdate(x), colnames(x), similar(_vals(x))) -@inline Base.similar(x::MVTSeries, ::Type{T}) where {T} = MVTSeries(firstdate(x), colnames(x), similar(_vals(x), T)) +Base.similar(x::MVTSeries) = MVTSeries(firstdate(x), colnames(x), similar(_vals(x))) +Base.similar(x::MVTSeries, ::Type{T}) where {T} = MVTSeries(firstdate(x), colnames(x), similar(_vals(x), T)) + +# ------------------------------------------------------------------------------- + +Base.hash(x::MVTSeries, h::UInt) = hash((_vals(x), firstdate(x), colnames(x)...), h) # ------------------------------------------------------------------------------- # Indexing with integers and booleans - same as matrices # Indexing with integers falls back to AbstractArray const _FallbackType = Union{Integer,Colon,AbstractUnitRange{<:Integer},AbstractArray{<:Integer},CartesianIndex} -@inline Base.getindex(sd::MVTSeries, i1::_FallbackType...) = getindex(_vals(sd), i1...) -@inline Base.setindex!(sd::MVTSeries, val, i1::_FallbackType...) = setindex!(_vals(sd), val, i1...) +Base.getindex(sd::MVTSeries, i1::_FallbackType...) = getindex(_vals(sd), i1...) +Base.setindex!(sd::MVTSeries, val, i1::_FallbackType...) = setindex!(_vals(sd), val, i1...) # ------------------------------------------------------------- # Some other constructors @@ -120,9 +115,9 @@ const _FallbackType = Union{Integer,Colon,AbstractUnitRange{<:Integer},AbstractA MVTSeries(T::Type{<:Number}, fd::MIT, vars) = MVTSeries(fd, vars, Matrix{T}(undef, 0, length(vars))) # Uninitialized from a range and list of variables -@inline MVTSeries(rng::UnitRange{<:MIT}, vars) = MVTSeries(Float64, rng, vars, undef) -@inline MVTSeries(rng::UnitRange{<:MIT}, vars, ::UndefInitializer) = MVTSeries(Float64, rng, vars, undef) -@inline MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars) = MVTSeries(T, rng, vars, undef) +MVTSeries(rng::UnitRange{<:MIT}, vars) = MVTSeries(Float64, rng, vars, undef) +MVTSeries(rng::UnitRange{<:MIT}, vars, ::UndefInitializer) = MVTSeries(Float64, rng, vars, undef) +MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars) = MVTSeries(T, rng, vars, undef) MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars, ::UndefInitializer) = MVTSeries(first(rng), vars, Matrix{T}(undef, length(rng), length(vars))) MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars::Symbol, ::UndefInitializer) = @@ -164,7 +159,7 @@ Base.similar(::AbstractArray{T}, shape::Tuple{UnitRange{<:MIT},NTuple{N,Symbol}} Base.fill(v::Number, rng::UnitRange{<:MIT}, vars::NTuple{N,Symbol}) where {N} = MVTSeries(first(rng), vars, fill(v, length(rng), length(vars))) # Empty (0 variables) from range -@inline function MVTSeries(rng::UnitRange{<:MIT}; args...) +function MVTSeries(rng::UnitRange{<:MIT}; args...) isempty(args) && return MVTSeries(rng, ()) keys, values = zip(args...) # figure out the element type @@ -279,11 +274,11 @@ Base.setindex!(x::MVTSeries, val, rng::UnitRange{MIT}) = mixed_freq_error(x, rng end # single argument - variable - return a TSeries of the column -@inline Base.getindex(x::MVTSeries, col::AbstractString) = _col(x, Symbol(col)) -@inline Base.getindex(x::MVTSeries, col::Symbol) = _col(x, col) +Base.getindex(x::MVTSeries, col::AbstractString) = _col(x, Symbol(col)) +Base.getindex(x::MVTSeries, col::Symbol) = _col(x, col) -@inline Base.setindex!(x::MVTSeries, val, col::AbstractString) = setindex!(x, val, Symbol(col)) -@inline function Base.setindex!(x::MVTSeries, val, col::Symbol) +Base.setindex!(x::MVTSeries, val, col::AbstractString) = setindex!(x, val, Symbol(col)) +function Base.setindex!(x::MVTSeries, val, col::Symbol) setproperty!(x, col, val) end @@ -303,15 +298,15 @@ end const _SymbolOneOrCollection = Union{Symbol,Vector{Symbol},NTuple{N,Symbol}} where {N} const _MITOneOrRange = Union{MIT,UnitRange{<:MIT}} -@inline Base.getindex(x::MVTSeries, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) -@inline Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) +Base.getindex(x::MVTSeries, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) +Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) # if one argument is Colon, fall back on single argument indexing -@inline Base.getindex(x::MVTSeries, p::_MITOneOrRange, ::Colon) = getindex(x, p) -@inline Base.getindex(x::MVTSeries, ::Colon, c::_SymbolOneOrCollection) = getindex(x, c) +Base.getindex(x::MVTSeries, p::_MITOneOrRange, ::Colon) = getindex(x, p) +Base.getindex(x::MVTSeries, ::Colon, c::_SymbolOneOrCollection) = getindex(x, c) -@inline Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, ::Colon) = setindex!(x, val, p, axes(x, 2)) -@inline Base.setindex!(x::MVTSeries, val, ::Colon, c::_SymbolOneOrCollection) = setindex!(x, val, axes(x, 1), c) +Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, ::Colon) = setindex!(x, val, p, axes(x, 2)) +Base.setindex!(x::MVTSeries, val, ::Colon, c::_SymbolOneOrCollection) = setindex!(x, val, axes(x, 1), c) # @@ -365,7 +360,7 @@ end end # with a range of MIT and a single column - we fall back on TSeries assignment -@inline function Base.setindex!(x::MVTSeries{F}, val, r::UnitRange{MIT{F}}, c::Symbol) where {F<:Frequency} +function Base.setindex!(x::MVTSeries{F}, val, r::UnitRange{MIT{F}}, c::Symbol) where {F<:Frequency} setindex!(_col(x, c), val, r) end @@ -378,7 +373,7 @@ end setindex!(_vals(x), val, i1, i2) end -@inline Base.setindex!(x::MVTSeries, val, ind::Tuple{<:MIT,Symbol}) = setindex!(x, val, ind...) +Base.setindex!(x::MVTSeries, val, ind::Tuple{<:MIT,Symbol}) = setindex!(x, val, ind...) @inline function Base.setindex!(x::MVTSeries{F}, val::MVTSeries{F}, r::UnitRange{MIT{F}}, c::Union{Vector{Symbol},NTuple{N,Symbol}}) where {F<:Frequency,N} @boundscheck checkbounds(x, r) @@ -423,11 +418,11 @@ end Base.fill!(x::MVTSeries, val) = fill!(_vals(x), val) -@inline Base.view(x::MVTSeries, I...) = view(_vals(x), I...) +Base.view(x::MVTSeries, I...) = view(_vals(x), I...) -@inline Base.view(x::MVTSeries, ::Colon, J::_SymbolOneOrCollection) = view(x, axes(x, 1), J) -@inline Base.view(x::MVTSeries, I::_MITOneOrRange, ::Colon) = view(x, I, axes(x, 2)) -@inline Base.view(x::MVTSeries, ::Colon, ::Colon) = view(x, axes(x, 1), axes(x, 2)) +Base.view(x::MVTSeries, ::Colon, J::_SymbolOneOrCollection) = view(x, axes(x, 1), J) +Base.view(x::MVTSeries, I::_MITOneOrRange, ::Colon) = view(x, I, axes(x, 2)) +Base.view(x::MVTSeries, ::Colon, ::Colon) = view(x, axes(x, 1), axes(x, 2)) function Base.view(x::MVTSeries, I::_MITOneOrRange, J::_SymbolOneOrCollection) where {F<:Frequency} @boundscheck checkbounds(x, I) @boundscheck checkbounds(x, J) @@ -445,17 +440,17 @@ include("mvtseries/mvts_show.jl") #### arraymath -@inline Base.promote_shape(x::MVTSeries, y::MVTSeries) = +Base.promote_shape(x::MVTSeries, y::MVTSeries) = axes(x, 2) == axes(y, 2) ? (intersect(rangeof(x), rangeof(y)), axes(x, 2)) : throw(DimensionMismatch("Columns do not match:\n\t$(axes(x,2))\n\t$(axes(y,2))")) -@inline Base.promote_shape(x::MVTSeries, y::AbstractArray) = +Base.promote_shape(x::MVTSeries, y::AbstractArray) = promote_shape(_vals(x), y) -@inline Base.promote_shape(x::AbstractArray, y::MVTSeries) = +Base.promote_shape(x::AbstractArray, y::MVTSeries) = promote_shape(x, _vals(y)) -@inline Base.LinearIndices(x::MVTSeries) = LinearIndices(_vals(x)) +Base.LinearIndices(x::MVTSeries) = LinearIndices(_vals(x)) Base.:*(x::Number, y::MVTSeries) = copyto!(similar(y), *(x, y.values)) Base.:*(x::MVTSeries, y::Number) = copyto!(similar(x), *(x.values, y)) @@ -500,19 +495,19 @@ end #### diff and cumsum -@inline shift(x::MVTSeries, k::Integer) = shift!(copy(x), k) -@inline shift!(x::MVTSeries, k::Integer) = (x.firstdate -= k; x) -@inline lag(x::MVTSeries, k::Integer = 1) = shift(x, -k) -@inline lag!(x::MVTSeries, k::Integer = 1) = shift!(x, -k) -@inline lead(x::MVTSeries, k::Integer = 1) = shift(x, k) -@inline lead!(x::MVTSeries, k::Integer = 1) = shift!(x, k) +shift(x::MVTSeries, k::Integer) = shift!(copy(x), k) +shift!(x::MVTSeries, k::Integer) = (x.firstdate -= k; x) +lag(x::MVTSeries, k::Integer = 1) = shift(x, -k) +lag!(x::MVTSeries, k::Integer = 1) = shift!(x, -k) +lead(x::MVTSeries, k::Integer = 1) = shift(x, k) +lead!(x::MVTSeries, k::Integer = 1) = shift!(x, k) -@inline Base.diff(x::MVTSeries; dims = 1) = diff(x, -1; dims) -@inline Base.diff(x::MVTSeries, k::Integer; dims = 1) = +Base.diff(x::MVTSeries; dims = 1) = diff(x, -1; dims) +Base.diff(x::MVTSeries, k::Integer; dims = 1) = dims == 1 ? x - shift(x, k) : diff(_vals(x); dims) -@inline Base.cumsum(x::MVTSeries; dims) = cumsum!(copy(x), _vals(x); dims) -@inline Base.cumsum!(out::MVTSeries, in::AbstractMatrix; dims) = (cumsum!(_vals(out), in; dims); out) +Base.cumsum(x::MVTSeries; dims) = cumsum!(copy(x), _vals(x); dims) +Base.cumsum!(out::MVTSeries, in::AbstractMatrix; dims) = (cumsum!(_vals(out), in; dims); out) #### moving average @@ -527,11 +522,11 @@ window is backward-looking `(-n+1:0)` and if `n < 0` the window is forward-looki function moving end export moving -@inline _moving_mean!(x_ma::TSeries, x, t, window) = x_ma[t] = mean(x[t.+window]) -@inline _moving_mean!(x_ma::MVTSeries, x, t, window) = x_ma[t, :] .= mean(x[t.+window, :]; dims = 1) +_moving_mean!(x_ma::TSeries, x, t, window) = x_ma[t] = mean(x[t.+window]) +_moving_mean!(x_ma::MVTSeries, x, t, window) = x_ma[t, :] .= mean(x[t.+window, :]; dims = 1) -@inline _moving_shape(x::TSeries, n) = (rangeof(x, drop = n - copysign(1, n)),) -@inline _moving_shape(x::MVTSeries, n) = (rangeof(x, drop = n - copysign(1, n)), axes(x, 2)) +_moving_shape(x::TSeries, n) = (rangeof(x, drop = n - copysign(1, n)),) +_moving_shape(x::MVTSeries, n) = (rangeof(x, drop = n - copysign(1, n)), axes(x, 2)) function moving(x::Union{TSeries,MVTSeries}, n::Integer) window = n > 0 ? (-n+1:0) : (0:-n-1) @@ -586,7 +581,7 @@ the same length as the number of columns of `dvar`. function undiff end, function undiff! end export undiff, undiff! -@inline undiff(dvar::TSeries) = undiff(dvar, firstdate(dvar) - 1 => zero(eltype(dvar))) +undiff(dvar::TSeries) = undiff(dvar, firstdate(dvar) - 1 => zero(eltype(dvar))) function undiff(dvar::TSeries, anchor::Pair{<:MIT,<:Number}) fromdate, value = anchor ET = Base.promote_eltype(dvar, value) @@ -608,7 +603,7 @@ function undiff!(var::TSeries, dvar::TSeries; fromdate = firstdate(dvar) - 1) if lastdate(var) < lastdate(dvar) resize!(var, firstdate(var):lastdate(dvar)) end - for t = fromdate+1:lastdate(dvar) + for t = fromdate+1:lastdate(dvar) var[t] = var[t-1] + dvar[t] end return var diff --git a/src/serialize.jl b/src/serialize.jl new file mode 100644 index 0000000..55d17e4 --- /dev/null +++ b/src/serialize.jl @@ -0,0 +1,55 @@ +# Copyright (c) 2020-2022, Bank of Canada +# All rights reserved. + +# Serialization (for parallel computing communications) + +using Serialization + +# ============== MIT ======================================== + +Base.write(io::IO, p::MIT) = write(io, Ref(p)) +Base.read(io::IO, T::Type{<:MIT}) = read!(io, Ref(T(0)))[] +Base.write(io::IO, p::Duration) = write(io, Ref(p)) +Base.read(io::IO, T::Type{<:Duration}) = read!(io, Ref(T(0)))[] + +# ============== TSeries ===================================== +# Not sure this part is really necessary, since it works with Julia's built-in +# implementation just fine + +function Serialization.serialize(s::AbstractSerializer, t::TSeries) + Serialization.serialize_type(s, typeof(t)) + write(s.io, length(t)) + write(s.io, firstdate(t)) + write(s.io, t.values) +end + +function Serialization.deserialize(s::AbstractSerializer, S::Type{<:TSeries{F,T}}) where {F,T} + n = read(s.io, Int) + fd = read(s.io, MIT{F}) + ret = TSeries(T, fd .+ (0:n-1), undef) + read!(s.io, ret.values) + return ret +end + +# ============== MVTSeries =================================== + +# This part is a must - it works with Julia's built-in implementation, but gives +# the wrong result (. variables are not views into the raw data but separate +# TSeries!!!) + +function Serialization.serialize(s::AbstractSerializer, sd::MVTSeries) + Serialization.serialize_type(s, typeof(sd)) + write(s.io, firstdate(sd)) + write(s.io, lastdate(sd)) + serialize(s, [colnames(sd)...]) + write(s.io, rawdata(sd)) +end + +function Serialization.deserialize(s::AbstractSerializer, S::Type{<:MVTSeries{F,T}}) where {F,T} + fd = read(s.io, MIT{F}) + ld = read(s.io, MIT{F}) + cols = deserialize(s) + ret = MVTSeries(T, fd:ld, cols, undef) + read!(s.io, rawdata(ret)) + return ret +end diff --git a/src/tseries.jl b/src/tseries.jl index 363581f..d2039a6 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -67,21 +67,21 @@ mutable struct TSeries{F<:Frequency,T<:Number,C<:AbstractVector{T}} <: AbstractV values::C end -@inline _vals(t::TSeries) = t.values -@inline rawdata(t::TSeries) = t.values +_vals(t::TSeries) = t.values +rawdata(t::TSeries) = t.values Base.values(t::TSeries) = values(t.values) -@inline firstdate(t::TSeries) = t.firstdate -@inline lastdate(t::TSeries) = t.firstdate + length(t.values) - one(t.firstdate) +firstdate(t::TSeries) = t.firstdate +lastdate(t::TSeries) = t.firstdate + length(t.values) - one(t.firstdate) -@inline frequencyof(::Type{<:TSeries{F}}) where {F<:Frequency} = F +frequencyof(::Type{<:TSeries{F}}) where {F<:Frequency} = F """ rangeof(s) Return the stored range of the given time series object. """ -@inline rangeof(t::TSeries) = firstdate(t) .+ (0:size(t.values, 1)-1) +rangeof(t::TSeries) = firstdate(t) .+ (0:size(t.values, 1)-1) """ firstdate(ts), lastdate(ts) @@ -94,9 +94,9 @@ firstdate, lastdate # ------------------------------------------------------------------------------- # some methods that make the AbstractArray infrastructure of Julia work with TSeries -@inline Base.size(t::TSeries) = size(t.values) -@inline Base.axes(t::TSeries) = (firstdate(t):lastdate(t),) -@inline Base.axes1(t::TSeries) = firstdate(t):lastdate(t) +Base.size(t::TSeries) = size(t.values) +Base.axes(t::TSeries) = (firstdate(t):lastdate(t),) +Base.axes1(t::TSeries) = firstdate(t):lastdate(t) # the following are needed for copy() and copyto!() (and a bunch of Julia internals that use them) Base.IndexStyle(::TSeries) = IndexLinear() @@ -106,6 +106,10 @@ Base.dataids(t::TSeries) = Base.dataids(getfield(t, :values)) # we add few other versions of similar below Base.similar(t::TSeries) = TSeries(t.firstdate, similar(t.values)) +# ------------------------------------------------------------------------------- + +Base.hash(t::TSeries, h::UInt) = hash((t.values, t.firstdate), h) + # ------------------------------------------------------------------------------- # Indexing with integers and booleans - same as vectors @@ -274,7 +278,7 @@ function Base.setindex!(t::TSeries{F}, vec::AbstractVector{<:Number}, rng::Abstr end Base.setindex!(t::TSeries{F1}, src::TSeries{F2}, rng::AbstractRange{MIT{F3}}) where {F1<:Frequency,F2<:Frequency,F3<:Frequency} = mixed_freq_error(t, src, rng) -@inline Base.setindex!(t::TSeries{F}, src::TSeries{F}, rng::AbstractRange{MIT{F}}) where {F<:Frequency} = copyto!(t, rng, src) +Base.setindex!(t::TSeries{F}, src::TSeries{F}, rng::AbstractRange{MIT{F}}) where {F<:Frequency} = copyto!(t, rng, src) """ typenan(x), typenan(T) @@ -332,7 +336,7 @@ end # Base.copyto!(dest::TSeries, src::TSeries) = mixed_freq_error(dest, src) -@inline Base.copyto!(dest::TSeries{F}, src::TSeries{F}) where {F<:Frequency} = copyto!(dest, rangeof(src), src) +Base.copyto!(dest::TSeries{F}, src::TSeries{F}) where {F<:Frequency} = copyto!(dest, rangeof(src), src) # Base.copyto!(dest::TSeries, drng::AbstractRange{<:MIT}, src::TSeries) = mixed_freq_error(dest, drng, src) @@ -347,18 +351,18 @@ end # view with MIT indexing Base.view(t::TSeries, I::AbstractRange{<:MIT}) = mixed_freq_error(t, I) -function Base.view(t::TSeries{F}, I::AbstractRange{MIT{F}}) where {F<:Frequency} +@inline function Base.view(t::TSeries{F}, I::AbstractRange{MIT{F}}) where {F<:Frequency} fi = firstindex(t.values) TSeries(first(I), view(t.values, oftype(fi, first(I) - firstindex(t) + fi):oftype(fi, last(I) - firstindex(t) + fi))) end # view with Int indexing -function Base.view(t::TSeries, I::AbstractRange{<:Integer}) +@inline function Base.view(t::TSeries, I::AbstractRange{<:Integer}) fi = firstindex(t.values) TSeries(firstindex(t) + first(I) - one(first(I)), view(t.values, oftype(fi, first(I)):oftype(fi, last(I)))) end -@inline Base.diff(x::TSeries, k::Integer = -1) = x - lag(x, -k) +Base.diff(x::TSeries, k::Integer = -1) = x - lag(x, -k) function Base.vcat(x::TSeries, args::AbstractVector...) return TSeries(firstdate(x), vcat(_vals(x), args...)) @@ -441,77 +445,7 @@ apct(ts::TSeries, args...) = error("apct for frequency $(frequencyof(ts)) not im Year-to-year percent change in x. """ -ytypct(x) = 100*(x ./ shift(x, -ppy(x)) .- 1) +ytypct(x) = 100 * (x ./ shift(x, -ppy(x)) .- 1) export ytypct -# function Base.cumsum(s::TSeries) -# TSeries(s.firstdate, cumsum(s.values)) -# end - -# function Base.cumsum!(s::TSeries) -# s.values = cumsum(s.values) -# return s -# end - - -# """ -# leftcropnan!(x::TSeries) - -# Remove `NaN` values from starting at the beginning of `x`, in-place. - -# __Note__: an internal function. -# """ -# function leftcropnan!(s::TSeries) -# while isequal(s[firstdate(s)], NaN) -# popfirst!(s.values) -# s.firstdate = s.firstdate + 1 -# end -# return s -# end - -# """ -# rightcropnan!(x::TSeries) - -# Remove `NaN` values from the end of `x` - -# __Note__: an internal function. -# """ -# function rightcropnan!(s::TSeries) -# while isequal(s[lastdate(s)], NaN) -# pop!(s.values) -# end -# return s -# end - - -# """ -# nanrm!(s::TSeries, type::Symbol) - -# Remove `NaN` values that are either at the beginning of the `s` and/or end of `x`. - -# Examples -# ``` -# julia> s = TSeries(yy(2018), [NaN, NaN, 1, 2, NaN]); - -# julia> nanrm!(s); - -# julia> s -# TSeries{Yearly} of length 2 -# 2020Y: 1.0 -# 2021Y: 2.0 -# ``` -# """ -# function nanrm!(s::TSeries, type::Symbol=:both) -# if type == :left -# leftcropnan!(s) -# elseif type == :right -# rightcropnan!(s) -# elseif type == :both -# leftcropnan!(s) -# rightcropnan!(s) -# else -# error("Please select between :left, :right, or :both.") -# end -# return s -# end diff --git a/test/runtests.jl b/test/runtests.jl index 77958a3..19970ce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,3 +8,4 @@ include("test_mit.jl") include("test_tseries.jl") include("test_mvtseries.jl") include("test_workspace.jl") +include("test_serialize.jl") diff --git a/test/test_serialize.jl b/test/test_serialize.jl new file mode 100644 index 0000000..f325ec3 --- /dev/null +++ b/test/test_serialize.jl @@ -0,0 +1,50 @@ + +using Distributed + +@testset "serialize" begin + + addprocs(1) + # @everywhere workers() begin + # using Pkg + # Pkg.activate(".") + # end + + @everywhere workers() using TimeSeriesEcon + + p = 2020Q1 + z = fetch(@spawnat :any (p; p + 1)) + @test typeof(p) == typeof(z) + @test z == 2020Q2 + # + @test fetch(@spawnat :any (1 .+ (2020Q1:2022Q4))) == 2020Q2:2023Q1 + # + t = TSeries(2020Q1, rand(5)) + z = fetch(@spawnat :any log.(t)) + @test typeof(t) == typeof(z) + @test firstdate(t) == firstdate(z) + @test length(t) == length(z) + @test z == log.(t) + # + sd = MVTSeries(2000Y, (:A, :Beta, :C), rand(5, 3)) + @everywhere foo_1234(s::MVTSeries) = hcat(s; Foo = s.A + s.Beta - 2s.C) + z = fetch(@spawnat :any foo_1234(sd)) + @test typeof(sd) == typeof(z) + @test rangeof(sd) == rangeof(z) + @test (colnames(z)...,) == (:A, :Beta, :C, :Foo) + @test rawdata(z)[:, 1:3] == rawdata(sd) + @test rawdata(z)[:, 4] == (rawdata(sd)[:, 1] .+ + rawdata(sd)[:, 2] .- 2 .* rawdata(sd)[:, 3]) + # + z = fetch(@spawnat :any MVTSeries(20Q1:22Q4, (:a, :b, :c), rand)) + z[20Q2, :a] = 100 + @test z.a[20Q2] == z[20Q2, :a] + @test all(z.a .== z[:, :a]) + @test all(z[20Q2] .== z[20Q2, :]) + z.b[21Q1] = 200 + @test z.b[21Q1] == z[21Q1, :b] + @test all(z.b .== z[:, :b]) + @test all(z[21Q1] .== z[21Q1, :]) + + rmprocs(workers()) + +end \ No newline at end of file From 10fe71ba52c1bd683c48bad4157d6b233a1b70a0 Mon Sep 17 00:00:00 2001 From: Nicholas Labelle St-Pierre Date: Fri, 11 Mar 2022 21:21:17 -0500 Subject: [PATCH 05/13] added serialization (for parallel computing comms) --- Project.toml | 2 + src/TimeSeriesEcon.jl | 9 ++- src/momentintime.jl | 9 +-- src/mvtseries.jl | 149 ++++++++++++++++++++--------------------- src/serialize.jl | 55 +++++++++++++++ src/tseries.jl | 104 ++++++---------------------- test/runtests.jl | 1 + test/test_serialize.jl | 50 ++++++++++++++ 8 files changed, 210 insertions(+), 169 deletions(-) create mode 100644 src/serialize.jl create mode 100644 test/test_serialize.jl diff --git a/Project.toml b/Project.toml index 90ff6be..7f0b61e 100644 --- a/Project.toml +++ b/Project.toml @@ -4,9 +4,11 @@ authors = ["Atai Akunov ", "Boyan Bejanov q[begin:begin+1] .= 1; @rec rangeof(q; drop=2) q[t] = q[t-1] + q[t-2]; q 21Q4 : 21.0 ``` """ -@inline rangeof(x::Union{TSeries, MVTSeries}; drop::Integer) = - (rng = rangeof(x); - drop > 0 ? (first(rng) + drop:last(rng)) : (first(rng):last(rng)+drop)) +@inline function rangeof(x::Union{TSeries,MVTSeries}; drop::Integer) + rng = rangeof(x) + return drop > 0 ? (first(rng)+drop:last(rng)) : (first(rng):last(rng)+drop) +end include("workspaces.jl") +include("serialize.jl") + end diff --git a/src/momentintime.jl b/src/momentintime.jl index 9d111f3..c3771df 100644 --- a/src/momentintime.jl +++ b/src/momentintime.jl @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2021, Bank of Canada +# Copyright (c) 2020-2022, Bank of Canada # All rights reserved. # ---------------------------------------- @@ -47,8 +47,8 @@ Yearly, Quarterly, Monthly # 2. MIT (moment in time) and Duration # ---------------------------------------- -primitive type MIT{F <: Frequency} <: Signed 64 end -primitive type Duration{F <: Frequency} <: Signed 64 end +primitive type MIT{F<:Frequency} <: Signed 64 end +primitive type Duration{F<:Frequency} <: Signed 64 end """ MIT{F <: Frequency}, Duration{F <: Frequency} @@ -295,7 +295,8 @@ Base.promote_rule(::Type{<:MIT}, ::Type{T}) where T <: AbstractFloat = T # ---------------------------------------- # added so MIT can be used as dictionary keys -Base.hash(x::MIT{T}) where T <: Frequency = hash(("$T", Int(x))) +Base.hash(x::MIT{T}, h::UInt) where T <: Frequency = hash(("$T", Int(x)), h) +Base.hash(x::Duration{T}, h::UInt) where T <: Frequency = hash(("$T", Int(x)), h) # # added for sorting Vector{MIT{T}} where T <: Frequency # Base.sub_with_overflow(x::MIT{T}, y::MIT{T}) where T <: Frequency = begin diff --git a/src/mvtseries.jl b/src/mvtseries.jl index e70bf00..4c8fe65 100644 --- a/src/mvtseries.jl +++ b/src/mvtseries.jl @@ -7,21 +7,13 @@ using OrderedCollections # MVTSeries -- multivariate TSeries # ------------------------------------------------------------------------------- -mutable struct MVTSeries{ - F<:Frequency, - T<:Number, - C<:AbstractMatrix{T} -} <: AbstractMatrix{T} - +mutable struct MVTSeries{F<:Frequency,T<:Number,C<:AbstractMatrix{T}} <: AbstractMatrix{T} firstdate::MIT{F} columns::OrderedDict{Symbol,TSeries{F,T}} values::C # inner constructor enforces constraints - function MVTSeries(firstdate::MIT{F}, - names::NTuple{N,Symbol}, - values::AbstractMatrix - ) where {F<:Frequency,N} + function MVTSeries(firstdate::MIT{F}, names::NTuple{N,Symbol}, values::AbstractMatrix) where {F<:Frequency,N} if N != size(values, 2) ArgumentError("Number of names and columns don't match:" * " $N ≠ $(size(values, 2)).") |> throw @@ -30,12 +22,11 @@ mutable struct MVTSeries{ for (nm, ind) in zip(names, axes(values, 2))) new{F,eltype(values),typeof(values)}(firstdate, columns, values) end - end -@inline _names_as_tuple(names::Symbol) = (names,) -@inline _names_as_tuple(names::AbstractString) = (Symbol(names),) -@inline _names_as_tuple(names) = tuple((Symbol(n) for n in names)...) +_names_as_tuple(names::Symbol) = (names,) +_names_as_tuple(names::AbstractString) = (Symbol(names),) +_names_as_tuple(names) = tuple((Symbol(n) for n in names)...) # standard constructor with default empty values @@ -54,8 +45,8 @@ end # see more constructors below # easy access to internals. -@inline _vals(x::MVTSeries) = getfield(x, :values) -@inline _cols(x::MVTSeries) = getfield(x, :columns) +_vals(x::MVTSeries) = getfield(x, :values) +_cols(x::MVTSeries) = getfield(x, :columns) function _col(x::MVTSeries, col::Symbol) ret = get(getfield(x, :columns), col, nothing) if ret === nothing @@ -66,49 +57,53 @@ end columns(x::MVTSeries) = getfield(x, :columns) -@inline colnames(x::MVTSeries) = keys(_cols(x)) -@inline rawdata(x::MVTSeries) = _vals(x) +colnames(x::MVTSeries) = keys(_cols(x)) +rawdata(x::MVTSeries) = _vals(x) # some methods to make MVTSeries function like a Dict (collection of named of columns) -@inline Base.pairs(x::MVTSeries) = pairs(_cols(x)) -@inline Base.keys(x::MVTSeries) = keys(_cols(x)) -@inline Base.haskey(x::MVTSeries, sym::Symbol) = haskey(_cols(x), sym) -@inline Base.get(x::MVTSeries, sym::Symbol, default) = get(_cols(x), sym, default) -@inline Base.get(f::Function, x::MVTSeries, sym::Symbol) = get(f, _cols(x), sym) +Base.pairs(x::MVTSeries) = pairs(_cols(x)) +Base.keys(x::MVTSeries) = keys(_cols(x)) +Base.haskey(x::MVTSeries, sym::Symbol) = haskey(_cols(x), sym) +Base.get(x::MVTSeries, sym::Symbol, default) = get(_cols(x), sym, default) +Base.get(f::Function, x::MVTSeries, sym::Symbol) = get(f, _cols(x), sym) # no get!() - can't add columns like this!! # methods related to TSeries -@inline firstdate(x::MVTSeries) = getfield(x, :firstdate) -@inline lastdate(x::MVTSeries) = firstdate(x) + size(_vals(x), 1) - one(firstdate(x)) -@inline frequencyof(::Type{<:MVTSeries{F}}) where {F<:Frequency} = F -@inline rangeof(x::MVTSeries) = firstdate(x) .+ (0:size(_vals(x), 1)-1) +firstdate(x::MVTSeries) = getfield(x, :firstdate) +lastdate(x::MVTSeries) = firstdate(x) + size(_vals(x), 1) - one(firstdate(x)) +frequencyof(::Type{<:MVTSeries{F}}) where {F<:Frequency} = F +rangeof(x::MVTSeries) = firstdate(x) .+ (0:size(_vals(x), 1)-1) # ------------------------------------------------------------------------------- # Make MVTSeries work properly as an AbstractArray -@inline Base.size(x::MVTSeries) = size(_vals(x)) -@inline Base.axes(x::MVTSeries) = (rangeof(x), tuple(colnames(x)...)) -@inline Base.axes1(x::MVTSeries) = rangeof(x) +Base.size(x::MVTSeries) = size(_vals(x)) +Base.axes(x::MVTSeries) = (rangeof(x), tuple(colnames(x)...)) +Base.axes1(x::MVTSeries) = rangeof(x) # the following are needed for copy() and copyto!() (and a bunch of Julia internals that use them) -@inline Base.IndexStyle(x::MVTSeries) = IndexStyle(_vals(x)) -@inline Base.dataids(x::MVTSeries) = Base.dataids(_vals(x)) +Base.IndexStyle(x::MVTSeries) = IndexStyle(_vals(x)) +Base.dataids(x::MVTSeries) = Base.dataids(_vals(x)) -@inline Base.eachindex(x::MVTSeries) = eachindex(_vals(x)) +Base.eachindex(x::MVTSeries) = eachindex(_vals(x)) # normally only the first of the following is sufficient. # we add few other versions of similar below -@inline Base.similar(x::MVTSeries) = MVTSeries(firstdate(x), colnames(x), similar(_vals(x))) -@inline Base.similar(x::MVTSeries, ::Type{T}) where {T} = MVTSeries(firstdate(x), colnames(x), similar(_vals(x), T)) +Base.similar(x::MVTSeries) = MVTSeries(firstdate(x), colnames(x), similar(_vals(x))) +Base.similar(x::MVTSeries, ::Type{T}) where {T} = MVTSeries(firstdate(x), colnames(x), similar(_vals(x), T)) + +# ------------------------------------------------------------------------------- + +Base.hash(x::MVTSeries, h::UInt) = hash((_vals(x), firstdate(x), colnames(x)...), h) # ------------------------------------------------------------------------------- # Indexing with integers and booleans - same as matrices # Indexing with integers falls back to AbstractArray const _FallbackType = Union{Integer,Colon,AbstractUnitRange{<:Integer},AbstractArray{<:Integer},CartesianIndex} -@inline Base.getindex(sd::MVTSeries, i1::_FallbackType...) = getindex(_vals(sd), i1...) -@inline Base.setindex!(sd::MVTSeries, val, i1::_FallbackType...) = setindex!(_vals(sd), val, i1...) +Base.getindex(sd::MVTSeries, i1::_FallbackType...) = getindex(_vals(sd), i1...) +Base.setindex!(sd::MVTSeries, val, i1::_FallbackType...) = setindex!(_vals(sd), val, i1...) # ------------------------------------------------------------- # Some other constructors @@ -120,9 +115,9 @@ const _FallbackType = Union{Integer,Colon,AbstractUnitRange{<:Integer},AbstractA MVTSeries(T::Type{<:Number}, fd::MIT, vars) = MVTSeries(fd, vars, Matrix{T}(undef, 0, length(vars))) # Uninitialized from a range and list of variables -@inline MVTSeries(rng::UnitRange{<:MIT}, vars) = MVTSeries(Float64, rng, vars, undef) -@inline MVTSeries(rng::UnitRange{<:MIT}, vars, ::UndefInitializer) = MVTSeries(Float64, rng, vars, undef) -@inline MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars) = MVTSeries(T, rng, vars, undef) +MVTSeries(rng::UnitRange{<:MIT}, vars) = MVTSeries(Float64, rng, vars, undef) +MVTSeries(rng::UnitRange{<:MIT}, vars, ::UndefInitializer) = MVTSeries(Float64, rng, vars, undef) +MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars) = MVTSeries(T, rng, vars, undef) MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars, ::UndefInitializer) = MVTSeries(first(rng), vars, Matrix{T}(undef, length(rng), length(vars))) MVTSeries(T::Type{<:Number}, rng::UnitRange{<:MIT}, vars::Symbol, ::UndefInitializer) = @@ -164,7 +159,7 @@ Base.similar(::AbstractArray{T}, shape::Tuple{UnitRange{<:MIT},NTuple{N,Symbol}} Base.fill(v::Number, rng::UnitRange{<:MIT}, vars::NTuple{N,Symbol}) where {N} = MVTSeries(first(rng), vars, fill(v, length(rng), length(vars))) # Empty (0 variables) from range -@inline function MVTSeries(rng::UnitRange{<:MIT}; args...) +function MVTSeries(rng::UnitRange{<:MIT}; args...) isempty(args) && return MVTSeries(rng, ()) keys, values = zip(args...) # figure out the element type @@ -279,11 +274,11 @@ Base.setindex!(x::MVTSeries, val, rng::UnitRange{MIT}) = mixed_freq_error(x, rng end # single argument - variable - return a TSeries of the column -@inline Base.getindex(x::MVTSeries, col::AbstractString) = _col(x, Symbol(col)) -@inline Base.getindex(x::MVTSeries, col::Symbol) = _col(x, col) +Base.getindex(x::MVTSeries, col::AbstractString) = _col(x, Symbol(col)) +Base.getindex(x::MVTSeries, col::Symbol) = _col(x, col) -@inline Base.setindex!(x::MVTSeries, val, col::AbstractString) = setindex!(x, val, Symbol(col)) -@inline function Base.setindex!(x::MVTSeries, val, col::Symbol) +Base.setindex!(x::MVTSeries, val, col::AbstractString) = setindex!(x, val, Symbol(col)) +function Base.setindex!(x::MVTSeries, val, col::Symbol) setproperty!(x, col, val) end @@ -303,15 +298,15 @@ end const _SymbolOneOrCollection = Union{Symbol,Vector{Symbol},NTuple{N,Symbol}} where {N} const _MITOneOrRange = Union{MIT,UnitRange{<:MIT}} -@inline Base.getindex(x::MVTSeries, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) -@inline Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) +Base.getindex(x::MVTSeries, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) +Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, c::_SymbolOneOrCollection) = mixed_freq_error(x, p) # if one argument is Colon, fall back on single argument indexing -@inline Base.getindex(x::MVTSeries, p::_MITOneOrRange, ::Colon) = getindex(x, p) -@inline Base.getindex(x::MVTSeries, ::Colon, c::_SymbolOneOrCollection) = getindex(x, c) +Base.getindex(x::MVTSeries, p::_MITOneOrRange, ::Colon) = getindex(x, p) +Base.getindex(x::MVTSeries, ::Colon, c::_SymbolOneOrCollection) = getindex(x, c) -@inline Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, ::Colon) = setindex!(x, val, p, axes(x, 2)) -@inline Base.setindex!(x::MVTSeries, val, ::Colon, c::_SymbolOneOrCollection) = setindex!(x, val, axes(x, 1), c) +Base.setindex!(x::MVTSeries, val, p::_MITOneOrRange, ::Colon) = setindex!(x, val, p, axes(x, 2)) +Base.setindex!(x::MVTSeries, val, ::Colon, c::_SymbolOneOrCollection) = setindex!(x, val, axes(x, 1), c) # @@ -365,7 +360,7 @@ end end # with a range of MIT and a single column - we fall back on TSeries assignment -@inline function Base.setindex!(x::MVTSeries{F}, val, r::UnitRange{MIT{F}}, c::Symbol) where {F<:Frequency} +function Base.setindex!(x::MVTSeries{F}, val, r::UnitRange{MIT{F}}, c::Symbol) where {F<:Frequency} setindex!(_col(x, c), val, r) end @@ -378,7 +373,7 @@ end setindex!(_vals(x), val, i1, i2) end -@inline Base.setindex!(x::MVTSeries, val, ind::Tuple{<:MIT,Symbol}) = setindex!(x, val, ind...) +Base.setindex!(x::MVTSeries, val, ind::Tuple{<:MIT,Symbol}) = setindex!(x, val, ind...) @inline function Base.setindex!(x::MVTSeries{F}, val::MVTSeries{F}, r::UnitRange{MIT{F}}, c::Union{Vector{Symbol},NTuple{N,Symbol}}) where {F<:Frequency,N} @boundscheck checkbounds(x, r) @@ -423,11 +418,11 @@ end Base.fill!(x::MVTSeries, val) = fill!(_vals(x), val) -@inline Base.view(x::MVTSeries, I...) = view(_vals(x), I...) +Base.view(x::MVTSeries, I...) = view(_vals(x), I...) -@inline Base.view(x::MVTSeries, ::Colon, J::_SymbolOneOrCollection) = view(x, axes(x, 1), J) -@inline Base.view(x::MVTSeries, I::_MITOneOrRange, ::Colon) = view(x, I, axes(x, 2)) -@inline Base.view(x::MVTSeries, ::Colon, ::Colon) = view(x, axes(x, 1), axes(x, 2)) +Base.view(x::MVTSeries, ::Colon, J::_SymbolOneOrCollection) = view(x, axes(x, 1), J) +Base.view(x::MVTSeries, I::_MITOneOrRange, ::Colon) = view(x, I, axes(x, 2)) +Base.view(x::MVTSeries, ::Colon, ::Colon) = view(x, axes(x, 1), axes(x, 2)) function Base.view(x::MVTSeries, I::_MITOneOrRange, J::_SymbolOneOrCollection) where {F<:Frequency} @boundscheck checkbounds(x, I) @boundscheck checkbounds(x, J) @@ -445,17 +440,17 @@ include("mvtseries/mvts_show.jl") #### arraymath -@inline Base.promote_shape(x::MVTSeries, y::MVTSeries) = +Base.promote_shape(x::MVTSeries, y::MVTSeries) = axes(x, 2) == axes(y, 2) ? (intersect(rangeof(x), rangeof(y)), axes(x, 2)) : throw(DimensionMismatch("Columns do not match:\n\t$(axes(x,2))\n\t$(axes(y,2))")) -@inline Base.promote_shape(x::MVTSeries, y::AbstractArray) = +Base.promote_shape(x::MVTSeries, y::AbstractArray) = promote_shape(_vals(x), y) -@inline Base.promote_shape(x::AbstractArray, y::MVTSeries) = +Base.promote_shape(x::AbstractArray, y::MVTSeries) = promote_shape(x, _vals(y)) -@inline Base.LinearIndices(x::MVTSeries) = LinearIndices(_vals(x)) +Base.LinearIndices(x::MVTSeries) = LinearIndices(_vals(x)) Base.:*(x::Number, y::MVTSeries) = copyto!(similar(y), *(x, y.values)) Base.:*(x::MVTSeries, y::Number) = copyto!(similar(x), *(x.values, y)) @@ -500,19 +495,19 @@ end #### diff and cumsum -@inline shift(x::MVTSeries, k::Integer) = shift!(copy(x), k) -@inline shift!(x::MVTSeries, k::Integer) = (x.firstdate -= k; x) -@inline lag(x::MVTSeries, k::Integer = 1) = shift(x, -k) -@inline lag!(x::MVTSeries, k::Integer = 1) = shift!(x, -k) -@inline lead(x::MVTSeries, k::Integer = 1) = shift(x, k) -@inline lead!(x::MVTSeries, k::Integer = 1) = shift!(x, k) +shift(x::MVTSeries, k::Integer) = shift!(copy(x), k) +shift!(x::MVTSeries, k::Integer) = (x.firstdate -= k; x) +lag(x::MVTSeries, k::Integer = 1) = shift(x, -k) +lag!(x::MVTSeries, k::Integer = 1) = shift!(x, -k) +lead(x::MVTSeries, k::Integer = 1) = shift(x, k) +lead!(x::MVTSeries, k::Integer = 1) = shift!(x, k) -@inline Base.diff(x::MVTSeries; dims = 1) = diff(x, -1; dims) -@inline Base.diff(x::MVTSeries, k::Integer; dims = 1) = +Base.diff(x::MVTSeries; dims = 1) = diff(x, -1; dims) +Base.diff(x::MVTSeries, k::Integer; dims = 1) = dims == 1 ? x - shift(x, k) : diff(_vals(x); dims) -@inline Base.cumsum(x::MVTSeries; dims) = cumsum!(copy(x), _vals(x); dims) -@inline Base.cumsum!(out::MVTSeries, in::AbstractMatrix; dims) = (cumsum!(_vals(out), in; dims); out) +Base.cumsum(x::MVTSeries; dims) = cumsum!(copy(x), _vals(x); dims) +Base.cumsum!(out::MVTSeries, in::AbstractMatrix; dims) = (cumsum!(_vals(out), in; dims); out) #### moving average @@ -527,11 +522,11 @@ window is backward-looking `(-n+1:0)` and if `n < 0` the window is forward-looki function moving end export moving -@inline _moving_mean!(x_ma::TSeries, x, t, window) = x_ma[t] = mean(x[t.+window]) -@inline _moving_mean!(x_ma::MVTSeries, x, t, window) = x_ma[t, :] .= mean(x[t.+window, :]; dims = 1) +_moving_mean!(x_ma::TSeries, x, t, window) = x_ma[t] = mean(x[t.+window]) +_moving_mean!(x_ma::MVTSeries, x, t, window) = x_ma[t, :] .= mean(x[t.+window, :]; dims = 1) -@inline _moving_shape(x::TSeries, n) = (rangeof(x, drop = n - copysign(1, n)),) -@inline _moving_shape(x::MVTSeries, n) = (rangeof(x, drop = n - copysign(1, n)), axes(x, 2)) +_moving_shape(x::TSeries, n) = (rangeof(x, drop = n - copysign(1, n)),) +_moving_shape(x::MVTSeries, n) = (rangeof(x, drop = n - copysign(1, n)), axes(x, 2)) function moving(x::Union{TSeries,MVTSeries}, n::Integer) window = n > 0 ? (-n+1:0) : (0:-n-1) @@ -586,7 +581,7 @@ the same length as the number of columns of `dvar`. function undiff end, function undiff! end export undiff, undiff! -@inline undiff(dvar::TSeries) = undiff(dvar, firstdate(dvar) - 1 => zero(eltype(dvar))) +undiff(dvar::TSeries) = undiff(dvar, firstdate(dvar) - 1 => zero(eltype(dvar))) function undiff(dvar::TSeries, anchor::Pair{<:MIT,<:Number}) fromdate, value = anchor ET = Base.promote_eltype(dvar, value) @@ -608,7 +603,7 @@ function undiff!(var::TSeries, dvar::TSeries; fromdate = firstdate(dvar) - 1) if lastdate(var) < lastdate(dvar) resize!(var, firstdate(var):lastdate(dvar)) end - for t = fromdate+1:lastdate(dvar) + for t = fromdate+1:lastdate(dvar) var[t] = var[t-1] + dvar[t] end return var diff --git a/src/serialize.jl b/src/serialize.jl new file mode 100644 index 0000000..55d17e4 --- /dev/null +++ b/src/serialize.jl @@ -0,0 +1,55 @@ +# Copyright (c) 2020-2022, Bank of Canada +# All rights reserved. + +# Serialization (for parallel computing communications) + +using Serialization + +# ============== MIT ======================================== + +Base.write(io::IO, p::MIT) = write(io, Ref(p)) +Base.read(io::IO, T::Type{<:MIT}) = read!(io, Ref(T(0)))[] +Base.write(io::IO, p::Duration) = write(io, Ref(p)) +Base.read(io::IO, T::Type{<:Duration}) = read!(io, Ref(T(0)))[] + +# ============== TSeries ===================================== +# Not sure this part is really necessary, since it works with Julia's built-in +# implementation just fine + +function Serialization.serialize(s::AbstractSerializer, t::TSeries) + Serialization.serialize_type(s, typeof(t)) + write(s.io, length(t)) + write(s.io, firstdate(t)) + write(s.io, t.values) +end + +function Serialization.deserialize(s::AbstractSerializer, S::Type{<:TSeries{F,T}}) where {F,T} + n = read(s.io, Int) + fd = read(s.io, MIT{F}) + ret = TSeries(T, fd .+ (0:n-1), undef) + read!(s.io, ret.values) + return ret +end + +# ============== MVTSeries =================================== + +# This part is a must - it works with Julia's built-in implementation, but gives +# the wrong result (. variables are not views into the raw data but separate +# TSeries!!!) + +function Serialization.serialize(s::AbstractSerializer, sd::MVTSeries) + Serialization.serialize_type(s, typeof(sd)) + write(s.io, firstdate(sd)) + write(s.io, lastdate(sd)) + serialize(s, [colnames(sd)...]) + write(s.io, rawdata(sd)) +end + +function Serialization.deserialize(s::AbstractSerializer, S::Type{<:MVTSeries{F,T}}) where {F,T} + fd = read(s.io, MIT{F}) + ld = read(s.io, MIT{F}) + cols = deserialize(s) + ret = MVTSeries(T, fd:ld, cols, undef) + read!(s.io, rawdata(ret)) + return ret +end diff --git a/src/tseries.jl b/src/tseries.jl index 363581f..d2039a6 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -67,21 +67,21 @@ mutable struct TSeries{F<:Frequency,T<:Number,C<:AbstractVector{T}} <: AbstractV values::C end -@inline _vals(t::TSeries) = t.values -@inline rawdata(t::TSeries) = t.values +_vals(t::TSeries) = t.values +rawdata(t::TSeries) = t.values Base.values(t::TSeries) = values(t.values) -@inline firstdate(t::TSeries) = t.firstdate -@inline lastdate(t::TSeries) = t.firstdate + length(t.values) - one(t.firstdate) +firstdate(t::TSeries) = t.firstdate +lastdate(t::TSeries) = t.firstdate + length(t.values) - one(t.firstdate) -@inline frequencyof(::Type{<:TSeries{F}}) where {F<:Frequency} = F +frequencyof(::Type{<:TSeries{F}}) where {F<:Frequency} = F """ rangeof(s) Return the stored range of the given time series object. """ -@inline rangeof(t::TSeries) = firstdate(t) .+ (0:size(t.values, 1)-1) +rangeof(t::TSeries) = firstdate(t) .+ (0:size(t.values, 1)-1) """ firstdate(ts), lastdate(ts) @@ -94,9 +94,9 @@ firstdate, lastdate # ------------------------------------------------------------------------------- # some methods that make the AbstractArray infrastructure of Julia work with TSeries -@inline Base.size(t::TSeries) = size(t.values) -@inline Base.axes(t::TSeries) = (firstdate(t):lastdate(t),) -@inline Base.axes1(t::TSeries) = firstdate(t):lastdate(t) +Base.size(t::TSeries) = size(t.values) +Base.axes(t::TSeries) = (firstdate(t):lastdate(t),) +Base.axes1(t::TSeries) = firstdate(t):lastdate(t) # the following are needed for copy() and copyto!() (and a bunch of Julia internals that use them) Base.IndexStyle(::TSeries) = IndexLinear() @@ -106,6 +106,10 @@ Base.dataids(t::TSeries) = Base.dataids(getfield(t, :values)) # we add few other versions of similar below Base.similar(t::TSeries) = TSeries(t.firstdate, similar(t.values)) +# ------------------------------------------------------------------------------- + +Base.hash(t::TSeries, h::UInt) = hash((t.values, t.firstdate), h) + # ------------------------------------------------------------------------------- # Indexing with integers and booleans - same as vectors @@ -274,7 +278,7 @@ function Base.setindex!(t::TSeries{F}, vec::AbstractVector{<:Number}, rng::Abstr end Base.setindex!(t::TSeries{F1}, src::TSeries{F2}, rng::AbstractRange{MIT{F3}}) where {F1<:Frequency,F2<:Frequency,F3<:Frequency} = mixed_freq_error(t, src, rng) -@inline Base.setindex!(t::TSeries{F}, src::TSeries{F}, rng::AbstractRange{MIT{F}}) where {F<:Frequency} = copyto!(t, rng, src) +Base.setindex!(t::TSeries{F}, src::TSeries{F}, rng::AbstractRange{MIT{F}}) where {F<:Frequency} = copyto!(t, rng, src) """ typenan(x), typenan(T) @@ -332,7 +336,7 @@ end # Base.copyto!(dest::TSeries, src::TSeries) = mixed_freq_error(dest, src) -@inline Base.copyto!(dest::TSeries{F}, src::TSeries{F}) where {F<:Frequency} = copyto!(dest, rangeof(src), src) +Base.copyto!(dest::TSeries{F}, src::TSeries{F}) where {F<:Frequency} = copyto!(dest, rangeof(src), src) # Base.copyto!(dest::TSeries, drng::AbstractRange{<:MIT}, src::TSeries) = mixed_freq_error(dest, drng, src) @@ -347,18 +351,18 @@ end # view with MIT indexing Base.view(t::TSeries, I::AbstractRange{<:MIT}) = mixed_freq_error(t, I) -function Base.view(t::TSeries{F}, I::AbstractRange{MIT{F}}) where {F<:Frequency} +@inline function Base.view(t::TSeries{F}, I::AbstractRange{MIT{F}}) where {F<:Frequency} fi = firstindex(t.values) TSeries(first(I), view(t.values, oftype(fi, first(I) - firstindex(t) + fi):oftype(fi, last(I) - firstindex(t) + fi))) end # view with Int indexing -function Base.view(t::TSeries, I::AbstractRange{<:Integer}) +@inline function Base.view(t::TSeries, I::AbstractRange{<:Integer}) fi = firstindex(t.values) TSeries(firstindex(t) + first(I) - one(first(I)), view(t.values, oftype(fi, first(I)):oftype(fi, last(I)))) end -@inline Base.diff(x::TSeries, k::Integer = -1) = x - lag(x, -k) +Base.diff(x::TSeries, k::Integer = -1) = x - lag(x, -k) function Base.vcat(x::TSeries, args::AbstractVector...) return TSeries(firstdate(x), vcat(_vals(x), args...)) @@ -441,77 +445,7 @@ apct(ts::TSeries, args...) = error("apct for frequency $(frequencyof(ts)) not im Year-to-year percent change in x. """ -ytypct(x) = 100*(x ./ shift(x, -ppy(x)) .- 1) +ytypct(x) = 100 * (x ./ shift(x, -ppy(x)) .- 1) export ytypct -# function Base.cumsum(s::TSeries) -# TSeries(s.firstdate, cumsum(s.values)) -# end - -# function Base.cumsum!(s::TSeries) -# s.values = cumsum(s.values) -# return s -# end - - -# """ -# leftcropnan!(x::TSeries) - -# Remove `NaN` values from starting at the beginning of `x`, in-place. - -# __Note__: an internal function. -# """ -# function leftcropnan!(s::TSeries) -# while isequal(s[firstdate(s)], NaN) -# popfirst!(s.values) -# s.firstdate = s.firstdate + 1 -# end -# return s -# end - -# """ -# rightcropnan!(x::TSeries) - -# Remove `NaN` values from the end of `x` - -# __Note__: an internal function. -# """ -# function rightcropnan!(s::TSeries) -# while isequal(s[lastdate(s)], NaN) -# pop!(s.values) -# end -# return s -# end - - -# """ -# nanrm!(s::TSeries, type::Symbol) - -# Remove `NaN` values that are either at the beginning of the `s` and/or end of `x`. - -# Examples -# ``` -# julia> s = TSeries(yy(2018), [NaN, NaN, 1, 2, NaN]); - -# julia> nanrm!(s); - -# julia> s -# TSeries{Yearly} of length 2 -# 2020Y: 1.0 -# 2021Y: 2.0 -# ``` -# """ -# function nanrm!(s::TSeries, type::Symbol=:both) -# if type == :left -# leftcropnan!(s) -# elseif type == :right -# rightcropnan!(s) -# elseif type == :both -# leftcropnan!(s) -# rightcropnan!(s) -# else -# error("Please select between :left, :right, or :both.") -# end -# return s -# end diff --git a/test/runtests.jl b/test/runtests.jl index 77958a3..19970ce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,3 +8,4 @@ include("test_mit.jl") include("test_tseries.jl") include("test_mvtseries.jl") include("test_workspace.jl") +include("test_serialize.jl") diff --git a/test/test_serialize.jl b/test/test_serialize.jl new file mode 100644 index 0000000..f325ec3 --- /dev/null +++ b/test/test_serialize.jl @@ -0,0 +1,50 @@ + +using Distributed + +@testset "serialize" begin + + addprocs(1) + # @everywhere workers() begin + # using Pkg + # Pkg.activate(".") + # end + + @everywhere workers() using TimeSeriesEcon + + p = 2020Q1 + z = fetch(@spawnat :any (p; p + 1)) + @test typeof(p) == typeof(z) + @test z == 2020Q2 + # + @test fetch(@spawnat :any (1 .+ (2020Q1:2022Q4))) == 2020Q2:2023Q1 + # + t = TSeries(2020Q1, rand(5)) + z = fetch(@spawnat :any log.(t)) + @test typeof(t) == typeof(z) + @test firstdate(t) == firstdate(z) + @test length(t) == length(z) + @test z == log.(t) + # + sd = MVTSeries(2000Y, (:A, :Beta, :C), rand(5, 3)) + @everywhere foo_1234(s::MVTSeries) = hcat(s; Foo = s.A + s.Beta - 2s.C) + z = fetch(@spawnat :any foo_1234(sd)) + @test typeof(sd) == typeof(z) + @test rangeof(sd) == rangeof(z) + @test (colnames(z)...,) == (:A, :Beta, :C, :Foo) + @test rawdata(z)[:, 1:3] == rawdata(sd) + @test rawdata(z)[:, 4] == (rawdata(sd)[:, 1] .+ + rawdata(sd)[:, 2] .- 2 .* rawdata(sd)[:, 3]) + # + z = fetch(@spawnat :any MVTSeries(20Q1:22Q4, (:a, :b, :c), rand)) + z[20Q2, :a] = 100 + @test z.a[20Q2] == z[20Q2, :a] + @test all(z.a .== z[:, :a]) + @test all(z[20Q2] .== z[20Q2, :]) + z.b[21Q1] = 200 + @test z.b[21Q1] == z[21Q1, :b] + @test all(z.b .== z[:, :b]) + @test all(z[21Q1] .== z[21Q1, :]) + + rmprocs(workers()) + +end \ No newline at end of file From 788c741face8b64802b2f4b5ca2ab174585ab88a Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Wed, 16 Mar 2022 00:47:01 -0400 Subject: [PATCH 06/13] refactored hcat for MVTSeries --- src/mvtseries.jl | 10 +--------- src/plotrecipes.jl | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/mvtseries.jl b/src/mvtseries.jl index 4c8fe65..d0eb5ef 100644 --- a/src/mvtseries.jl +++ b/src/mvtseries.jl @@ -399,15 +399,7 @@ Base.copyto!(dest::MVTSeries, src::MVTSeries) = (copyto!(dest.values, src.values function Base.hcat(x::MVTSeries; KW...) T = reduce(Base.promote_eltype, (x, values(KW)...)) - y = MVTSeries(rangeof(x), tuple(colnames(x)..., keys(KW)...), typenan(T)) - # copyto!(y, x) - for (k, v) in pairs(x) - setproperty!(y, k, v) - end - for (k, v) in KW - setproperty!(y, k, v) - end - return y + return MVTSeries(T, rangeof(x); pairs(x)..., KW...) end function Base.vcat(x::MVTSeries, args::AbstractVecOrMat...) diff --git a/src/plotrecipes.jl b/src/plotrecipes.jl index a2ab5f2..c575c27 100644 --- a/src/plotrecipes.jl +++ b/src/plotrecipes.jl @@ -110,7 +110,7 @@ end vname = var[1] title := var[2] else - vname = var + vname = Symbol(var) title := string(vname) end From 76fecc15fe35eee71a15364a634ed0060f196116 Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Mon, 28 Mar 2022 14:08:44 -0400 Subject: [PATCH 07/13] updating docs --- src/fconvert.jl | 17 ++++++------ src/mvtseries.jl | 69 ++++++++++++++++++++++++++++++++++-------------- src/tseries.jl | 36 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/src/fconvert.jl b/src/fconvert.jl index 10219d9..96cf533 100644 --- a/src/fconvert.jl +++ b/src/fconvert.jl @@ -7,23 +7,24 @@ import Statistics: mean """ overlay(t1, t2, ...) -Construct a TSeries in which each observation is taken from the first +Construct a [`TSeries`](@ref) in which each observation is taken from the first non-missing observation in the list of arguments. A missing observation is one for which [`istypenan`](@ref) returns `true`. -All TSeries in the argument list must be of the same frequency. The data type of -the resulting TSeries is computed by the standard promotion of numerical types -in Julia. Its range is the union of the ranges of the arguments. +All [`TSeries`](@ref)` in the arguments list must be of the same frequency. The +data type of the resulting [`TSeries`](@ref) is decided by the standard +promotion of numerical types in Julia. Its range is the union of the ranges of +the arguments. """ -@inline overlay(ts::Vararg{<:TSeries}) = overlay(mapreduce(rangeof, union, ts), ts...) +@inline overlay(ts::TSeries...) = overlay(mapreduce(rangeof, union, ts), ts...) """ overlay(rng, t1, t2, ...) -If the first argument is a range (must be of the same frequency), that becomes -the range of the resulting TSeries. +If the first argument is a range, it becomes the range of the resulting +[`TSeries`](@ref). """ -function overlay(rng::AbstractRange{<:MIT}, ts::Vararg{<:TSeries}) +function overlay(rng::AbstractRange{<:MIT}, ts::TSeries...) T = mapreduce(eltype, promote_type, ts) ret = TSeries(rng, typenan(T)) # na = collection of periods where the entry of ret is missing (typenan(T)) diff --git a/src/mvtseries.jl b/src/mvtseries.jl index d0eb5ef..73b2e4f 100644 --- a/src/mvtseries.jl +++ b/src/mvtseries.jl @@ -30,7 +30,7 @@ _names_as_tuple(names) = tuple((Symbol(n) for n in names)...) # standard constructor with default empty values -MVTSeries(fd::MIT, names = ()) = (names = _names_as_tuple(names); MVTSeries(fd, names, zeros(0, length(names)))) +MVTSeries(fd::MIT, names=()) = (names = _names_as_tuple(names); MVTSeries(fd, names, zeros(0, length(names)))) MVTSeries(fd::MIT, names::Union{AbstractVector,Tuple,Base.KeySet{Symbol,<:OrderedDict}}, data::AbstractMatrix) = begin names = _names_as_tuple(names) MVTSeries(fd, names, data) @@ -61,7 +61,17 @@ colnames(x::MVTSeries) = keys(_cols(x)) rawdata(x::MVTSeries) = _vals(x) # some methods to make MVTSeries function like a Dict (collection of named of columns) -Base.pairs(x::MVTSeries) = pairs(_cols(x)) +# Base.pairs(x::MVTSeries) = pairs(_cols(x)) +""" + pairs(data::MVTSeries; copy = false) + +Returns an iterator over the named columns of `data`. Each iteration gives a +name-value pair where name is a `Symbol` and value is a [`TSeries`](@ref). + +Setting `copy=true` is equivalent to `pairt(copy(data))` but slightly more +efficient. +""" +Base.pairs(x::MVTSeries; copy=false) = copy ? pairs(deepcopy(_cols(x))) : pairs(_cols(x)) Base.keys(x::MVTSeries) = keys(_cols(x)) Base.haskey(x::MVTSeries, sym::Symbol) = haskey(_cols(x), sym) Base.get(x::MVTSeries, sym::Symbol, default) = get(_cols(x), sym, default) @@ -88,8 +98,27 @@ Base.dataids(x::MVTSeries) = Base.dataids(_vals(x)) Base.eachindex(x::MVTSeries) = eachindex(_vals(x)) -# normally only the first of the following is sufficient. -# we add few other versions of similar below +""" + similar(t::MVTSeries, [eltype], [shape]) + similar(array, [eltype], shape) + similar(array_type, [eltype], shape) + +Create an uninitialized [`MVTSeries`](@ref) with the given element type and `shape`. + +If the first argument is an [`MVTSeries`](@ref) then the element type and shape +of the output will match those of the input, unless they are explicitly given in +subsequent arguments. If the first argument is another array or an array type, +then `shape` must be given in the form of a tuple where the first element is an +MIT range and the second is a list of column names. The element type, `eltype`, +also can be given optionally; if not given it will be deduced from the first +argument. + +Example: +``` +similar(Array{Float64}, (2000Q1:2001Q4, (:a, :b))) +``` + +""" Base.similar(x::MVTSeries) = MVTSeries(firstdate(x), colnames(x), similar(_vals(x))) Base.similar(x::MVTSeries, ::Type{T}) where {T} = MVTSeries(firstdate(x), colnames(x), similar(_vals(x), T)) @@ -464,11 +493,11 @@ end #### sum(x::MVTSeries; dims=2) -> TSeries for func in (:sum, :prod, :minimum, :maximum) - @eval @inline Base.$func(x::MVTSeries; dims) = - dims == 2 ? TSeries(firstdate(x), $func(rawdata(x); dims = dims)[:]) : $func(rawdata(x); dims = dims) + @eval @inline Base.$func(x::MVTSeries; dims=:) = + dims == 2 ? TSeries(firstdate(x), $func(rawdata(x); dims=dims)[:]) : $func(rawdata(x); dims=dims) - @eval @inline Base.$func(f, x::MVTSeries; dims) = - dims == 2 ? TSeries(firstdate(x), $func(f, rawdata(x); dims = dims)[:]) : $func(f, rawdata(x); dims = dims) + @eval @inline Base.$func(f, x::MVTSeries; dims=:) = + dims == 2 ? TSeries(firstdate(x), $func(f, rawdata(x); dims=dims)[:]) : $func(f, rawdata(x); dims=dims) end @@ -489,13 +518,13 @@ end shift(x::MVTSeries, k::Integer) = shift!(copy(x), k) shift!(x::MVTSeries, k::Integer) = (x.firstdate -= k; x) -lag(x::MVTSeries, k::Integer = 1) = shift(x, -k) -lag!(x::MVTSeries, k::Integer = 1) = shift!(x, -k) -lead(x::MVTSeries, k::Integer = 1) = shift(x, k) -lead!(x::MVTSeries, k::Integer = 1) = shift!(x, k) +lag(x::MVTSeries, k::Integer=1) = shift(x, -k) +lag!(x::MVTSeries, k::Integer=1) = shift!(x, -k) +lead(x::MVTSeries, k::Integer=1) = shift(x, k) +lead!(x::MVTSeries, k::Integer=1) = shift!(x, k) -Base.diff(x::MVTSeries; dims = 1) = diff(x, -1; dims) -Base.diff(x::MVTSeries, k::Integer; dims = 1) = +Base.diff(x::MVTSeries; dims=1) = diff(x, -1; dims) +Base.diff(x::MVTSeries, k::Integer; dims=1) = dims == 1 ? x - shift(x, k) : diff(_vals(x); dims) Base.cumsum(x::MVTSeries; dims) = cumsum!(copy(x), _vals(x); dims) @@ -515,10 +544,10 @@ function moving end export moving _moving_mean!(x_ma::TSeries, x, t, window) = x_ma[t] = mean(x[t.+window]) -_moving_mean!(x_ma::MVTSeries, x, t, window) = x_ma[t, :] .= mean(x[t.+window, :]; dims = 1) +_moving_mean!(x_ma::MVTSeries, x, t, window) = x_ma[t, :] .= mean(x[t.+window, :]; dims=1) -_moving_shape(x::TSeries, n) = (rangeof(x, drop = n - copysign(1, n)),) -_moving_shape(x::MVTSeries, n) = (rangeof(x, drop = n - copysign(1, n)), axes(x, 2)) +_moving_shape(x::TSeries, n) = (rangeof(x, drop=n - copysign(1, n)),) +_moving_shape(x::MVTSeries, n) = (rangeof(x, drop=n - copysign(1, n)), axes(x, 2)) function moving(x::Union{TSeries,MVTSeries}, n::Integer) window = n > 0 ? (-n+1:0) : (0:-n-1) @@ -588,7 +617,7 @@ function undiff(dvar::TSeries, anchor::Pair{<:MIT,<:Number}) return result end -function undiff!(var::TSeries, dvar::TSeries; fromdate = firstdate(dvar) - 1) +function undiff!(var::TSeries, dvar::TSeries; fromdate=firstdate(dvar) - 1) if fromdate < firstdate(var) error("Range mismatch: `fromdate == $(fromdate) < $(firstdate(var)) == firstdate(var): ") end @@ -602,7 +631,7 @@ function undiff!(var::TSeries, dvar::TSeries; fromdate = firstdate(dvar) - 1) end # undiff(dvar::MVTSeries) = undiff(dvar, firstdate(dvar) - 1 => zeros(eltype(dvar), size(dvar, 2))) -undiff(dvar::MVTSeries, anchor_value::Number = 0) = undiff(dvar, firstdate(dvar) - 1 => fill(anchor_value, size(dvar, 2))) +undiff(dvar::MVTSeries, anchor_value::Number=0) = undiff(dvar, firstdate(dvar) - 1 => fill(anchor_value, size(dvar, 2))) function undiff(dvar::MVTSeries, anchor::Pair{<:MIT,<:AbstractVecOrMat}) fromdate, value = anchor ET = Base.promote_eltype(dvar, value) @@ -615,7 +644,7 @@ function undiff(dvar::MVTSeries, anchor::Pair{<:MIT,<:AbstractVecOrMat}) dvar .= tmp end result = similar(dvar, ET) - result .= cumsum(dvar; dims = 1) + result .= cumsum(dvar; dims=1) correction = reshape(value .- result[fromdate], 1, :) result .+= correction return result diff --git a/src/tseries.jl b/src/tseries.jl index d2039a6..d88edf5 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -104,6 +104,19 @@ Base.dataids(t::TSeries) = Base.dataids(getfield(t, :values)) # normally only the first of the following is sufficient. # we add few other versions of similar below +""" + similar(t::TSeries, [eltype], [range]) + similar(array, [eltype], range) + similar(array_type, [eltype], range) + +Create an uninitialized [`TSeries`](@ref) with the given element type and range. + +If the first argument is a [`TSeries`](@ref) then the element type and range of +the output will match those of the input, unless they are explicitly given in +subsequent arguments. If the first argument is another array or an array type, +then `range` must be given. The element type, `eltype`, can be given; if not it +will be deduced from the first argument. +""" Base.similar(t::TSeries) = TSeries(t.firstdate, similar(t.values)) # ------------------------------------------------------------------------------- @@ -301,6 +314,13 @@ istypenan(x::Integer) = x == typenan(x) istypenan(x::AbstractFloat) = isnan(x) # n::Integer - only the length changes. We keep the starting date +""" + resize!(t::TSeries, n::Integer) + +Extend or shrink the allocated storage for `t` to `n` entries. The first date of +`t` does not change. If allocation is extended, the new entries are set to +`NaN`. +""" function Base.resize!(t::TSeries, n::Integer) lt = length(t) # the old length if lt ≠ n @@ -313,6 +333,12 @@ end # if range is given Base.resize!(t::TSeries, rng::UnitRange{<:MIT}) = mixed_freq_error(t, eltype(rng)) +""" + resize!(t::TSeries, rng) + +Extend or shrink the allocated storage for `t` so that the new range of `t` +equals the given `rng`. If `t` is extended, new entries are set to `NaN`. +""" function Base.resize!(t::TSeries{F}, rng::UnitRange{MIT{F}}) where {F<:Frequency} orng = rangeof(t) # old range if first(rng) == first(orng) @@ -362,6 +388,16 @@ end TSeries(firstindex(t) + first(I) - one(first(I)), view(t.values, oftype(fi, first(I)):oftype(fi, last(I)))) end +""" + diff(x::TSeries) + diff(x::TSeries, k) + +Construct the first difference, or the `k`-th difference, of time series `t`. If +`y = diff(x,k)` then `y[t] = x[t] - x[t+k]`. A negative value of `k` means that +we subtract a lag and positive value means that we subtract a lead. `k` not +given is the same as `k=-1`, which is the standard definition of first +difference. +""" Base.diff(x::TSeries, k::Integer = -1) = x - lag(x, -k) function Base.vcat(x::TSeries, args::AbstractVector...) From da0c440f5b1169ce69035df6d23733ad7ddb98e8 Mon Sep 17 00:00:00 2001 From: Nicholas Labelle St-Pierre Date: Tue, 29 Mar 2022 10:05:31 -0400 Subject: [PATCH 08/13] Function reindex --- src/mvtseries.jl | 26 ++++++++++++++++++++++++++ src/tseries.jl | 9 +++++++++ 2 files changed, 35 insertions(+) diff --git a/src/mvtseries.jl b/src/mvtseries.jl index 73b2e4f..1bace39 100644 --- a/src/mvtseries.jl +++ b/src/mvtseries.jl @@ -649,3 +649,29 @@ function undiff(dvar::MVTSeries, anchor::Pair{<:MIT,<:AbstractVecOrMat}) result .+= correction return result end + +#### reindex + +""" + reindex(ts, from => to; copy = false) + +The function `reindex` re-indexes the `TSeries` or `MVTSeries` `ts` +so that the `MIT` `from` becomes the `MIT` `to` leaving the data unchanged. + +By default, the data is not copied. + +Example: +``` +ts = MVTSeries(2020Q1,(:y1,:y2),randn(10,2)) +ts2 = reindex(ts,2021Q1 => 1U; copy = true) +ts2.y2[3U] = 9999 +ts +ts2 +``` +""" +function reindex(ts::MVTSeries,pair::Pair{<:MIT,<:MIT}; copy = false) + ts_lag = firstdate(ts)-pair[1] + return MVTSeries(pair[2]+Int(ts_lag),keys(ts), copy ? Base.copy(ts.values) : ts.values) +end +export reindex + diff --git a/src/tseries.jl b/src/tseries.jl index d88edf5..d97fedd 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -485,3 +485,12 @@ ytypct(x) = 100 * (x ./ shift(x, -ppy(x)) .- 1) export ytypct +#### reindex + +function reindex(ts::TSeries,pair::Pair{<:MIT,<:MIT}; copy = false) + ts_lag = firstdate(ts)-pair[1] + return TSeries(pair[2]+Int(ts_lag), copy ? Base.copy(ts.values) : ts.values) +end +export reindex + + From 0118cb297cb996e90629ec8932d778f41eedd38f Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Wed, 30 Mar 2022 11:06:23 -0400 Subject: [PATCH 09/13] Updated documentation and faster overlay --- .gitignore | 4 - src/TimeSeriesEcon.jl | 98 ++++++++-------- src/fconvert.jl | 101 ++++++++--------- src/momentintime.jl | 124 +++++++++++++------- src/mvtseries.jl | 110 ++++++++++++++++-- src/mvtseries/mvts_broadcast.jl | 162 +-------------------------- src/recursive.jl | 6 +- src/tsbroadcast.jl | 116 ------------------- src/tseries.jl | 193 ++++++++++++++++++-------------- src/tsmath.jl | 82 ++++++++------ src/various.jl | 193 ++++++++++++++++++++++++++++++++ src/workspaces.jl | 173 +++++++--------------------- test/test_mit.jl | 2 + 13 files changed, 672 insertions(+), 692 deletions(-) create mode 100644 src/various.jl diff --git a/.gitignore b/.gitignore index 50eebdc..d7a4b24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,10 @@ -vscode_workspace* *.db *.jld2 *.svg *.pdf *.html -various.jl Manifest.toml -archive .vscode docs/build docs/Manifest.toml -tbd.jl \ No newline at end of file diff --git a/src/TimeSeriesEcon.jl b/src/TimeSeriesEcon.jl index 65e72bc..f851446 100644 --- a/src/TimeSeriesEcon.jl +++ b/src/TimeSeriesEcon.jl @@ -2,56 +2,45 @@ # All rights reserved. -# """ -# TimeSeriesEcon - -# This package is part of the StateSpaceEcon ecosystem. -# TimeSeriesEcon.jl provides functionality to work with -# low-Frequency discrete macroeconomic time-series data. - -# ### Frequencies (abstract type): -# - Unit -# - Monthly -# - Quarterly -# - Yearly - -# ### Types: - -# - `MIT{Frequency}` (aka "Moment In Time") -# - a primitive type denoting monthly, quarterly, and yearly dates -# - `TSeries{Frequency}` -# - an `AbstractVector` that can be indexed using `MIT` - -# ### Functions: - -# - `MIT` Constructors/Functions -# - `mm(year::Int, period::Int)`: returns a monthly `MIT` type instance -# - `qq(year::Int, period::Int)`: returns a quarterly `MIT` type instance -# - `yy(year::Int)`: returns a yearly `MIT` type instance -# - `ii(x::Int)`: returns a unit `MIT` type instance -# - `year(x::MIT)`: returns a `Int64` year value associated with `x` -# - `period(x::MIT)`: returns a `Int64` period value associated with `x` -# - `frequencyof(x::MIT)`: returns `<: Frequency` assosicated wtih `x` - - -# - Functions operating on `TSeries` -# - `mitrange(x::TSeries)`: returns a `UnitRange{MIT{Frequency}}` for the given `x` -# - `firstdate(x::TSeries)`: returns `MIT{Frequency}` first date associated with `x` -# - `lastdate(x::TSeries)`: returns `MIT{Frequency}` last date associated with `x` -# - `ppy(x::TSeries)`: returns the number of periods per year for `x::TSeries`. (`ppy` also accepts `x::MIT` and `x::Frequency`) -# - `shift(x::TSeries, i::Int64)`: shifts the dates of `x` by `firstdate(x) - i` -# - `shift!`: in-place version of `shift` -# - `pct(x::TSeries, shift_value::Int64; islog::Bool = false)`: calculates percent rate of change of `x::TSeries` -# - `apct(x::TSeries, islog::Bool = false)`: calculates annualized percent rate of change of `x::TSeries` -# - `nanrm!(x::TSeries, type::Symbol=:both)`: removes `NaN` from `x::TSeries` -# """ +""" + TimeSeriesEcon + +This package is part of the StateSpaceEcon ecosystem. Provides the data types +and functionality necessary to work with macroeconomic discrete time models. + +### Working with time + * Frequencies are represented by abstract type [`Frequency`](@ref). + * Concrete frequencies include [`Yearly`](@ref), [`Quarterly`](@ref) and + [`Monthly`](@ref). + * Moments in time are represented by data type [`MIT`](@ref). + * Lengths of time are represented by data type [`Duration`](@ref). + +### Working with time series + * Data type [`TSeries`](@ref) represents a single time series. + * Data type [`MVTSeries`](@ref) represents a multivariate time series. + +### Working with other data + * Data type [`Workspace`](@ref) is a general purpose dictionary-like collection + of "variable"-like objects. + +### Tutorial + * [TimeSeriesEcon tutorial](https://bankofcanada.github.io/DocsEcon.jl/dev/Tutorials/TimeSeriesEcon/main/) +""" module TimeSeriesEcon +# other packages using MacroTools +using RecipesBase +using OrderedCollections + +# standard library +using Statistics +using Serialization +using Distributed include("momentintime.jl") export MIT, Duration -export mm, qq, yy +# export mm, qq, yy export Monthly, Quarterly, Yearly, Frequency, YPFrequency, Unit export year, period, mit2yp, ppy export frequencyof @@ -80,6 +69,13 @@ export @rec include("plotrecipes.jl") +include("workspaces.jl") + +include("serialize.jl") + +include("various.jl") + + """ rangeof(s; drop::Integer) @@ -89,13 +85,16 @@ end. This adds convenience when using [`@rec`](@ref) Example ``` -julia> q = TSeries(20Q1:21Q4); rangeof(q; drop=1) +julia> q = TSeries(20Q1:21Q4); +julia> rangeof(q; drop=1) 20Q2:21Q4 julia> rangeof(q; drop=-4) 20Q1:20Q4 -julia> q[begin:begin+1] .= 1; @rec rangeof(q; drop=2) q[t] = q[t-1] + q[t-2]; q +julia> q[begin:begin+1] .= 1; +julia> @rec rangeof(q; drop=2) q[t] = q[t-1] + q[t-2]; +julia> q 8-element TSeries{Quarterly} with range 20Q1:21Q4: 20Q1 : 1.0 20Q2 : 1.0 @@ -107,14 +106,9 @@ julia> q[begin:begin+1] .= 1; @rec rangeof(q; drop=2) q[t] = q[t-1] + q[t-2]; q 21Q4 : 21.0 ``` """ -@inline function rangeof(x::Union{TSeries,MVTSeries}; drop::Integer) +@inline function rangeof(x::Union{TSeries,MVTSeries,Workspace}; drop::Integer) rng = rangeof(x) return drop > 0 ? (first(rng)+drop:last(rng)) : (first(rng):last(rng)+drop) end - -include("workspaces.jl") - -include("serialize.jl") - end diff --git a/src/fconvert.jl b/src/fconvert.jl index 96cf533..61939d4 100644 --- a/src/fconvert.jl +++ b/src/fconvert.jl @@ -3,47 +3,7 @@ import Statistics: mean - -""" - overlay(t1, t2, ...) - -Construct a [`TSeries`](@ref) in which each observation is taken from the first -non-missing observation in the list of arguments. A missing observation is one -for which [`istypenan`](@ref) returns `true`. - -All [`TSeries`](@ref)` in the arguments list must be of the same frequency. The -data type of the resulting [`TSeries`](@ref) is decided by the standard -promotion of numerical types in Julia. Its range is the union of the ranges of -the arguments. -""" -@inline overlay(ts::TSeries...) = overlay(mapreduce(rangeof, union, ts), ts...) - -""" - overlay(rng, t1, t2, ...) - -If the first argument is a range, it becomes the range of the resulting -[`TSeries`](@ref). -""" -function overlay(rng::AbstractRange{<:MIT}, ts::TSeries...) - T = mapreduce(eltype, promote_type, ts) - ret = TSeries(rng, typenan(T)) - # na = collection of periods where the entry of ret is missing (typenan(T)) - na = collect(rng) - for t in ts - if isempty(na) - # if na is empty, then we've assigned all slots - break - end - # keep = periods that are not yet assigned and t has valid values in them - keep = intersect(na, rangeof(t)[values(@. !istypenan(t))]) - # assign - ret[keep] = t[keep] - # update na by removing the periods we just assigned - na = setdiff(na, keep) - end - return ret -end -export overlay +#### strip and strip! function _valid_range(t::TSeries) fd = firstdate(t) @@ -57,7 +17,19 @@ function _valid_range(t::TSeries) return fd:ld end +""" + strip(t:TSeries) + +Remove leading and trailing `NaN` from the given time series. This version +creates a new [`TSeries`](@ref) instance. +""" Base.strip(t::TSeries) = getindex(t, _valid_range(t)) +""" + strip!(t::TSeries) + +Remove leading and training `NaN` from the given time series. This is +done in-place. +""" strip!(t::TSeries) = resize!(t, _valid_range(t)) export strip! @@ -71,16 +43,34 @@ Conversion from $(frequencyof(t)) to $F not implemented. """) # do nothing when the source and target frequencies are the same. -fconvert(::Type{F}, t::TSeries{F}) where {F <: Frequency} = t +fconvert(::Type{F}, t::TSeries{F}) where {F<:Frequency} = t """ - fconvert(F1, t::TSeries{F2}; method) where {F1 <: YPFrequency, F2 <: YPFrequency} - -Convert between frequencies of the [`YPFrequency`](@ref) variety. - -TODO: describe `method` when converting to a higher frequency (interpolation) -TODO: describe `method` when converting to a lower frequency (aggregation) - + fconvert(F1, x::TSeries{F2}; method) where {F1 <: YPFrequency, F2 <: YPFrequency} + +Convert between frequencies derived from [`YPFrequency`](@ref). + +Currently this works only when the periods per year of the higher frequency is +an exact multiple of the periods per year of the lower frequency. + +### Converting to Higher Frequency +The only method available is `method=:const`, where the value at each period of +the higher frequency is the value of the period of the lower frequency it +belongs to. +``` +x = TSeries(2000Q1:2000Q3, collect(Float64, 1:3)) +fconvert(Monthly, x) +``` + +### Converting to Lower Frequency +The range of the result includes periods that are fully included in the range of +the input. For each period of the lower frequency we aggregate all periods of +the higher frequency within it. We have 4 methods currently available: `:mean`, +`:sum`, `:begin`, and `:end`. The default is `:mean`. +``` +x = TSeries(2000M1:2000M7, collect(Float64, 1:7)) +fconvert(Quarterly, x; method = :sum) +``` """ function fconvert(F::Type{<:YPFrequency{N1}}, t::TSeries{<:YPFrequency{N2}}; method=nothing) where {N1,N2} args = Dict() @@ -121,20 +111,19 @@ function _to_lower(F::Type{<:YPFrequency{N1}}, t::TSeries{<:YPFrequency{N2}}; me (d2, r2) = divrem(p2 - 1, np) li = MIT{F}(y2, d2 + 1) - (r2 < np - 1) # println("y2 = $y2, p2 = $p2, d2 = $d2, r2 = $r2, li = $li") - ret = TSeries(eltype(t), fi:li) - vals = t[begin + (r1 > 0) * (np - r1):end - (r2 < np-1)*(1+r2)].values + vals = t[begin+(r1>0)*(np-r1):end-(r2 2 || length(shape2) > 2 -# throw(ArgumentError("broadcasting MVTSeries with ndims > 2.")) -# end -# if length(shape2) == 2 -# return (mit_common_axes(shape1[1], shape2[1]), sym_common_axes(shape1[2], shape2[2])) -# else # length(shape1) == 2 && length(shape2) == 1 -# if length(shape1[1]) == length(shape2[1]) -# return (mit_common_axes(shape1[1], shape2[1]), shape1[2]) -# elseif length(shape1[1]) == 1 && length(shape1[2]) == length(shape2[1]) -# return (shape1[1], sym_common_axes(shape1[2], shape2[1])) -# else -# throw(ArgumentError("Cannot broadcast with $shape1 and $shape2")) -# end -# end -# end - -# @inline mit_common_axes(a::UnitRange{<:MIT}, b::Base.OneTo) = -# length(b) == 1 || length(b) == length(a) ? a : -# throw(DimensionMismatch("Cannot broadcast with $a and $b.")) - -# # mit_common_axes is the same as in TSeries -# @inline sym_common_axes(a::NTuple{N1,Symbol}, b::NTuple{N2,Symbol}) where {N1,N2} = a == b ? a : tuple(intersect(a, b)...) -# @inline sym_common_axes(a::NTuple{N,Symbol}, b::Any) where {N} = length(a) == length(b) && first(b) == 1 ? a : throw(DimensionMismatch("Cannot broadcast with $(a) and $b.")) -# @inline sym_common_axes(a::Any, b::NTuple{N,Symbol}) where {N} = sym_common_axes(b, a) - - -# function mvts_instantiate(bc::Base.Broadcast.Broadcasted{S}, shape) where {S<:Base.Broadcast.BroadcastStyle} -# args = map(bc.args) do arg -# mvts_check_axes(shape, arg) -# end -# return Base.Broadcast.Broadcasted{S}(bc.f, args, shape) -# end - -# struct MVTSBroadcasted{Axes,BC} -# singleton::Tuple{Bool,Bool} -# shape::Axes -# bc::BC -# function MVTSBroadcasted(shape, bc) -# bcshape = axes(bc) -# singleton = length(bcshape) == 0 ? (true, true) : -# length(bcshape) == 1 ? (length(bcshape[1]) == 1, true) : -# (length(bcshape[1]) == 1, length(bcshape[2]) == 1) -# # bc1 = Base.Broadcast.instantiate(bc) -# new{typeof(shape),typeof(bc)}(singleton, shape, bc) -# end -# end - -# @inline Base.eltype(x::MVTSBroadcasted) = eltype(x.bc) - -# function mvts_get_index(x::MVTSBroadcasted, p::MIT, c::Symbol) -# ip = x.singleton[1] ? 1 : Int(p - first(x.shape[1]) + 1) -# ic = x.singleton[2] ? 1 : indexin([c], collect(x.shape[2]))[1] -# ind = CartesianIndex(ip, ic) -# x.bc[ind] -# end - -# # nested broadcasts are processed recursively -# @inline mvts_check_axes(shape::MVTSeriesIndexType, bc::Base.Broadcast.Broadcasted{<:MVTSeriesStyle}) = mvts_instantiate(bc, shape) -# @inline mvts_check_axes(shape::MVTSeriesIndexType, bc::Base.Broadcast.Broadcasted{<:TSeriesStyle}) = mvts_instantiate(bc, shape) -# @inline mvts_check_axes(shape::MVTSeriesIndexType, bc::Base.Broadcast.Broadcasted) = MVTSBroadcasted(shape, bc) - -# # MVTSs are left alone. If axes are wrong it'll error when indexing later. -# @inline mvts_check_axes(shape::MVTSeriesIndexType, x::MVTSeries) = x -# # TSeries are also left alone -# @inline mvts_check_axes(shape::MVTSeriesIndexType, x::TSeries) = x -# # Leave numbers alone too -# @inline mvts_check_axes(shape::MVTSeriesIndexType, x) = x[] -# # For Vector, we wrap it in a TSeries if first dimension matches, otherwise we wrap it in a MVTSBroadcasted MVTSeries with one row . . . -# @inline mvts_check_axes(shape::MVTSeriesIndexType, x::AbstractVector) = -# length(shape[1]) == length(x) ? TSeries(shape[1], x) : -# MVTSBroadcasted(shape, MVTSeries(shape[1], shape[2], reshape(x, 1, :))) - -# # For Matrix, we wrap them in an MVTSeries with the same dimensions -# @inline mvts_check_axes(shape::MVTSeriesIndexType, x::AbstractMatrix) = -# MVTSBroadcasted(shape, MVTSeries(first(shape[1]), shape[2], x)) - - - -# function Base.axes(bc::Base.Broadcast.Broadcasted{<:MVTSeriesStyle}) -# bc.axes === nothing ? mvts_combine_axes(bc.args...) : bc.axes -# end - -# function Base.axes(bc::Base.Broadcast.Broadcasted{<:MVTSeriesStyle}, d::Integer) -# 1 <= d <= 2 ? axes(bc)[d] : Base.OneTo(1) -# end diff --git a/src/recursive.jl b/src/recursive.jl index 6a4e961..e88c374 100644 --- a/src/recursive.jl +++ b/src/recursive.jl @@ -4,13 +4,13 @@ """ @rec [index=]range expression -Computes recursive operations on time series. The first argument is the range +Compute recursive operations on time series. The first argument is the range and the second argument is an expression to be evaluated over that range. The expression is meant to be an assignment, but it doesn't have to be. -The the range can specify an optional indexing variable (as in a for loop). If -not given, the variable is assumed to be `t`. +The range specification can include an optional indexing variable name. If not +given, the variable name defaults to `t`. ### Examples ```julia-repl diff --git a/src/tsbroadcast.jl b/src/tsbroadcast.jl index 6198c28..d356afe 100644 --- a/src/tsbroadcast.jl +++ b/src/tsbroadcast.jl @@ -136,119 +136,3 @@ function Base.Broadcast.dotview(t::TSeries, rng::AbstractUnitRange{<:MIT}) Base.maybeview(resize!(t, union(eachindex(t), rng)), rng) end -############################################################################# -# OLD IMPLEMENTATION -############################################################################# - - - - -# # This so far is sufficient to do things like `t .+ 5` and even `t1 .+ t2` when `t1` and `t2` have identical axes. - -# # now we want to do things like `t .+ v` where `v` is a non-TSeries vector of the same length -# # we also want to do `t1 .+ t2` when t1 and t2 are of the same frequency but may have different ranges. -# # in this case the operation will reduce itself to the common range. -# # we also want to do `s .= t1 .+ t2` where all three are of the same frequency but may have -# # different axes ranges. In this case the operation on t1 and t2 is done over their common range and -# # the result is stored in s over the same range. If the original range of s is smaller, it gets -# # resized to include the broadcasted range. If the original range of s is already larger, then -# # values outside the broadcasted range are left unchanged. - - -# # figure out the axes range of the result of the broadcast operation -# @inline ts_combine_axes(A, B...) = ts_broadcast_shape(axes(A), ts_combine_axes(B...)) -# @inline ts_combine_axes(A, B) = ts_broadcast_shape(axes(A), axes(B)) -# @inline ts_combine_axes(A) = axes(A) - -# ts_broadcast_shape(shape::Tuple) = shape -# ts_broadcast_shape(::Tuple{}, shape::Tuple) = shape -# ts_broadcast_shape(shape::Tuple, ::Tuple{}) = shape -# function ts_broadcast_shape(shape1::Tuple, shape2::Tuple) -# if length(shape1) > 1 || length(shape2) > 1 -# throw(ArgumentError("broadcasting TSeries with ndims > 1.")) -# else -# return (mit_common_axes(shape1[1], shape2[1]),) -# end -# end - -# mit_common_axes(a::AbstractRange{<:MIT}, b::AbstractRange{<:MIT}) = mixed_freq_error(a, b) -# mit_common_axes(a::AbstractRange{MIT{F}}, b::AbstractRange{MIT{F}}) where F <: Frequency = intersect(a, b) -# mit_common_axes(a::AbstractRange{<:MIT}, b::Any) = length(a) == length(b) && first(b) == 1 ? a : throw(DimensionMismatch("Cannot broadcast with $(a) and $b.")) -# mit_common_axes(a::Any, b::AbstractRange{<:MIT}) = mit_common_axes(b, a) - -# # given the broadcasted range in `shape`, check that all arguments are of compatible shapes and convert the non-TSeries to -# # TSeries with the appropriate range. This is necessary because the indexing of the broadcast is done using the MIT ranges, so -# # plain vectors must be viewed as TSeries. -# ts_check_axes(shape, x) = x # fall back for Number and other things. -# @inline ts_check_axes(shape, t::TSeries) = -# shape == axes(t) ? t : -# # if the axes are not identical, we create a "view" into the broadcasted range -# TSeries(first(shape[1]), view(t.values, Int(first(shape[1]) - firstindex(t)) .+ (1:length(shape[1])))) -# # For vectors other than TSeries, we create a "view" as a TSeries with the broadcasted range -# @inline ts_check_axes(shape, t::AbstractVector) = TSeries(first(shape[1]), view(t, Base.OneTo(length(shape[1])))) -# # For nested broadcasted argument, we process the same way recursively. -# ts_check_axes(shape, bc::BC) where BC <: Base.Broadcast.Broadcasted = ts_instantiate(bc, shape) - -# function ts_instantiate(bc::Base.Broadcast.Broadcasted{S}, shape) where S <: Base.Broadcast.BroadcastStyle -# args = map(bc.args) do arg -# ts_check_axes(shape, arg) -# end -# return Base.Broadcast.Broadcasted{S}(bc.f, args, shape) -# end - -# function Base.Broadcast.instantiate(bc::Base.Broadcast.Broadcasted{S,Nothing}) where S <: TSeriesStyle -# shape = ts_combine_axes(bc.args...) -# ts_instantiate(bc, shape) -# end - -# # the following two specializations are necessary in order to be able to have destination on the left of .= that has a different range than the broadcasted result on the right -# function Base.Broadcast.instantiate(bc::Base.Broadcast.Broadcasted{S,A}) where {S <: Base.Broadcast.BroadcastStyle,A <: Tuple{<:AbstractRange{<:MIT}}} -# shape = ts_combine_axes(bc.args...) -# I = mit_common_axes(bc.axes[1], shape[1]) -# ts_instantiate(bc, (I,)) -# end - -# # function Base.Broadcast.instantiate(bc::Base.Broadcast.Broadcasted{S,A}) where {S <: TSeriesStyle,A <: Tuple{<:AbstractRange{<:MIT}}} -# # shape = my_combine_axes(bc.args...) -# # I = _common_axes(bc.axes[1], shape[1]) -# # ts_instantiate(bc, (I,)) -# # end - -# function Base.Broadcast.instantiate(bc::Base.Broadcast.Broadcasted{S,A}) where {S <: Base.Broadcast.AbstractArrayStyle{0},A <: Tuple{<:AbstractRange{<:MIT}}} -# bc -# end - -# function Base.axes(bc::Base.Broadcast.Broadcasted{<:TSeriesStyle}) -# bc.axes === nothing ? ts_combine_axes(bc.args...) : bc.axes -# end - -# function Base.axes(bc::Base.Broadcast.Broadcasted{<:TSeriesStyle}, d::Integer) -# d == 1 ? axes(bc)[1] : Base.OneTo(1) -# end - -# @inline ts_get_index(x, p::MIT) = x[] -# @inline ts_get_index(x::TSeries, p::MIT) = x[p] -# @inline ts_get_index(x::Base.Broadcast.Broadcasted, p::MIT) = x[p] - -# function Base.Broadcast.getindex(bc::Base.Broadcast.Broadcasted, p::MIT) -# args = (ts_get_index(arg, p) for arg in bc.args) -# return bc.f(args...) -# end - -# # this specialization allows for the result to be stored in a TSeries -# function Base.copyto!(dest::TSeries, bc::Base.Broadcast.Broadcasted{Nothing}) -# bcrng = bc.axes[1] -# drng = eachindex(dest) -# if frequencyof(drng) != frequencyof(bcrng) -# mixed_freq_error(drng, bcrng) -# end -# bc′ = Base.Broadcast.preprocess(dest, bc) -# @simd for I = intersect(bcrng, drng) -# @inbounds dest[I] = bc′[I] -# end -# return dest -# end - -# function Base.Broadcast.dotview(t::TSeries, rng::UnitRange{<:MIT}) -# rng ⊆ eachindex(t) ? Base.maybeview(t, rng) : Base.maybeview(resize!(t, union(eachindex(t), rng)), rng) -# end diff --git a/src/tseries.jl b/src/tseries.jl index d97fedd..faed2f0 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -11,56 +11,60 @@ values::C end -Time series with frequency `F` with values of type `T` stored in a container of +Time series with frequency `F` and values of type `T` stored in a container of type `C`. By default the type is `Float64` and the container is `Vector{Float64}`. -Construction: +### Construction: ts = TSeries(args...) - The standard construction is `TSeries(firstdate::MIT, values::AbstractVector)` +The standard construction is +`TSeries(firstdate::MIT, values::AbstractVector)`. If the second argument is +not given, the `TSeries` is constructed empty. - If the first argument is an MIT-range (instead or an MIT), then the length - of the `values` container must match the length of the given range. +Alternatively, the first argument can be a range. In this case, the second +argument is interpreted as an initializer. If it is omitted or set to +`undef`, the storage is left uninitialized. If it is a number, the storage +is filled with it. It can also be an initializer function, such as `zeros`, +`ones` or `rand`. Lastly, if the second argument is an array, it must be +1-dimensional and of the same length as the range given in the first +argument. - In the case of a range argument, the `values` can be omitted, in which case - the container is initializes with `undef`. Or you can also pass a constant - and then the `values` will be filled with that constant. To accomplish this, - you can also use `fill`, e.g., `TSeries(20Q1:20Q4, 5)` is the same as - `fill(5, 20Q1:20Q4)`. +If only an integer number is given, as in `TSeries(n::Integer)`, the +constructed `TSeries` will have frequency `Unit`, first date `1U` and length +`n`. An initialization argument is not allowed in this case, so the storage +remains uninitialized. - If only a `firstdate::MIT` is given, the `values` container is initialized - to an empty `Vector`. +A `TSeries` can also be constructed with `copy`, `similar`, and `fill`, `ones`, +`zeros`. - If only an `n::Integer` is given, it is the same as passing the range - `0U .+ (1:n)`. An initialization argument is not allowed in this case. +### Indexing: +Indexing with an [`MIT`](@ref) or a range of [`MIT`](@ref) works as you'd +expect. - A `TSeries` can also be constructed with `copy`, `similar`, and `fill`. +Indexing with `Integer`s works the same as with `Vector`. -Indexing: +Indexing with `Bool`-array works as you'd expect. For example, +`s[s .< 0.0] .*= -1` multiplies in place the negative entries of `s` by -1, +so effectively it's the same as `s .= abs.(s)`. - Indexing with an `MIT` or a range of `MIT` works as you'd expect. +There are important differences between indexing with MIT and not +using MIT (i.e., using `Integer` or `Bool`-array). - Indexing with `Integer`s works the same as with `Vector`. +* with MIT-range we return a `TSeries`, otherwise we + return a `Vector`. - Indexing with `Bool`-array works as you'd expect. For example, - `s[s .< 0.0] .*= -1` multiplies in place the negative entries of `s` by -1, - so effectively it's the same as `s .= abs.(s)`. +* the range can be extended (the `TSeries` resized appropriately) by + assigning outside the current range. This works only with [`MIT`](@ref). + With anything else you get a BoundsError if you try to assign outside the + Integer range. - There are important differences between indexing with MIT and not - using MIT (i.e., using Integer or Bool-array). - - * with MIT-range we return a TSeries with the given range, otherwise we - return a `Vector` - - * the range can be extended (the TSeries resized appropriately) by assigning - outside the current range. This works only with MIT (you get a BoundsError - if you try to assign outside the Integer range). - - * `begin` and `end` are MIT, so either use both or none of them. For example - `s[2:end]` doesn't work because 2 is an `Int` and `end` is an `MIT`. You - should use `s[begin+1:end]`. +* `begin` and `end` are [`MIT`](@ref), so either use both or none of them. + For example `s[2:end]` doesn't work because 2 is an `Int` and `end` is an + `MIT`. You should use `s[begin+1:end]`. +Check out the tutorial at +[https://bankofcanada.github.io/DocsEcon.jl/dev/Tutorials/TimeSeriesEcon/main/](https://bankofcanada.github.io/DocsEcon.jl/dev/Tutorials/TimeSeriesEcon/main/) """ mutable struct TSeries{F<:Frequency,T<:Number,C<:AbstractVector{T}} <: AbstractVector{T} firstdate::MIT{F} @@ -68,10 +72,31 @@ mutable struct TSeries{F<:Frequency,T<:Number,C<:AbstractVector{T}} <: AbstractV end _vals(t::TSeries) = t.values +""" + rawdata(t) + +Return the raw storage of `t`. For a [`TSeries`](@ref) this is a `Vector`. For +an [`MVTSeries`](@ref) this is a `Matrix`. +""" rawdata(t::TSeries) = t.values Base.values(t::TSeries) = values(t.values) + + +""" + firstdate(x) + +Return the first date of the range of allocated storage for the given +[`TSeries`](@ref) or [`MVTSeries`] instance. +""" firstdate(t::TSeries) = t.firstdate + +""" + lastdate(x) + +Return the last date of the range of allocated storage for the given +[`TSeries`](@ref) or [`MVTSeries`] instance. +""" lastdate(t::TSeries) = t.firstdate + length(t.values) - one(t.firstdate) frequencyof(::Type{<:TSeries{F}}) where {F<:Frequency} = F @@ -79,17 +104,12 @@ frequencyof(::Type{<:TSeries{F}}) where {F<:Frequency} = F """ rangeof(s) -Return the stored range of the given time series object. +Return the stored range of the given [`TSeries`](@ref) or [`MVTSeries`](@ref) +instance. """ -rangeof(t::TSeries) = firstdate(t) .+ (0:size(t.values, 1)-1) - -""" - firstdate(ts), lastdate(ts) +function rangeof end -Return the first and last date of the allocated data for the given `TSeries`. -These are identical to `firstindex` and `lastindex`. -""" -firstdate, lastdate +rangeof(t::TSeries) = firstdate(t) .+ (0:size(t.values, 1)-1) # ------------------------------------------------------------------------------- # some methods that make the AbstractArray infrastructure of Julia work with TSeries @@ -162,11 +182,27 @@ Base.similar(::AbstractArray, T::Type{<:Number}, shape::Tuple{UnitRange{<:MIT}}) Base.similar(::AbstractArray{T}, shape::Tuple{UnitRange{<:MIT}}) where {T<:Number} = TSeries(T, shape[1]) # construct from range and fill with the given constant or array -Base.fill(v::Number, rng::UnitRange{<:MIT}) = TSeries(first(rng), fill(v, length(rng))) +Base.fill(v, shape::Tuple{UnitRange{<:MIT}}) = fill(v, shape...) +Base.fill(v, rng::UnitRange{<:MIT}) = TSeries(first(rng), fill(v, length(rng))) TSeries(rng::UnitRange{<:MIT}, v::Number) = fill(v, rng) TSeries(rng::UnitRange{<:MIT}, v::AbstractVector{<:Number}) = length(rng) == length(v) ? TSeries(first(rng), v) : throw(ArgumentError("Range and data lengths mismatch.")) +for (fname, felt) in ((:zeros, :zero), (:ones, :one)) + @eval begin + Base.$fname(rng::UnitRange{<:MIT}) = fill($felt(Float64), rng) + Base.$fname(::Type{T}, rng::UnitRange{<:MIT}) where {T} = fill($felt(T), rng) + Base.$fname(shape::Tuple{UnitRange{<:MIT}}) = fill($felt(Float64), shape) + Base.$fname(::Type{T}, shape::Tuple{UnitRange{<:MIT}}) where {T} = fill($felt(T), shape) + end +end + +for (fname, felt) in ((:trues, true), (:falses, false)) + @eval begin + Base.$fname(rng::UnitRange{<:MIT}) = TSeries(rng, $fname(length(rng))) + Base.$fname(shape::Tuple{UnitRange{<:MIT}}) = TSeries(shape[1], $fname(length(shape[1]))) + end +end # ------------------------------------------------------------- # Pretty printing @@ -209,12 +245,6 @@ function Base.show(io::IO, t::TSeries) end end -macro showall(a) - return esc(:(show(IOContext(stdout, :limit => false), $a))) -end -export @showall - - # ------------------------------------------------------------------ # indexing with MIT @@ -294,22 +324,31 @@ Base.setindex!(t::TSeries{F1}, src::TSeries{F2}, rng::AbstractRange{MIT{F3}}) wh Base.setindex!(t::TSeries{F}, src::TSeries{F}, rng::AbstractRange{MIT{F}}) where {F<:Frequency} = copyto!(t, rng, src) """ - typenan(x), typenan(T) + typenan(x) + typenan(T) -Return a value that indicates Not-A-Number of the same type as the given `x` or +Return a value that indicates not-a-number of the same type as the given `x` or of the given type `T`. -For floating point types, this is the IEEE-defined NaN. -For integer types, we use typemax(). This is not ideal, but it'll do for now. +For floating point types, this is `NaN`. For integer types, we use `typemax()`. +This is not ideal, but it'll do for now. """ function typenan end -typenan(x::T) where {T<:Real} = typenan(T) +typenan(::T) where {T<:Real} = typenan(T) typenan(T::Type{<:AbstractFloat}) = T(NaN) typenan(T::Type{<:Integer}) = typemax(T) typenan(T::Type{<:Union{MIT,Duration}}) = T(typemax(Int64)) +""" + istypenan(x) + +Return `true` if the given `x` is a not-n-number of its type, otherwise return +`false`. +""" istypenan(x) = false +istypenan(::Nothing) = true +istypenan(::Missing) = true istypenan(x::Integer) = x == typenan(x) istypenan(x::AbstractFloat) = isnan(x) @@ -332,13 +371,14 @@ function Base.resize!(t::TSeries, n::Integer) end # if range is given -Base.resize!(t::TSeries, rng::UnitRange{<:MIT}) = mixed_freq_error(t, eltype(rng)) """ resize!(t::TSeries, rng) Extend or shrink the allocated storage for `t` so that the new range of `t` -equals the given `rng`. If `t` is extended, new entries are set to `NaN`. +equals the given `rng`. If `t` is extended, new entries are set to `NaN`, or the +appropriate Not-A-Number value (see [`typenan`](@ref)). """ +Base.resize!(t::TSeries, rng::UnitRange{<:MIT}) = mixed_freq_error(t, eltype(rng)) function Base.resize!(t::TSeries{F}, rng::UnitRange{MIT{F}}) where {F<:Frequency} orng = rangeof(t) # old range if first(rng) == first(orng) @@ -395,35 +435,22 @@ end Construct the first difference, or the `k`-th difference, of time series `t`. If `y = diff(x,k)` then `y[t] = x[t] - x[t+k]`. A negative value of `k` means that we subtract a lag and positive value means that we subtract a lead. `k` not -given is the same as `k=-1`, which is the standard definition of first +given is the same as `k=-1`, which matches the standard definition of first difference. """ -Base.diff(x::TSeries, k::Integer = -1) = x - lag(x, -k) +Base.diff(x::TSeries, k::Integer=-1) = x - lag(x, -k) function Base.vcat(x::TSeries, args::AbstractVector...) return TSeries(firstdate(x), vcat(_vals(x), args...)) end -# """ -# pct(x::TSeries, shift_value::Int=-1, islog::Bool) - -# Calculate percentage growth in `x` given a `shift_value`. -# __Note:__ The implementation is similar to IRIS. - -# Examples -# ```julia-repl -# julia> x = TSeries(yy(2000), Vector(1:4)); +""" + pct(x; islog=false) -# julia> pct(x, -1) -# TSeries{Yearly} of length 3 -# 2001Y: 100.0 -# 2002Y: 50.0 -# 2003Y: 33.33333333333333 -# ``` -# See also: [`apct`](@ref) -# """ -function pct(ts::TSeries, shift_value::Int = -1; islog::Bool = false) +Observation-to-observation percent rate of change in x. +""" +function pct(ts::TSeries, shift_value::Int=-1; islog::Bool=false) if islog a = exp.(ts) b = shift(exp.(ts), shift_value) @@ -441,9 +468,7 @@ export pct """ apct(x::TSeries, islog::Bool) -Calculate annualised percent rate of change in `x`. - -__Note:__ The implementation is similar to IRIS. +Annualised percent rate of change in `x`. Examples ```julia-repl @@ -462,7 +487,7 @@ TSeries{Quarterly} of length 7 See also: [`pct`](@ref) """ -function apct(ts::TSeries{<:YPFrequency{N}}, islog::Bool = false) where {N} +function apct(ts::TSeries{<:YPFrequency{N}}, islog::Bool=false) where {N} if islog a = exp.(ts) b = shift(exp.(ts), -1) @@ -487,9 +512,9 @@ export ytypct #### reindex -function reindex(ts::TSeries,pair::Pair{<:MIT,<:MIT}; copy = false) - ts_lag = firstdate(ts)-pair[1] - return TSeries(pair[2]+Int(ts_lag), copy ? Base.copy(ts.values) : ts.values) +function reindex(ts::TSeries, pair::Pair{<:MIT,<:MIT}; copy=false) + ts_lag = firstdate(ts) - pair[1] + return TSeries(pair[2] + Int(ts_lag), copy ? Base.copy(ts.values) : ts.values) end export reindex diff --git a/src/tsmath.jl b/src/tsmath.jl index 367622f..9c7fc9f 100644 --- a/src/tsmath.jl +++ b/src/tsmath.jl @@ -17,14 +17,14 @@ # function applications are not valid, so we must use dot to broadcast, e.g. log(t) throws an error, we must do log.(t) -@inline Base.promote_shape(a::TSeries, b::TSeries) = mixed_freq_error(a, b) -@inline Base.promote_shape(a::TSeries{F}, b::TSeries{F}) where F <: Frequency = intersect(eachindex(a), eachindex(b)) +Base.promote_shape(a::TSeries, b::TSeries) = mixed_freq_error(a, b) +Base.promote_shape(a::TSeries{F}, b::TSeries{F}) where {F<:Frequency} = intersect(eachindex(a), eachindex(b)) -@inline shape_error(A::Type,B::Type) = throw(ArgumentError("This operation is not valid for $(A) and $(B). Try using . to do it element-wise.")) -@inline shape_error(a,b) = shape_error(typeof(a), typeof(b)) +shape_error(A::Type, B::Type) = throw(ArgumentError("This operation is not valid for $(A) and $(B). Try using . to do it element-wise.")) +shape_error(a, b) = shape_error(typeof(a), typeof(b)) -@inline Base.promote_shape(a::TSeries, b::AbstractVector) = promote_shape(_vals(a), b) -@inline Base.promote_shape(a::AbstractVector, b::TSeries) = promote_shape(a, _vals(b)) +Base.promote_shape(a::TSeries, b::AbstractVector) = promote_shape(_vals(a), b) +Base.promote_shape(a::AbstractVector, b::TSeries) = promote_shape(a, _vals(b)) # +, -, *, / work out of the box with the above methods for promote_shape. @@ -44,26 +44,14 @@ end # Now we implement some time-series operations that do not really apply to vectors. -@inline shift(ts::TSeries, k::Int) = TSeries(ts.firstdate - k, copy(ts.values)) -@inline shift!(ts::TSeries, k::Int) = (ts.firstdate -= k; ts) -@inline lag(t::TSeries, k::Int=1) = shift(t, -k) -@inline lag!(t::TSeries, k::Int=1) = shift!(t, -k) -@inline lead(t::TSeries, k::Int=1) = shift(t, k) -@inline lead!(t::TSeries, k::Int=1) = shift!(t, k) - """ - shift(x, n) - shift!(x, n) - lag(x, n=1) - lag!(x, n=1) - lead(x, n=1) - lead!(x, n=1) - -Shift, lag or lead the TSeries `x` by `n` periods. -By convention `shift` is the same as `lead` while `lag(x,n)` is the same as `shift(x, -n)`. -The versions ending in ! do it in place, while the others create a new TSeries instance. - -Examples + shift(x::TSeries, n) + +Shift the dates of `x` by `n` periods. By convention positive `n` gives the lead +and negative `n` gives the lag. `shift` creates a new [`TSeries`](@ref) and +copies the data over. See [`shift!`](@ref) for in-place version. + +For example: ```julia-repl julia> shift(TSeries(2020Q1, 1:4), 1) TSeries{Quarterly} of length 4 @@ -79,20 +67,44 @@ TSeries{Quarterly} of length 4 2020Q3: 2.0 2020Q4: 3.0 2021Q1: 4.0 +``` +""" +shift(ts::TSeries, k::Int) = copyto!(TSeries(ts.firstdate - k), ts.values) -julia> x = TSeries(2020Q1, 1:4); +""" + shift!(x::TSeries, n) -julia> shift!(x, 1); +In-place version of [`shift`](@ref). +""" +shift!(ts::TSeries, k::Int) = (ts.firstdate -= k; ts) -julia> x -TSeries{Quarterly} of length 4 -2019Q4: 1.0 -2020Q1: 2.0 -2020Q2: 3.0 -2020Q3: 4.0 -``` """ -shift, shift!, lead, lead!, lag, lag! + lag(x::TSeries, k=1) +Shift the dates of `x` by `k` period to produce the `k`-th lag of `x`. This is +the same [`shift(x, -k)`](@ref). +""" +lag(t::TSeries, k::Int=1) = shift(t, -k) + +""" + lag!(x::TSeries, k=1) + +In-place version of [`lag`](@ref) +""" +lag!(t::TSeries, k::Int=1) = shift!(t, -k) + +""" + lead(x::TSeries, k=1) + +Shift the dates of `x` by `k` period to produce the `k`-th lead of `x`. This is +the same [`shift(x, k)`](@ref). +""" +lead(t::TSeries, k::Int=1) = shift(t, k) +""" + lead!(x::TSeries, k=1) + +In-place version of [`lead`](@ref) +""" +lead!(t::TSeries, k::Int=1) = shift!(t, k) diff --git a/src/various.jl b/src/various.jl new file mode 100644 index 0000000..cba8eca --- /dev/null +++ b/src/various.jl @@ -0,0 +1,193 @@ +# Copyright (c) 2020-2022, Bank of Canada +# All rights reserved. + + +""" + @showall X + +Print all data in `X` without truncating the output to fit the size of the +screen. +""" +macro showall(a) + return esc(:(show(IOContext(stdout, :limit => false), $a))) +end +export @showall + +#### overlay + +""" + overlay(arg1, args...) + +Return the first argument, from left to right, that is valid. At least one +argument must be given. Validity is determined by calling [`istypenan`](@ref). +If it returns `true`, the observation is not valid; `false` means it is. +""" +function overlay end +export overlay + +overlay(onething) = onething +overlay(head, tail...) = istypenan(head) ? overlay(tail...) : head + +""" + overlay([rng,] t1, t2, ...) + +Construct a [`TSeries`](@ref) in which each observation is taken from the first +valid observation in the list of arguments. A valid observation is one +for which [`istypenan`](@ref) returns `false`. + +All [`TSeries`](@ref) in the arguments list must be of the same frequency. The +data type of the resulting [`TSeries`](@ref) is decided by the standard +promotion of numerical types in Julia. Its range is the union of the ranges of +the arguments, unless the optional `rng` is ginven in which case it becomes the +range. +""" +overlay(tseries::TSeries...) = overlay(mapreduce(rangeof, union, tseries), tseries...) +function overlay(rng::AbstractRange{<:MIT}, tseries::TSeries...) + T = mapreduce(eltype, promote_type, tseries) + ret = TSeries(rng, typenan(T)) + # todo = contains `true` for locations that don't yet contain valid values. + todo = trues(rng) + for ts in tseries + # quit if nothing left to do + any(todo) || break + for (mit, val) in zip(rangeof(ts), ts.values) + # skip if outside overlay range + mit ∈ rng || continue + # skip if already done + todo[mit] || continue + # skip if not valid + istypenan(val) && continue + # still here? assign and mark done + ret[mit] = val + todo[mit] = false + end + end + return ret +end + +const LikeWorkspace = Union{Workspace,MVTSeries,AbstractDict{Symbol,<:Any}} + +_c(x::MVTSeries) = _cols(x) +_c(x::AbstractDict) = x + +""" + overlay(data1, data2, ...) + +When overlaying `Workspace`s and `MVTSeries` the result is a `Workspace` and +each member is overlaid recursively. +""" +function overlay(workspaces::LikeWorkspace...) + ret = Workspace() + names = mapreduce(keys, union, workspaces) + for name in names + things = [w[name] for w in workspaces if haskey(w, name)] + ret[name] = overlay(things...) + end + return ret +end + +# overlay(stuff::Vararg{LikeWorkspace}) = +# Workspace(mergewith(overlay, (_c(w) for w in stuff)...)) + + +#### compare and @compare + +""" +@compare x y [options] +compare(x, y [; options]) + +Compare two `Workspace` recursively and print out the differences. `MVTSeries` +and `Dict` with keys of type `Symbol` are treated like `Workspace`. `TSeries` and +other `Vector` are compared using `isapprox`, so feel free to supply `rtol` or +`atol`. + +Optional argument `name` can be used for the top name. Default is `"_"`. + +Parameter `showequal=true` causes the report to include objects that are the +same. Default behaviour, with `showequal=false`, is to report only the +differences. + +Parameter `ignoremissing=true` causes objects that appear in one but not the +other workspace to be ignored. That is, they are not printed and do not affect +the return value `true` or `false`. Default is `ignoremissing=false` meaning +they will be printed and return value will be `false`. + +""" +function compare end, macro compare end +export compare, @compare + + +@inline compare_equal(x, y; kwargs...) = isequal(x, y) +@inline compare_equal(x::Number, y::Number; atol=0, rtol=atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) = isapprox(x, y; atol, rtol, nans) +@inline compare_equal(x::AbstractVector, y::AbstractVector; atol=0, rtol=atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) = isapprox(x, y; atol, rtol, nans) +function compare_equal(x::TSeries, y::TSeries; trange=nothing, atol=0, rtol=atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) + if trange === nothing || !(frequencyof(x) == frequencyof(y) == frequencyof(trange)) + trange = intersect(rangeof(x), rangeof(y)) + end + isapprox(x[trange], y[trange]; atol, rtol, nans) +end + +function compare_equal(x::LikeWorkspace, y::LikeWorkspace; kwargs...) + equal = true + for name in union(keys(x), keys(y)) + xval = get(x, name, missing) + yval = get(y, name, missing) + if !compare(xval, yval, name; kwargs...) + equal = false + end + end + return equal +end + +@inline compare_print(names, message, quiet) = quiet ? nothing : println(join(names, "."), ": ", message) + +function compare(x, y, name=Symbol("_"); + showequal=false, ignoremissing=false, quiet=false, + left=:left, right=:right, + names=Symbol[], + kwargs...) + push!(names, name) + if ismissing(x) + equal = ignoremissing + ignoremissing || compare_print(names, "missing in $left", quiet) + elseif ismissing(y) + equal = ignoremissing + ignoremissing || compare_print(names, "missing in $right", quiet) + elseif compare_equal(x, y; showequal, ignoremissing, names, left, right, kwargs...) + (showequal || length(names) == 1) && compare_print(names, "same", quiet) + equal = true + else + compare_print(names, "different", quiet) + equal = false + end + pop!(names) + return equal +end + +macro compare(x, y, kwargs...) + # build the basic compare call + ret = MacroTools.unblock(quote + compare($x, $y; left=$(QuoteNode(x)), right=$(QuoteNode(y))) + end) + # find the array of kw-parameters in ret + params = [] + for a in ret.args + if MacroTools.isexpr(a, :parameters) + params = a.args + break + end + end + # convert arguments to this macro to kw-parameters to the compare() call + for arg in kwargs + if arg isa Symbol + kw = Expr(:kw, arg, true) + elseif MacroTools.isexpr(arg, :(=)) + kw = Expr(:kw, arg.args...) + else + kw = arg + end + push!(params, kw) + end + # done + return esc(ret) +end diff --git a/src/workspaces.jl b/src/workspaces.jl index 12ce25a..31abe3f 100644 --- a/src/workspaces.jl +++ b/src/workspaces.jl @@ -6,9 +6,23 @@ export Workspace """ - struct Workspace … end + struct Workspace + … + end + +A collection of variables. `Workspace`s can store data of any kind, including +numbers, `MIT`s, ranges, strings, `TSeries`, `MVTSeries`, even nested +`Workspace`s. + +### Construction +Easiest is to start with and empty `Workspace` and fill it up later. Otherwise, +content can be provided at construction time as a collection of name-value +pairs, where the name must be a `Symbol` and the value can be anything. + +### Access +Members of the `Workspace` can be accessed using "dot" notation or using +`[]` indexing, like a dictionary. -A collection of variables. """ struct Workspace _c::OrderedDict{Symbol,Any} @@ -18,10 +32,21 @@ struct Workspace Workspace(; kw...) = new(OrderedDict{Symbol,Any}(kw)) end -@inline _c(w::Workspace) = getfield(w, :_c) +_c(w::Workspace) = getfield(w, :_c) + +_dict_to_workspace(x) = x +_dict_to_workspace(x::AbstractDict) = Workspace(x) +function Workspace(fromdict::AbstractDict; recursive=false) + w = Workspace() + convert_value = recursive ? _dict_to_workspace : identity + for (key, value) in fromdict + w[Symbol(key)] = convert_value(value) + end + return w +end -Base.propertynames(w::Workspace, private::Bool = false) = tuple(keys(w)...) -Base.getproperty(w::Workspace, sym::Symbol) = sym == :_c ? _c(w) : getindex(w, sym) +Base.propertynames(w::Workspace, private::Bool=false) = tuple(keys(w)...) +Base.getproperty(w::Workspace, sym::Symbol) = sym === :_c ? _c(w) : getindex(w, sym) Base.setproperty!(w::Workspace, sym::Symbol, val) = setindex!(w, val, sym) # MacroTools.@forward Workspace._c (Base.getindex,) @@ -39,6 +64,14 @@ MacroTools.@forward Workspace._c (Base.eltype,) Base.get(f::Function, w::Workspace, key) = get(f, _c(w), key) Base.get!(f::Function, w::Workspace, key) = get!(f, _c(w), key) +""" + rangeof(w) + +Calculate the range of a [`Workspace`] as the intersection of the ranges of all +[`TSeries`](@ref), [`MVTSeries`](@ref) and [`Workspace`](@ref) members of `w`. +If there are objects of different frequencies there will be a mixed-frequency +error. +""" rangeof(w::Workspace) = ( iterable = (v for v in values(w) if hasmethod(rangeof, (typeof(v),))); mapreduce(rangeof, intersect, iterable) @@ -81,15 +114,15 @@ function Base.show(io::IO, ::MIME"text/plain", w::Workspace) for (i, (k, v)) ∈ enumerate(w) top < i < bot && continue - sk = sprint(print, k, context = io, sizehint = 0) + sk = sprint(print, k, context=io, sizehint=0) if v isa Union{AbstractString,Symbol,AbstractRange} # It's a string or a Symbol - sv = sprint(show, v, context = io, sizehint = 0) + sv = sprint(show, v, context=io, sizehint=0) elseif typeof(v) == eltype(v) # it's a scalar value - sv = sprint(print, v, context = io, sizehint = 0) + sv = sprint(print, v, context=io, sizehint=0) else - sv = sprint(summary, v, context = io, sizehint = 0) + sv = sprint(summary, v, context=io, sizehint=0) end max_align = max(max_align, length(sk)) @@ -107,126 +140,6 @@ function Base.show(io::IO, ::MIME"text/plain", w::Workspace) end -_dict_to_workspace(x) = x -_dict_to_workspace(x::AbstractDict) = Workspace(x) -function Workspace(fromdict::AbstractDict; recursive = false) - w = Workspace() - convert_value = ifelse(recursive, _dict_to_workspace, identity) - for (key, value) in fromdict - push!(w, Symbol(key) => convert_value(value)) - end - return w -end - -const LikeWorkspace = Union{Workspace,MVTSeries,AbstractDict{Symbol,<:Any}} - -@inline _c(x::MVTSeries) = _cols(x) -@inline _c(x::AbstractDict) = x - -overlay(stuff...) = stuff[1] -overlay(stuff::Vararg{LikeWorkspace}) = Workspace(mergewith(overlay, (_c(w) for w in stuff)...)) - -########################### - -""" - @compare x y [options] - compare(x, y [; options]) - -Compare two `Workspace` recursively and print out the differences. `MVTSeries` -and `Dict` with keys of type `Symbol` are treated like `Workspace`. `TSeries` and -other `Vector` are compared using `isapprox`, so feel free to supply `rtol` or -`atol`. - -Optional argument `name` can be used for the top name. Default is `"_"`. - -Parameter `showequal=true` causes the report to include objects that are the -same. Default behaviour, with `showequal=false`, is to report only the -differences. - -Parameter `ignoremissing=true` causes objects that appear in one but not the -other workspace to be ignored. That is, they are not printed and do not affect -the return value `true` or `false`. Default is `ignoremissing=false` meaning -they will be printed and return value will be `false`. - -""" -function compare end, macro compare end -export compare, @compare - - -@inline compare_equal(x, y; kwargs...) = isequal(x, y) -@inline compare_equal(x::Number, y::Number; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) = isapprox(x, y; atol, rtol, nans) -@inline compare_equal(x::AbstractVector, y::AbstractVector; atol = 0, rtol = atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) = isapprox(x, y; atol, rtol, nans) -function compare_equal(x::TSeries, y::TSeries; trange = nothing, atol = 0, rtol = atol > 0 ? 0.0 : √eps(), nans::Bool=false, kwargs...) - if trange === nothing || !(frequencyof(x) == frequencyof(y) == frequencyof(trange)) - trange = intersect(rangeof(x), rangeof(y)) - end - isapprox(x[trange], y[trange]; atol, rtol, nans) -end - -function compare_equal(x::LikeWorkspace, y::LikeWorkspace; kwargs...) - equal = true - for name in union(keys(x), keys(y)) - xval = get(x, name, missing) - yval = get(y, name, missing) - if !compare(xval, yval, name; kwargs...) - equal = false - end - end - return equal -end - -@inline compare_print(names, message, quiet) = quiet ? nothing : println(join(names, "."), ": ", message) - -function compare(x, y, name = Symbol("_"); - showequal = false, ignoremissing = false, quiet = false, - left = :left, right = :right, - names = Symbol[], - kwargs...) - push!(names, name) - if ismissing(x) - equal = ignoremissing - ignoremissing || compare_print(names, "missing in $left", quiet) - elseif ismissing(y) - equal = ignoremissing - ignoremissing || compare_print(names, "missing in $right", quiet) - elseif compare_equal(x, y; showequal, ignoremissing, names, left, right, kwargs...) - (showequal || length(names) == 1) && compare_print(names, "same", quiet) - equal = true - else - compare_print(names, "different", quiet) - equal = false - end - pop!(names) - return equal -end - -macro compare(x, y, kwargs...) - # build the basic compare call - ret = MacroTools.unblock(quote - compare($x, $y; left = $(QuoteNode(x)), right = $(QuoteNode(y))) - end) - # find the array of kw-parameters in ret - params = [] - for a in ret.args - if MacroTools.isexpr(a, :parameters) - params = a.args - break - end - end - # convert arguments to this macro to kw-parameters to the compare() call - for arg in kwargs - if arg isa Symbol - kw = Expr(:kw, arg, true) - elseif MacroTools.isexpr(arg, :(=)) - kw = Expr(:kw, arg.args...) - else - kw = arg - end - push!(params, kw) - end - # done - return esc(ret) -end ########################### @@ -237,7 +150,7 @@ Apply [`strip!`](@ref) to all TSeries members of the given workspace. This includes nested workspaces, unless `recursive=false`. """ -function strip!(w::Workspace; recursive = true) +function strip!(w::Workspace; recursive=true) for (key, value) in w._c if value isa TSeries strip!(value) diff --git a/test/test_mit.jl b/test/test_mit.jl index fb65d47..4b42b5e 100644 --- a/test/test_mit.jl +++ b/test/test_mit.jl @@ -1,6 +1,8 @@ # Copyright (c) 2020-2021, Bank of Canada # All rights reserved. +import TimeSeriesEcon: qq, mm, yy + @testset "MIT,Duration" begin # mit2yp conversions @test mit2yp(MIT{Quarterly}(5)) == (1, 2) From 350a89a83793b6f2f5a27b9eb93a42b2bcc25acf Mon Sep 17 00:00:00 2001 From: Boyan Bejanov Date: Wed, 30 Mar 2022 11:14:42 -0400 Subject: [PATCH 10/13] fixed bug in shift() --- src/tseries.jl | 4 ++-- src/tsmath.jl | 2 +- src/workspaces.jl | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tseries.jl b/src/tseries.jl index faed2f0..391ac97 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -87,7 +87,7 @@ Base.values(t::TSeries) = values(t.values) firstdate(x) Return the first date of the range of allocated storage for the given -[`TSeries`](@ref) or [`MVTSeries`] instance. +[`TSeries`](@ref) or [`MVTSeries`](@ref) instance. """ firstdate(t::TSeries) = t.firstdate @@ -95,7 +95,7 @@ firstdate(t::TSeries) = t.firstdate lastdate(x) Return the last date of the range of allocated storage for the given -[`TSeries`](@ref) or [`MVTSeries`] instance. +[`TSeries`](@ref) or [`MVTSeries`](@ref) instance. """ lastdate(t::TSeries) = t.firstdate + length(t.values) - one(t.firstdate) diff --git a/src/tsmath.jl b/src/tsmath.jl index 9c7fc9f..91a4189 100644 --- a/src/tsmath.jl +++ b/src/tsmath.jl @@ -69,7 +69,7 @@ TSeries{Quarterly} of length 4 2021Q1: 4.0 ``` """ -shift(ts::TSeries, k::Int) = copyto!(TSeries(ts.firstdate - k), ts.values) +shift(ts::TSeries, k::Int) = copyto!(TSeries(rangeof(ts) .- k), ts.values) """ shift!(x::TSeries, n) diff --git a/src/workspaces.jl b/src/workspaces.jl index 31abe3f..f885688 100644 --- a/src/workspaces.jl +++ b/src/workspaces.jl @@ -67,10 +67,10 @@ Base.get!(f::Function, w::Workspace, key) = get!(f, _c(w), key) """ rangeof(w) -Calculate the range of a [`Workspace`] as the intersection of the ranges of all -[`TSeries`](@ref), [`MVTSeries`](@ref) and [`Workspace`](@ref) members of `w`. -If there are objects of different frequencies there will be a mixed-frequency -error. +Calculate the range of a [`Workspace`](@ref) as the intersection of the ranges +of all [`TSeries`](@ref), [`MVTSeries`](@ref) and [`Workspace`](@ref) members of +`w`. If there are objects of different frequencies there will be a +mixed-frequency error. """ rangeof(w::Workspace) = ( iterable = (v for v in values(w) if hasmethod(rangeof, (typeof(v),))); From b8965faee4f520374689cf4807765ccbbd0bdcc2 Mon Sep 17 00:00:00 2001 From: Nicholas Labelle St-Pierre Date: Wed, 30 Mar 2022 17:03:54 -0400 Subject: [PATCH 11/13] filter! and filter for Workspaces --- src/workspaces.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/workspaces.jl b/src/workspaces.jl index f885688..1a9cdd1 100644 --- a/src/workspaces.jl +++ b/src/workspaces.jl @@ -159,4 +159,9 @@ function strip!(w::Workspace; recursive=true) end end return w -end \ No newline at end of file +end + +########################### +Base.filter(f,w::Workspace) = Workspace(filter(f,_c(w))) +Base.filter!(f,w::Workspace) = (filter!(f,_c(w)); w) + From d0f306f3fce8d8f2b753cae78da4ec6066b3230b Mon Sep 17 00:00:00 2001 From: Nicholas Labelle St-Pierre Date: Wed, 30 Mar 2022 17:07:03 -0400 Subject: [PATCH 12/13] filter! and filter for Workspaces --- src/workspaces.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/workspaces.jl b/src/workspaces.jl index f885688..1a9cdd1 100644 --- a/src/workspaces.jl +++ b/src/workspaces.jl @@ -159,4 +159,9 @@ function strip!(w::Workspace; recursive=true) end end return w -end \ No newline at end of file +end + +########################### +Base.filter(f,w::Workspace) = Workspace(filter(f,_c(w))) +Base.filter!(f,w::Workspace) = (filter!(f,_c(w)); w) + From d4891de44ae2c85ef3588eb35806386ff7614a23 Mon Sep 17 00:00:00 2001 From: Nicholas Labelle St-Pierre Date: Thu, 31 Mar 2022 15:13:27 -0400 Subject: [PATCH 13/13] reindex for Workspace --- src/mvtseries.jl | 28 -------------------- src/tseries.jl | 11 -------- src/various.jl | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/mvtseries.jl b/src/mvtseries.jl index f766e07..867088a 100644 --- a/src/mvtseries.jl +++ b/src/mvtseries.jl @@ -735,31 +735,3 @@ function undiff(dvar::MVTSeries, anchor::Pair{<:MIT,<:AbstractVecOrMat}) result .+= correction return result end - -#### reindex - -""" - reindex(ts, from => to; copy = false) - -The function `reindex` re-indexes the `TSeries` or `MVTSeries` `ts` -so that the `MIT` `from` becomes the `MIT` `to` leaving the data unchanged. - -By default, the data is not copied. - -Example: -``` -ts = MVTSeries(2020Q1,(:y1,:y2),randn(10,2)) -ts2 = reindex(ts,2021Q1 => 1U; copy = true) -ts2.y2[3U] = 9999 -ts -ts2 -``` -""" -function reindex end -export reindex - -function reindex(ts::MVTSeries, pair::Pair{<:MIT,<:MIT}; copy=false) - ts_lag = firstdate(ts) - pair[1] - return MVTSeries(pair[2] + Int(ts_lag), keys(ts), copy ? Base.copy(ts.values) : ts.values) -end - diff --git a/src/tseries.jl b/src/tseries.jl index 391ac97..8a2bb2f 100644 --- a/src/tseries.jl +++ b/src/tseries.jl @@ -508,14 +508,3 @@ Year-to-year percent change in x. """ ytypct(x) = 100 * (x ./ shift(x, -ppy(x)) .- 1) export ytypct - - -#### reindex - -function reindex(ts::TSeries, pair::Pair{<:MIT,<:MIT}; copy=false) - ts_lag = firstdate(ts) - pair[1] - return TSeries(pair[2] + Int(ts_lag), copy ? Base.copy(ts.values) : ts.values) -end -export reindex - - diff --git a/src/various.jl b/src/various.jl index cba8eca..e09ac2a 100644 --- a/src/various.jl +++ b/src/various.jl @@ -191,3 +191,70 @@ macro compare(x, y, kwargs...) # done return esc(ret) end + + + +#### reindex + +""" + reindex(ts, from => to; copy = false) + reindex(w, from => to; copy = false) + +The function `reindex` re-indexes the `TSeries` or `MVTSeries` `ts` +or those contained in the `Workspace` `w` +so that the `MIT` `from` becomes the `MIT` `to` leaving the data unchanged. +For a `Workspace`, only the `TSeries` with the same frequency as the first element of the pair +will be reindexed. + +By default, the data is not copied. + +Example: +With a `TSeries` or an `MVTSeries` +``` +ts = MVTSeries(2020Q1,(:y1,:y2),randn(10,2)) +ts2 = reindex(ts,2021Q1 => 1U; copy = true) +ts2.y2[3U] = 9999 +ts +ts2 +``` +With a `Workspace` +``` +w = Workspace(); +w.a = TSeries(2020Q1,randn(10)) +w.b = TSeries(2021Q1,randn(10)) +w.c = 1 +w.d = "string" +w1 = reindex(w, 2021Q1 => 1U) +w2 = reindex(w, 2021Q1 => 1U; copy = true) +w.a[2020Q1] = 9999 +MVTSeries(; w1_a = w1.a, w2_a = w2.a) +``` +""" +function reindex end +export reindex + +function reindex(ts::TSeries, pair::Pair{<:MIT,<:MIT}; copy=false) + ts_lag = firstdate(ts) - pair[1] + return TSeries(pair[2] + Int(ts_lag), copy ? Base.copy(ts.values) : ts.values) +end + +function reindex(ts::MVTSeries, pair::Pair{<:MIT,<:MIT}; copy=false) + ts_lag = firstdate(ts) - pair[1] + return MVTSeries(pair[2] + Int(ts_lag), keys(ts), copy ? Base.copy(ts.values) : ts.values) +end + +function reindex(w::Workspace, pair::Pair{<:MIT,<:MIT}; copy=false) + freq_from = frequencyof(pair[1]) + wo = Workspace() + for (k,v) in w + if isa(v,Union{TSeries,MVTSeries}) && frequencyof(v) == freq_from + wo[k] = reindex(v,pair; copy = copy) + elseif copy && hasmethod(Base.copy,(typeof(v),)) + wo[k] = Base.copy(w[k]) + else + wo[k] = w[k] + end + end + return wo +end +