diff --git a/src/periodic.jl b/src/periodic.jl index 0c25ba2..beaab77 100644 --- a/src/periodic.jl +++ b/src/periodic.jl @@ -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 """ diff --git a/src/power.jl b/src/power.jl index 95010a5..d628e85 100644 --- a/src/power.jl +++ b/src/power.jl @@ -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 diff --git a/src/transformers.jl b/src/transformers.jl index 238b8ef..a356315 100644 --- a/src/transformers.jl +++ b/src/transformers.jl @@ -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) @@ -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. diff --git a/test/periodic.jl b/test/periodic.jl index 2ad8ee1..0aa81e1 100644 --- a/test/periodic.jl +++ b/test/periodic.jl @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/test/power.jl b/test/power.jl index a1ccb38..8ec2066 100644 --- a/test/power.jl +++ b/test/power.jl @@ -14,6 +14,15 @@ _x = copy(x) Transforms.apply!(_x, p) @test _x == expected + + @testset "inds" begin + @test Transforms.apply(x, p; inds=2:5) == expected[2:5] + @test Transforms.apply(x, p; dims=:) == expected + @test Transforms.apply(x, p; dims=1) == expected + @test Transforms.apply(x, p; dims=1, inds=[2, 3, 4, 5]) == expected[2:5] + + @test_throws BoundsError Transforms.apply(x, p; dims=2) + end end @testset "Matrix" begin @@ -28,46 +37,37 @@ Transforms.apply!(_M, p; dims=d) @test _M == expected 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 + @testset "inds" begin + @test Transforms.apply(M, p; inds=[2, 3]) == expected[[2, 3]] + @test Transforms.apply(M, p; dims=:, inds=[2, 3]) == expected[[2, 3]] + @test Transforms.apply(M, p; dims=1, inds=[2]) == [64 125 216] + @test Transforms.apply(M, p; dims=2, inds=[2]) == reshape([8, 125], 2, 1) 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 + + @testset "inds" begin + @test Transforms.apply(A, p; inds=[2, 3]) == expected[[2, 3]] + @test Transforms.apply(A, p; dims=:, inds=[2, 3]) == expected[[2, 3]] + @test Transforms.apply(A, p; dims=1, inds=[2]) == [64 125 216] + @test Transforms.apply(A, p; dims=2, inds=[2]) == reshape([8, 125], 2, 1) + end end @testset "AxisKey" begin @@ -82,23 +82,59 @@ _A = copy(A) Transforms.apply!(_A, p) + @test _A isa KeyedArray @test _A == expected + + @testset "inds" begin + @test Transforms.apply(A, p; inds=[2, 3]) == [64, 8] + @test Transforms.apply(A, p; dims=:, inds=[2, 3]) == [64, 8] + @test Transforms.apply(A, p; dims=1, inds=[2]) == [64 125 216] + @test Transforms.apply(A, p; dims=2, inds=[2]) == reshape([8, 125], 2, 1) + end + 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