Calculating half-life of carbon over time in R

Viewed 291

First off, let me say that I'm not certain whether this belongs more in StackOverfloww or CrossValidated. In the end, I figured it was more a question of coding/execution than underlying statistical concepts, so I elected to put it here. If it belongs elsewhere, let me know.

I am attempting to calculate the half-life of the carbon in several types of organic materials. I incubated these materials for several weeks and measured the CO2 emitted over time. I then converted the cumulative mL CO2 emitted to mg C, which allowed me to calculate the C remaining in each of the samples at each sampling interval. As you'll note, there is an initial drop (whose degree differs between the materials) as the easily mineralized C is consumed, after which C loss is limited.

enter image description here

I then attempted to calculate the half-life for each sample using the SSAsymp function with 0 as the asymptote. I've included the code and some sample data below:

dat<-structure(list(Item = c("litter", "woodlt10", "litter", "woodlt10", 
"chargt10", "woodlt10", "litter", "chargt10", "chargt10"), `0` = c(161.4599767, 
178.78608, 154.3154933, 179.5406033, 177.9216, 185.262, 150.8786667, 
195.4312667, 227.50085), `1` = c(161.0021445, 178.3139851, 153.6009328, 
179.2539234, 177.8203349, 185.262, 150.7417449, 195.358527, 227.3496655
), `2.5` = c(158.8259128, 177.5134301, 152.5134086, 178.6545425, 
177.7889754, 184.3638163, 149.216371, 195.358527, 227.3496655
), `4.5` = c(156.5077532, 175.921231, 151.4148628, 177.7793692, 
177.4767007, 183.2183622, 147.201998, 195.0909267, 227.0262222
), `6.5` = c(154.7131141, 174.9474735, 150.4432374, 177.1403608, 
177.2406706, 182.4578207, 146.234637, 194.8740861, 226.7688705
), `9.5` = c(153.2392748, 174.0175268, 149.3042064, 176.5575212, 
176.8846807, 181.7943539, 145.5862023, 194.6301544, 226.4292793
), `13` = c(152.0007445, 173.2103072, 148.4350239, 176.0309575, 
176.5002673, 181.1742383, 145.0268347, 194.4425645, 226.3546808
), `16.5` = c(150.9846197, 172.6132263, 147.6816509, 175.5924338, 
176.3494115, 180.7311843, 144.555467, 194.3811803, 226.2060901
), `21` = c(150.2721712, 172.192254, 147.2036125, 175.3900685, 
176.341071, 180.4498045, 144.2670636, 194.355281, 226.1714114
), `25.5` = c(149.6342556, 171.7482415, 146.6502626, 175.1314172, 
176.2993861, 180.0476477, 143.9400763, 194.2879702, 226.1714114
), `30.5` = c(149.0119875, 171.2716008, 146.1358666, 174.8327655, 
176.1848876, 179.7659473, 143.5427987, 194.2297192, 226.0823399
), `36.5` = c(148.5402568, 170.8086499, 145.6660173, 174.5592093, 
176.0286056, 179.5362906, 143.2190717, 194.1430492, 225.949889
), `43` = c(148.0427195, 170.2820678, 145.1835833, 174.1679759, 
175.8830912, 179.218831, 142.8504933, 194.0126381, 225.76894), 
`49.5` = c(147.7386827, 170.0050513, 144.8519388, 173.8786241, 
175.7664341, 178.9888063, 142.5957979, 193.9764544, 225.6975125
), `56.5` = c(147.4501476, 169.7254062, 144.5900736, 173.6467626, 
175.6701446, 178.805284, 142.3922732, 193.9764544, 225.6401166
), `64.5` = c(147.0743873, 169.3696494, 144.2525808, 173.2666537, 
175.5422531, 178.5399775, 142.1173998, 193.8920486, 225.5513622
), `73` = c(146.7558811, 169.0445058, 143.940297, 172.871404, 
175.4054422, 178.2874291, 141.7951601, 193.7639492, 225.3946395
), `81` = c(146.6028383, 168.9047583, 143.8443744, 172.6848769, 
175.4054422, 178.1929838, 141.664276, 193.7639492, 225.3946395
), `88.5` = c(146.3920556, 168.7163201, 143.7024872, 172.488525, 
175.3520018, 178.0604067, 141.4846825, 193.7430944, 225.3551643
), `99.5` = c(146.1854778, 168.5426061, 143.5068639, 172.3002049, 
175.2961331, 177.9321711, 141.290387, 193.7237412, 225.2926565
)), row.names = c(1L, 2L, 4L, 6L, 7L, 11L, 12L, 16L, 38L), class = "data.frame")

dat.a<-data.frame(t(dat))
dat.a$Days<-rownames(dat.a)
colnames(dat.a)<-dat.a[1,]
colnames(dat.a)<-paste("X",seq(1:ncol(dat.a)),sep="")
dat.a<-dat.a[-1,]
library(dplyr)
dat.a<-mutate_all(dat.a, function(x) as.numeric(as.character(x)))
storage <- list()
for(i in names(dat.a)){
tryCatch({
    storage[[i]] <- log(2)/exp(coefficients(nls(dat.a[,i] ~ SSasymp(X10, 0.0001, R0, lrc),                 
data=dat.a))[3])
}, error=function(e){cat("ERROR :",conditionMessage(e), "\n")})} 
library(dplyr)
halflives<-melt(storage)
samplelist<-data.frame(matrix(NA, nrow = 10, ncol = 1))
samplelist$L1<-colnames(dat.a)
halflives<-merge(samplelist,halflives,by="L1",all=TRUE)
library(readr)
halflives$ord<-parse_number(halflives$L1)
halflives <- halflives[order(halflives$ord),]
colnames(halflives)<-c("L1","junk","halflife","ord")
halflives<-halflives[-10,]
halflives$material<-dat$Item

