Skip to content

Commit

Permalink
Merge pull request #15 from invenia/ne/general-apply
Browse files Browse the repository at this point in the history
Implement general case non-mutating apply
  • Loading branch information
nicoleepp authored Feb 11, 2021
2 parents 2e23f97 + 2becaea commit 670f4b3
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 96 deletions.
31 changes: 7 additions & 24 deletions src/periodic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,34 +35,17 @@ Returns a `Periodic` transform with zero phase shift.
"""
Periodic(f, period::T) where T = Periodic(f, period, zero(T))

function _apply!(x::AbstractArray{T}, P::Periodic; kwargs...) where T <: Real
x[:] = P.f.(2π .* (x .- P.phase_shift) / P.period)
return x
function _apply(x, P::Periodic; kwargs...)
return P.f.(2π .* (x .- P.phase_shift) / P.period)
end

# `U <: Period` needed to avoid method ambiguity with
# `apply(table, P::Periodic{T}; cols=nothing) where T <: Period`
function apply(
x::AbstractArray{T},
P::Periodic{U};
kwargs...
) where {T <: TimeType, U <: Period}
return map(xi -> _periodic(P.f, xi, P.period, P.phase_shift), x)
function _apply(x, P::Periodic{T}; kwargs...) where T <: Period
map(xi -> _periodic(P.f, xi, P.period, P.phase_shift), x)
end

"""
Transforms.apply(table, ::Periodic{T}; cols=nothing) where T <: Period -> Array
Applies [`Periodic`](@ref) to each of the specified columns in `table`.
If no `cols` are specified, then [`Periodic`](@ref) is applied to all columns.
Returns an array containing each transformed column, in the same order as `cols`.
"""
function apply(table, P::Periodic{T}; cols=nothing) where T <: Period
Tables.istable(table) || throw(MethodError(apply, (table, P)))

columntable = Tables.columns(table)
cnames = cols === nothing ? propertynames(columntable) : cols
return [apply(getproperty(columntable, cname), P) for cname in cnames]
function _apply!(x::AbstractArray{T}, P::Periodic; kwargs...) where T <: Real
x[:] = _apply(x, P; kwargs...)
return x
end

"""
Expand Down
6 changes: 5 additions & 1 deletion src/power.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ struct Power <: Transform
exponent::Real
end

function _apply(x::AbstractArray{T}, P::Power; kwargs...) where T <: Real
return x .^ P.exponent
end

function _apply!(x::AbstractArray{T}, P::Power; kwargs...) where T <: Real
x[:] = x .^ P.exponent
x[:] = _apply(x, P; kwargs...)
return x
end
78 changes: 64 additions & 14 deletions src/transformers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,82 @@ Non-mutating version of [`transform!`](@ref).
function transform end

"""
Transforms.apply!(data::T, ::Transform; kwargs...) -> T
apply(data::T, ::Transform; kwargs...)
Applies the [`Transform`](@ref) to the data. New transforms should usually only extend
`_apply` which this method delegates to.
Where necessary, this should be extended for new data types `T`.
"""
function apply end

Applies the [`Transform`](@ref) mutating the input `data`.
Where possible, this should be extended for new data types `T`.
"""
apply!(data::T, ::Transform; kwargs...) -> T
Applies the [`Transform`](@ref) mutating the input `data`. New transforms should usually
only extend `_apply!` which this method delegates to.
Where necessary, this should be extended for new data types `T`.
"""
function apply! end


"""
Transforms.apply(data::T, ::Transform; kwargs...) -> T
apply(A::AbstractArray, ::Transform; dims=:, inds=:, kwargs...)
Non-mutating version of [`apply!`](@ref), which it delegates to by default.
Does not need to be extended unless a mutating [`Transform`](@ref) is not possible.
Applies the [`Transform`](@ref) to the elements of `A`.
Provide the `dims` keyword to apply the [`Transform`](@ref) along a certain dimension.
Provide the `inds` keyword to apply the [`Transform`](@ref) to certain indices along the
`dims` specified.
Note: if `dims === :` (all dimensions), then `inds` will be the global indices of the array,
instead of being relative to a certain dimension.
This method does not guarantee the data type of what is returned. It will try to conserve
type but the returned type depends on what the original `A` was, and the `dims` and `inds`
specified.
"""
function apply end
function apply(A::AbstractArray, t::Transform; dims=:, inds=:, kwargs...)
if dims === Colon()
if inds === Colon()
return _apply(A, t; kwargs...)
else
return _apply(A[:][inds], t; kwargs...)
end
end

