1.3 Exemple : dosage des nitrates dans l’eau de mer

Les nitrates NO3- sont des nutriments essentiels pour la croissance des plantes et des algues. Cependant, un excès de nitrates dans l’eau peut entraîner une prolifération excessive d’algues, ce qui peut avoir des effets néfastes sur les écosystèmes aquatiques. Parfois, le nitrates sont présents en trop faibles quantités dans des eaux très oligothrophes, et les algues peinent à s’y développer. Il est donc important de surveiller les niveaux de nitrates dans l’eau de mer, tant en océanographie biologique qu’en aquariophilie.

L’autoanalyzer AA3 de Seal permet d’automatiser les mesures de nitrates dans l’eau de mer. Le principe du dosage consiste à d’abord réduire les nitrates en nitrites NO2- en passant l’échantillon sur des billes de cadmium-cuivre, puis à faire réagir ces derniers avec du N-(1-naphthyl)-éthylènediamine en présence de sulfanilamide. Cette réaction produit un colorant azoïque dont l’absorbance est mesurée à la longueur d’onde de 520 nm. À noter que cette technique mesure, en fait, la somme nitrates + nitrites, si des nitrites sont déjà présents dans les échantillons. Pour déterminer les nitrates seuls, il faut réaliser la même réaction sans l’étape de réduction sur colonne de cadmium et retirer la concentration en nitrites ainsi mesurée. Pour cette raison, nous parlerons désormais de NOx comme étant la somme des concentrations en nitrates et en nitrites dans nos échantillons.

1.3.1 Encodage ou importation des données

Comme toujours, la première étape de notre analyse consiste à introduire les données brutes dans R dans un format correct. Si nos données ne sont pas encore encodées dans un fichier, nous pouvons utiliser toutes les techniques vues au cours 1, notamment lors de l’exercice d’encodage des données biométriques. Les options sont :

  • Encodage direct dans R avec les fonctions data.frame()(R de base), dplyr::tribble() ou tibble::tibble() (tidyverse), ou encore avec dtx() ou dtx_rows() (SciViews::R).
  • Encodage dans un fichier Excel, Google Sheet, Libreoffice Calc… et importation, par exemple avec read() de SciViews::R ou readxl::read_excel() (tidyverse)
  • Encodage dand un fichier texte, en utilisant la virgule comme séparateur de champs et l’extension .csv, et importation avec read() de SciViews::R ou readr::read_csv() (tidyverse). Si vous encodez manuellement un fichier CSV, nous vous conseillons d’utiliser un éditeur spécialisé comme ModernCSV qui vous rendra la tâche plus facile et vous évitera de faire des erreurs de formattage des données.

Une autre situation qui peut se présenter à vous est un appareil qui enregistre les résultats directement dans un fichier texte. Dans ce cas, il est souvent nécessaire de nettoyer le fichier avant de pouvoir l’importer dans R. Il n’est pas rare d’avoir, en effet, des informations supplémentaires fournies par l’appareil dans l’entête du fichier (ses premières lignes). Voyez la documentation de votre appareil de mesures. L’autoanalyzer AA3 étant dans ce cas de figure, nous allons maintenant voir comment traiter ce type de données avec R. Ci-dessous, les premières lignes d’un fichier issus directement de l’AA3 :

readLines("data/sdd2_01/171124B.TXT", n = 20)
#  [1] "\"ANAL\";\"NPinorganique.ANL\";;;;;;;;;;;;;;;;;"                                                                                                                                       
#  [2] "\"RUN \";\"171124B.RUN\";;;;;;;;;;;;;;;;;"                                                                                                                                             
#  [3] "\"DATE\";\"24/11/2017\";;;;;;;;;;;;;;;;;"                                                                                                                                              
#  [4] "\"TIME\";\"14:41:59\";;;;;;;;;;;;;;;;;"                                                                                                                                                
#  [5] "\"OPER\";\"testcal\";;;;;;;;;;;;;;;;;"                                                                                                                                                 
#  [6] "\"COMM\";\"Testcal\";;;;;;;;;;;;;;;;;"                                                                                                                                                 
#  [7] "\"TYPE\";;;;;;;;;\"Real\";;;\"Real\";;;\"Real\";;;"                                                                                                                                    
#  [8] "\"CHAN\";;;;;;;;;1;;;2;;;3;;;"                                                                                                                                                         
#  [9] "\"METH\";;;;;;;;;\"PO4\";;;\"NO3\";;;\"NH4\";;;"                                                                                                                                       
# [10] "\"UNIT\";;;;;;;;;\"\xb5mol/L\";;;\"\xb5mol/L\";;;\"\xb5mol/L\";;;"                                                                                                                     
# [11] "\"Base\";;;;;;;;;-6620;;;-6339;;;-7963;;;"                                                                                                                                             
# [12] "\"Gain\";;;;;;;;;140;;;52;;;143;;;"                                                                                                                                                    
# [13] "\"Lamp\";;;;;;;;;257;;;300;;;292;;;"                                                                                                                                                   
# [14] "\"Sample ID\";\"Peak Number\";\"Cup Number\";\"Cup Type\";\"Cup Group\";;;\"Date Time Stamp\";;\"Results 1\";\"AD Values\";;\"Results 2\";\"AD Values\";;\"Results 3\";\"AD Values\";;"
# [15] "\"Primer\";1;1;\"PRIM\";0;0;0;\"24/11/2017 14:44:33\";;9.899;60097;;9.966;58043;;9.190;56006;;"                                                                                        
# [16] "\"cal10\";2;2;\"CALB\";0;0;0;\"24/11/2017 14:45:33\";10.000;9.964;60461;10.000;9.948;57946;10.000;9.216;56156;;"                                                                       
# [17] "\"cal5\";3;3;\"CALB\";0;0;0;\"24/11/2017 14:46:33\";5.000;5.051;32786;5.000;5.089;31734;5.000;5.907;36959;;"                                                                           
# [18] "\"cal2\";4;4;\"CALB\";0;0;0;\"24/11/2017 14:47:33\";2.000;2.020;15708;2.000;2.030;15236;2.000;2.445;16870;;"                                                                           
# [19] "\"cal1\";5;5;\"CALB\";0;0;0;\"24/11/2017 14:48:33\";1.000;1.023;10090;1.000;0.994;9648;1.000;1.188;9581;;"                                                                             
# [20] "\"cal0.5\";6;6;\"CALB\";0;0;0;\"24/11/2017 14:49:33\";0.500;0.600;7711;0.500;0.521;7094;0.500;0.778;7200;;"

