Author: Kevin Wright
Date: 2021-04-16

# Lucid printing of floating-point vectors

#### Kevin Wright

#### 2021-04-16

## Setup

    library("knitr")
        opts_chunk$set(fig.align="center", fig.width=6, fig.height=6)
    options(width=90)

## Abstract

Farquhar and Farquhar (1891) provide a humorous quote about tables:

> The graphic method has considerable superiority for the exposition of statistical facts over the tabular. A heavy bank of figures is grievously wearisome to the eye, and the popular mind is as incapable of drawing any useful lessons from it as of extracting sunbeams from cucumbers.

The `lucid` package intends to make your life easier by helping you extract information from tables. The package has functions for printing vectors and tables of floating-point numbers in a human-friendly format. An application is presented for printing of variance components from mixed models.

## Introduction

Numerical output from R is often in scientific notation, which can make it difficult to quickly glance at numbers and understand the relative sizes of the numbers. This not a new phenomenon. Before R had been created, Finney (1988) had this to say about numerical output:

> Certainly, in initiating analyses by standard software or in writing one’s own software, the aim should be to have output that is easy to read and easily intelligible to others. … Especially undesirable is the so-called ‘scientific notation’ for numbers in which every number is shown as a value between 0.0 and 1.0 with a power of 10 by which it must be multiplied. For example:

>     0.1234E00 is 0.1234
>     0.1234E02 is 12.34
>     0.1234E-1 is 0.01234

> This is an abomination which obscures the comparison of related quantities; tables of means or of analyses of variance become very difficult to read. It is acceptable as a default when a value is unexpectedly very much larger or smaller than its companions, but its appearance as standard output denotes either lazy programming or failure to use good software properly. Like avoidance of ‘E,’ neat arrangement of output values in columns, with decimal points on a vertical line, requires extra effort by a programmer but should be almost mandatory for any software that is to be used often.

One recommendation for improving the display of tables of numbers is to round numbers to 2 (Wainer 1997) or 3 Clark (1965) digits for the following reasons:

1. We cannot comprehend more than three digits very easily.
2. We seldom care about accuracy of more than three digits.
3. We can rarely justify more than three digits of accuracy statistically.

An alternative to significant digits is the concept of effective digits Kozak et al. (2011), which considers the amount of variation in the data.

In R, the `round()` and `signif()` functions can be used to round to 3 digits of accuracy, but those functions can still print results in scientific notation and leave much to be desired. The `lucid` package provides functions to improve the presentation of floating point numbers in a clear (or lucid) way that makes interpretation of the numbers immediately apparent.

Consider the following vector of coefficients from a fitted model:

    ##                    effect
    ## (Intercept)  1.135000e+02
    ## A           -1.350000e+01
    ## B            4.500000e+00
    ## C            2.450000e+01
    ## C1           6.927792e-14
    ## C2          -1.750000e+00
    ## D            1.650000e+01

Questions of interest about the coefficients might include:

1. Which coefficient is zero?
2. How large is the intercept?

Both questions can be answered using the output shown above, but it takes too much effort to answer the questions. Now examine the same vector of coefficients with prettier formatting:

    require("lucid")
    options(digits=7) # knitr defaults to 4, R console uses 7
    lucid(df1)

    ##             effect
    ## (Intercept) 114
    ## A           -13.5
    ## B             4.5
    ## C            24.5
    ## C1            0
    ## C2           -1.75
    ## D            16.5

Which coefficient is zero? How large is the intercept?

Printing the numbers with the `lucid()` function has made the questions much easier to answer.

The sequence of steps used by `lucid()` to format and print the output is.

1. Zap small numbers to zero using `zapsmall()`.
2. Round using 3 significant digits (user controllable option).
3. Drop trailing zeros.
4. Align numbers at the decimal point (text format).

The `lucid` package contains a generic function `lucid()` with specific methods for numeric vectors, data frames, and lists. The method for data frames applies formatting to each numeric column and leaves other columns unchanged. The `lucid()` function is primarily a formatting function, the results of which are passed to the regular `print()` functions.

## Example: Antibiotic effectiveness

