# Significant Figures Woes

My Laboratory recently needed to produce a report for a new method detection limit (MDL). The MDL is, effectively, the smallest value of an compound that an instrument can reliably distinguish from zero. This is an important value for environmental purposes; we want to be able to demonstrate that we can detect toxic compounds at levels as close to zero as possible.

While preparing our report, we determined that our process was capable of producing MDLs to three significant figures. We took our measurements for two different analytical methods, performed our calculations, and came up with two values:

```
mdl_1 <- 1.72307562596959
mdl_2 <- 1.20482925259075
```

When I generated the report, I chose to use the `signif`

function to reduce those values to just the significant figures. According to the documentation in `?signif`

, I expected this would “round the values in its first argument to the specified number of significant digits.” Here’s what I got:

`signif(mdl_1, 3)`

`## [1] 1.72`

`signif(mdl_2, 3)`

`## [1] 1.2`

For `mdl_1`

, I got the three significant figures I was expecting, but I only got two significant figures for `mdl_2`

. That may not seem like a big deal, but 1.2 does not give us the credit our process deserves for being able to detect our compound. `signif`

robbed us of an entire order of magnitude in our precision.

There’s a certain difficulty in reporting trailing zeros, and in all the reading I’ve done on the topic, it really boils down to *trailing zeros may or may not be significant, depending on where you learned your significant figures rules*. But as a matter of principle, a trailing zero is significant when the measurement process can demonstrate that level of precision.

In the current application, I *know* that that trailing zero is significant, I just need to tell R to give it back to me somehow.

## Generalizing Significant Figures Reporting

We can make reporting significant figures a little more robust with a couple of simple concepts.

First, let’s define the *precision* of a measurement to be the smallest decimal position in which we have confidence in the measurement. For our purposes, we can say the MDLs are precise to the hundredth place. We can denote precision as an integer, and for convenience, we’ll use the same value we would pass to the `round`

function. For us, the MDLs have precision 2–if we were to use round, we’d use `round(mdl_1, 2)`

. Similarly, we can represent precision to the left of the decimal place as negative integers, with precision to the ones place denoted by a 0. (`round(x, 0)`

rounds `x`

to the nearest integer).

Second, we recognize that leading zeros are never significant. This lets us utilize logarithms to determine what the place of the first significant figure is. Importantly, `log10`

will return a low-ball estimate of the (negative) *precision* value of the first significant figure. From there, we just need to count out the remaining significant figures.

Let’s walk through this with `mdl_1 =`

1.7230756.

`log10(mdl_1)`

`## [1] 0.2363043`

This tells us that the `mdl_1`

is more than 0 but less than 10. Taking the `ceiling`

of this value gives us a starting point for counting out significant figures. We want three significant figures, so we do

`ceiling(log10(mdl_1)) - 3`

`## [1] -2`

Scientifically, this means the terminal digit in our result should be at the `1e-2`

place, or the hundredth place. As it turns out, `round`

looks at rounding positions backwards, so we need to multiply this result by -1.

```
precision <- (ceiling(log10(mdl_1)) - 3) * -1
precision
```

`## [1] 2`

At last, we can use this to print out the value we want. We’ll need to use either `sprintf`

or `format`

, because the print method for `numeric`

objects cuts off trailing zeros.

`sprintf(paste0("%0.", precision, "f"), mdl_1)`

`## [1] "1.72"`

`format(round(mdl_1, precision), nsmall = precision)`

`## [1] "1.72"`

`sprintf(paste0("%0.", precision, "f"), mdl_2)`

`## [1] "1.20"`

`format(round(mdl_2, precision), nsmall = precision)`

`## [1] "1.20"`

## Wrapping It Into a Function

This is easily placed into a function as follows:

```
sigfig <- function(x, sigfig){
precision <- (ceiling(log10(x)) - sigfig) * -1
sprintf(paste0("%0.", precision, "f"), x)
}
sigfig(mdl_1, 3)
```

`## [1] "1.72"`

`sigfig(mdl_2, 3)`

`## [1] "1.20"`

This also has the benefit of working with various vectorizations.

`sigfig(c(mdl_1, mdl_2), 3)`

`## [1] "1.72" "1.20"`

```
sigfig(c(mdl_1, mdl_2),
sigfig = c(2, 3))
```

`## [1] "1.7" "1.20"`

## Limitations

This process works pretty well for cases where the terminal digit is to the right of the decimal point. It requires a little extra handling to deal with cases where the terminal digit is to the left of the decimal point. This is the kind of thing I really ought to bundle into a package.

There are some other benefits to approaching significant figures like this. Primarily, the work of determining how many significant figures will result from a calculation of measured values can be automated.

## One Last Observation

You can trick yourself into believing that `signif`

is doing what you want it to do if you put `mdl_1`

and `mdl_2`

into a vector.

`signif(c(mdl_1, mdl_2), 3)`

`## [1] 1.72 1.20`

However, the trailing zero is appearing there because the `print.numeric`

method likes to print numeric values with the same number of decimal places. This is why I recommend reporting your significant figures using a character value–it gives you control over the result and avoids unpleasant surprises.