Nous pouvons voir immédiatement plusieurs choses :

  • Le fichier est bien un fichier texte,
  • Le séparateur de champs est ici le point virgule ;,
  • L’entête occupe les 13 premières lignes et renseigne une série de métadonnées relatives à l’analyse et à l’appareil. Par exemple, ligne 9, on peut lire que les 3 méthodes d’analyse sont, dans l’ordre, , PO4 (les phosphates), NO3 (les nitrates + nitrites), et NH4 (ammonium + ammoniac). De même, ligne 10, on voit que les unités des concentrations sont “xb5mol/L”… Huummm ! Drôle d’unité. En fait ici, nous n’avons pas utilisé le bon encodage. Si nous relisons cette ligne 10 avec un encodage “latin1”, typique d’un Windows en français, nous avons alors ceci :
readLines("data/sdd2_01/171124B.TXT", n = 10, encoding = "latin1")[10]
# [1] "\"UNIT\";;;;;;;;;\"µmol/L\";;;\"µmol/L\";;;\"µmol/L\";;;"

Donc, les unités sont des µmol/L.

L’entête des colonnes est en ligne 14 et les données commencent en ligne 15. Comme nous avons un peu de remaniement des données à réaliser également, nous pouvons écrire une petite fonction en R pour automatiser le tout :

read_aa3 <- function(aa3_file) {
  # Lecture du fichier brut (encodage latin1, séparateur ';', décimale '.')
  data <- read.csv2(aa3_file, skip = 13, header = TRUE, dec = ".",
    fileEncoding = "latin1", stringsAsFactors = FALSE, na.strings = "")
  # Sélection et renommage des colonnes d'intérêt
  data <- data[, c("Sample.ID", "Peak.Number", "Cup.Number", "Cup.Type",
    "Cup.Group", "Date.Time.Stamp", "X.2", "AD.Values",
    "X.3", "AD.Values.1", "X.4", "AD.Values.2")]
  names(data) <- c("sample_id", "peak_number", "cup_number", "cup_type",
    "cup_group", "date_time", "PO4__umol_L", "abs.PO4",
    "NOx__umol_L", "abs.NOx", "NH4__umol_L", "abs.NH4")
  # Conversion de variables
  data$cup_type <- as.factor(data$cup_type)
  data$cup_group <- as.factor(data$cup_group)
  data$date_time <- as.POSIXct(data$date_time, format = "%d/%m/%Y %H:%M:%OS")
 
  data
}

Vous êtes maintenant habitué à ce type de reman,iement de données. Ici, nous n’avons utilisé que du R de base pour le réaliser, mais vous pouvez également utiliser la syntaxe SciViews::R à laquelle vous êtes peut-être plus habitué•e. Nous pouvons maintenant importer nos données proprement et les placer sur un graphique :

