Physical units in R

R has little support for physical measurement units. The exception is formed by time differences: time differences objects of class difftime have a units attribute that can be modified:

t1 = Sys.time() 
t2 = t1 + 3600 
d = t2 - t1
class(d)
## [1] "difftime"
units(d)
## [1] "hours"
d
## Time difference of 1 hours
units(d) = "secs"
d
## Time difference of 3600 secs

We see here that the units method is used to set and modify the unit of time difference.

This idea can be generalized to other physical units. The units package, presented here, does this, and builds upon the udunits2 R package, which in turn is build upon the udunits2 C library. The udunits2 library provides the following operations:

This R package, called units, uses R package udunits2 to extend R with functionality for manipulating numeric vectors that have physical measurement units associated with them, in a similar way as difftime objects behave.

Setting units, unit conversion

Numeric data with explicit physical units can be specified by as.units:

library(units)
(a = as.units(1:10, "m/s"))
## Units: m/s
##  [1]  1  2  3  4  5  6  7  8  9 10

and converted by setting a new physical unit:

b = a
units(b) = "km/h"
b
## Units: km/h
##  [1]  3.6  7.2 10.8 14.4 18.0 21.6 25.2 28.8 32.4 36.0

Impossible conversions lead to an error:

x = try(units(a) <- "secs")
x
## [1] "Error in `units<-.units`(`*tmp*`, value = \"secs\") : \n  cannot convert m/s into secs\n"
## attr(,"class")
## [1] "try-error"
## attr(,"condition")
## <simpleError in `units<-.units`(`*tmp*`, value = "secs"): cannot convert m/s into secs>

Basic manipulations

Arithmetic operations

Arithmetic operations verify units, and create new ones

a + a
## Units: m/s
##  [1]  2  4  6  8 10 12 14 16 18 20
a * a
## Units: (m/s)*(m/s)
##  [1]   1   4   9  16  25  36  49  64  81 100
a ^ 2.5
## Units: (m/s)^2.5
##  [1]   1.000000   5.656854  15.588457  32.000000  55.901699  88.181631
##  [7] 129.641814 181.019336 243.000000 316.227766

and convert if necessary:

a + b # m/s + km/h -> m/s
## Units: m/s
##  [1]  2  4  6  8 10 12 14 16 18 20

but units are not simplified:

t = as.units(1, "s")
a * t
## Units: (m/s)*(s)
##  [1]  1  2  3  4  5  6  7  8  9 10

Allowed operations that require convertable units are +, -, ==, !=, <, >, <=, >=. Operations that lead to new units are *, /, and the power operations ** and ^.

Mathematical functions

Mathematical operations allowed are: abs, sign, floor, ceiling, trunc, round, signif, cumsum, cummax, cummin.

signif(a^2.5, 3)
## Units: (m/s)^2.5
##  [1]   1.00   5.66  15.60  32.00  55.90  88.20 130.00 181.00 243.00 316.00
cumsum(a)
##  [1]  1  3  6 10 15 21 28 36 45 55

(for reasons beyond my comprehension, here cumsum strips units, but signif does not)

Summary functions

Summary functions sum, min, max, and range are allowed:

sum(a)
## 55 m/s
min(a)
## 1 m/s
max(a)
## 10 m/s
range(a)
## Units: m/s
## [1]  1 10
min(as.units(1, "m/s"), as.units(1, "km/h")) # converts to first unit:
## 0.2777778 m/s

Printing

Following difftime, printing behaves differently for length-one vectors:

a
## Units: m/s
##  [1]  1  2  3  4  5  6  7  8  9 10
a[1]
## 1 m/s

Subsetting

The usual subsetting rules work:

a[2:5]
## Units: m/s
## [1] 2 3 4 5
a[-(1:9)]
## 10 m/s

Concatenation

c(a,a)
## Units: m/s
##  [1]  1  2  3  4  5  6  7  8  9 10  1  2  3  4  5  6  7  8  9 10

concatenation converts to the units of the first argument, if necessary:

c(a,b) # m/s, km/h -> m/s
## Units: m/s
##  [1]  1  2  3  4  5  6  7  8  9 10  1  2  3  4  5  6  7  8  9 10
c(b,a) # km/h, m/s -> km/h
## Units: km/h
##  [1]  3.6  7.2 10.8 14.4 18.0 21.6 25.2 28.8 32.4 36.0  3.6  7.2 10.8 14.4
## [15] 18.0 21.6 25.2 28.8 32.4 36.0

Conversion to/from difftime

From difftime to units:

t1 = Sys.time() 
t2 = t1 + 3600 
d = t2 - t1
as.units(d)
## 1 h
(du = as.units(d, "d"))
## 0.04166667 d

vice versa:

dt = as.dt(du)
class(dt)
## [1] "difftime"
dt
## Time difference of 0.04166667 days

units in matrix objects

as.units(matrix(1:4,2,2), "m/s")
## Units: m/s
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
as.units(matrix(1:4,2,2), "m/s") * as.units(4, "m/s")
## Units: (m/s)*(m/s)
##      [,1] [,2]
## [1,]    4   12
## [2,]    8   16

but

as.units(matrix(1:4,2,2), "m/s") %*% as.units(4:3, "m/s")
##      [,1]
## [1,]   13
## [2,]   20

strips units.

units objects in data.frames

set.seed(131)
d = data.frame(x = runif(4), y = as.units(runif(4), "s"), z = as.units(1:4, "m/s"))
d
##           x         y z
## 1 0.2064370 0.8463468 1
## 2 0.1249422 0.5292048 2
## 3 0.2932732 0.5186254 3
## 4 0.3757797 0.2378545 4
summary(d)
##        x                y                z       
##  Min.   :0.1249   Min.   :0.2379   Min.   :1.00  
##  1st Qu.:0.1861   1st Qu.:0.4484   1st Qu.:1.75  
##  Median :0.2499   Median :0.5239   Median :2.50  
##  Mean   :0.2501   Mean   :0.5330   Mean   :2.50  
##  3rd Qu.:0.3139   3rd Qu.:0.6085   3rd Qu.:3.25  
##  Max.   :0.3758   Max.   :0.8463   Max.   :4.00
d$yz = with(d, y * z)
d
##           x         y z        yz
## 1 0.2064370 0.8463468 1 0.8463468
## 2 0.1249422 0.5292048 2 1.0584095
## 3 0.2932732 0.5186254 3 1.5558761
## 4 0.3757797 0.2378545 4 0.9514180
d[1, "yz"]
## 0.8463468 (s)*(m/s)