Wainer and Larsen (2009) present data published by Will Burtin in 1951 on the effectiveness of antibiotics against 16 types of bacteria. The data is included in the `lucid` package as a dataframe called `antibiotic`. The default view of this data is:

    print(antibiotic)

    ##                      bacteria penicillin streptomycin neomycin gramstain
    ## 1        Aerobacter aerogenes    870.000         1.00    1.600       neg
    ## 2            Brucella abortus      1.000         2.00    0.020       neg
    ## 3            Escherichia coli    100.000         0.40    0.100       neg
    ## 4       Klebsiella pneumoniae    850.000         1.20    1.000       neg
    ## 5  Mycobacterium tuberculosis    800.000         5.00    2.000       neg
    ## 6            Proteus vulgaris      3.000         0.10    0.100       neg
    ## 7      Pseudomonas aeruginosa    850.000         2.00    0.400       neg
    ## 8          Salmonella typhosa      1.000         0.40    0.008       neg
    ## 9   Salmonella schottmuelleri     10.000         0.80    0.090       neg
    ## 10         Bacillis anthracis      0.001         0.01    0.007       pos
    ## 11     Diplococcus pneumoniae      0.005        11.00   10.000       pos
    ## 12       Staphylococcus albus      0.007         0.10    0.001       pos
    ## 13      Staphylococcus aureus      0.030         0.03    0.001       pos
    ## 14      Streptococcus fecalis      1.000         1.00    0.100       pos
    ## 15  Streptococcus hemolyticus      0.001        14.00   10.000       pos
    ## 16     Streptococcus viridans      0.005        10.00   40.000       pos

Due to the wide range in magnitude of the values, nearly half of the floating-point numbers in the default view contain trailing zeros after the decimal, which adds significant clutter and impedes interpretation. The `lucid()` display of the data is:

    lucid(antibiotic)

    ##                      bacteria penicillin streptomycin neomycin gramstain
    ## 1        Aerobacter aerogenes    870             1       1.6         neg
    ## 2            Brucella abortus      1             2       0.02        neg
    ## 3            Escherichia coli    100             0.4     0.1         neg
    ## 4       Klebsiella pneumoniae    850             1.2     1           neg
    ## 5  Mycobacterium tuberculosis    800             5       2           neg
    ## 6            Proteus vulgaris      3             0.1     0.1         neg
    ## 7      Pseudomonas aeruginosa    850             2       0.4         neg
    ## 8          Salmonella typhosa      1             0.4     0.008       neg
    ## 9   Salmonella schottmuelleri     10             0.8     0.09        neg
    ## 10         Bacillis anthracis      0.001         0.01    0.007       pos
    ## 11     Diplococcus pneumoniae      0.005        11      10           pos
    ## 12       Staphylococcus albus      0.007         0.1     0.001       pos
    ## 13      Staphylococcus aureus      0.03          0.03    0.001       pos
    ## 14      Streptococcus fecalis      1             1       0.1         pos
    ## 15  Streptococcus hemolyticus      0.001        14      10           pos
    ## 16     Streptococcus viridans      0.005        10      40           pos

The `lucid()` display is dramatically simplified, providing a clear picture of the effectiveness of the antibiotics against bacteria. This view of the data matches exactly the appearance of Table 1 in Wainer and Larsen (2009).

A stem-and-leaf plot is a semi-graphical display of data, in that the positions of the numbers create a display similar to a histogram. In a similar manner, the `lucid()` output is a semi-graphical view of the data. The figure below shows a dotplot of the penicillin values on a reverse log10 scale. The values are also shown along the right axis in `lucid()` format. Note the similarity in the overall shape of the dots and the positions of the left-most significant digit in the numerical values along the right axis.

## Example: Using `lucid` with `broom`

