Yeo-Johnson Likelihood Inference in R

Power transformations implemented from scratch in R, from their geometry to likelihood-based inference on Darwin's classic plant experiment, where my estimate of the transformation matches the published one.

Yeo-Johnson, live. Click to replay.λ = 0
Darwin's 15 differencestheir densitynormal curve
1.305estimated λ on Darwin's data, as in the original paper
0.07skewness after the transformation, from −0.82
0.92Shapiro-Wilk p-value after, from 0.09
3.92likelihood-ratio statistic against λ = 1
4R scripts, every formula written by hand
01 / The problem

When the data are not normal enough.

Many statistical tools, from the t-test to linear models, assume roughly normal data. Real measurements are often skewed. A power transformation can make them more symmetric, but the classic Box-Cox family only works on strictly positive values.

Yeo and Johnson (Biometrika, 2000) proposed a family that accepts negative values, zero and positive values alike, which makes it the right tool for signed measurements such as the difference between two paired plants. I implemented it directly in R, with its logarithmic special cases:

Tλ(x)={(x+1)λ−1λx ≥ 0, λ ≠ 0log(x+1)x ≥ 0, λ = 0−(1−x)2−λ−12−λx < 0, λ ≠ 2−log(1−x)x < 0, λ = 2
The Yeo-Johnson transformation
02 / Geometry

One parameter, many shapes.

The second script studies the family for λ = 0, 0.5, 1, 1.5 and 2 and compares it with Box-Cox. At λ = 1 the transformation is the identity. Below 1 it compresses large positive values and stretches negative ones, which corrects right skew; above 1 it does the opposite and corrects left skew.

Tλ(x) for five values of λλ = 00.511.52
−446−6xT(x)

Computed from the transformation's formula, in the style of the original paper's figure. Curves are clipped at ±6.

03 / Mixtures

Testing the limits on purpose.

The first script builds a deliberately asymmetric distribution: 30% standard normal and 70% Gamma(4, 1) shifted by −2, discretized on a 3,000-point grid, from which it draws 10,000 observations. It then picks the transformation parameter by bounded numerical optimization (L-BFGS-B) and compares three curves: the original mixture, a kernel density of the transformed sample, and the Gaussian fitted to it.

The point is to see how far a single power transformation goes on a shape it was not designed for, and where it stops helping. That is what motivates the more flexible Yeo-Johnson family in the next step.

04 / Darwin's data

Back to 1876.

Darwin grew 15 pairs of plants, one cross-fertilized and one self-fertilized in each pair, and measured their heights. The question is simple: do cross-fertilized plants grow taller? The data are the 15 differences, two of them negative, the same ones the demo at the top transforms.

Under a normal model for the transformed values, the log-likelihood includes the Jacobian of the transformation, the term that connects the model to the original measurements:

ℓ(λ,μ,σ2)=−n2log(2πσ2)−12σ2∑i=1n(Tλ(xi)−μ)2+(λ−1)∑i=1nsgn(xi)log(1+|xi|)
Log-likelihood with the Jacobian term

The script optimizes λ first, then refines λ, μ and σ² jointly, and computes their covariance matrix from the numerical Hessian. The estimate is λ̂ = 1.305, the same value Yeo and Johnson report for this data. The transformation fixes the shape:

Darwin's differencesSkewnessShapiro-Wilk WShapiro-Wilk pOne-sided tp-value
Original−0.820.8990.0932.140.025
Transformed, λ̂ = 1.3050.070.9750.9213.130.004

After the transformation the data are almost perfectly symmetric, normality is far more plausible, and the evidence that cross-fertilized plants grow taller gets stronger.

functions.R · yeo_johnson() and log_likelihood()
yeo_johnson <- function(x, lambda) {  y <- numeric(length(x))  pos_values <- x >= 0  neg_values <- x < 0  # negative values: a modified power transformation, continuous across zero  y[neg_values] <- if (lambda == 2) {    -log(-x[neg_values] + 1)  } else {    -((-x[neg_values] + 1)^(2 - lambda) - 1) / (2 - lambda)  }  # positive values: the Box-Cox form, shifted by one  y[pos_values] <- if (lambda == 0) {    log(x[pos_values] + 1)  } else {    ((x[pos_values] + 1)^lambda - 1) / lambda  }  return(y)}log_likelihood <- function(lambda, mu, sigma2, x) {  transformed_x <- yeo_johnson(x, lambda)  n <- length(x)  # Gaussian likelihood of the transformed data plus the Jacobian of the transformation  log_likelihood_value <- -(n / 2) * log(2 * pi) - (n / 2) * log(sigma2) -    (1 / (2 * sigma2)) * sum((transformed_x - mu)^2) +    (lambda - 1) * sum(sign(x) * log(abs(x) + 1))  return(-log_likelihood_value)   # negative, for minimization with optim()}
The two core functions, shortened: input checks and documentation removed.
05 / Inference

Is the transformation worth it?

A transformation is a modeling choice, so it has to earn its place. A likelihood-ratio test compares the optimized model with λ = 1, the untransformed data: the statistic is 3.92 against a χ² distribution with one degree of freedom, a p-value of about 0.048. The improvement is real, at the edge of the usual 5% threshold.

The script closes by returning to the original scale: it back-transforms a lower 0.01 quantile for the mean difference, based on a Student-t reference, so that the conclusion is stated in the units Darwin measured.

From theory to Darwin's data.

The four R scripts, with the shared library of transformations, likelihoods and diagnostics, are on GitHub, together with the reference to the original paper.

Next case study · 08 / 08BPMN & Petri Nets: Internship Process