Here's some R code to illustrate one way to handle this situation.
First, let's generate some toy data.
x <- c(1, 1, 1, 3, 3, 3, 10, 10, 10, 30, 30, 30)
# toy parameters
M <- 1000
Bclean <- 20
Bdirty <- -20
SD <- 2
MATRIX_EFFECT <- 0.95
# assembling data into dataframe
clean <- M*x + Bclean + rnorm(mean = 0, sd = SD, n = length(x))
dirty <- (MATRIX_EFFECT)*M*x + Bdirty + rnorm(mean = 0, sd = SD, n = length(x))
tibble(x = x, clean = clean, dirty = dirty) %>%
pivot_longer(-x, names_to = 'matrix', values_to = 'response') %>%
x is the independent variable, presumably a concentration.
dirty are names for two separate response vectors, one for a "clean" sample matrix and one for a "dirty" sample matrix.
(This way of creating the toy data assumes that you make authentic standards at the same concentrations when you make the calibration curves; but this isn't required for the statistical modeling that follows; the model will work if the two calibration curves both have totally different concentrations.)
I've assumed a value for
MATRIX_EFFECT in this code, but to see how they would affect results I'd vary them and re-run the below code. The meaning of those parameters is:
M is the slope or response coefficient for the "clean" sample matrix.
Bclean is the intercept (or offset) for the "clean" sample matrix.
Bdirty is the intercept for the "dirty" matrix.
SD is the standard deviation (in response units) of both the clean and dirty responses. Here, I've assumed an unrealistically low value, which is helpful for being able to understand the meaning of the parameters returned by the fit.
MATRIX_EFFECT is a fraction by which
M is modified in the "dirty" matrix relative to the clean matrix.
The result of this code is a "dataframe" that looks like this:
x matrix response
<dbl> <chr> <dbl>
1 clean 352.590857
1 clean 34.753328
1 clean 365.959853
3 clean 554.485864
3 clean 382.928287
3 clean -7.990008
10 clean 814.286593
10 clean 941.055911
10 clean 998.846565
30 clean 3480.930678
30 clean 3152.718692
30 clean 2840.198150
1 dirty -123.531402
1 dirty 48.107685
1 dirty 46.156976
3 dirty 235.697833
3 dirty 368.444690
3 dirty 139.615775
10 dirty 1147.136660
10 dirty 812.492316
10 dirty 1015.146423
30 dirty 3255.479129
30 dirty 3206.667272
30 dirty 3340.837902
We can model this data as follows:
lm(data=., formula = response ~ x*matrix) %>%
This is models the response as a linear (
lm()) function of a bunch of parameters, with parameters implicitly defined by the formula.
response ~ x*matrix means, since
x is a continuous variable and
matrix in this case is a factor variable, to regress response on concentration interacting with matrix. The results are:
Each row gives results, including standard errors, point estimates, and p-values that the parameter is non-zero, for a single parameter. One subtlety is that the algorithm has implicitly assumed one (of in this case two) levels of our factor variable
matrix to be the "baseline", and only parameters for the other non-baseline level (in this case "dirty") are referred to directly. So the results mean:
(Intercept) is the model estimate of
Bclean. The estimate of 20.28 has an uncertainty of $\pm$ 0.740. Gratifyingly, the confidence interval overlaps the "true"
Bclean value we picked for this toy data above.
x is the model estimate of
matrixdirty is the model estimate of
Bclean - Bdirty
x:matrixdirty is the model estimate of
M*(1-MATRIX_EFFECT) and is the parameter that you are really interested in. Dividing by
M, we see the regression says it is -49.96. Dividing by M, in this case estimated as 1000.02839, gives an estimate for the matrix effect of 0.95004. Thus, for these (made up) numbers, the model would say your dirty matrix has a calibration slope that is 4.996 percent of the clean matrix. Of course, you also have error estimates here. The
std.error of 0.0658 is also in response units; after dividing by M, we can see the model thinks it's matrix effect estimate is good to the third decimal place.
These amazing results are because I've picked an
SD of 2 for the simulated data, but the response varies between 100 and 30,000. So we're assuming relative error of 2% down to 0.006%. If you pick more realistic values for SD, the
std.error of all of the parameters will be much larger.