Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement general case non-mutating apply #15

Merged
merged 4 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
74 changes: 61 additions & 13 deletions src/transformers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,80 @@ 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 possible, this should be extended for new data types `T`.
nicoleepp marked this conversation as resolved.
Show resolved Hide resolved
"""
function apply end

"""
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.

Applies the [`Transform`](@ref) mutating the input `data`.
Where possible, 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...)

Applies the [`Transform`](@ref) to each element of `A`.
nicoleepp marked this conversation as resolved.
Show resolved Hide resolved
Optionally specify the `dims` to apply the [`Transform`](@ref) along certain dimensions
and `inds` will be the indices to apply the Transform to along the `dims` specified.
If `dims` === : (all dimensions), then `inds` will be the global indices of the array,
instead of being relative to a certain dimension.
nicoleepp marked this conversation as resolved.
Show resolved Hide resolved

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.
May not return the same data type depending on what the data type is, and what `dims` and
`inds` were 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...)
nicoleepp marked this conversation as resolved.
Show resolved Hide resolved
nicoleepp marked this conversation as resolved.
Show resolved Hide resolved
end
end

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

"""
apply(table, ::Transform; cols=nothing, kwargs...)

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

#TODO: should this be apply! ?
nicoleepp marked this conversation as resolved.
Show resolved Hide resolved
_apply(x, t::Transform; kwargs...) = apply!(_try_copy(x), t; kwargs...)


"""
apply!(A::AbstractArray{T}, ::Transform; dims=:, kwargs...) where T <: Real
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 +111,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
41 changes: 20 additions & 21 deletions test/periodic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,14 @@

@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
end

Expand Down Expand Up @@ -163,12 +165,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 +179,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 +193,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 +269,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
82 changes: 44 additions & 38 deletions test/power.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,44 +30,21 @@
end
end

@testset "NamedTuple" begin
nt = (a = [1, 2, 3], b = [4, 5, 6])
expected = (a = [1, 8, 27], b = [64, 125, 216])

@testset "all cols" begin
transformed = Transforms.apply(nt, p)
@test transformed isa NamedTuple{(:a, :b)}
@test transformed == expected
@test p(nt) == expected

_nt = deepcopy(nt)
Transforms.apply!(_nt, p)
@test _nt == expected
end

@testset "cols = $c" for c in (:a, :b)
nt_mutated = NamedTuple{(Symbol("$c"), )}((expected[c], ))
nt_expected = merge(nt, nt_mutated)

@test Transforms.apply(nt, p; cols=[c]) == nt_expected
@test p(nt; cols=[c]) == nt_expected

_nt = deepcopy(nt)
Transforms.apply!(_nt, p; cols=[c])
@test _nt == nt_expected
end
end

@testset "AxisArray" begin
A = AxisArray([1 2 3; 4 5 6], foo=["a", "b"], bar=["x", "y", "z"])
expected = AxisArray([1 8 27; 64 125 216], foo=["a", "b"], bar=["x", "y", "z"])
expected = [1 8 27; 64 125 216]

@testset "dims = $d" for d in (Colon(), 1, 2)
transformed = Transforms.apply(A, p; dims=d)
@test transformed isa AxisArray
# AxisArray doesn't preserve the type it operates on
@test transformed isa AbstractArray
@test transformed == expected
end

_A = copy(A)
Transforms.apply!(_A, p)
@test _A isa AxisArray
@test _A == expected
end

@testset "AxisKey" begin
Expand All @@ -82,23 +59,52 @@

_A = copy(A)
Transforms.apply!(_A, p)
@test _A isa KeyedArray
@test _A == expected
end

@testset "NamedTuple" begin
nt = (a = [1, 2, 3], b = [4, 5, 6])
expected = [[1, 8, 27], [64, 125, 216]]
expected_nt = (a = [1, 8, 27], b = [64, 125, 216])

@testset "all cols" begin
@test Transforms.apply(nt, p) == expected
@test p(nt) == expected

_nt = deepcopy(nt)
Transforms.apply!(_nt, p)
@test _nt isa NamedTuple{(:a, :b)}
@test _nt == expected_nt
end

@testset "cols = $c" for c in (:a, :b)
nt_mutated = NamedTuple{(Symbol("$c"), )}((expected_nt[c], ))
expected_nt_mutated = merge(nt, nt_mutated)

@test Transforms.apply(nt, p; cols=[c]) == [expected_nt[c]]
@test p(nt; cols=[c]) == [expected_nt[c]]

_nt = deepcopy(nt)
Transforms.apply!(_nt, p; cols=[c])
@test _nt == expected_nt_mutated
@test _nt isa NamedTuple
end
end

@testset "DataFrame" begin
df = DataFrame(:a => [1, 2, 3], :b => [4, 5, 6])
expected = DataFrame(:a => [1, 8, 27], :b => [64, 125, 216])
expected_df = DataFrame(:a => [1, 8, 27], :b => [64, 125, 216])
expected = [expected_df.a, expected_df.b]

transformed = Transforms.apply(df, p)
@test transformed isa DataFrame
@test transformed == expected
@test Transforms.apply(df, p) == expected

@test Transforms.apply(df, p; cols=[:a]) == DataFrame(:a => [1, 8, 27], :b => [4, 5, 6])
@test Transforms.apply(df, p; cols=[:b]) == DataFrame(:a => [1, 2, 3], :b => [64, 125, 216])
@test Transforms.apply(df, p; cols=[:a]) == [expected_df.a]
@test Transforms.apply(df, p; cols=[:b]) ==[expected_df.b]

_df = deepcopy(df)
Transforms.apply!(_df, p)
@test _df == expected
@test _df isa DataFrame
@test _df == expected_df
end

end