The `broom` package by Robinson (2016) can be used to collect statistics from fitted models into tidy data frames. For example, using the `Orange` tree data, it is possible to fit a separate regression line for each tree. (The straight-line regression here is not entirely sensible, but illustrates a point.)

    require(dplyr)

    ## Loading required package: dplyr

    ##
    ## Attaching package: 'dplyr'

    ## The following objects are masked from 'package:stats':
    ##
    ##     filter, lag

    ## The following objects are masked from 'package:base':
    ##
    ##     intersect, setdiff, setequal, union

    require(broom)

    ## Loading required package: broom

    Orange %>% group_by(Tree) %>% do(tidy(lm(circumference ~ age, data=.)))

    ## # A tibble: 10 x 6
    ## # Groups:   Tree [5]
    ##    Tree  term        estimate std.error statistic   p.value
    ##    <ord> <chr>          <dbl>     <dbl>     <dbl>     <dbl>
    ##  1 3     (Intercept)  19.2      5.86         3.28 0.0221
    ##  2 3     age           0.0811   0.00563     14.4  0.0000290
    ##  3 1     (Intercept)  24.4      6.54         3.73 0.0135
    ##  4 1     age           0.0815   0.00628     13.0  0.0000485
    ##  5 5     (Intercept)   8.76     8.18         1.07 0.333
    ##  6 5     age           0.111    0.00785     14.1  0.0000318
    ##  7 2     (Intercept)  20.0      9.35         2.13 0.0859
    ##  8 2     age           0.125    0.00898     13.9  0.0000343
    ##  9 4     (Intercept)  14.6     11.2          1.30 0.249
    ## 10 4     age           0.135    0.0108      12.5  0.0000573

Extracting information from the sea of numbers above is difficult. The `lucid` function comes to the rescue, simply by adding one more step to the sequence of pipes.

    Orange %>% group_by(Tree) %>% do(tidy(lm(circumference ~  age, data=.))) %>% lucid

    ##    Tree        term estimate std.error statistic   p.value
    ## 1     3 (Intercept)  19.2      5.86         3.28 0.0221
    ## 2     3         age   0.0811   0.00563     14.4  0.000029
    ## 3     1 (Intercept)  24.4      6.54         3.73 0.0135
    ## 4     1         age   0.0815   0.00628     13    0.0000485
    ## 5     5 (Intercept)   8.76     8.18         1.07 0.333
    ## 6     5         age   0.111    0.00785     14.1  0.0000318
    ## 7     2 (Intercept)  20        9.35         2.13 0.0859
    ## 8     2         age   0.125    0.00898     13.9  0.0000343
    ## 9     4 (Intercept)  14.6     11.2          1.3  0.249
    ## 10    4         age   0.135    0.0108      12.5  0.0000573

After formatting, information in the table almost jumps out at the reader, reducing the amount of cognitive effort needed for interpretation.

## Example: Application to mixed models

During the process of iterative fitting of mixed models, it is often useful to compare fits of different models to data, for example using loglikelihood or AIC values, or with the help of residual plots. It can also be very informative to inspect the estimated values of variance components.

To that end, the generic `VarCorr()` function found in the `nlme` Pinheiro et al. (2014) and `lme4` Bates et al. (2014) packages can be used to print variance estimates from fitted models. The `VarCorr()` function is not available for models obtained using the `asreml` Butler (2009) package.

The `lucid` package provides a generic function called `vc()` that provides a unified interface for extracting the variance components from fitted models obtained from the `asreml`, `lme4`, `nlme`, and `rjags` packages. The `vc()` function has methods specific to each package that make it easy to extract the estimated variances and correlations from fitted models and formats the results using the `lucid()` function.

Pearce et al. (1988) suggest showing four significant digits for the error mean square and two decimal places digits for \(F\) values. The `lucid()` function uses a similar philosophy, presenting the variances with four significant digits and `asreml` \(Z\) statistics with two significant digits.

### vc() example 1 - Rail data

The following simple example illustrates use of the `vc()` function for identical REML models in the `nlme`, `lme4`, and `asreml` packages. The travel times of ultrasonic waves in six steel rails was modeled as an overall mean, a random effect for each rail, and a random residual. The package `rjags` is used to fit a similar Bayesian model inspired by Wilkinson (2014).