calib <- read_aa3("data/sdd2_01/171124B.TXT")
calib
#    sample_id peak_number cup_number cup_type cup_group           date_time
# 1     Primer           1          1     PRIM         0 2017-11-24 14:44:33
# 2      cal10           2          2     CALB         0 2017-11-24 14:45:33
# 3       cal5           3          3     CALB         0 2017-11-24 14:46:33
# 4       cal2           4          4     CALB         0 2017-11-24 14:47:33
# 5       cal1           5          5     CALB         0 2017-11-24 14:48:33
# 6     cal0.5           6          6     CALB         0 2017-11-24 14:49:33
# 7     cal0.3           7          7     CALB         0 2017-11-24 14:50:33
# 8     cal0.2           8          8     CALB         0 2017-11-24 14:51:33
# 9     cal0.1           9          9     CALB         0 2017-11-24 14:52:33
# 10      cal0          10         10     CALB         0 2017-11-24 14:53:33
# 11     Drift          11         11     DRIF         0 2017-11-24 14:54:33
# 12     cal10          12         12     SAMP         0 2017-11-24 14:55:33
# 13      cal5          13         13     SAMP         0 2017-11-24 14:56:33
# 14      cal2          14         14     SAMP         0 2017-11-24 14:57:33
# 15      cal1          15         15     SAMP         0 2017-11-24 14:58:33
# 16    cal0.5          16         16     SAMP         0 2017-11-24 14:59:33
# 17    cal0.3          17         17     SAMP         0 2017-11-24 15:00:33
# 18    cal0.2          18         18     SAMP         0 2017-11-24 15:01:33
# 19    cal0.1          19         19     SAMP         0 2017-11-24 15:02:33
# 20      cal0          20         20     SAMP         0 2017-11-24 15:03:33
# 21     Drift          21         21     DRIF         0 2017-11-24 15:04:33
# 22      cal0          22         22     SAMP         0 2017-11-24 15:05:33
# 23    cal0.1          23         23     SAMP         0 2017-11-24 15:06:33
# 24    cal0.2          24         24     SAMP         0 2017-11-24 15:07:33
# 25    cal0.3          25         25     SAMP         0 2017-11-24 15:08:33
# 26    cal0.5          26         26     SAMP         0 2017-11-24 15:09:33
# 27      cal1          27         27     SAMP         0 2017-11-24 15:10:33
# 28      cal2          28         28     SAMP         0 2017-11-24 15:11:33
# 29      cal5          29         29     SAMP         0 2017-11-24 15:12:33
# 30     cal10          30         30     SAMP         0 2017-11-24 15:13:33
# 31     Drift          31         31     DRIF         0 2017-11-24 15:14:33
# 32     cal10          32         32     SAMP         0 2017-11-24 15:15:33
# 33      cal0          33         33     SAMP         0 2017-11-24 15:16:33
# 34      cal5          34         34     SAMP         0 2017-11-24 15:17:33
# 35    cal0.1          35         35     SAMP         0 2017-11-24 15:18:33
# 36      cal2          36         36     SAMP         0 2017-11-24 15:19:33
# 37    cal0.2          37         37     SAMP         0 2017-11-24 15:20:33
# 38      cal1          38         38     SAMP         0 2017-11-24 15:21:33
# 39    cal0.3          39         39     SAMP         0 2017-11-24 15:22:33
# 40    cal0.5          40         40     SAMP         0 2017-11-24 15:23:33
# 41     Drift          41         41     DRIF         0 2017-11-24 15:24:33
# 42     cal10          42         42     SAMP         0 2017-11-24 15:25:33
# 43      cal0          43         43     SAMP         0 2017-11-24 15:26:33
# 44      cal5          44         44     SAMP         0 2017-11-24 15:27:33
# 45    cal0.1          45         45     SAMP         0 2017-11-24 15:28:33
# 46      cal2          46         46     SAMP         0 2017-11-24 15:29:33
# 47    cal0.2          47         47     SAMP         0 2017-11-24 15:30:33
# 48      cal1          48         48     SAMP         0 2017-11-24 15:31:33
# 49    cal0.3          49         49     SAMP         0 2017-11-24 15:32:33
# 50    cal0.5          50         50     SAMP         0 2017-11-24 15:33:33
# 51     Drift          51         51     DRIF         0 2017-11-24 15:34:33
# 52       End          52          0     END          0 2017-11-24 15:35:33
#    PO4__umol_L abs.PO4 NOx__umol_L abs.NOx NH4__umol_L abs.NH4
# 1           NA   60097          NA   58043          NA   56006
# 2         10.0   60461        10.0   57946        10.0   56156
# 3          5.0   32786         5.0   31734         5.0   36959
# 4          2.0   15708         2.0   15236         2.0   16870
# 5          1.0   10090         1.0    9648         1.0    9581
# 6          0.5    7711         0.5    7094         0.5    7200
# 7          0.3    5770         0.3    5940         0.3    4899
# 8          0.2    5212         0.2    5134         0.2    1846
# 9          0.1    4715         0.1    4901         0.1    1212
# 10         0.0    4112         0.0    3952         0.0     267
# 11          NA   59945          NA   58246          NA   56088
# 12          NA   60105          NA   58336          NA   56446
# 13          NA   32420          NA   31907          NA   37299
# 14          NA   15757          NA   16443          NA   14898
# 15          NA   10203          NA    9793          NA    7970
# 16          NA    7472          NA    8025          NA    6682
# 17          NA    6531          NA    5836          NA    1945
# 18          NA    5464          NA    5140          NA     996
# 19          NA    5089          NA    4562          NA     909
# 20          NA    4585          NA    4015          NA     314
# 21          NA   59945          NA   58246          NA   56088
# 22          NA    4852          NA    4232          NA     412
# 23          NA    6280          NA    4580          NA    1163
# 24          NA    6954          NA    5774          NA    4022
# 25          NA    6517          NA    5686          NA    2383
# 26          NA    7280          NA    6768          NA    5065
# 27          NA   10356          NA    9584          NA    7702
# 28          NA   16111          NA   15673          NA   13736
# 29          NA   32925          NA   31748          NA   36854
# 30          NA   59713          NA   58085          NA   56064
# 31          NA   59945          NA   58246          NA   56088
# 32          NA   59801          NA   58479          NA   56198
# 33          NA    4166          NA    4180          NA     133
# 34          NA   32051          NA   31927          NA   36522
# 35          NA    4850          NA    5078          NA     875
# 36          NA   15482          NA   16030          NA   13502
# 37          NA    5407          NA    5158          NA    1264
# 38          NA   10069          NA   10076          NA    8497
# 39          NA    6194          NA    6436          NA    1854
# 40          NA    7244          NA    6825          NA    4860
# 41          NA   59945          NA   58246          NA   56088
# 42          NA   59892          NA   58120          NA   55986
# 43          NA    4360          NA    4081          NA       0
# 44          NA   32389          NA   33524          NA   37252
# 45          NA    5044          NA    4709          NA       0
# 46          NA   15817          NA   15209          NA   11625
# 47          NA    5602          NA    5147          NA     346
# 48          NA   10102          NA    9732          NA    7090
# 49          NA    6352          NA    5733          NA    3023
# 50          NA    7787          NA    7411          NA    3970
# 51          NA   59945          NA   58246          NA   56088
# 52          NA    4311          NA    3646          NA       5

