This document presents mathematical details about the
Dendrochronology Program Library in R (dplR) (Bunn 2008, 2010) which is an add-on package for
R (R Core Team 2015). 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 (Bunn et al. 2025), it is stated that the
frequency response of the spline is f at a wavelength
(period) of nyrs years1, 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.
The manual of the caps function cites Cook and Kairiukstis (1990). On page 111, they
give the following frequency (amplitude) response function for the
spline:
where is frequency and 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 Cook and Peters (1981), the reference used by Cook and Kairiukstis (1990). I did not find a copy of Peters and Cook (1981) 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
.
Setting parameters f and nyrs in
caps is equivalent to the following directive: set the
smoothing parameter to a value that fulfills
. By making the variable
substitutions and rearranging equation (1) we get the following equation
for
:
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:
Since
that line is exactly equation (2). In other words, caps
uses the Lagrange multiplier of Cook and
Kairiukstis (1990) directly, with no reparameterization. This is
a pleasant state of affairs: the quantity named p in the
source code and the quantity named
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.
Figure 1. Theoretical frequency response of the spline
filter (equations 1 and 2, green line) versus the response measured with
i.i.d. normal series of 1536 samples, mean of 500 repeats, using
caps (blue circles). The legend on the bottom panel applies
to all panels.
Figure 1 shows the result. Theory meets practice well, particularly
for low frequencies. The measured response at the nominal cutoff
frequency
is 0.504, 0.511 and 0.530 for
,
16 and 64 respectively, against a nominal
.
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.
| Series | nyrs | f | max. abs. difference |
|---|---|---|---|
| i.i.d. normal, n = 200 | 10 | 0.5 | 2.84e-14 |
| i.i.d. normal, n = 200 | 10 | 0.9 | 1.42e-14 |
| i.i.d. normal, n = 200 | 32 | 0.5 | 2.46e-12 |
| i.i.d. normal, n = 200 | 32 | 0.9 | 3.41e-13 |
| noisy sine wave, n = 100 | 10 | 0.5 | 3.55e-15 |
| noisy sine wave, n = 100 | 10 | 0.9 | 3.55e-15 |
| noisy sine wave, n = 100 | 32 | 0.5 | 7.28e-13 |
| noisy sine wave, n = 100 | 32 | 0.9 | 6.39e-14 |
| AR(1), phi = 0.7, n = 500 | 10 | 0.5 | 7.11e-15 |
| AR(1), phi = 0.7, n = 500 | 10 | 0.9 | 7.11e-15 |
| AR(1), phi = 0.7, n = 500 | 32 | 0.5 | 2.84e-13 |
| AR(1), phi = 0.7, n = 500 | 32 | 0.9 | 4.97e-14 |
| ca533 series CAM011 | 10 | 0.5 | 3.33e-16 |
| ca533 series CAM011 | 10 | 0.9 | 2.22e-16 |
| ca533 series CAM011 | 32 | 0.5 | 2.26e-14 |
| ca533 series CAM011 | 32 | 0.9 | 4.00e-15 |
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 2.5e-12, 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
, two thirds of the series length,
the two differ by 1.23e-04, which is 0.01% of the range of the series.
Passing the truncated value
to ffcsaps instead brings the difference back down to
1.1e-11, 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.
Figure 2. Top: series CAM011 of the ca533
data set (grey) with a 32-year spline fitted by caps (blue)
and by the legacy ffcsaps (vermillion, dashed); the two
curves are indistinguishable. Bottom: the difference between the two
fitted curves, in ring-width units.
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
where and its inverse are variables used in the code. Writing equation (2) for the inverse,
we find that equations (5) and (4) are connected by
or equivalently
So the variable named p in ffcsaps and the
Lagrange multiplier
of Cook and Kairiukstis (1990) 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
with
.
Following from equations (7) and (8), the splines described in Cook and Kairiukstis (1990), and hence those
computed by caps, are the result of minimizing
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 Biondi and Qeadan
(2008) which uses the following formula for the Gini coefficient
():
In equation (10), the Gini coefficient is defined in terms of pairwise differences between all pairs of observations (). 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:
where is the number of observations and is the th cumulative sum
of sorted observations :
Equation (11) can be reformulated as
or as
When we assign
and
equation (15) becomes
or equivalently
Figure 3 is a graphical representation of the Gini coefficient using an example data set of the following six observed values: . It shows the definition of the Gini coefficient as the ratio of the area above the Lorenz curve (Lorenz 1905) to the total area of the triangle (Xu 2003). 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.
Figure 3. Graphical representation of the Gini coefficient based on areas defined by the Lorenz curve (n = 6). See equations (16), (17), (18) and (19).
Comparing Figure 3 to equation (17), is the sum of the areas of the cyan bars. Summing the areas of the teal triangles, we get
Note that 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 . When all observed values are equal, the Lorenz curve matches the line of equality, and the Gini coefficient is . We have assumed that all values are non-negative.
The equivalence of different definitions of the Gini coefficient is
reviewed in Xu (2003). 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## [1] 2.542411e-14
Over all 34 series of the ca533 data set the two agree
to 2.5e-14.
assuming that the sampling rate is once per year↩︎