knitr::opts_chunk$set( echo = FALSE, message = FALSE, warning = FALSE, fig.width = 7, fig.height = 4.5, fig.align = "center", dev = "png", dpi = 96 ) library(dplR) # caps() library(Matrix) # the legacy ffcsaps() used sparse matrices ## A colour-blind-safe palette (Okabe and Ito) COLOR_SIM <- "#0072B2" # blue COLOR_ALT <- "#D55E00" # vermillion COLOR_LINE <- "#009E73" # bluish green COLOR_REF <- "#999999" # grey
## Cook, E. R. and Kairiukstis, L. A. (1990) Methods of ## Dendrochronology: Applications in the Environmental Sciences. ## Cook, E. R. and Peters, K. (1981) The smoothing spline: a new ## approach to standardizing forest interior tree-ring width series ## for dendroclimatic studies. ## Smoothing parameter p as a function of the period (nyrs) at which a ## frequency response of f is desired. This is equation (2) below, and ## it is what the Fortran behind caps() computes. pCook <- function(nyrs, f = 0.5) { 6 * f * (cos(2 * pi / nyrs) - 1)^2 / ((1 - f) * (cos(2 * pi / nyrs) + 2)) } ## Frequency response according to Cook and Kairiukstis (citing Cook ## and Peters). This is equation (1) below. respCook <- function(f, p) { pif2 <- 2 * pi * f 1 - 1 / (1 + (p * (cos(pif2) + 2)) / (6 * (cos(pif2) - 1)^2)) }
This document presents mathematical details about the Dendrochronology
Program Library in R (dplR) [@Bunn2008115; @Bunn2010251] which is an add-on
package for R [@Rman]. The first half deals with the spline smoothing
function caps; the second covers the computation of Gini coefficients in
gini.coef.
The original implementations of the functions covered here were not written by the author of this document. Therefore the functions were analyzed with a reverse engineering approach.
This document was first written when spline smoothing in dplR was performed
by ffcsaps, a pure R function. As of dplR version 1.7.3 that role belongs
to caps, a wrapper around a Fortran subroutine from Ed Cook's ARSTAN, and
ffcsaps is deprecated. The two functions parameterize the spline
differently but, as the section on equivalence demonstrates, they compute the
same spline. The analysis below has been rewritten around caps, with the
ffcsaps parameterization retained at the end because it explains a factor
of two that a reader comparing the two implementations will otherwise trip
over.
capsThe caps function fits a cubic smoothing spline to a given data vector. In
the manual (Rd file) of the function [@dplRman], it is stated that the
frequency response of the spline is f at a wavelength (period) of nyrs
years[^1], where these two are parameters of the function. We aim to clarify
how they relate to the single smoothing parameter of the spline and what that
parameter stands for.
[^1]: assuming that the sampling rate is once per year
The manual of the caps function cites @cook1990methods. On page 111, they
give the following frequency (amplitude) response function for the spline:
$$ u(f)=1-\frac{1}{1 + \frac{p(\cos (2\pi f) +2)}{6(\cos (2\pi f) -1)^2}} \qquad (1) $$
where (f) is frequency and (p) is stated to be the Lagrange multiplier of the spline, the single parameter that determines the frequency response. However, the exact definition of the optimization problem is absent. Neither is it given in @cook1981smoothing, the reference used by @cook1990methods. I did not find a copy of @peters1981cubic when trying to follow the chain of references further.
Note that the relationship between frequency and period using mixed notation
of caps and equation (1) is (f = 1/\mathtt{nyrs}). Setting parameters f
and nyrs in caps is equivalent to the following directive: set the
smoothing parameter to a value that fulfills (u(1/\mathtt{nyrs}) =
\mathtt{f}). By making the variable substitutions and rearranging equation
(1) we get the following equation for (p):
$$ p = \frac{6 \mathtt{f} (\cos (2\pi / \mathtt{nyrs}) -1)^2}{(1 - \mathtt{f})(\cos (2\pi / \mathtt{nyrs}) +2)} \qquad (2) $$
The Fortran subroutine called by caps (caps_f in src/capsf.f95) sets
its smoothing parameter with the following line, where pct is f and v
is nyrs:
p=((1.d0/(1.d0-pct)-1.d0)*6.d0*(cos(pi*2.d0/v)-1.d0)**2)/(cos(pi*2.d0/v)+2.d0)
Since
$$ \frac{1}{1 - \mathtt{f}} - 1 = \frac{\mathtt{f}}{1 - \mathtt{f}} \qquad (3) $$
that line is exactly equation (2). In other words, caps uses the Lagrange
multiplier of @cook1990methods directly, with no reparameterization. This is
a pleasant state of affairs: the quantity named p in the source code and
the quantity named (p) in the book are the same number. As the last section
describes, that was not true of the ffcsaps implementation that caps
replaced.
Whether the fitted spline actually has the advertised frequency response is a separate question from whether the code implements equation (2) correctly, and it is worth checking. We smooth 500 independent series of 1536 i.i.d. standard normal samples, take the ratio of the modulus of the discrete Fourier transform of the smoothed series to that of the input, and average over the repeats.
N <- 1536 K <- 500 NYRS <- c(4, 16, 64) nFreq <- N / 2 + 1 halfseq <- seq_len(nFreq) ratio1 <- array(NA_real_, c(nFreq, K, length(NYRS))) if (!exists(".Random.seed", globalenv(), mode = "numeric")) { foo <- sample(TRUE) } seed <- get(".Random.seed", globalenv()) set.seed(123) for (k in seq_len(K)) { x <- rnorm(N) fftx <- abs(fft(x))[halfseq] for (j in seq_along(NYRS)) { fft1 <- abs(fft(caps(x, nyrs = NYRS[j], f = 0.5)))[halfseq] ratio1[, k, j] <- fft1 / fftx } } assign(".Random.seed", seed, globalenv()) response1 <- matrix(NA_real_, nFreq, length(NYRS)) colnames(response1) <- NYRS for (j in seq_along(NYRS)) { response1[, j] <- rowMeans(ratio1[, , j]) } fftFreq <- seq(from = 0, to = 0.5, length.out = nFreq) ## Simulated response at the nominal cutoff frequency atCutoff <- vapply(seq_along(NYRS), function(j) approx(fftFreq, response1[, j], xout = 1 / NYRS[j])$y, numeric(1))
op <- par(mfcol = c(3, 1), mgp = c(2, 0.75, 0), mar = c(4, 4, 2.5, 1)) LWD <- 3 PCH_1 <- 1 ## The simulation has one value per Fourier frequency, which is far too ## dense to plot as points. Show every SUBth one so that the theoretical ## curve underneath stays visible. SUB <- seq(from = 1, to = nFreq, by = 10) for (j in seq_along(NYRS)) { plot(fftFreq, response1[, j], type = "n", ylim = c(0, 1), xlab = "Frequency (1 / year)", ylab = "Amplitude response", main = sprintf("nyrs = %d, f = 0.5", NYRS[j])) lines(fftFreq, respCook(fftFreq, pCook(NYRS[j])), col = COLOR_LINE, lwd = LWD) points(fftFreq[SUB], response1[SUB, j], pch = PCH_1, col = COLOR_SIM, cex = 0.8, lwd = 1.5) abline(h = 0.5, lty = "dashed") abline(v = 1 / NYRS[j], lty = "dashed") text(0.35, 0.5, "50% response", pos = 1, offset = 1) text(1 / NYRS[j], 0.6, sprintf("%d yr period", NYRS[j]), pos = 4, srt = 90, offset = 1) } legend("topright", bg = "white", legend = c("Simulation (caps)", "Theoretical (Cook and Peters)"), col = c(COLOR_SIM, COLOR_LINE), lty = c(NA, "solid"), pch = c(PCH_1, NA), lwd = c(1, LWD)) par(op)
Figure 1 shows the result. Theory meets practice well, particularly for low
frequencies. The measured response at the nominal cutoff frequency
(1/\mathtt{nyrs}) is r sprintf("%.3f", atCutoff[1]),
r sprintf("%.3f", atCutoff[2]) and r sprintf("%.3f", atCutoff[3]) for
(\mathtt{nyrs} = r NYRS[1]), r NYRS[2] and r NYRS[3] respectively,
against a nominal (\mathtt{f} = 0.5). It must be noted that the theoretical
result does not take into account the effect of having a series of finite
length, which is why the agreement degrades as nyrs grows toward the length
of the series.
caps and the legacy ffcsapsUsers with results produced by dplR 1.7.2 or earlier will want to know
whether caps changed any numbers. It did not, with one documented
exception. Below, the pure R implementation of ffcsaps as it stood in dplR
1.6.9 is reproduced as ffcsaps.legacy and compared against caps.
## Helper used by ffcsaps.legacy() inc <- function(from, to) { if (is.numeric(to) && is.numeric(from) && to >= from) { seq(from = from, to = to) } else { integer(length = 0) } } ## The pure R implementation of ffcsaps() as it stood in dplR 1.6.9, ## before caps() replaced it. Reproduced here only so that the two ## implementations can be compared; use caps() for real work. ffcsaps.legacy <- function(y, x = seq_along(y), nyrs = length(y)/2, f = 0.5) { ffppual <- function(breaks, c1, c2, c3, c4, x, left) { if (left) { ix <- order(x) x2 <- x[ix] } else { x2 <- x } n.breaks <- length(breaks) if (left) { index <- pmax(ffsorted(breaks[-n.breaks], x2), 1) } else { index <- ffsorted2(breaks[-1], x2) } x2 <- x2 - breaks[index] v <- x2 * (x2 * (x2 * c1[index] + c2[index]) + c3[index]) + c4[index] if (left) v[ix] <- v v } ffsorted <- function(meshsites, sites) { index <- order(c(meshsites, sites)) which(index > length(meshsites)) - seq_along(sites) } ffsorted2 <- function(meshsites, sites) { index <- order(c(sites, meshsites)) which(index <= length(sites)) - seq(from = 0, to = length(sites) - 1) } ## Similar in function to spdiags(B, d, n, n) in MATLAB spdiags <- function(B, d, n) { n.d <- length(d) A <- matrix(0, n.d * n, 3) count <- 0 for (k in seq_len(n.d)) { this.diag <- d[k] i <- inc(max(1, 1 - this.diag), min(n, n - this.diag)) n.i <- length(i) if (n.i > 0) { j <- i + this.diag row.idx <- seq(from = count + 1, by = 1, length.out = n.i) A[row.idx, 1] <- i A[row.idx, 2] <- j A[row.idx, 3] <- B[j, k] count <- count + n.i } } A <- A[A[, 3] != 0, , drop = FALSE] A[order(A[, 2], A[, 1]), , drop = FALSE] } y2 <- as.numeric(y) x2 <- as.numeric(x) n <- length(x2) if (n < 3) stop("there must be at least 3 data points") ix <- order(x2) zz1 <- n - 1 xi <- x2[ix] zz2 <- n - 2 diff.xi <- diff(xi) if (any(diff.xi == 0)) stop("the data abscissae must be distinct") if (n != length(y2)) stop("abscissa and ordinate vector must be of the same length") arg2 <- -1:1 odx <- 1 / diff.xi R <- spdiags(cbind(c(diff.xi[-c(1, zz1)], 0), 2 * (diff.xi[-1] + diff.xi[-zz1]), c(0, diff.xi[-c(1, 2)])), arg2, zz2) R2 <- spdiags(cbind(c(odx[-zz1], 0, 0), c(0, -(odx[-1] + odx[-zz1]), 0), c(0, 0, odx[-1])), arg2, n) R2[, 1] <- R2[, 1] - 1 forR <- Matrix(0, zz2, zz2, sparse = TRUE) forR2 <- Matrix(0, zz2, n, sparse = TRUE) forR[R[, 1:2, drop = FALSE]] <- R[, 3] forR2[R2[, 1:2, drop = FALSE]] <- R2[, 3] ## This is equation (4): the ffcsaps parameterization p.inv <- (1 - f) * (cos(2 * pi / nyrs) + 2) / (12 * f * (cos(2 * pi / nyrs) - 1)^2) + 1 yi <- y2[ix] p <- 1 / p.inv mplier <- 6 - 6 / p.inv u <- as.numeric(solve(mplier * tcrossprod(forR2) + forR * p, diff(diff(yi) / diff.xi))) yi <- yi - mplier * diff(c(0, diff(c(0, u, 0)) / diff.xi, 0)) test0 <- xi[-c(1, n)] c3 <- c(0, u / p.inv, 0) x3 <- c(test0, seq(from = xi[1], to = xi[n], length = 101)) cc.1 <- diff(c3) / diff.xi cc.2 <- 3 * c3[-n] cc.3 <- diff(yi) / diff.xi - diff.xi * (2 * c3[-n] + c3[-1]) cc.4 <- yi[-n] to.sort <- c(test0, x3) ix.final <- order(to.sort) tmp <- unique(data.frame( to.sort[ix.final], c(ffppual(xi, cc.1, cc.2, cc.3, cc.4, test0, FALSE), ffppual(xi, cc.1, cc.2, cc.3, cc.4, x3, TRUE))[ix.final])) tmp2 <- tmp tmp2[[1]] <- round(tmp2[[1]], 5) res <- tmp2[[2]][tmp2[[1]] %in% x2] if (length(res) != n) res <- approx(x = tmp[[1]], y = tmp[[2]], xout = x2, ties = "ordered")$y res }
if (!exists(".Random.seed", globalenv(), mode = "numeric")) { foo <- sample(TRUE) } seed <- get(".Random.seed", globalenv()) set.seed(42) data(ca533) cam011 <- as.numeric(na.omit(ca533[, "CAM011"])) cases <- list( "i.i.d. normal, n = 200" = rnorm(200, 100, 20), "noisy sine wave, n = 100" = 5 * sin(seq(from = 0, to = 6 * pi, length.out = 101)[-101]) + rnorm(100) + 20, "AR(1), phi = 0.7, n = 500" = as.numeric(arima.sim(list(ar = 0.7), 500)) + 50, "ca533 series CAM011" = cam011) assign(".Random.seed", seed, globalenv()) equivGrid <- expand.grid(case = names(cases), nyrs = c(10, 32), f = c(0.5, 0.9), stringsAsFactors = FALSE) equivGrid$maxdiff <- vapply(seq_len(nrow(equivGrid)), function(i) { y <- cases[[equivGrid$case[i]]] max(abs(ffcsaps.legacy(y, nyrs = equivGrid$nyrs[i], f = equivGrid$f[i]) - caps(y, nyrs = equivGrid$nyrs[i], f = equivGrid$f[i]))) }, numeric(1)) worstInteger <- max(equivGrid$maxdiff) ## The one place the two differ: caps() coerces nyrs to an integer fracNyrs <- 2 * length(cam011) / 3 diffFrac <- max(abs(ffcsaps.legacy(cam011, nyrs = fracNyrs, f = 0.5) - caps(cam011, nyrs = fracNyrs, f = 0.5))) diffTrunc <- max(abs(ffcsaps.legacy(cam011, nyrs = trunc(fracNyrs), f = 0.5) - caps(cam011, nyrs = fracNyrs, f = 0.5))) camRange <- diff(range(cam011))
ord <- order(match(equivGrid$case, names(cases)), equivGrid$nyrs, equivGrid$f) tab <- equivGrid[ord, c("case", "nyrs", "f", "maxdiff")] tab$maxdiff <- sprintf("%.2e", tab$maxdiff) names(tab) <- c("Series", "nyrs", "f", "max. abs. difference") knitr::kable(tab, row.names = FALSE, align = "lrrr", caption = paste("**Table 1.** Largest absolute difference", "between `ffcsaps.legacy` and `caps` over all", "fitted values, for integer `nyrs`."))
Table 1 gives the largest absolute difference between the two implementations
across four test series, two values of nyrs and two values of f. The
worst case is r sprintf("%.1e", worstInteger), which is floating-point
noise. For integer nyrs, caps and ffcsaps compute the same spline.
There is one genuine difference. caps passes nyrs to Fortran as an
integer, so a fractional nyrs is truncated, whereas ffcsaps used it as
given. Fitting series CAM011 of the ca533 data set with (\mathtt{nyrs} =
r sprintf("%.2f", fracNyrs)), two thirds of the series length, the two
differ by r sprintf("%.2e", diffFrac), which is
r sprintf("%.2f", 100 * diffFrac / camRange)% of the range of the series.
Passing the truncated value (r trunc(fracNyrs)) to ffcsaps instead
brings the difference back down to r sprintf("%.1e", diffTrunc), confirming
that truncation is the whole of the discrepancy.
This is reachable in ordinary use, by two routes. A nyrs between 0 and 1
selects the proportion-of-series-length shorthand, and caps multiplies it
by the series length, which will rarely land on a whole number. Internally,
plot.crn, wavelet.plot and ssf pass a fractional nyrs of their own,
computed as a fixed proportion of the series length; detrend.series and
rcs apply floor first and so are unaffected. The effect on the fitted
curve is small, but it is not zero.
capsFit <- caps(cam011, nyrs = 32, f = 0.5) ffFit <- ffcsaps.legacy(cam011, nyrs = 32, f = 0.5) op <- par(mfcol = c(2, 1), mgp = c(2, 0.75, 0), mar = c(4, 4, 2.5, 1)) plot(cam011, type = "l", col = COLOR_REF, xlab = "Index", ylab = "Ring width (mm)", main = "CAM011 with a 32-year spline") lines(capsFit, col = COLOR_SIM, lwd = 3) lines(ffFit, col = COLOR_ALT, lwd = 2, lty = "dashed") legend("topright", bty = "n", cex = 0.85, legend = c("data", "caps", "ffcsaps (legacy)"), col = c(COLOR_REF, COLOR_SIM, COLOR_ALT), lty = c("solid", "solid", "dashed"), lwd = c(1, 3, 2)) plot(capsFit - ffFit, type = "l", col = COLOR_SIM, xlab = "Index", ylab = "caps - ffcsaps", main = "Difference between the two fits") abline(h = 0, lty = "dashed", col = COLOR_REF) par(op)
Figure 2 shows the two fits on a real ring-width series together with their difference, which is at the level of the floating-point representation.
ffcsaps parameterizationThe deprecated ffcsaps contained code lines corresponding to the equation
$$ \mathtt{p.inv} = \frac{1}{\mathtt{p}} = \frac{(1 - \mathtt{f})(\cos (2\pi / \mathtt{nyrs}) +2)}{12 \mathtt{f} (\cos (2\pi / \mathtt{nyrs}) -1)^2} + 1 \qquad (4) $$
where (\mathtt{p}) and its inverse (\mathtt{p.inv}) are variables used in the code. Writing equation (2) for the inverse,
$$ \frac{1}{p} = \frac{(1 - \mathtt{f})(\cos (2\pi / \mathtt{nyrs}) +2)}{6 \mathtt{f} (\cos (2\pi / \mathtt{nyrs}) -1)^2} \qquad (5) $$
we find that equations (5) and (4) are connected by
$$ \frac{1}{p} = 2 \left(\frac{1}{\mathtt{p}} - 1\right) \qquad (6) $$
or equivalently
$$ \frac{\mathtt{p}}{1 - \mathtt{p}} = 2 p \qquad (7) $$
So the variable named p in ffcsaps and the Lagrange multiplier (p) of
@cook1990methods were not the same quantity, despite sharing a name. They are
two parameterizations of the same penalty, related by equation (7). This is
the factor of two that a reader comparing src/capsf.f95 against ffcsaps
would otherwise have to discover the hard way.
The ffcsaps form is the convex-combination parameterization, in which the
spline minimizes
$$ \mathtt{p} \times \text{Error} + (1 - \mathtt{p}) \times \text{Roughness} \qquad (8) $$
with (\mathtt{p} \in [0, 1]). Following from equations (7) and (8), the
splines described in @cook1990methods, and hence those computed by caps,
are the result of minimizing
$$ 2 p \times \text{Error} + \text{Roughness} \qquad (9) $$
with the same definitions of Error and Roughness, details of which are omitted here. The section above confirms empirically that the two forms give the same fitted curve.
gini.coefThe gini.coef function computes the Gini coefficient (Gini index) of a
given data vector. The manual (Rd file) of the function has a reference to
@biondi2008inequality which uses the following formula for the Gini
coefficient ((G)):
$$ G = \frac{1}{2 n \sum_{i=1}^{n} x_i} \sum_{i=1}^{n} \sum_{j=1}^{n} \left| x_i - x_j \right| \qquad (10) $$
In equation (10), the Gini coefficient is defined in terms of pairwise differences between all pairs of observations ((x_i,\ i \in 1, \dots, n)). More specifically, the Gini coefficient is one half of the relative mean difference, which is defined as the mean of the absolute pairwise distances divided by the mean of the observations.
The C source code of the gini.coef function uses the following formula for
the Gini index:
$$ G = \left(X_n (n - 1) - 2 \sum_{i=1}^{n-1}X_i\right) / (X_n n) \qquad (11) $$
where (n) is the number of observations and (X_i) is the (i)th cumulative sum
$$ X_i = \sum_{j=1}^{i} x_j \qquad (12) $$
of sorted observations (x_j):
$$ \forall i: i < j \Rightarrow x_i \leq x_j \qquad (13) $$
Equation (11) can be reformulated as
$$ G = 1 - \frac{1}{n} - \frac{2}{X_n n} \sum_{i=1}^{n-1}X_i \qquad (14) $$
or as
$$ G = \left(\frac{1}{2} - \left(\frac{1}{2n} + \frac{1}{X_n n} \sum_{i=1}^{n-1}X_i\right)\right) / \frac{1}{2} \qquad (15) $$
When we assign
$$ A + B = \frac{1}{2} \qquad (16) $$
and
$$ B = B_1 + B_2 = \frac{1}{2n} + \frac{1}{X_n n} \sum_{i=1}^{n-1}X_i \qquad (17) $$
equation (15) becomes
$$ G = A / (A + B) \qquad (18) $$
or equivalently
$$ G = 1 - 2 B \qquad (19) $$
Figure 3 is a graphical representation of the Gini coefficient using an example data set of the following six observed values: ({0.2, 0.4, 0.75, 0.95, 1.2, 2.5}). It shows the definition of the Gini coefficient as the ratio of the area above the Lorenz curve [@lorenz1905] to the total area of the triangle [@xu2003has]. The Lorenz curve is defined by the cumulative distribution function of the empirical probability distribution of the observations. The sides of the triangle corresponding to the axes are normalized to length 1.
xg <- sort(c(0.2, 0.4, 0.75, 0.95, 1.2, 2.5)) ng <- length(xg) Xg <- cumsum(xg) px <- c(0, seq_len(ng) / ng) # cumulative portion of population py <- c(0, Xg / Xg[ng]) # cumulative sum of values / total COL_A <- "#FFC0CB" # pink COL_B1 <- "#008080" # teal COL_B2 <- "#00FFFF" # cyan op <- par(mar = c(5, 5, 1, 3), pty = "s") plot(NA, xlim = c(0, 1), ylim = c(0, 1), asp = 1, axes = FALSE, xlab = "Cumulative portion of population\n(ordered from lowest to highest value)", ylab = "Cumulative sum of values\ndivided by total") ## A: between the line of equality and the Lorenz curve polygon(c(px, rev(px)), c(py, rev(px)), col = COL_A, border = NA) ## B2: the staircase of bars under the curve, height = left endpoint for (i in seq_len(ng)) { rect(px[i], 0, px[i + 1], py[i], col = COL_B2, border = NA) } ## B1: the triangles capping each bar for (i in seq_len(ng)) { polygon(c(px[i], px[i + 1], px[i + 1]), c(py[i], py[i], py[i + 1]), col = COL_B1, border = NA) } ## Outlines polygon(c(0, 1, 1), c(0, 0, 1)) # the triangle lines(px, py, lwd = 1.5) # Lorenz curve points(px[-1], py[-1], pch = 21, bg = "white", cex = 0.9) for (i in seq_len(ng)) { # staircase outline lines(c(px[i], px[i + 1], px[i + 1]), c(py[i], py[i], py[i + 1]), col = "grey30", lwd = 0.6) } axis(1, at = px, labels = c("0", paste0(seq_len(ng), "/", ng)), cex.axis = 0.85) axis(4, at = c(0, 1), labels = c("0", "1"), las = 1, cex.axis = 0.85) text(0.42, 0.60, "Line of equality", srt = 45, cex = 0.9) text(0.55, 0.33, "Lorenz curve", srt = 44, cex = 0.9) legend("topleft", bty = "n", cex = 0.9, legend = c("A", expression(B[1]), expression(B[2])), fill = c(COL_A, COL_B1, COL_B2), border = NA) par(op)
Comparing Figure 3 to equation (17), (B_2 = \sum_{i=1}^{n-1}X_i / (X_n n)) is the sum of the areas of the cyan bars. Summing the areas of the teal triangles, we get
$$ \sum_{i=1}^{n}\left( \frac{1}{2} \frac{1}{n} \frac{x_i}{X_n} \right) = \frac{1}{2 n X_n}\sum_{i=1}^{n} x_i = \frac{1}{2 n} = B_1 \qquad (20) $$
Note that (B_1) only depends on the number of observations, not on their values. From equations (17) and (19) we find that the value of the Gini coefficient at maximum inequality (winner takes all) is (G_{\text{max}}(n)=1 - 1 / n). When all observed values are equal, the Lorenz curve matches the line of equality, and the Gini coefficient is (G_{\text{min}}=0). We have assumed that all values (x_i) are non-negative.
The equivalence of different definitions of the Gini coefficient is reviewed
in @xu2003has. One of the results shown in the paper is that the geometric
definition (18) used by the gini.coef function is equivalent to the
definition based on the relative mean difference (10). This can be
experimentally verified by comparing the results of the following R function
to those of gini.coef.
## Gini index is one half of relative mean difference. ## x should not have NA values. gini.rmd <- function(x) { mean(abs(outer(x, x, "-"))) / mean(x) * 0.5 }
giniMax <- max(abs(vapply(ca533, function(x) { x <- x[!is.na(x)] gini.rmd(x) - gini.coef(x) }, numeric(1)))) giniMax
Over all r ncol(ca533) series of the ca533 data set the two agree to
r sprintf("%.1e", giniMax).
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.