Nous voyons qu’il s’agit ici de solution standard de calibration nommée “calxx” où “xx” est la concentration connue en µmol/L… mais la concentration n’a été correctement encodée que pour la première série indiquée cub_type == "CALB". Nous allons y remédier en indiquant les concentrations correctes pour les autres lignes du tableau :

# Récupération des concentrations à partir de l'identifiant des échantillons
conc <- substring(calib$sample_id, 4) |> as.numeric()
# Warning: NAs introduced by coercion
conc
#  [1]   NA 10.0  5.0  2.0  1.0  0.5  0.3  0.2  0.1  0.0   NA 10.0  5.0  2.0  1.0
# [16]  0.5  0.3  0.2  0.1  0.0   NA  0.0  0.1  0.2  0.3  0.5  1.0  2.0  5.0 10.0
# [31]   NA 10.0  0.0  5.0  0.1  2.0  0.2  1.0  0.3  0.5   NA 10.0  0.0  5.0  0.1
# [46]  2.0  0.2  1.0  0.3  0.5   NA   NA

… et nous remplaçons les concentrations par ces valeurs dans le tableau. Ensuite, nous éliminons les lignes qui ne correspondent pas à une solution de calibration, dont la concentration est NA :

calib$PO4__umol_L <- conc
calib$NOx__umol_L <- conc
calib$NH4__umol_L <- conc
calib <- drop_na_(calib, "PO4__umol_L")
calib
#    sample_id peak_number cup_number cup_type cup_group           date_time
# 1      cal10           2          2     CALB         0 2017-11-24 14:45:33
# 2       cal5           3          3     CALB         0 2017-11-24 14:46:33
# 3       cal2           4          4     CALB         0 2017-11-24 14:47:33
# 4       cal1           5          5     CALB         0 2017-11-24 14:48:33
# 5     cal0.5           6          6     CALB         0 2017-11-24 14:49:33
# 6     cal0.3           7          7     CALB         0 2017-11-24 14:50:33
# 7     cal0.2           8          8     CALB         0 2017-11-24 14:51:33
# 8     cal0.1           9          9     CALB         0 2017-11-24 14:52:33
# 9       cal0          10         10     CALB         0 2017-11-24 14:53:33
# 10     cal10          12         12     SAMP         0 2017-11-24 14:55:33
# 11      cal5          13         13     SAMP         0 2017-11-24 14:56:33
# 12      cal2          14         14     SAMP         0 2017-11-24 14:57:33
# 13      cal1          15         15     SAMP         0 2017-11-24 14:58:33
# 14    cal0.5          16         16     SAMP         0 2017-11-24 14:59:33
# 15    cal0.3          17         17     SAMP         0 2017-11-24 15:00:33
# 16    cal0.2          18         18     SAMP         0 2017-11-24 15:01:33
# 17    cal0.1          19         19     SAMP         0 2017-11-24 15:02:33
# 18      cal0          20         20     SAMP         0 2017-11-24 15:03:33
# 19      cal0          22         22     SAMP         0 2017-11-24 15:05:33
# 20    cal0.1          23         23     SAMP         0 2017-11-24 15:06:33
# 21    cal0.2          24         24     SAMP         0 2017-11-24 15:07:33
# 22    cal0.3          25         25     SAMP         0 2017-11-24 15:08:33
# 23    cal0.5          26         26     SAMP         0 2017-11-24 15:09:33
# 24      cal1          27         27     SAMP         0 2017-11-24 15:10:33
# 25      cal2          28         28     SAMP         0 2017-11-24 15:11:33
# 26      cal5          29         29     SAMP         0 2017-11-24 15:12:33
# 27     cal10          30         30     SAMP         0 2017-11-24 15:13:33
# 28     cal10          32         32     SAMP         0 2017-11-24 15:15:33
# 29      cal0          33         33     SAMP         0 2017-11-24 15:16:33
# 30      cal5          34         34     SAMP         0 2017-11-24 15:17:33
# 31    cal0.1          35         35     SAMP         0 2017-11-24 15:18:33
# 32      cal2          36         36     SAMP         0 2017-11-24 15:19:33
# 33    cal0.2          37         37     SAMP         0 2017-11-24 15:20:33
# 34      cal1          38         38     SAMP         0 2017-11-24 15:21:33
# 35    cal0.3          39         39     SAMP         0 2017-11-24 15:22:33
# 36    cal0.5          40         40     SAMP         0 2017-11-24 15:23:33
# 37     cal10          42         42     SAMP         0 2017-11-24 15:25:33
# 38      cal0          43         43     SAMP         0 2017-11-24 15:26:33
# 39      cal5          44         44     SAMP         0 2017-11-24 15:27:33
# 40    cal0.1          45         45     SAMP         0 2017-11-24 15:28:33
# 41      cal2          46         46     SAMP         0 2017-11-24 15:29:33
# 42    cal0.2          47         47     SAMP         0 2017-11-24 15:30:33
# 43      cal1          48         48     SAMP         0 2017-11-24 15:31:33
# 44    cal0.3          49         49     SAMP         0 2017-11-24 15:32:33
# 45    cal0.5          50         50     SAMP         0 2017-11-24 15:33:33
#    PO4__umol_L abs.PO4 NOx__umol_L abs.NOx NH4__umol_L abs.NH4
# 1         10.0   60461        10.0   57946        10.0   56156
# 2          5.0   32786         5.0   31734         5.0   36959
# 3          2.0   15708         2.0   15236         2.0   16870
# 4          1.0   10090         1.0    9648         1.0    9581
# 5          0.5    7711         0.5    7094         0.5    7200
# 6          0.3    5770         0.3    5940         0.3    4899
# 7          0.2    5212         0.2    5134         0.2    1846
# 8          0.1    4715         0.1    4901         0.1    1212
# 9          0.0    4112         0.0    3952         0.0     267
# 10        10.0   60105        10.0   58336        10.0   56446
# 11         5.0   32420         5.0   31907         5.0   37299
# 12         2.0   15757         2.0   16443         2.0   14898
# 13         1.0   10203         1.0    9793         1.0    7970
# 14         0.5    7472         0.5    8025         0.5    6682
# 15         0.3    6531         0.3    5836         0.3    1945
# 16         0.2    5464         0.2    5140         0.2     996
# 17         0.1    5089         0.1    4562         0.1     909
# 18         0.0    4585         0.0    4015         0.0     314
# 19         0.0    4852         0.0    4232         0.0     412
# 20         0.1    6280         0.1    4580         0.1    1163
# 21         0.2    6954         0.2    5774         0.2    4022
# 22         0.3    6517         0.3    5686         0.3    2383
# 23         0.5    7280         0.5    6768         0.5    5065
# 24         1.0   10356         1.0    9584         1.0    7702
# 25         2.0   16111         2.0   15673         2.0   13736
# 26         5.0   32925         5.0   31748         5.0   36854
# 27        10.0   59713        10.0   58085        10.0   56064
# 28        10.0   59801        10.0   58479        10.0   56198
# 29         0.0    4166         0.0    4180         0.0     133
# 30         5.0   32051         5.0   31927         5.0   36522
# 31         0.1    4850         0.1    5078         0.1     875
# 32         2.0   15482         2.0   16030         2.0   13502
# 33         0.2    5407         0.2    5158         0.2    1264
# 34         1.0   10069         1.0   10076         1.0    8497
# 35         0.3    6194         0.3    6436         0.3    1854
# 36         0.5    7244         0.5    6825         0.5    4860
# 37        10.0   59892        10.0   58120        10.0   55986
# 38         0.0    4360         0.0    4081         0.0       0
# 39         5.0   32389         5.0   33524         5.0   37252
# 40         0.1    5044         0.1    4709         0.1       0
# 41         2.0   15817         2.0   15209         2.0   11625
# 42         0.2    5602         0.2    5147         0.2     346
# 43         1.0   10102         1.0    9732         1.0    7090
# 44         0.3    6352         0.3    5733         0.3    3023
# 45         0.5    7787         0.5    7411         0.5    3970