aggregate(x = halflives$halflife,                 
          by = list(halflives$material),              
          FUN = mean)

However, I keep getting an error:

ERROR : singular gradient matrix at initial parameter estimates

which I presume is because I'm setting the asymptote to zero? Or because the response value doesn't change enough over time? Can anyone identify a solution, either in terms of my existing code or another way of calculating half-life?

Many thanks for sifting through my crude code and taking the time to help me out!

1 Answers

The half lives in days can be calculated as the negative of the reciprocal of the slope of log2(y) vs. days.

dat2 <- t(dat[-1])
days <- as.numeric(rownames(dat2))
colnames(dat2) <- paste0("X", 1:ncol(dat2))
fm <- lm(log2(dat2) ~ days)
-1/coef(fm)[2, ] # half lives
##        X1        X2        X3        X4        X5        X6        X7        X8        X9 
##  816.2918 1306.5804 1063.3762 1854.8503 4882.6257 1914.4168 1275.1235 8489.2436 7799.1691 

For a constant half life to exist the graph of log2(y) vs. days should be a straight line; however, as seen in the plot below this does not seem to be the case over the range of the data shown in the question as it seems that there is a different half life in the early days and in the later days. You may wish to divide the days into two or more segments and calculate the half life separately for each segment. We will divide the range of days into two segments. We will start out by using 10 days as the dividing line but we will use the results of that to run an nls to fit two lines -- one before the cutoff and one after not assuming any specific cutoff period. The equations of the two lines are log2(y) = b1 + m1 * days and log2(y) = b2 + m2 * days. Because the area above the lines is convex we can take the maximum of these two as the value to use at any point. The cutoff point is obtained by solving the equation b1 + m1 * T0 = b2 + m2 * T0 for T0.

# starting values
fm1 <- lm(log2(dat2) ~ days, subset = days < 10)
co1 <- coef(fm1)
fm2 <- lm(log2(dat2) ~ days, subset = days >= 10)
co2 <- coef(fm2)

# calculate list of nls objects
fmList <- lapply(1:ncol(dat2), function(i) {
  st <- list(b1 = co1[1,i], m1 = co1[2,i], b2 = co2[1,i], m2 = co2[2,i])
  nls(log2(dat2)[, i] ~ pmax(b1 + m1 * days, b2 + m2 * days), start = st)
})

# plot
fits <- sapply(fmList, fitted)
matplot(days, log2(dat2), col = 1, cex = 0.5)
matplot(days, fits, type = "l", col = 1, lty = 1, add = TRUE)

# calculate statistics (half lives, etc.)
stats <- sapply(fmList, function(fm) {
  co <- as.list(coef(fm))
  with(co, c(co, half1 = -1/m1, half2 = -1/m2, T0 = (b1 - b2) / (m2 - m1)))
})
stats

giving:

      [,1]          [,2]          [,3]          [,4]          [,5]         
b1    7.334025      7.48148       7.267299      7.487643      7.475502     
m1    -0.008487284  -0.004336574  -0.005028027  -0.002671511  -0.0009155759
b2    7.242063      7.435454      7.210472      7.461353      7.46409      
m2    -0.0005902792 -0.0004453948 -0.0005338854 -0.0003596622 -0.0001180285
half1 117.8233      230.5968      198.8852      374.3201      1092.209     
half2 1694.113      2245.199      1873.061      2780.387      8472.529     
T0    11.6452       11.8284       12.64488      11.37197      14.30921     
      [,6]          [,7]          [,8]          [,9]         
b1    7.534028      7.236452      7.610853      7.829888     
m1    -0.003162845  -0.006031012  -0.0006520529 -0.0007230756
b2    7.500386      7.180011      7.60358       7.82288      
m2    -0.0002895582 -0.0004227974 -6.616548e-05 -8.131695e-05
half1 316.1711      165.8096      1533.618      1382.981     
half2 3453.538      2365.199      15113.62      12297.56     
T0    11.70858      10.06396      12.41357      10.92091  

Check

Just to double check suppose we have

x <- c(0, 3, 6)
y <- c(8, 4, 2)

which clearly has a half life of 3. Then

-1/coef(lm(log2(y) ~ x))[[2]]
## [1] 3

screenshot

Added

Regarding the comment, assuming by double exponential that you mean the model fitted by pracma::mexpfit

library(pracma)

# fmList has one element per dat2 column with fit parameters, etc.
fmList <- lapply(1:ncol(dat2), function(i) {
  mexpfit(days, dat2[, i], -seq(2))
})

# plots
fits <- sapply(fmList, with,
  a0 + a[1] * exp(b[1] * days) + a[2] * exp(b[2] * days)
)
matplot(days, dat2, col = 1, cex = 0.5)
matplot(days, fits, type = "l", col = 1, lty = 1, add = TRUE)
Related