Transforms

A Transform defines a transformation of data for feature engineering purposes. Some examples are scaling, periodic functions, linear combination, and one-hot encoding. Transforms can be stateless, for example the power transform, or they can be stateful and fit to the data, such as the StandardScaling.

Defining a transform

A Transform often has one or more parameters. For example, the following defines a squaring operation (i.e. raise to the power of 2):

julia> p = Power(2);

A stateful transform, such as a StandardScaling should also be fit to the data before it is applied:

julia> s = StandardScaling();

julia> x = rand(5);

julia> FeatureTransforms.fit!(s, x);

Methods to apply a transform

Given some data x, there are three main methods to apply a transform. Firstly, it can be applied in a non-mutating fashion using apply:

julia> p = Power(2);

julia> x = [1.0, 2.0, 3.0];

julia> FeatureTransforms.apply(x, p)
3-element Vector{Float64}:
 1.0
 4.0
 9.0

julia> x
3-element Vector{Float64}:
 1.0
 2.0
 3.0

Equivalently, the Transform object can be called directly on the data:

julia> p(x)
3-element Vector{Float64}:
 1.0
 4.0
 9.0

Secondly, the data can be mutated using the apply! method.

Note

Some Transform subtypes do not support mutation, such as those which change the type or dimension of the input.

julia> FeatureTransforms.apply!(x, p)
3-element Vector{Float64}:
 1.0
 4.0
 9.0

julia> x
3-element Vector{Float64}:
 1.0
 4.0
 9.0

Finally, the result can be appended to the input using the apply_append method.

julia> x = [1.0, 2.0, 3.0];

julia> FeatureTransforms.apply_append(x, p, append_dim=2)
3×2 Matrix{Float64}:
 1.0  1.0
 2.0  4.0
 3.0  9.0

A single Transform instance can be applied to different data types, with support for AbstractArrays and Tables.

Note

Some Transform subtypes have restrictions on how they can be applied once constructed. For instance, StandardScaling stores the mean and standard deviation of some data, potentially specified via some dimension and column names. So StandardScaling should only be applied to the same data, and for the same dimension and subset of column names, as those used in construction.

Applying to AbstractArray

Default

Without specifying optional arguments, a Transform is applied to every element of an AbstractArray and in an element-wise fashion:

julia> M = [2.0 4.0; 1.0 5.0; 3.0 6.0];

julia> p = Power(2);

julia> FeatureTransforms.apply(M, p)
3×2 Matrix{Float64}:
 4.0  16.0
 1.0  25.0
 9.0  36.0

Applying to specific array indices with inds

Transforms can be applied to AbstractArray data with an inds keyword argument. This will apply the Transform to certain indices of an array. For example, to only square the second column:

julia> FeatureTransforms.apply(M, p; inds=[4, 5, 6])
3-element Vector{Float64}:
 16.0
 25.0
 36.0

Applying along dimensions using dims

Transforms can be applied to AbstractArray data with a dims keyword argument. This will apply the Transform to slices of the array along this dimension, which can be selected by the inds keyword. So when dims and inds are used together, the inds change from being the global indices of the array to the relative indices of each slice.

For example, given a Matrix, dims=1 slices the data column-wise and inds=[2, 3] selects the 2nd and 3rd rows.

Note

In general, users can expect the dims keyword to behave exactly as mean(A; dims=d) would; the transformation will be applied to the elements along the dimension d and, for operations like mean or sum, reduce across this dimension.

julia> M
3×2 Matrix{Float64}:
 2.0  4.0
 1.0  5.0
 3.0  6.0

julia> normalize_row = StandardScaling();

julia> fit!(normalize_row, M; dims=1, inds=[2])
StandardScaling(3.0, 2.8284271247461903)

julia> normalize_row(M; dims=1, inds=[2])
1×2 Matrix{Float64}:
 -0.707107  0.707107

julia> normalize_col = StandardScaling();

julia> fit!(normalize_col, M; dims=2, inds=[2])
StandardScaling(5.0, 1.0)

julia> normalize_col(M; dims=2, inds=[2])
3×1 Matrix{Float64}:
 -1.0
  0.0
  1.0

Applying to Table

Default

Without specifying optional arguments, a Transform will be applied to all the data in a Table and return a Table of the same type. One can specify the header for the output by passing it as a keyword argument. If no header is given, the default from Tables.table is used.

julia> nt = (a = [2.0, 1.0, 3.0], b = [4.0, 5.0, 6.0]);

julia> scaling = StandardScaling();

julia> fit!(scaling, nt);  # compute statistics using all data

julia> FeatureTransforms.apply(nt, scaling; header=[:a_norm, :b_norm])
(a_norm = [-0.8017837257372732, -1.3363062095621219, -0.2672612419124244], b_norm = [0.2672612419124244, 0.8017837257372732, 1.3363062095621219])

However, calling the mutating apply! will keep the original column names:

julia> FeatureTransforms.apply!(nt, scaling)
(a = [-0.8017837257372732, -1.3363062095621219, -0.2672612419124244], b = [0.2672612419124244, 0.8017837257372732, 1.3363062095621219])

Applying to specific columns with cols

For Table data, all Transforms support a cols keyword argument in their apply methods. This applies the transform to the specified columns.

Using cols, we can apply different transformations to different kinds of data from the same table:

julia> df = DataFrame(
           :time => DateTime(2021, 2, 27, 12):Hour(1):DateTime(2021, 2, 27, 14),
           :temperature_A => [18.1, 19.5, 21.1],
           :temperature_B => [16.2, 17.2, 17.5],
       );

julia> hod = HoD();

julia> lc = LinearCombination([0.5, 0.5]);

julia> hod_df = hod(df; cols=:time, header=[:hour_of_day]);

julia> lc_df = lc(df; cols=[:temperature_A, :temperature_B], header=[:aggregate_temperature]);

julia> feature_df = hcat(hod_df, lc_df)
3×2 DataFrame
 Row │ hour_of_day  aggregate_temperature 
     │ Int64        Float64               
─────┼────────────────────────────────────
   1 │          12                  17.15
   2 │          13                  18.35
   3 │          14                  19.3

Transform-specific keyword arguments

Some transforms have specific keyword arguments that can be passed to apply/apply!. For example, StandardScaling can invert the original scaling using the inverse argument:

julia> nt = (a = [2.0, 1.0, 3.0], b = [4.0, 5.0, 6.0]);

julia> scaling = StandardScaling();

julia> fit!(scaling, nt);

julia> FeatureTransforms.apply!(nt, scaling);

julia> nt
(a = [-0.8017837257372732, -1.3363062095621219, -0.2672612419124244], b = [0.2672612419124244, 0.8017837257372732, 1.3363062095621219])

julia> FeatureTransforms.apply!(nt, scaling; inverse=true);

julia> nt
(a = [2.0, 1.0, 3.0], b = [4.0, 5.0, 6.0])