Chapter 4 Perturbation vector theory

4.1 Objective


The objective of this chapter is to develop a mathematical workflow useful to adjust the ionome of potato crops for diagnostic purpose. Perturbation in compositional space plays the same role as translation plays in real space. Some natural processes in nature can be interpreted as a change from one composition C1 to another C2 through the application of a perturbation:

p ⊕ C1 ===> C2.

The difference between a new observation and the closest healthy composition (closest TN) can be back-transformed to the compositional space. The obtained vector is the perturbation vector. Theoretically, a misbalanced composition could be balanced (translated into a healthy zone) using a perturbation vector. Also, ionome of a new cultivar could be assigned to the closest healthy sample’s cultivar, where nutrient requirements have been already documented by fertilizer trials.

I compute a dissimilarity (or imbalance) index using the true negative clr as references for cultivars. Then, I describe the procedure used to rebalance a misbalanced composition.


4.2 Data set and useful libraries

We need package compositions for further clr back-transformation to compositional space. The package reshape will be used to melt an intermediate data frame. As explained at the end of the Chapter 3, I consider as True Negatives (TN) specimens (or healthy points) for this study, observations of the training data set having a high yield (HY) and correctly predicted by the random forest model.

library("tidyverse")
library("extrafont")
library('compositions')
library("reshape")
train_df = read_csv("output/train_df.csv")
train_df$Cultivar = factor(train_df$Cultivar)
TNs = train_df %>% 
  filter(yieldClass == 'HY' & pred_yield == 'HY')
clrNo = c("clrN", "clrP", "clrK", "clrCa", "clrMg", "clrFv") # for simplicity

4.3 Dissimilarity index between compositions

The first step is to compute a dissimilarity index according to the distance from the closest healthy point (the closest TN). I use the Aitchison distance (multivariate Euclidean distance) between clr composition as the dissimilarity index. The chunk below shows the custom function used to compute euclidean distance.

eucl_dist_f <- function(x, y) {
    sqrt(sum((x-y)^2))
}

For each unbalanced composition, I use the next loop to compute all the euclidean distances between all the compositions in “TNs” of the corresponding group. The computation is possible even if the ionomics groups is unknown. The loop returns the smallest Euclidean distance as the unbalanced index of that composition. The computed debalance (or imbalance) index is stored by a new vector debal.

debal <- c()
debal_index <- c()
for (i in 1:nrow(train_df)) {
    clr_i <- as.numeric(train_df[i, clrNo])
    eucl_dist <- apply(TNs %>% filter(Cultivar == train_df$Cultivar[i]) %>% select(clrNo), # 
                       1, function(x) eucl_dist_f(x=x, y=clr_i))
    debal_index[i] <- which.min(eucl_dist)
    debal[i] <- eucl_dist[debal_index[i]]
}
train_df$debal <- debal
train_df %>% glimpse()
## Observations: 2,537
## Variables: 16
## $ NoEssai      <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1...
## $ NoBloc       <dbl> 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3...
## $ NoTraitement <dbl> 1, 2, 4, 6, 7, 8, 2, 3, 4, 6, 7, 1, 2, 4, 6, 7, 8...
## $ clrN         <dbl> 0.3321186, 0.4252302, 0.3993264, 0.4885647, 0.720...
## $ clrP         <dbl> -2.518325, -2.394957, -2.599180, -2.525335, -2.30...
## $ clrK         <dbl> 0.8379383, 1.1387291, 0.7945826, 0.9418880, 0.720...
## $ clrMg        <dbl> -0.4794688, -0.5154924, -0.6220171, -0.6426036, -...
## $ clrCa        <dbl> -1.639076, -2.346167, -1.577529, -1.763195, -1.70...
## $ clrFv        <dbl> 3.466813, 3.692658, 3.604817, 3.500681, 3.432323,...
## $ RendVendable <dbl> 18.94420, 40.35180, 37.55050, 41.01670, 46.79370,...
## $ rv_cut       <dbl> 41.33053, 41.33053, 41.33053, 41.33053, 41.33053,...
## $ yieldClass   <chr> "LY", "LY", "LY", "LY", "HY", "HY", "LY", "HY", "...
## $ Cultivar     <fct> Goldrush, Goldrush, Goldrush, Goldrush, Goldrush,...
## $ Maturity5    <chr> "mid-season", "mid-season", "mid-season", "mid-se...
## $ pred_yield   <chr> "LY", "LY", "LY", "LY", "HY", "HY", "LY", "HY", "...
## $ debal        <dbl> 0.2915435, 0.3247296, 0.2628167, 0.2987338, 0.000...

simplement pour apprécier la corrélation entre indice de debalancement et rendement