## nlme

    require("nlme")

    ## Loading required package: nlme

    ##
    ## Attaching package: 'nlme'

    ## The following object is masked from 'package:dplyr':
    ##
    ##     collapse

    data(Rail)
    mn <- lme(travel~1, random=~1|Rail, data=Rail)
    vc(mn)

    ##       effect variance stddev
    ##  (Intercept)   615.3  24.81
    ##     Residual    16.17  4.021

# lme4

    require("lme4")

    ## Loading required package: lme4

    ## Loading required package: Matrix

    ##
    ## Attaching package: 'lme4'

    ## The following object is masked from 'package:nlme':
    ##
    ##     lmList

    m4 <- lmer(travel~1 + (1|Rail), data=Rail)
    vc(m4)

    ##       grp        var1 var2   vcov  sdcor
    ##      Rail (Intercept) <NA> 615.3  24.81
    ##  Residual        <NA> <NA>  16.17  4.021

# asreml

    # require("asreml")
    # ma <- asreml(travel~1, random=~Rail, data=Rail)
    # vc(ma)
    ##         effect component std.error z.ratio constr
    ##  Rail!Rail.var    615.3      392.6     1.6    pos
    ##     R!variance     16.17       6.6     2.4    pos

# JAGS

In a Bayesian model all effects can be considered as random.

    require("nlme")
    data(Rail)
    require("rjags")
    m5 <-
    "model {
    for(i in 1:nobs){
      travel[i] ~ dnorm(mu + theta[Rail[i]], tau)
    }
    for(j in 1:6) {
      theta[j] ~ dnorm(0, tau.theta)
    }
    mu ~ dnorm(50, 0.0001) # Overall mean. dgamma()
    tau ~ dgamma(1, .001)
    tau.theta ~ dgamma(1, .001)
    residual <- 1/sqrt(tau)
    sigma.rail <- 1/sqrt(tau.theta)
    }"
    jdat <- list(nobs=nrow(Rail), travel=Rail$travel, Rail=Rail$Rail)
    jinit <- list(mu=50, tau=1, tau.theta=1)
    tc5 <- textConnection(m5)
    j5 <- jags.model(tc5, data=jdat, inits=jinit, n.chains=2, quiet=TRUE)
    close(tc5)
    c5 <- coda.samples(j5, c("mu","theta", "residual", "sigma.rail"),
                       n.iter=100000, thin=5, progress.bar="none")

    vc(c5)

    ##               Mean     SD     2.5%  Median   97.5%
    ## mu          65.6   9.772   45.8     65.78   84.73
    ## residual     3.954 0.8203   2.734    3.823   5.92
    ## sigma.rail  23.41  7.54    13.68    21.84   42.37
    ## theta[1]   -33.5   9.96   -53.13   -33.6   -13.46
    ## theta[2]   -15.4   9.965  -34.96   -15.52    4.609
    ## theta[3]   -11.45  9.933  -31.03   -11.61    8.482
    ## theta[4]    16.87  9.955   -2.604   16.69   37.17
    ## theta[5]    18.84  9.954   -0.4339  18.68   39.04
    ## theta[6]    30.02  9.976   10.6     29.82   50.31

Compare the JAGS point estimates and quantiles (above) with the results from `lme4` below.

    m4

    ## Linear mixed model fit by REML ['lmerMod']
    ## Formula: travel ~ 1 + (1 | Rail)
    ##    Data: Rail
    ## REML criterion at convergence: 122.177
    ## Random effects:
    ##  Groups   Name        Std.Dev.
    ##  Rail     (Intercept) 24.805
    ##  Residual              4.021
    ## Number of obs: 18, groups:  Rail, 6
    ## Fixed Effects:
    ## (Intercept)
    ##        66.5

    ranef(m4)

    ## $Rail
    ##   (Intercept)
    ## 2   -34.53091
    ## 5   -16.35675
    ## 1   -12.39148
    ## 6    16.02631
    ## 3    18.00894
    ## 4    29.24388
    ##
    ## with conditional variances for "Rail"

While the `lucid()` function is primarily a formatting function and uses the standard `print()` functions in R, the `vc()` function defines an additional class for the value of the function and has dedicated `print` methods for the class. This was done to allow additional formatting of the results.

### vc() example 2 - Analysis of federer.diagcheck data