Parfait ! Maintenant, le graphique…

chart(data = calib, abs.NOx ~ NOx__umol_L) +
  geom_point() +
  labs(x = "Concentration en NOx [µmol/L]", y = "Absorbance")

Notez que nous plaçons la concentration en abscisse et l’absorbance en ordonnée. C’est la convention pour les droites de calibration. Nous verrons dans les deux chapitres suivants que la méthode statistique d’ajustement de la droite de calibration considère qu’il n’y a pas d’erreur sur l’axe X et que toute l’erreur est reportée su Y. Or nous avons moins d’erreur sur la concentration de nos solutions standard que sur l’absorbance. Donc, nous plaçons la concentration en X pour se rapprocher de cette contrainte.

1.3.2 Calcul de la droite de calibration avec R

Nous voyons que les points s’alignent bien le long d’une droite dans notre jeu de données exemple, ce qui est une bonne nouvelle. Nous pouvons rajouter la droite qui se place au mieux dans le nuage de points comme ceci sur notre graphique :

SciViews::R
chart(data = calib, abs.NOx ~ NOx__umol_L) +
  geom_point() +
  # Droite de calibration
  geom_smooth(method = "lm", color = "blue") +
  labs(x = "Concentration en NOx [µmol/L]", y = "Absorbance")
# `geom_smooth()` using formula = 'y ~ x'

Dans R, cette droite se calcule avec la fonction lm() comme ceci :

calib_lm <- lm(data = calib, abs.NOx ~ NOx__umol_L)
calib_lm
# 
# Call:
# lm(formula = abs.NOx ~ NOx__umol_L, data = calib)
# 
# Coefficients:
# (Intercept)  NOx__umol_L  
#        4397         5422