Add regression line, correlation coefficient and equantions of the fitted line. Key functions: > stat_smooth() [ggplot2] > stat_cor() [ggpubr] > stat_poly_eq()[ggpmisc]

library("ggpubr")
library("ggpmisc")
# Polynomial regression. Show equation and adjusted R2
data <- train_df %>% filter(debal != 0); x = data$debal; y = data$RendVendable
formula <- y ~ poly(x, 2)#, raw = TRUE)

p <- ggplot(data = data, aes(x = debal, y = RendVendable)) +
  geom_point() +
  geom_smooth(formula = formula) +
  stat_poly_eq(
    aes(label =  paste(..eq.label.., ..adj.rr.label.., sep = "~~~~")),
    formula = formula, parse = TRUE, label.x = 0.50
    )
p
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'

Fin appréciation.

4.4 Rebalancing a misbalanced sample by perturbation

Let’s suppose that we got this point selected at random in unbalanced specimens.

set.seed(932559) 
unbalanced <- subset(train_df, debal !=0)
unbalanced <- unbalanced[sample(nrow(unbalanced), 1), ]
t(unbalanced)
##              [,1]        
## NoEssai      "355"       
## NoBloc       "1"         
## NoTraitement "2"         
## clrN         "0.8015935" 
## clrP         "-2.00352"  
## clrK         "0.1670366" 
## clrMg        "-0.8453153"
## clrCa        "-1.563568" 
## clrFv        "3.443773"  
## RendVendable "33.14916"  
## rv_cut       "33.63704"  
## yieldClass   "LY"        
## Cultivar     "Mystere"   
## Maturity5    "late"      
## pred_yield   "LY"        
## debal        "0.2125451"

Or even, we could rather use the most unbalanced occurrence, why not !

unbalanced <- subset(train_df, debal !=0)
unbalanced <- unbalanced[which.max(unbalanced$debal), ]
misbalanced <- unbalanced # copy
t(misbalanced)
##              [,1]              
## NoEssai      "200"             
## NoBloc       "1"               
## NoTraitement "2"               
## clrN         "0.957698"        
## clrP         "-1.83199"        
## clrK         "-0.04637332"     
## clrMg        "0.4678508"       
## clrCa        "-2.247862"       
## clrFv        "2.700677"        
## RendVendable "30.37194"        
## rv_cut       "32.6"            
## yieldClass   "LY"              
## Cultivar     "Superior"        
## Maturity5    "early mid-season"
## pred_yield   "LY"              
## debal        "1.256154"

How could we rebalance it? The first step is to find in TNs of the corresponding cultivar the closest balanced point. Let’s re-compute its Euclidean distances from TNs and identify the sample from which the distance is minimum.

misbalanced <- misbalanced[clrNo]
eucl_dist_misbal <- apply(TNs[, clrNo], 1, function(x) eucl_dist_f(x = x, y = misbalanced))
index_misbal <- which.min(t(data.frame(eucl_dist_misbal))) 
index_misbal # return the index of the sample
## [1] 610

The closest healthy sample is the one which index is 610. Using this index we could refind the imbalance index value (debal) of the misbalanced sample for check.

(misbal = eucl_dist_misbal[index_misbal])
## [1] 1.256154

The Euclidean distance matches with the corresponding debal value: unbalanced$debal[1] = 1.2561535.

The closest point in the TNs subset is this one:

closest <- TNs[index_misbal, ]
t(closest)
##              [,1]              
## NoEssai      "196"             
## NoBloc       "3"               
## NoTraitement "6"               
## clrN         "0.6219769"       
## clrP         "-2.544439"       
## clrK         "0.3293346"       
## clrMg        "0.02902074"      
## clrCa        "-1.635584"       
## clrFv        "3.199691"        
## RendVendable "41.2204"         
## rv_cut       "32.6"            
## yieldClass   "HY"              
## Cultivar     "Superior"        
## Maturity5    "early mid-season"
## pred_yield   "HY"

Note that Cultivar of the misbalanced and the closest healthy composition are the same. I compute the clr difference between the closest and the misbalanced points.

closest = closest[clrNo]
clr_diff = closest - misbalanced
t(clr_diff)
##             [,1]
## clrN  -0.3357211
## clrP  -0.7124496
## clrK   0.3757079
## clrCa  0.6122787
## clrMg -0.4388300
## clrFv  0.4990141

The perturbation vector is that clr difference back-transformed to leaf compositional space.

comp_names <- c("N", "P", "K", "Ca", "Mg", "Fv")
perturbation_vector <- clrInv(clr_diff)
names(perturbation_vector) <- comp_names
t(perturbation_vector)
##    [,1]      
## N  0.10515492
## P  0.07214704
## K  0.21419006
## Ca 0.27135681
## Mg 0.09485276
## Fv 0.24229840
## attr(,"class")
## [1] acomp