return mapslices(x -> _apply(x[inds], t; kwargs...), A, dims=dims)
end

"""
apply!(A::AbstractArray{T}, ::Transform; dims=:, kwargs...) where T <: Real
apply(table, ::Transform; cols=nothing, kwargs...) -> Vector
Applies the [`Transform`](@ref) to each of the specified columns in the `table`.
If no `cols` are specified, then the [`Transform`](@ref) is applied to all columns.
Returns an array containing each transformed column, in the same order as `cols`.
"""
function apply(table, t::Transform; cols=nothing, kwargs...)
Tables.istable(table) || throw(MethodError(apply, (table, t)))

# Extract a columns iterator that we should be able to use to mutate the data.
# NOTE: Mutation is not guaranteed for all table types, but it avoid copying the data
columntable = Tables.columns(table)

cnames = cols === nothing ? propertynames(columntable) : cols
return [_apply(getproperty(columntable, cname), t; kwargs...) for cname in cnames]
end

_apply(x, t::Transform; kwargs...) = _apply!(_try_copy(x), t; kwargs...)


"""
apply!(A::AbstractArray, ::Transform; dims=:, kwargs...)
Applies the [`Transform`](@ref) to each element of `A`.
Optionally specify the `dims` to apply the [`Transform`](@ref) along certain dimensions.
"""
function apply!(
A::AbstractArray{T}, t::Transform; dims=:, kwargs...
) where T <: Real
function apply!(A::AbstractArray, t::Transform; dims=:, kwargs...)
dims == Colon() && return _apply!(A, t; kwargs...)