Dans le petit tableau “Coefficients”, “(Intercept)” est l’ordonnée à l’origine et “NOx__umol_L” est la pente de la droite par rapport à cette variable. Notre droite de calibration a donc la forme mathématique suivante :

\[Absorbance = 5422 \times NOx + 4397\]

Dans les deux chapitres suivants, nous étudierons la théorie relative à la droitegalement quelques descripteurs et tests statistiques autour de cette droite, afin de vérifier que le modèle utilisé est correct, ou de calculer l’incertitiude sur la position de la droite. En cas de problème, ces outils statistiques nous aideront également à les diagnostiquer plus en détails.

1.3.3 Droite de calibration avec {chemCal}

Le package additionnel {chemCal} fournit des fonctions qui vont nous faciliter la vie et aussi nous permettre d’avoir plus d’information sur nos dosages. Par exemple, en partant de note objet calib_lm, nous pouvons réaliser un graphique plus complet à l’aide de la fonction calplot() :

chemCal::calplot(calib_lm,
  xlab = "Concentration en NOx [µmol/L]", ylab = "Absorbance")

Ici, nous avons une présentation un peu différente du graphique (c’est un graphique R de base), mais surtout, nous avons des traits verts et rouges qui sont ajoutés autour de la droite de calibration en noir. Ces traits matérialisent des informations statistiques complémentaires et importantes. Les traits verts correspondent à l’enveloppe de confiance à 95% qui indiquent l’incertitude statistique sur le calcul de la droite (l’équivalent de l’intervalle de confiance sur une moyenne que nous avons étudié en SDD II). Ici, ces traits sont très, très proches de la droite de calibration, ce qui nous indique qu’elle est calculée très précisément.

Les traits pointillés rouges indiquent l’incertitude sur la prédiction de la concentration d’un échatillon inconnu, toujours avec une probabilité de 95% (un argument alpha=, qui vaut 0.05 par défaut permet éventuellement de changer la probabilité de ces bandes supplémentaires, si on le désire). Ces intervalles de prédiction sont à rapprocher du lod() que nous aborderons un peu plus loin.

1.3.4 Détermination de la concentration d’échantillons inconnus

Avec le jeu de données traité jusqu’ici, nous n’avions que des solutions standard de concentration connue. Naturellement, l’intérêt d’établir une droite de calibration avec de telles solutions standards, est de pouvoir ensuite passer dans le même appareil des solutions de concentration inconnue, et de les calculer. Nous allons refaire le traitement avec un autre jeu de données issu du même autoanalyze AA3, et qui contient des solutions de concentration inconnue. Nous travaillerons directement avec {chemCal} et réutilisons naturellement notre fonction read_aa3() bien pratique ici :

ana <- read_aa3("data/sdd2_01/171124AR1.TXT")
# Nous ne conservons ici que les 20 premières lignes du tableau
ana <- ana[1:20, ]
ana
#            sample_id peak_number cup_number cup_type cup_group
# 1             Primer           1          1     PRIM         0
# 2              cal10           2          2     CALB         0
# 3               cal5           3          3     CALB         0
# 4               cal2           4          4     CALB         0
# 5               cal1           5          5     CALB         0
# 6             cal0.5           6          6     CALB         0
# 7             cal0.3           7          7     CALB         0
# 8             cal0.2           8          8     CALB         0
# 9             cal0.1           9          9     CALB         0
# 10              cal0          10         10     CALB         0
# 11             Drift          11         11     DRIF         0
# 12 20171122 09:01 1F          12         12     SAMP         0
# 13 20171122 09:01 2F          13         13     SAMP         0
# 14 20171122 09:01 3F          14         14     SAMP         0
# 15 20171122 09:01 4F          15         15     SAMP         0
# 16 20171122 09:01 5F          16         16     SAMP         0
# 17 20171122 09:01 6F          17         17     SAMP         0
# 18 20171122 09:59 1O          18         18     SAMP         0
# 19 20171122 09:59 2O          19         19     SAMP         0
# 20 20171122 09:59 3O          20         20     SAMP         0
#              date_time PO4__umol_L abs.PO4 NOx__umol_L abs.NOx NH4__umol_L
# 1  2017-11-24 12:10:30          NA   59968          NA   60756          NA
# 2  2017-11-24 12:11:30        10.0   59529        10.0   60936        10.0
# 3  2017-11-24 12:12:30         5.0   32560         5.0   32840         5.0
# 4  2017-11-24 12:13:30         2.0   16006         2.0   15809         2.0
# 5  2017-11-24 12:14:30         1.0   10813         1.0    9694         1.0
# 6  2017-11-24 12:15:30         0.5    7083         0.5    6931         0.5
# 7  2017-11-24 12:16:30         0.3    5880         0.3    5726         0.3
# 8  2017-11-24 12:17:30         0.2    5384         0.2    6609         0.2
# 9  2017-11-24 12:18:30         0.1    4714         0.1    4482         0.1
# 10 2017-11-24 12:19:30         0.0    4649         0.0    3850         0.0
# 11 2017-11-24 12:20:30          NA   60113          NA   60729          NA
# 12 2017-11-24 12:21:30          NA   18413          NA    4734          NA
# 13 2017-11-24 12:22:30          NA   19226          NA    6983          NA
# 14 2017-11-24 12:23:30          NA   18957          NA    4526          NA
# 15 2017-11-24 12:24:30          NA   18878          NA    3844          NA
# 16 2017-11-24 12:25:30          NA   19037          NA    3290          NA
# 17 2017-11-24 12:26:30          NA   18244          NA    3601          NA
# 18 2017-11-24 12:27:30          NA   19080          NA    7805          NA
# 19 2017-11-24 12:28:30          NA   19280          NA    8137          NA
# 20 2017-11-24 12:29:30          NA   19309          NA    7726          NA
#    abs.NH4
# 1    62202
# 2    62456
# 3    35828
# 4    12978
# 5     6853
# 6     3505
# 7     3713
# 8     2602
# 9     1682
# 10    2826
# 11   61629
# 12     644
# 13    2476
# 14     550
# 15     405
# 16     394
# 17     825
# 18     229
# 19     698
# 20     813

