Skip to content

Commit

Permalink
Merge pull request #60 from JuliaML/new-functional
Browse files Browse the repository at this point in the history
New Functional
  • Loading branch information
juliohm committed Apr 29, 2022
2 parents c50d658 + 43ac691 commit 01364ef
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 13 deletions.
73 changes: 60 additions & 13 deletions src/transforms/functional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,34 @@
# ------------------------------------------------------------------

"""
Functional(function)
Functional(func)
The transform that applies a `function` elementwise.
The transform that applies a `func` elementwise.
Functional(col₁ => func₁, col₂ => func₂, ..., colₙ => funcₙ)
Apply the corresponding `funcᵢ` function to each `colᵢ` column.
# Examples
```julia
Functional(cos)
Functional(sin)
Functional(:a => cos, :b => sin)
Functional("a" => cos, "b" => sin)
```
"""
struct Functional{F} <: Colwise
struct Functional{F} <: Stateless
func::F
end

isrevertible(transform::Functional) =
!isnothing(inverse(transform.func))
Functional(pairs::Pair{Symbol}...) =
Functional(NamedTuple(pairs))

Functional(pairs::Pair{K}...) where {K<:AbstractString} =
Functional(NamedTuple(Symbol(k) => v for (k, v) in pairs))

Functional() = throw(ArgumentError("Cannot create a Functional object without arguments."))

# known invertible functions
inverse(::typeof(log)) = exp
Expand All @@ -28,18 +39,54 @@ inverse(::typeof(cos)) = acos
inverse(::typeof(acos)) = cos
inverse(::typeof(sin)) = asin
inverse(::typeof(asin)) = sin
inverse(::typeof(cosd)) = acosd
inverse(::typeof(acosd)) = cosd
inverse(::typeof(sind)) = asind
inverse(::typeof(asind)) = sind
inverse(::typeof(identity)) = identity

# fallback to nothing
inverse(::Any) = nothing

colcache(::Functional, x) = nothing
isrevertible(transform::Functional) =
!isnothing(inverse(transform.func))

function colapply(transform::Functional, x, c)
f = transform.func
f.(x)
isrevertible(transform::Functional{<:NamedTuple}) =
all(!isnothing, inverse.(values(transform.func)))

_functuple(func, names) = NamedTuple(nm => func for nm in names)
_functuple(func::NamedTuple, names) = func

function apply(transform::Functional, table)
cols = Tables.columns(table)
names = Tables.columnnames(table)
funcs = _functuple(transform.func, names)

ncols = map(names) do nm
x = Tables.getcolumn(cols, nm)
func = get(funcs, nm, identity)
func.(x)
end

𝒯 = (; zip(names, ncols)...)
newtable = 𝒯 |> Tables.materializer(table)
return newtable, nothing
end

function colrevert(transform::Functional, y, c)
g = inverse(transform.func)
g.(y)
end
function revert(transform::Functional, newtable, cache)
@assert isrevertible(transform) "Transform is not revertible."

cols = Tables.columns(newtable)
names = Tables.columnnames(newtable)
funcs = _functuple(transform.func, names)

ocols = map(names) do nm
x = Tables.getcolumn(cols, nm)
func = get(funcs, nm, identity)
invfunc = inverse(func)
invfunc.(x)
end

𝒯 = (; zip(names, ocols)...)
𝒯 |> Tables.materializer(newtable)
end
44 changes: 44 additions & 0 deletions test/transforms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,50 @@
@test all((1), n.x)
@test all((1), n.y)
@test isrevertible(T) == false

# apply functions to specific columns
x = π*rand(1500)
y = π*(rand(1500) .- 0.5)
z = x + y
t = Table(; x, y, z)
T = Functional(:x => cos, :y => sin)
n, c = apply(T, t)
@test all(x -> -1 x 1, n.x)
@test all(y -> -1 y 1, n.y)
@test t.z == n.z
tₒ = revert(T, n, c)
@test Tables.matrix(t) Tables.matrix(tₒ)

x = 2*(rand(1500) .- 0.5)
y = 2*(rand(1500) .- 0.5)
z = x + y
t = Table(; x, y, z)
T = Functional("x" => acos, "y" => asin)
n, c = apply(T, t)
@test all(x -> 0 x π, n.x)
@test all(y -> -π/2 y π/2, n.y)
@test t.z == n.z
tₒ = revert(T, n, c)
@test Tables.matrix(t) Tables.matrix(tₒ)

T = Functional(:x => cos, :y => sin)
@test isrevertible(T) == true
T = Functional("x" => cos, "y" => sin)
@test isrevertible(T) == true
T = Functional(:x => abs, :y => sin)
@test isrevertible(T) == false
T = Functional("x" => abs, "y" => sin)
@test isrevertible(T) == false

# throws
@test_throws ArgumentError Functional()
t = Table(x = rand(15), y = rand(15))
T = Functional(Polynomial(1, 2, 3))
n, c = apply(T, t)
@test_throws AssertionError revert(T, n, c)
T = Functional(:x => abs, :y => sin)
n, c = apply(T, t)
@test_throws AssertionError revert(T, n, c)
end

@testset "EigenAnalysis" begin
Expand Down

0 comments on commit 01364ef

Please sign in to comment.