The second, more complex example is based on a model in Federer and Wolfinger (2003) in which orthogonal polynomials are used to model trends along the rows and columns of a field experiment. The data are available in the `agridat` package (Wright 2014) as the `federer.diagcheck` data frame. The help page for that data shows how to reproduce the analysis of Federer and Wolfinger (2003). When using the `lme4` package to reproduce the analysis, two different optimizers are available. Do the two different optimizers lead to similar estimated variances?

In the output below, the first column identifies terms in the model, the next two columns are the variance and standard deviation from the ‘bobyqa’ optimizer, while the final two columns are from the ‘NelderMead’ optimizer. Note, these results are from `lme4` version 1.1-7 and are likely to be different than the results from more recent versions of `lme4`.

The default output printing is shown first.

    print(out)

    ##           term     vcov-bo  sdcor-bo sep      vcov-ne     sdcor-ne
    ## 1  (Intercept)   2869.4469  53.56722     3.228419e+03  56.81917727
    ## 2        r1:c3   5531.5724  74.37454     7.688139e+03  87.68203447
    ## 3        r1:c2  58225.7678 241.30016     6.974755e+04 264.09761622
    ## 4        r1:c1 128004.1561 357.77668     1.074270e+05 327.76064925
    ## 5           c8   6455.7495  80.34768     6.787004e+03  82.38327224
    ## 6           c6   1399.7294  37.41296     1.636128e+03  40.44907560
    ## 7           c4   1791.6507  42.32790     1.226846e+04 110.76308194
    ## 8           c3   2548.8847  50.48648     2.686302e+03  51.82954364
    ## 9           c2   5941.7908  77.08301     7.644730e+03  87.43414634
    ## 10          c1      0.0000   0.00000     1.225143e-03   0.03500204
    ## 11         r10   1132.9501  33.65932     1.975505e+03  44.44665149
    ## 12          r8   1355.2291  36.81344     1.241429e+03  35.23391157
    ## 13          r4   2268.7296  47.63118     2.811241e+03  53.02113582
    ## 14          r2    241.7894  15.54958     9.282275e+02  30.46682578
    ## 15          r1   9199.9022  95.91612     1.036358e+04 101.80169429
    ## 16        <NA>   4412.1096  66.42371     4.126832e+03  64.24042100

How similar are the variance estimates obtained from the two optimization methods? It is difficult to compare the results due to the clutter of extra digits, and because of some quirks in the way R formats the output. The variances in column 2 are shown in non-scientific format, while the variances in column 5 are shown in scientific format. The standard deviations are shown with 5 decimal places in column 3 and 8 decimal places in column 6. (All numbers were stored with 15 digits of precision.)

The `lucid()` function is now used to show the results in the manner of the `vc()` function.

    lucid(out, dig=4)

    ##           term  vcov-bo sdcor-bo sep  vcov-ne sdcor-ne
    ## 1  (Intercept)   2869      53.57       3228     56.82
    ## 2        r1:c3   5532      74.37       7688     87.68
    ## 3        r1:c2  58230     241.3       69750    264.1
    ## 4        r1:c1 128000     357.8      107400    327.8
    ## 5           c8   6456      80.35       6787     82.38
    ## 6           c6   1400      37.41       1636     40.45
    ## 7           c4   1792      42.33      12270    110.8
    ## 8           c3   2549      50.49       2686     51.83
    ## 9           c2   5942      77.08       7645     87.43
    ## 10          c1      0       0             0      0.035
    ## 11         r10   1133      33.66       1976     44.45
    ## 12          r8   1355      36.81       1241     35.23
    ## 13          r4   2269      47.63       2811     53.02
    ## 14          r2    241.8    15.55        928.2   30.47
    ## 15          r1   9200      95.92      10360    101.8
    ## 16        <NA>   4412      66.42       4127     64.24

The formatting of the variance columns is consistent as is the formatting of the standard deviation columns. Fewer digits are shown. It is easy to compare the columns and see that the two optimizers are giving quite different answers. Note: The Bobyqa results are almost identical to the results obtained when using ASREML or SAS.

