diff --git a/src/apply.jl b/src/apply.jl index 2acffa52..c2b384d5 100644 --- a/src/apply.jl +++ b/src/apply.jl @@ -1,11 +1,27 @@ import Base: +, - import Base.diff +""" + +(ta::TimeArray) + +Element-wise unary plus for a `TimeArray`. +""" (+)(ta::TimeArray) = .+ta + +""" + -(ta::TimeArray) + +Element-wise unary minus for a `TimeArray`. +""" (-)(ta::TimeArray) = .-ta ###### lag, lead ################ +""" + lag(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) + +Lag a `TimeArray` by `n` periods. Optionally pad with NaN or use the deprecated `period` keyword. +""" function lag(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) where {T,N} if period != 0 @warn("the period kwarg is deprecated, use lag(ta::TimeArray, period::Int) instead") @@ -26,6 +42,11 @@ function lag(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) w return ta end # lag +""" + lead(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) + +Lead a `TimeArray` by `n` periods. Optionally pad with NaN or use the deprecated `period` keyword. +""" function lead(ta::TimeArray{T,N}, n::Int=1; padding::Bool=false, period::Int=0) where {T,N} if period != 0 @warn( @@ -49,6 +70,11 @@ end # lead ###### diff ##################### +""" + diff(ta::TimeArray, n::Int=1; padding::Bool=false, differences::Int=1) + +Difference a `TimeArray` by `n` periods, optionally multiple times (`differences`). +""" function diff(ta::TimeArray, n::Int=1; padding::Bool=false, differences::Int=1) cols = colnames(ta) for d in 1:differences @@ -60,6 +86,28 @@ end # diff ###### percentchange ############ +""" + percentchange(ta::TimeArray, returns::Symbol=:simple; padding::Bool=false, method::AbstractString="") + +Compute percent change of a `TimeArray`. Use `:simple` or `:log` returns. + +# Arguments +- `padding::Bool=false`: If true, pads the beginning of the result with `NaN` values so the output has the same length as the input. If false, the result is shorter by one (or more, depending on differencing). + +# Example +```julia +using TimeSeries, Dates +ta = TimeArray(Date(2020,1,1):Day(1):Date(2020,1,3), [1,2,4], ["A"]) +percentchange(ta, :simple; padding=true) +# Output: +# 3×1 TimeArray{Float64,1,Date,Array{Float64,2}} +# │ │ A │ +# ├────────────┼────────┤ +# │ 2020-01-01 │ NaN │ +# │ 2020-01-02 │ 1.0 │ +# │ 2020-01-03 │ 1.0 │ +``` +""" function percentchange( ta::TimeArray, returns::Symbol=:simple; padding::Bool=false, method::AbstractString="" ) @@ -158,6 +206,11 @@ end ###### upto ##################### +""" + upto(f, ta::TimeArray{T,1}) + +Apply function `f` cumulatively up to each index of a 1D `TimeArray`. +""" function upto(f, ta::TimeArray{T,1}) where {T} vals = zero(values(ta)) for i in 1:length(vals) @@ -166,6 +219,11 @@ function upto(f, ta::TimeArray{T,1}) where {T} return TimeArray(timestamp(ta), vals, colnames(ta), meta(ta)) end +""" + upto(f, ta::TimeArray{T,2}) + +Apply function `f` cumulatively up to each index of a 2D `TimeArray` (column-wise). +""" function upto(f, ta::TimeArray{T,2}) where {T} vals = zero(values(ta)) for i in 1:size(vals, 1), j in 1:size(vals, 2) @@ -176,12 +234,22 @@ end ###### basecall ################# +""" + basecall(ta::TimeArray, f::Function; cnames=colnames(ta)) + +Apply function `f` to the values of a `TimeArray` and return a new `TimeArray`. +""" function basecall(ta::TimeArray, f::Function; cnames=colnames(ta)) return TimeArray(timestamp(ta), f(values(ta)), cnames, meta(ta)) end ###### uniform observations ##### +""" + uniformspaced(ta::TimeArray) + +Check if the timestamps of a `TimeArray` are uniformly spaced. +""" function uniformspaced(ta::TimeArray) gap1 = timestamp(ta)[2] - timestamp(ta)[1] i, n, is_uniform = 2, length(ta), true @@ -192,6 +260,11 @@ function uniformspaced(ta::TimeArray) return is_uniform end # uniformspaced +""" + uniformspace(ta::TimeArray{T,N}) + +Return a new `TimeArray` with a uniform time grid, filling missing values with NaN. +""" function uniformspace(ta::TimeArray{T,N}) where {T,N} min_gap = minimum(timestamp(ta)[2:end] - timestamp(ta)[1:(end - 1)]) newtimestamp = timestamp(ta)[1]:min_gap:timestamp(ta)[end] @@ -205,6 +278,11 @@ end # uniformspace ###### dropnan #################### +""" + dropnan(ta::TimeArray, method::Symbol=:all) + +Drop rows from a `TimeArray` where values are NaN. Use `:all` or `:any` for method. +""" function dropnan(ta::TimeArray, method::Symbol=:all) return if method == :all ta[findall(reshape(values(any(.!isnan.(ta); dims=2)), :))] diff --git a/src/basemisc.jl b/src/basemisc.jl index 595199e9..18fa2029 100644 --- a/src/basemisc.jl +++ b/src/basemisc.jl @@ -37,12 +37,10 @@ macro _mapbase(sig::Expr, imp::Expr) doc = " $sig" fdef = Expr(:function, sig, fbody) - return esc( - quote - $doc - $fdef - end, - ) + return esc(quote + $doc + $fdef + end) end # Cumulative functions diff --git a/src/broadcast.jl b/src/broadcast.jl index 0a7bd59e..7b597210 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -13,7 +13,7 @@ Base.broadcastable(x::AbstractTimeSeries) = x Base.Broadcast.instantiate(bc::Broadcasted{<:TimeArrayStyle}) = # skip the default axes checking - Broadcast.flatten(bc) +Broadcast.flatten(bc) function Base.copy(bc′::Broadcasted{<:TimeArrayStyle}) tas = find_ta(bc′) diff --git a/src/combine.jl b/src/combine.jl index beec8692..45ec3361 100644 --- a/src/combine.jl +++ b/src/combine.jl @@ -109,6 +109,11 @@ end # hcat ########################## +""" + hcat(x::TimeArray, y::TimeArray) + +Horizontally concatenate two `TimeArray` objects with matching timestamps and columns. +""" function hcat(x::TimeArray, y::TimeArray) tsx = timestamp(x) tsy = timestamp(y) @@ -122,6 +127,11 @@ function hcat(x::TimeArray, y::TimeArray) return TimeArray(tsx, [values(x) values(y)], [colnames(x); colnames(y)], m) end +""" + hcat(x::TimeArray, y::TimeArray, zs::Vararg{TimeArray}) + +Horizontally concatenate multiple `TimeArray` objects. +""" hcat(x::TimeArray, y::TimeArray, zs::Vararg{TimeArray}) = hcat(hcat(x, y), zs...) # collapse ###################### @@ -206,9 +216,11 @@ collapse(ta, month, last) collapse(ta, month, last, mean) ``` """ -collapse( +function collapse( ta::TimeArray, period::Period, timestamp::Function, value::Function=timestamp; kw... -) = collapse(ta, x -> floor(x, period), timestamp, value; kw...) +) + collapse(ta, x -> floor(x, period), timestamp, value; kw...) +end # vcat ###################### @@ -266,6 +278,27 @@ end # map ###################### +""" + map(f, ta::TimeArray{T,N}) + +Apply function `f` to each row of a `TimeArray`, returning a new `TimeArray`. + +# Example + +```julia +using TimeSeries, Dates +ta = TimeArray(Date(2020,1,1):Day(1):Date(2020,1,3), [1,2,3], ["A"]) +# Add 10 to each value, keep timestamp +map((t, v) -> (t, v .+ 10), ta) +# Output: +# 3×1 TimeArray{Int64,1,Date,Array{Int64,2}} +# │ │ A │ +# ├────────────┼─────┤ +# │ 2020-01-01 │ 11 │ +# │ 2020-01-02 │ 12 │ +# │ 2020-01-03 │ 13 │ +``` +""" @generated function map(f, ta::TimeArray{T,N}) where {T,N} input_val = (N == 1) ? :(values(ta)[i]) : :(vec(values(ta)[i, :])) output_val = (N == 1) ? :(vals[i]) : :(vals[i, :]) diff --git a/src/modify.jl b/src/modify.jl index 6088adcb..3d0585f4 100644 --- a/src/modify.jl +++ b/src/modify.jl @@ -49,12 +49,15 @@ for f in [:rename, :rename!] @eval begin $f(ta::TimeArray, colnames::Symbol) = $f(ta, [colnames]) $f(ta::TimeArray) = throw(MethodError($f, (ta,))) - $f(ta::TimeArray, pairs::Pair{Symbol,Symbol}...) = - $f(ta, _mapcol(colnames(ta), pairs)) - $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{Symbol}=Symbol) = - $f(ta, map(f, colnames(ta))) - $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{String}) = - $f(Symbol ∘ f ∘ string, ta) + $f(ta::TimeArray, pairs::Pair{Symbol,Symbol}...) = $f( + ta, _mapcol(colnames(ta), pairs) + ) + $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{Symbol}=Symbol) = $f( + ta, map(f, colnames(ta)) + ) + $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{String}) = $f( + Symbol ∘ f ∘ string, ta + ) end end diff --git a/src/readwrite.jl b/src/readwrite.jl index 4e1a7a94..383fcd44 100644 --- a/src/readwrite.jl +++ b/src/readwrite.jl @@ -1,5 +1,10 @@ ###### readtimearray ############ +""" + readtimearray(source; delim=',', meta=nothing, format="", header=true) + +Read a CSV or delimited file into a `TimeArray`. +""" function readtimearray( source; delim::Char=',', meta=nothing, format::AbstractString="", header::Bool=true ) @@ -44,6 +49,11 @@ function insertNaN(aa::Array{Any,N}) where {N} return convert(Array{Float64,N}, aa) end +""" + writetimearray(ta::TimeArray, fname::AbstractString; delim=',', format="", header=true) + +Write a `TimeArray` to a CSV or delimited file. +""" function writetimearray( ta::TimeArray, fname::AbstractString; diff --git a/src/split.jl b/src/split.jl index a8d3eb17..c1dd7d6f 100644 --- a/src/split.jl +++ b/src/split.jl @@ -1,5 +1,10 @@ # when ############################ +""" + when(ta::TimeArray, period::Function, t::Integer) + +Return a subset of `TimeArray` where the period function applied to timestamps equals `t`. +""" function when(ta::TimeArray, period::Function, t::Integer) return ta[findall(period.(timestamp(ta)) .== t)] end @@ -7,6 +12,11 @@ when(ta::TimeArray, period::Function, t::String) = ta[findall(period.(timestamp( # from, to ###################### +""" + from(ta::TimeArray{T,N,D}, d::D) + +Return the part of `TimeArray` from the first timestamp >= `d`. +""" function from(ta::TimeArray{T,N,D}, d::D) where {T,N,D} return if length(ta) == 0 ta @@ -19,6 +29,11 @@ function from(ta::TimeArray{T,N,D}, d::D) where {T,N,D} end end +""" + to(ta::TimeArray{T,N,D}, d::D) + +Return the part of `TimeArray` up to the last timestamp <= `d`. +""" function to(ta::TimeArray{T,N,D}, d::D) where {T,N,D} return if length(ta) == 0 ta @@ -42,10 +57,20 @@ end ###### findwhen ################# +""" + findwhen(ta::TimeArray{Bool,1}) + +Return timestamps where the values of a boolean `TimeArray` are true. +""" findwhen(ta::TimeArray{Bool,1}) = timestamp(ta)[findall(values(ta))] ###### head, tail ########### +""" + head(ta::TimeArray{T,N}, n::Int=6) + +Return the first `n` rows of a `TimeArray`. +""" @generated function head(ta::TimeArray{T,N}, n::Int=6) where {T,N} new_values = (N == 1) ? :(values(ta)[1:n]) : :(values(ta)[1:n, :]) @@ -55,6 +80,11 @@ findwhen(ta::TimeArray{Bool,1}) = timestamp(ta)[findall(values(ta))] end end +""" + tail(ta::TimeArray{T,N}, n::Int=6) + +Return the last `n` rows of a `TimeArray`. +""" @generated function tail(ta::TimeArray{T,N}, n::Int=6) where {T,N} new_values = (N == 1) ? :(values(ta)[start:end]) : :(values(ta)[start:end, :]) @@ -81,8 +111,9 @@ Split `data` by `period` function, returns a vector of `TimeSeries.TimeArray`. - `data::TimeSeries.TimeArray`: Data to split - `period::Function`: Function, e.g. `Dates.day` that is used to split the `data`. """ -Base.split(data::TimeSeries.TimeArray, period::Function) = +function Base.split(data::TimeSeries.TimeArray, period::Function) Iterators.map(i -> data[i], _split(TimeSeries.timestamp(data), period)) +end function _split(ts::AbstractVector{D}, period::Function) where {D<:TimeType} m = length(ts) diff --git a/src/timearray.jl b/src/timearray.jl index 351b44ca..2e1e6dc6 100644 --- a/src/timearray.jl +++ b/src/timearray.jl @@ -237,8 +237,8 @@ end io::IO, ::MIME"text/plain", ta::TimeArray; - allrows=!get(io, :limit, false), - allcols=!get(io, :limit, false), + allrows=(!get(io, :limit, false)), + allcols=(!get(io, :limit, false)), ) nrow = size(values(ta), 1) ncol = size(values(ta), 2) @@ -278,8 +278,8 @@ else io::IO, ::MIME"text/plain", ta::TimeArray; - allrows=!get(io, :limit, false), - allcols=!get(io, :limit, false), + allrows=(!get(io, :limit, false)), + allcols=(!get(io, :limit, false)), ) nrow = size(values(ta), 1) ncol = size(values(ta), 2) @@ -301,8 +301,8 @@ else row_label_column_alignment=:r, column_label_alignment=:l, continuation_row_alignment=:c, - fit_table_in_display_horizontally=!allcols, # replaced crop keyword for v3 of PrettyTables - fit_table_in_display_vertically=!allrows, # replaced crop keyword for v3 of PrettyTables + fit_table_in_display_horizontally=(!allcols), # replaced crop keyword for v3 of PrettyTables + fit_table_in_display_vertically=(!allrows), # replaced crop keyword for v3 of PrettyTables vertical_crop_mode=:middle, ) end @@ -318,27 +318,34 @@ getindex(ta::TimeArray) = throw(BoundsError(typeof(ta), [])) # single row @propagate_inbounds getindex(ta::TimeArray, n::Integer) = # avoid conversion to column vector - TimeArray(timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta)) +TimeArray( + timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta) +) # single row 1d -@propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = - TimeArray(timestamp(ta)[n], values(ta)[[n]], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = TimeArray( + timestamp(ta)[n], values(ta)[[n]], colnames(ta), meta(ta) +) # range of rows -@propagate_inbounds getindex(ta::TimeArray, r::UnitRange{<:Integer}) = - TimeArray(timestamp(ta)[r], values(ta)[r, :], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray, r::UnitRange{<:Integer}) = TimeArray( + timestamp(ta)[r], values(ta)[r, :], colnames(ta), meta(ta) +) # range of 1d rows -@propagate_inbounds getindex(ta::TimeArray{T,1}, r::UnitRange{<:Integer}) where {T} = - TimeArray(timestamp(ta)[r], values(ta)[r], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, r::UnitRange{<:Integer}) where {T} = TimeArray( + timestamp(ta)[r], values(ta)[r], colnames(ta), meta(ta) +) # array of rows -@propagate_inbounds getindex(ta::TimeArray, a::AbstractVector{<:Integer}) = - TimeArray(timestamp(ta)[a], values(ta)[a, :], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray, a::AbstractVector{<:Integer}) = TimeArray( + timestamp(ta)[a], values(ta)[a, :], colnames(ta), meta(ta) +) # array of 1d rows -@propagate_inbounds getindex(ta::TimeArray{T,1}, a::AbstractVector{<:Integer}) where {T} = - TimeArray(timestamp(ta)[a], values(ta)[a], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, a::AbstractVector{<:Integer}) where {T} = TimeArray( + timestamp(ta)[a], values(ta)[a], colnames(ta), meta(ta) +) # single column by name @propagate_inbounds function getindex(ta::TimeArray, s::Symbol) @@ -348,15 +355,12 @@ end # array of columns by name @propagate_inbounds getindex(ta::TimeArray, ss::Symbol...) = getindex(ta, collect(ss)) -@propagate_inbounds getindex(ta::TimeArray, ss::Vector{Symbol}) = - TimeArray(ta; values=values(ta)[:, map(s -> findcol(ta, s), ss)], colnames=ss) +@propagate_inbounds getindex(ta::TimeArray, ss::Vector{Symbol}) = TimeArray( + ta; values=values(ta)[:, map(s -> findcol(ta, s), ss)], colnames=ss +) # ta[rows, cols] -@propagate_inbounds getindex( - ta::TimeArray, - rows::Union{AbstractVector{<:Integer},Colon}, - cols::AbstractVector{Symbol}, -) = TimeArray( +@propagate_inbounds getindex(ta::TimeArray, rows::Union{AbstractVector{<:Integer},Colon}, cols::AbstractVector{Symbol}) = TimeArray( ta; timestamp=timestamp(ta)[rows], values=values(ta)[rows, map(s -> findcol(ta, s), cols)], @@ -371,8 +375,9 @@ end @propagate_inbounds getindex(ta::TimeArray, rows, col::Symbol) = getindex(ta, rows, [col]) # ta[n, col] -@propagate_inbounds getindex(ta::TimeArray, n::Integer, col::Symbol) = - getindex(ta, [n], [col]) +@propagate_inbounds getindex(ta::TimeArray, n::Integer, col::Symbol) = getindex( + ta, [n], [col] +) # single date @propagate_inbounds function getindex(ta::TimeArray{T,N,D}, d::D) where {T,N,D} @@ -388,8 +393,9 @@ end end # StepRange{Date,...} -@propagate_inbounds getindex(ta::TimeArray{T,N,D}, r::StepRange{D}) where {T,N,D} = - ta[collect(r)] +@propagate_inbounds getindex(ta::TimeArray{T,N,D}, r::StepRange{D}) where {T,N,D} = ta[collect( + r +)] @propagate_inbounds getindex(ta::TimeArray, k::TimeArray{Bool,1}) = ta[findwhen(k)]