Les lignes 2 à 10 se réfèrent à nos solutions standard en les lignes 12 à 20 correspondent à des échantillons de concentration inconnue. Les lignes 1 et 11 correspondent au signal de base obtenu à l’aide d’eau ultrapure, et ne nous intéressent pas. Nous pouvons donc diviser notre tableau en calib2 pour la calibration et samples avec nos échantillons inconnus.

calib2 <- ana[2:10, ]
samples <- ana[12:20, ]

Voici le calcul et la représentation de notre droite de calibration dans ce second cas pour les NOx :

calib2_lm <- lm(data = calib2, abs.NOx ~ NOx__umol_L)
calib2_lm
# 
# Call:
# lm(formula = abs.NOx ~ NOx__umol_L, data = calib2)
# 
# Coefficients:
# (Intercept)  NOx__umol_L  
#        4272         5677
chemCal::calplot(calib2_lm,
  xlab = "Concentration en NOx [µmol/L]", ylab = "Absorbance")
# Warning in object$coef: partial match of 'coef' to 'coefficients'
# Warning in seq.default(from = xlim[[1]], to = xlim[[2]], length = 250): partial
# argument match of 'length' to 'length.out'

Notez que les enveloppes de confiance et de prédiction sont un tout petit peu plus large. En effet, nous n’avons pas de réplicas pour les solutions standards (elle ne sont passées qu’une seule fois). Cependant, le résultat reste très précis et les points s’alignent très bien linéairement, à part un en troisième position, très légèrement décalé vers le haut. La pente et l’ordonnée à l’origine sont relativement proches de ceux obtenus avec notre premier jeu de données. La réponse de l’appareil est assez stable d’un lot à l’autre.

Nous pouvons maintenant utiliser cette droite pour calculer la concentration des échantillons inconnus à partir de leur absorbance, ainsi que le vecteur des absorbances mesurées pour un seul échantillon. Mais ici, nous n’avons qu’une seule valeur mesurée pour différents échantillons. Nous pouvons appliquer la fonction sur le premeir échantillon comme ceci :

chemCal::inverse.predict(calib2_lm, newdata = samples$abs.NOx[1])
# $Prediction
# [1] 0.081453
# 
# $`Standard Error`
# [1] 0.1009759
# 
# $Confidence
# [1] 0.2387701
# 
# $`Confidence Limits`
# [1] -0.1573171  0.3202231

La concentration prédite pour ce premier échantillon est de 0,081 µmol/L. L’’ereur standard sur cette prédiction est de 0,100 µmol/L, et l’intervalle de confiance vers le haut et vers le bas est de 0,239 µmol/L, ce qui donne un intervalle de prédiction allant de -0,157 à 0,320 µmol/L. Naturellement, des concentrations négatives n’ont aucun sens. Cela veut seulement dire que, comme zéro est compris dans l’intervalle de confiance à 95%, nous ne pouvons pas certifier que des nitrates sont effectivement présentes dans cet échantillon. Et si ils le sont, leur concentration est très, très faible.

Le package {chemCal} fournit également une fonction lod() qui calcule la limite de détection (LOD) et loq(), la limite de quantification (LOQ) pour notre dosage. La LOD est la plus petite concentration que nous pouvons détecter avec certitude, tandis que la LOQ est la plus petite concentration que nous pouvons quantifier avec précision. Ces deux valeurs sont calculées à partir de l’erreur standard de la droite de calibration et du niveau de confiance choisi (par défaut 95%). Voici comment les calculer :

chemCal::lod(calib2_lm)
# $NOx__umol_L
# [1] 0.3818868
# 
# $abs.NOx
# [1] 6439.595
chemCal::loq(calib2_lm)
# $NOx__umol_L
# [1] 0.7088053
# 
# $abs.NOx
# [1] 8295.547

Ainsi avec notre seconde droite de calibration, et si nous n’utilisons qu’une seule mesure par échantillons, nous pourrons détecter des concentrations siginificatives en nitrates + nitrites à partir de 0,38 µmol/L. La quantification ne devient effective qu’à partir de concentrations supérieures ou égales à 0,71 µmol/L. Pour pouvoir détecter ou quantifier des concentrations plus faibles, il faut augmenter le nombres de solutions standards pour obtenir une droite de calibration plus précise et/ou augmenter le nombre de réplicas pour les échantillons inconnus… jusqu’à une certaine limite inhérante à la méthode de dosage. S’il faut être encore plus précis, alors il faut rechercher un autre technique de dosage. Voiyez les pages d’aides ?chemCal::lod et ?chemCal::loq pour plus de détails et pour déterminer comment calculer ces valeurs lorsque les échatillons inconnus sont répliqués.