Note: Data frames have no quotes, but numeric matrices are printed with quotes. Use `noquote()` to print without quotes, for example:

    noquote(lucid(as.matrix(head(mtcars)),2))

    ##                   mpg cyl disp hp  drat wt  qsec vs am gear carb
    ## Mazda RX4         21  6   160  110 3.9  2.6 16   0  1  4    4
    ## Mazda RX4 Wag     21  6   160  110 3.9  2.9 17   0  1  4    4
    ## Datsun 710        23  4   110   93 3.8  2.3 19   1  1  4    1
    ## Hornet 4 Drive    21  6   260  110 3.1  3.2 19   1  0  3    1
    ## Hornet Sportabout 19  8   360  180 3.2  3.4 17   0  0  3    2
    ## Valiant           18  6   220  100 2.8  3.5 20   1  0  3    1

## References

Bates, Douglas, Martin Maechler, Ben Bolker, and S. Walker. 2014. lme4: Linear mixed-effects models using Eigen and S4. https://CRAN.R-project.org/package=lme4.
Butler, David. 2009. asreml: asreml() fits the linear mixed model. www.vsni.co.uk.
Clark, R. T. 1965. “The Presentation of Numerical Results of Experiments for Publication in a Scientific Agricultural Journal.” Experimental Agriculture 1: 315–19. https://doi.org/10.1017/S001447970002161X.
Ehrenberg, A. S. C. 1977. “Rudiments of Numeracy.” Journal of the Royal Statistical Society. Series A, 277–97. https://doi.org/10.2307/2344922.
Farquhar, Arthur B., and Henry Farquhar. 1891. Economic and Industrial Delusions. New York: G. P. Putnam’s Sons. https://books.google.com/books?id=BHkpAAAAYAAJ.
Federer, Walter T., and Russell D. Wolfinger. 2003. “Handbook of Formulas and Software for Plant Geneticists and Breeders.” In, edited by Manjit Kang. Haworth Press.
Feinberg, Richard A., and Howard Wainer. 2011. “Extracting Sunbeams from Cucumbers.” Journal of Computational and Graphical Statistics 20 (4): 793–810. https://doi.org/10.1198/jcgs.2011.204a.
Finney, D. J. 1988. “Was This in Your Statistics Textbook? II. Data Handling.” Experimental Agriculture 24: 343–53. https://doi.org/10.1017/S0014479700016197.
Kozak, Marcin, Ricardo Antunes Azevedo, Justyna Jupowicz-Kozak, and Wojtek Krzanowski. 2011. “Reporting Numbers in Agriculture and Biology: Don’t Overdo the Digits.” Australian Journal of Crop Science 5: 1876–81. http://www.cropj.com/kozak_5_13_2011_1876_1881.pdf.
Pearce, S. C., G. M. Clarke, G. V. Dyke, and R. E. Kempson. 1988. A Manual of Crop Experimentation. Charles Griffin; Company.
Pinheiro, Jose, Douglas Bates, Saikat DebRoy, Deepayan Sarkar, and R Core Team. 2014. nlme: Linear and Nonlinear Mixed Effects Models. https://CRAN.R-project.org/package=nlme.
Robinson, David. 2016. broom: Convert Statistical Analysis Objects into Tidy Data Frames. https://CRAN.R-project.org/package=broom.
Wainer, Howard. 1997. “Improving Tabular Displays, with NAEP Tables as Examples and Inspirations.” Journal of Educational and Behavioral Statistics 22: 1–30. https://doi.org/10.3102/10769986022001001.
Wainer, Howard, and Mike Larsen. 2009. “Pictures at an Exhibition.” Chance 22 (2): 46–54. https://doi.org/10.1080/09332480.2009.10722958.
Wilkinson, Darren. 2014. “One-Way ANOVA with Fixed and Random Effects from a Bayesian Perspective.” https://darrenjw.wordpress.com/2014/12/22/one-way-anova-with-fixed-and-random-effects-from-a-bayesian-perspective/.
Wright, Kevin. 2014. Agridat: Agricultural Datasets. https://CRAN.R-project.org/package=agridat.