Next, we should compute the compositions of the clr coordinates of the misbalanced point, as well as the closest TN point. The vectors could be gathered in a table made up of perturbation vector, misbalanced composition and the closest reference sample (pmc).

misbal_comp <- clrInv(misbalanced)
names(misbal_comp) <- comp_names

closest_comp <- clrInv(closest)
names(closest_comp) <- comp_names

pmc = rbind(perturbation_vector, misbal_comp, closest_comp)
rownames(pmc) = c("perturbation_vector","misbal_comp","closest_comp")
pmc
##                              N           P         K        Ca         Mg
## perturbation_vector 0.10515492 0.072147042 0.2141901 0.2713568 0.09485276
## misbal_comp         0.12828046 0.007881602 0.0470000 0.0052000 0.07860000
## closest_comp        0.06405025 0.002700000 0.0478000 0.0067000 0.03540000
##                            Fv
## perturbation_vector 0.2422984
## misbal_comp         0.7330379
## closest_comp        0.8433497

We could even check that the simplex is closed to 1 for each vector.

sum(perturbation_vector); sum(misbal_comp); sum(closest_comp)
## [1] 1
## [1] 1
## [1] 1

The closest composition minus the misbalanced composition should return the perturbation vector.

print(closest_comp - misbal_comp) # soustraction
##      N         P          K         Ca        Mg         Fv       
## [1,] 0.1051549 0.07214704 0.2141901 0.2713568 0.09485276 0.2422984
## attr(,"class")
## [1] acomp
print(perturbation_vector)        # for comparison
##      N         P          K         Ca        Mg         Fv       
## [1,] 0.1051549 0.07214704 0.2141901 0.2713568 0.09485276 0.2422984
## attr(,"class")
## [1] acomp

Or even, perturb the misbalanced point by the perturbation vector, you should obtain the closest TN point:

print(misbal_comp + perturbation_vector) # perturbation
##      N          P      K      Ca     Mg     Fv       
## [1,] 0.06405025 0.0027 0.0478 0.0067 0.0354 0.8433497
## attr(,"class")
## [1] acomp
print(closest_comp)                      # for comparison
##      N          P      K      Ca     Mg     Fv       
## [1,] 0.06405025 0.0027 0.0478 0.0067 0.0354 0.8433497
## attr(,"class")
## [1] acomp

So, the assumption is true. The next codes show the concept using plots. I aranged a data frame for ggplot.

df = data.frame(rbind(misbalanced, closest, clr_diff))
vectors = c("observation", "reference", "perturbation")
df$vectors = factor(vectors)
dfreshape = melt(df) # reshapes df for ggplot
## Using vectors as id variables
dfreshape
##         vectors variable       value
## 1   observation     clrN  0.95769802
## 2     reference     clrN  0.62197688
## 3  perturbation     clrN -0.33572114
## 4   observation     clrP -1.83198972
## 5     reference     clrP -2.54443931
## 6  perturbation     clrP -0.71244958
## 7   observation     clrK -0.04637332
## 8     reference     clrK  0.32933456
## 9  perturbation     clrK  0.37570788
## 10  observation    clrCa -2.24786229
## 11    reference    clrCa -1.63558355
## 12 perturbation    clrCa  0.61227874
## 13  observation    clrMg  0.46785078
## 14    reference    clrMg  0.02902074
## 15 perturbation    clrMg -0.43883004
## 16  observation    clrFv  2.70067654
## 17    reference    clrFv  3.19969068
## 18 perturbation    clrFv  0.49901414

The next two chunks plot with dots and histograms for each vector. Visualization is better with histograms.

ggplot(data = dfreshape, aes(x = value, y = vectors, colour = vectors)) +
    geom_point() +
    facet_wrap(~ variable, scales = "free_x") +
    labs(x='clr coordinate', y ='') +
    theme(text=element_text(family="Arial", face="bold", size=12))
Perturbation vector computation example dotplot using the most imbalanced foliar sample.

Figure 4.1: Perturbation vector computation example dotplot using the most imbalanced foliar sample.

ggsave("images/perturb_dotplot.tiff",  width = 6, height = 3)
ggplot(data=dfreshape, aes(x=variable, y=value, fill=vectors)) +
    geom_bar(stat="identity", position=position_dodge()) +
    coord_flip() +
    theme_bw() +
    theme(legend.title=element_blank()) +
    theme(text=element_text(family="Arial", face="bold", size=12))
Perturbation vector computation example barplot using the most imbalanced foliar sample.

Figure 4.2: Perturbation vector computation example barplot using the most imbalanced foliar sample.

ggsave("images/perturb_barplot.tiff",  width = 6, height = 3)