for x in eachslice(A; dims=dims)
Expand All @@ -61,10 +113,8 @@ function apply!(
return A
end

apply(x, t::Transform; kwargs...) = apply!(_try_copy(x), t; kwargs...)

"""
Transforms.apply!(table::T, ::Transform; cols=nothing)::T where T
apply!(table::T, ::Transform; cols=nothing)::T where T
Applies the [`Transform`](@ref) to each of the specified columns in the `table`.
If no `cols` are specified, then the [`Transform`](@ref) is applied to all columns.
Expand Down
71 changes: 50 additions & 21 deletions test/periodic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@
_x = copy(x)
Transforms.apply!(_x, p)
@test _x expected atol=1e-14

@testset "inds" begin
@test Transforms.apply(x, p; inds=2:5) expected[2:5] atol=1e-14
@test Transforms.apply(x, p; dims=:) expected atol=1e-14
@test Transforms.apply(x, p; dims=1) expected atol=1e-14
@test Transforms.apply(x, p; dims=1, inds=[2, 3, 4, 5]) expected[2:5] atol=1e-14

@test_throws BoundsError Transforms.apply(x, p; dims=2)
end
end

@testset "Matrix" begin
Expand All @@ -113,6 +122,13 @@
Transforms.apply!(_M, p; dims=d)
@test _M M_expected atol=1e-14
end

@testset "inds" begin
@test Transforms.apply(M, p; inds=[2, 3]) M_expected[[2, 3]] atol=1e-14
@test Transforms.apply(M, p; dims=:, inds=[2, 3]) M_expected[[2, 3]] atol=1e-14
@test Transforms.apply(M, p; dims=1, inds=[2]) reshape(M_expected[[2, 5]], 1, 2) atol=1e-14
@test Transforms.apply(M, p; dims=2, inds=[2]) reshape(M_expected[[4, 5, 6]], 3, 1) atol=1e-14
end
end

@testset "AxisArray" begin
Expand All @@ -127,13 +143,22 @@

@testset "dims = $d" for d in (Colon(), 1, 2)
transformed = Transforms.apply(A, p; dims=d)
@test transformed isa AxisArray
# AxisArray doesn't preserve type when operations are performed on it
@test transformed isa AbstractArray
@test transformed A_expected atol=1e-14
end

_A = copy(A)
Transforms.apply!(_A, p)
@test _A isa AxisArray
@test _A A_expected atol=1e-14

@testset "inds" begin
@test Transforms.apply(A, p; inds=[2, 3]) A_expected[[2, 3]] atol=1e-14
@test Transforms.apply(A, p; dims=:, inds=[2, 3]) A_expected[[2, 3]] atol=1e-14
@test Transforms.apply(A, p; dims=1, inds=[2]) reshape(A_expected[[2, 5]], 1, 2) atol=1e-14
@test Transforms.apply(A, p; dims=2, inds=[2]) reshape(A_expected[[4, 5, 6]], 3, 1) atol=1e-14
end
end

@testset "AxisKey" begin
Expand All @@ -155,6 +180,13 @@
_A = copy(A)
Transforms.apply!(_A, p)
@test _A A_expected atol=1e-14

@testset "inds" begin
@test Transforms.apply(A, p; inds=[2, 3]) [A_expected[2], A_expected[3]] atol=1e-14
@test Transforms.apply(A, p; dims=:, inds=[2, 3]) [A_expected[2], A_expected[3]] atol=1e-14
@test Transforms.apply(A, p; dims=1, inds=[2]) reshape([A_expected[2], A_expected[5]], 1, 2) atol=1e-14
@test Transforms.apply(A, p; dims=2, inds=[2]) reshape([A_expected[4], A_expected[5], A_expected[6]], 3, 1) atol=1e-14
end
end

@testset "NamedTuple" begin
Expand All @@ -163,12 +195,12 @@

@testset "all cols" begin
transformed = Transforms.apply(nt, p)
@test transformed isa NamedTuple{(:a, :b)}
@test collect(transformed) collect(nt_expected) atol=1e-14
@test collect(p(nt)) collect(nt_expected) atol=1e-14
@test transformed collect(nt_expected) atol=1e-14
@test p(nt) collect(nt_expected) atol=1e-14

_nt = deepcopy(nt)
Transforms.apply!(_nt, p)
@test _nt isa NamedTuple{(:a, :b)}
@test collect(_nt) collect(nt_expected) atol=1e-14
end

Expand All @@ -177,12 +209,12 @@
nt_expected_ = merge(nt, nt_mutated)

transformed = Transforms.apply(nt, p; cols=[c])
@test transformed isa NamedTuple{(:a, :b)} # before applying `collect`
@test collect(transformed) collect(nt_expected_) atol=1e-14
@test collect(p(nt; cols=[c])) collect(nt_expected_) atol=1e-14
@test transformed [collect(nt_expected_[c])] atol=1e-14
@test p(nt; cols=[c]) [collect(nt_expected_[c])] atol=1e-14

_nt = deepcopy(nt)
Transforms.apply!(_nt, p; cols=[c])
@test _nt isa NamedTuple{(:a, :b)} # before applying `collect`
@test collect(_nt) collect(nt_expected_) atol=1e-14
end
end
Expand All @@ -191,23 +223,19 @@
df = DataFrame(:a => collect(0.:2.), :b => collect(3.:5.))
df_expected = DataFrame(:a => expected[1:3], :b => expected[4:6])

transformed = Transforms.apply(df, p)
@test transformed isa DataFrame
@test transformed df_expected atol=1e-14
@test Transforms.apply(df, p) [df_expected.a, df_expected.b] atol=1e-14

@test (
Transforms.apply(df, p; cols=[:a]),
DataFrame(:a => expected[1:3], :b => collect(3.:5.)),
atol=1e-14
)
@test (
Transforms.apply(df, p; cols=[:b]),
DataFrame(:a => collect(0.:2.), :b => expected[4:6]),
atol=1e-14
)
@testset "cols = $c" for c in (:a, :b)
@test (
Transforms.apply(df, p; cols=[c]),
[df_expected[!, c]],
atol=1e-14
)
end

_df = deepcopy(df)
Transforms.apply!(_df, p)
@test _df isa DataFrame
@test _df df_expected atol=1e-14
end
end
Expand Down Expand Up @@ -271,7 +299,8 @@

@testset "dims = $d" for d in (Colon(), 1, 2)
transformed = Transforms.apply(A, p; dims=d)
@test transformed isa AxisArray
# AxisArray doesn't preserve type when operations are performed on it
@test transformed isa AbstractArray
@test transformed expected atol=1e-14
end
end
Expand Down
Loading

0 comments on commit 670f4b3

Please sign in to comment.