Revenons à nos échantillons inconnus. Nous pouvons calculer leurs concentrations en une seule fois et inclure les résultats dans le tableau samples comme ceci :

concs <- purrr::map(samples$abs.NOx, chemCal::inverse.predict, object = calib2_lm) |>
  sapply(unlist) |>
  t()
concs
#        Prediction Standard Error Confidence Confidence Limits1
#  [1,]  0.08145300     0.10097593  0.2387701         -0.1573171
#  [2,]  0.47760543     0.10026469  0.2370883          0.2405171
#  [3,]  0.04481463     0.10104915  0.2389433         -0.1941286
#  [4,] -0.07531697     0.10129796  0.2395316         -0.3148486
#  [5,] -0.17290187     0.10150989  0.2400328         -0.4129346
#  [6,] -0.11812045     0.10138984  0.2397489         -0.3578693
#  [7,]  0.62239747     0.10004186  0.2365614          0.3858361
#  [8,]  0.68087795     0.09995755  0.2363620          0.4445159
#  [9,]  0.60848193     0.10006241  0.2366100          0.3718719
#       Confidence Limits2
#  [1,]         0.32022314
#  [2,]         0.71469376
#  [3,]         0.28375790
#  [4,]         0.16421465
#  [5,]         0.06713088
#  [6,]         0.12162843
#  [7,]         0.85895888
#  [8,]         0.91724000
#  [9,]         0.84509193

Ce calcul est un peu compliqué. Il utilise la fonction purrr::map() qui va appliquer de manière récursive la fonction chemCal::inverse.predict() à chaque valeur de samples$abs.NOx. Le résultat est une liste de listes, soit un objet pas très maniable. Nous appelons ensuite la fonction sapply() qui applique à chaque élément de la liste de premier niveau unlist() qui transforme les listes imbriquées en vecteurs. Ensuite, le tout est simplifié en une matrice, mais les lignes et les colonnes sont inversées. Donc, t() transpose ce tableau pour que chaque ligne corresponde à un échantillon. Nous pouvons rajouter cela au tableau samples dont on ne garde que les colonnes pertinentes comme ceci :

cbind(samples [ , c("sample_id", "abs.NOx")], concs)
#            sample_id abs.NOx  Prediction Standard Error Confidence
# 12 20171122 09:01 1F    4734  0.08145300     0.10097593  0.2387701
# 13 20171122 09:01 2F    6983  0.47760543     0.10026469  0.2370883
# 14 20171122 09:01 3F    4526  0.04481463     0.10104915  0.2389433
# 15 20171122 09:01 4F    3844 -0.07531697     0.10129796  0.2395316
# 16 20171122 09:01 5F    3290 -0.17290187     0.10150989  0.2400328
# 17 20171122 09:01 6F    3601 -0.11812045     0.10138984  0.2397489
# 18 20171122 09:59 1O    7805  0.62239747     0.10004186  0.2365614
# 19 20171122 09:59 2O    8137  0.68087795     0.09995755  0.2363620
# 20 20171122 09:59 3O    7726  0.60848193     0.10006241  0.2366100
#    Confidence Limits1 Confidence Limits2
# 12         -0.1573171         0.32022314
# 13          0.2405171         0.71469376
# 14         -0.1941286         0.28375790
# 15         -0.3148486         0.16421465
# 16         -0.4129346         0.06713088
# 17         -0.3578693         0.12162843
# 18          0.3858361         0.85895888
# 19          0.4445159         0.91724000
# 20          0.3718719         0.84509193

Nous voyons que certaines prédictions de concentrations sont négatives. Ce n’est pas gênant. Cela se produit lorsque la concentration est très très faible et très inférieure à la limite de détection. Dès lors, l’erreur de mesure sur l’absorbance, et l’incertitude sur la droite de calibration puet avoir pour conséquence que certaines valeurs prédites sont légèrement négatives. Naturellement, dans le rapport final, nous reprterons ces valeurs comme “<xxx”, ou “xxx” est la limite de détection.

1.3.5 Calculs directement dans Excel

Pour des droites de calibration et des calculs de concentrations inconnues simples, sans statistiques supplémentaires telles que les intervalles de confiance, le LOD et le LOQ, il est possible de travailler directement dans un fichier Excel. Cela peut rendre des services, même si c’est moins complet, et nettement moins pratique pour des grands séries d’analyse qu’avec R, où les scripts sont automatisables facilement.

Nous ne présentons pas dans le cours comment faire, mais vous le réaliserez vous-même dans le projet individuel.

À vous de jouer !

Réalisez le tutoriel ci-dessous pour affinier votre compréhension de la droite de calibration et son utilisation pour calculer des concentrations inconnues.

Effectuez maintenant les exercices du tutoriel B01La_calib (Tutoriel droite de calibration).

BioDataScience2::run("B01La_calib")
À vous de jouer !

Réalisez le travail B01Ib_calib.

Travail individuel pour les étudiants inscrits au cours de Science des Données Biologiques II à l’UMONS (Q1 : modélisation) à terminer avant le 2026-10-05 23:59:59.

Votre projet dans GitHub

Voyez les explications dans le fichier README.md.