blob: 331f3290679044c57907febb949c975b37c12bce [file]
#-------------------------------------------------------------
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#-------------------------------------------------------------
# Power transformation using the selected method.
# Reduces feature skewness by estimating and applying an optimal transformation parameter for each column.
#
# INPUT:
# -------------------------------------------------------------------------------------
# X Input feature matrix of shape n-by-m
# method Power transformation method: "yeo-johnson" (default) or "box-cox"
# standardize Whether to normalize transformed columns to zero mean and unit variance
# -------------------------------------------------------------------------------------
#
# OUTPUT:
# -------------------------------------------------------------------------------------
# Y Power-transformed matrix of shape n-by-m
# lambdas Estimated lambda parameters of shape 1-by-m, one per column
# means Transformed column means of shape 1-by-m, or an empty matrix when not standardized
# scales Transformed column scales of shape 1-by-m, or an empty matrix when not standardized
# -------------------------------------------------------------------------------------
m_powerTransform = function(
Matrix[Double] X,
String method="yeo-johnson",
Boolean standardize=TRUE)
return (
Matrix[Double] Y,
Matrix[Double] lambdas,
Matrix[Double] means,
Matrix[Double] scales)
{
if (method != "yeo-johnson" & method != "box-cox") {
stop("powerTransform: unsupported method '" + method +
"'; expected 'yeo-johnson' or 'box-cox'")
}
validatedX = replace(target=X, pattern=NaN, replacement=1.0)
if (method == "box-cox" & min(validatedX) <= 0.0) {
stop("powerTransform: Box-Cox requires strictly positive input")
}
m = ncol(X)
lambdas = matrix(1.0, rows=1, cols=m) # Initialize first, then replace each column with the best lambdas
# Estimate lambda for each column separately
for (j in 1:m){
x = X[,j]
xObserved = removeEmpty(target=x, margin="rows", select=(is.na(x) == 0))
observedN = nrow(xObserved)
# Yeo-Johnson leaves constant columns unchanged; Box-Cox rejects them
if (observedN == 0) {
lambdas[1,j] = 1.0
}
else if (max(xObserved) == min(xObserved)) {
if (method == "yeo-johnson") {
lambdas[1,j] = 1.0;
}
else {
stop("powerTransform: Box-Cox does not support constant columns")
}
}
else{
lambdas[1,j] = ptEstimateLambda(xObserved, method);
}
}
# Apply the fitted transformation before optional standardization
emptyStats = matrix(0.0, rows=0, cols=0)
Y = powerTransformApply(X, lambdas, emptyStats, emptyStats, method);
means = matrix(0.0, rows=0, cols=0)
scales = matrix(0.0, rows=0, cols=0)
if (standardize) {
means = matrix(0.0, rows=1, cols=m)
scales = matrix(1.0, rows=1, cols=m)
for (j in 1:m) {
y = Y[,j]
yObserved = removeEmpty(target=y, margin="rows", select=(is.na(y) == 0))
observedN = nrow(yObserved)
if (observedN > 0) {
means[1,j] = mean(yObserved)
scale = sqrt(sum((yObserved - means[1,j])^2) / observedN)
if (!is.na(scale) & !is.infinite(scale) & scale != 0.0) {
scales[1,j] = scale
}
}
Y[,j] = ifelse(is.na(y), NaN, (y - means[1,j]) / scales[1,j])
}
}
}
ptEstimateLambda = function(Matrix[Double] x, String method)
return (Double lambda)
{
lower = -2.0;
upper = 2.0;
if (method == "box-cox") {
jacTerm = sum(log(x))
}
else {
jacTerm = sum(sign(x) * log(abs(x) + 1.0))
}
lambda = ptBrentSearch(x, lower, upper, method, jacTerm);
}
# Compute negative log likelihood; lower lambda score is better
ptNegLogLikelihood = function(
Matrix[Double] x,
Double lambda,
String method,
Double jacTerm)
return (Double negLogLikelihood)
{
eps = 1e-12
if (method == "box-cox") {
if (abs(lambda) < eps) {
y = log(x)
}
else {
y = (x^lambda - 1.0) / lambda
}
}
else {
nonnegative = x >= 0
xPos = ifelse(nonnegative, x, 0.0)
xNeg = ifelse(nonnegative, 0.0, x)
if (abs(lambda) < eps) {
yPos = log(xPos + 1.0)
}
else {
yPos = ((xPos + 1.0)^lambda - 1.0) / lambda
}
if (abs(lambda - 2.0) < eps) {
yNeg = -log(1.0 - xNeg)
}
else {
yNeg = -((1.0 - xNeg)^(2.0 - lambda) - 1.0) / (2.0 - lambda)
}
y = ifelse(nonnegative, yPos, yNeg)
}
n = nrow(x);
yMean = mean(y);
yVariance = sum((y - yMean)^2) / n;
if (is.na(yVariance) | is.infinite(yVariance) | yVariance <= 0.0) {
negLogLikelihood = 1e300
}
else {
logLikelihood = -n / 2.0 * log(yVariance) + (lambda - 1.0) * jacTerm;
negLogLikelihood = -logLikelihood;
if (is.na(negLogLikelihood) | is.infinite(negLogLikelihood)) {
negLogLikelihood = 1e300
}
}
}
# Minimize the negative log likelihood with Brent optimization
ptBrentSearch = function(
Matrix[Double] x,
Double lower,
Double upper,
String method,
Double jacTerm)
return (Double lambdaOptimal)
{
# Expand the initial interval until it brackets a minimum
goldenRatio = 1.618034;
maxBracketIterations = 1000;
lowerScore = ptNegLogLikelihood(x, lower, method, jacTerm);
upperScore = ptNegLogLikelihood(x, upper, method, jacTerm);
lambdaOptimal = 1.0
bestScore = ptNegLogLikelihood(x, lambdaOptimal, method, jacTerm)
if (lowerScore < bestScore) {
lambdaOptimal = lower
bestScore = lowerScore
}
if (upperScore < bestScore) {
lambdaOptimal = upper
bestScore = upperScore
}
if (lowerScore < upperScore) {
xa = upper;
fa = upperScore;
xb = lower;
fb = lowerScore;
}
else {
xa = lower;
fa = lowerScore;
xb = upper;
fb = upperScore;
}
initialXc = xb + goldenRatio * (xb - xa);
initialFc = ptNegLogLikelihood(x, initialXc, method, jacTerm);
xc = initialXc;
fc = initialFc;
if (fc < bestScore) {
lambdaOptimal = xc
bestScore = fc
}
bracketIteration = 0;
while ((fc < fb) & (bracketIteration < maxBracketIterations)) {
nextXc = xc + goldenRatio * (xc - xb);
nextFc = ptNegLogLikelihood(x, nextXc, method, jacTerm);
xa = xb;
fa = fb;
xb = xc;
fb = fc;
xc = nextXc;
fc = nextFc;
if (fc < bestScore) {
lambdaOptimal = xc
bestScore = fc
}
bracketIteration = bracketIteration + 1;
}
validBracket = bracketIteration < maxBracketIterations &
(((fb < fa) & (fb <= fc)) | ((fb <= fa) & (fb < fc)))
if (validBracket) {
a = min(xa, xc);
b = max(xa, xc);
goldenMean = 0.3819660112501051;
sqrtEpsilon = sqrt(2.2e-16);
tolerance = 1.48e-8;
maxIterations = 500;
xf = a + goldenMean * (b - a);
nfc = xf;
fulc = xf;
fx = ptNegLogLikelihood(x, xf, method, jacTerm);
fnfc = fx;
ffulc = fx;
if (fx < bestScore) {
lambdaOptimal = xf
bestScore = fx
}
rat = 0.0;
e = 0.0;
midpoint = 0.5 * (a + b);
tol1 = sqrtEpsilon * abs(xf) + tolerance / 3.0;
tol2 = 2.0 * tol1;
iteration = 0;
while ((abs(xf - midpoint) > (tol2 - 0.5 * (b - a))) &
(iteration < maxIterations)) {
goldenStep = TRUE;
if (abs(e) > tol1) {
goldenStep = FALSE;
r = (xf - nfc) * (fx - ffulc);
q = (xf - fulc) * (fx - fnfc);
p = (xf - fulc) * q - (xf - nfc) * r;
q = 2.0 * (q - r);
if (q > 0.0) {
p = -p;
}
q = abs(q);
previousE = e;
e = rat;
if ((q > 0.0) & (abs(p) < abs(0.5 * q * previousE)) &
(p > q * (a - xf)) & (p < q * (b - xf))) {
rat = p / q;
candidate = xf + rat;
if (((candidate - a) < tol2) | ((b - candidate) < tol2)) {
if (midpoint >= xf) {
rat = tol1;
}
else {
rat = -tol1;
}
}
}
else {
goldenStep = TRUE;
}
}
if (goldenStep) {
if (xf >= midpoint) {
e = a - xf;
}
else {
e = b - xf;
}
rat = goldenMean * e;
}
if (rat >= 0.0) {
candidate = xf + max(abs(rat), tol1);
}
else {
candidate = xf - max(abs(rat), tol1);
}
fCandidate = ptNegLogLikelihood(x, candidate, method, jacTerm);
if (fCandidate < bestScore) {
lambdaOptimal = candidate
bestScore = fCandidate
}
if (fCandidate <= fx) {
if (candidate >= xf) {
a = xf;
}
else {
b = xf;
}
fulc = nfc;
ffulc = fnfc;
nfc = xf;
fnfc = fx;
xf = candidate;
fx = fCandidate;
}
else {
if (candidate < xf) {
a = candidate;
}
else {
b = candidate;
}
if ((fCandidate <= fnfc) | (nfc == xf)) {
fulc = nfc;
ffulc = fnfc;
nfc = candidate;
fnfc = fCandidate;
}
else if ((fCandidate <= ffulc) | (fulc == xf) | (fulc == nfc)) {
fulc = candidate;
ffulc = fCandidate;
}
}
midpoint = 0.5 * (a + b);
tol1 = sqrtEpsilon * abs(xf) + tolerance / 3.0;
tol2 = 2.0 * tol1;
iteration = iteration + 1;
}
}
}