7.2 Données textuelles

7.2.1 Encodage

Nous avons tous déjà été confrontés à ce genre de caractères é ou . Il s’agit d’un problème d’encodage. Vous avez peut-être déjà entendu les termes suivants, ASCII, UTF-8, LATIN1,…

Le problème de la représentation de texte sous forme numérique se réduit à convertir une chaîne de caractères en bits qui seront interprétés par l’ordinateur. Il faut donc attribuer à chaque caractère un code unique. Cette série de codes va permettre la traduction des caractères. Les fameux termes cités précédemment sont des encodages qui permettent cette traduction.

Historiquement, le code américain normalisé pour l’échange d’informations (ASCII, “American Standard Code for Information Interchange”) a été l’un des premiers systèmes d’encodage standardisé massivement adopté. Cet encodage se fait sur 8 bits par caractère, permettant ainsi 256 caractères maximum. Cette proposition était simple et adaptée uniquement à l’anglais. Il ne prenait par exemple pas en compte les accents de la langue française. Il s’en suit le développement de plusieurs systèmes d’encodages avec des adaptations spécifiques aux très nombreuses langues mondiales comme ISO 8859 pour les langues latines (ISO 8859-1, latin1) ou encore ISO-2020 pour les langues asiatiques.

Afin de réduire les problèmes d’encodage, un jeu universel de caractères a ensuite été développé : ISO 10646. Le consortium Unicode propose une surcouche à cette norme appelée unicode. Vous vous en doutez : plus il va y avoir des caractères plus le système d’encodage sera lourd et va utiliser un nombre de bits important pour chaque caractère. Le 8-bit limité à 256 caractères est ici largement insuffisant.

Ainsi, l’UTF-16 encode chaque caractère sur 16-bit, l’UTF-32 sur 32-bit parce qu’il faut encore plus de caractères différents et enfin… l’UTF-8 est apparu et a rapidement mis tout le monde d’accord (du moins dans le monde Unix/Linux et MacOS puisque Windows reste un peu à la traîne, accroché à l’UTF-16, mais est en cours de transition aussi). Vous allez dire, mais là, on revient en arrière si on encode sur 8-bit ! Pas tout à fait. En fait, avec UTF-8, l’encodage est variable. La plupart des caractères courants (a-z, A-Z, 0-9, ponctuations usuelles, etc.) équivalents à l’ASCII sont encodés sur 8-bit. Mais un caractère particulier est préservé pour indiquer que, pour un caractère particulier, nous passons à 16-bit, ou à 32-bit, ou plus, il n’y a théoriquement pas de limites. Donc, UTF-8 est compatible avec l’ASCII, compact pour un texte écrit avec des lettres latines, mais parfaitement extensibles et théoriquement capables d’encoder une infinité de caractères différents. Le souci, par contre, c’est qu’il n’est plus possible de déterminer la taille d’un texte en comptant simplement le nombre d’octets (= 8 bits) qui le compose. Toutes les fonctions de manipulation de textes doivent être adaptées !

Nous vous conseillons donc de toujours employer l(encodage UTF-8 qui est le format le plus universel qui soit. Si votre document ne contient que du texte, des chiffres et des ponctuations usuelles, vous pouvez à la limite vous limiter à ASCII qui est l’encodage ayant la plus large compatibilité. Dans la SciViews Box, l’encodage de textes par défaut est fixé à UTF-8, y compris sous Windows pour un maximum de comptabilités et d’interopérabilité.

7.2.2 Manipulation de texte

On utilise des chaînes de caractères dans de nombreux contextes :

  • noms de fichiers
  • analyse de textes
  • analyse de dates
  • coordonnées
  • ADN/ARN
  • facteur-niveaux

Afin de manipuler des chaînes de caractères, de nombreuses fonctions sont disponibles dans R. Nous utiliserons principalement les fonctions du package {stringr}.

library(stringr)

Partons de la chaîne de caractères suivante :

string <- "Je suis étudiant en science des données biologiques"

Quelle est la longueur de cette chaîne de caractère ?

length(string)
# [1] 1
str_length(string) # ou bien la version en R de base nchar(string)
# [1] 51

Vous attendiez-vous à ces deux résultats ? La fonction length() renvoie la longueur de l’objet sous forme de vecteur, c’est-à-dire, le nombre d’éléments différents qu’il contient. Ici, il n’y en a donc qu’un seul. La fonction str_length() (ou bien nchar()) renvoie le nombre de caractères au sein d’une ou plusieurs chaînes de caractères. La phrase stockée dans string est formée de 51 caractères.

Reposons-nous la même question cette fois-ci avec un vecteur contenant plusieurs chaînes de caractères.

strings <- c(
  "Je suis étudiant en science des données biologiques",
  "Je suis le cours de sdd4",
  "J'aime appliquer les concepts de la science des données lors d'expériences en biologie"
  )

length(strings)
# [1] 3
str_length(strings) # ou bien la version en R de base nchar(strings)
# [1] 51 24 86

Cet exemple montre une fois de plus l’importance de lire l’aide d’une fonction et de tenter de faire des tests très simples pour s’assurer de bien la comprendre ainsi que ses arguments.

Plaçons-nous maintenant dans un contexte pratique. Vous êtes engagé dans un laboratoire pour développer une nouvelle méthode de dosage des nitrates. Imaginons que vous avez attribué un identifiant à six expériences que vous avez réalisées en routine lors de dosages des nitrates. Nous pouvons observer que le nom de fichier suit la logique suivante.

nom de l’expérience + numéro de l’expérience + méthode employée + date

id <- c(
  "Nitrate.1100.Version1.2019-10-21", "Nitrate.1101.Version2.2019-10-21",
  "Nitrate.1109.Version1.2019-10-24","Nitrate.1110.Version2.2019-10-24",
  "Nitrate.1114.Version1.2019-10-26","Nitrate.1115.Version2.2019-10-26")
id
# [1] "Nitrate.1100.Version1.2019-10-21" "Nitrate.1101.Version2.2019-10-21"
# [3] "Nitrate.1109.Version1.2019-10-24" "Nitrate.1110.Version2.2019-10-24"
# [5] "Nitrate.1114.Version1.2019-10-26" "Nitrate.1115.Version2.2019-10-26"

Votre chef vous demande de lui présenter les documents en lien avec les dosages des nitrates version 2, une méthode que vous avez développée. Il est possible de le faire avec une simple instruction.

str_subset(id, "Version2")
# [1] "Nitrate.1101.Version2.2019-10-21" "Nitrate.1110.Version2.2019-10-24"
# [3] "Nitrate.1115.Version2.2019-10-26"

À la suite de votre réunion, votre chef vous demande de modifier le nom de vos dossiers, car il ne respecte pas les conventions de notation en vigueur dans le laboratoire. Votre supérieur vous demande donc de remplacer toutes les majuscules par des minuscules. Avec la bonne fonction dans R, cela est réalisable en une seule instruction simple.

str_to_lower(id)
# [1] "nitrate.1100.version1.2019-10-21" "nitrate.1101.version2.2019-10-21"
# [3] "nitrate.1109.version1.2019-10-24" "nitrate.1110.version2.2019-10-24"
# [5] "nitrate.1114.version1.2019-10-26" "nitrate.1115.version2.2019-10-26"

Il vous demande également de remplacer tous les . par des _.

str_replace_all(id, ".", "_")
# [1] "________________________________" "________________________________"
# [3] "________________________________" "________________________________"
# [5] "________________________________" "________________________________"

Vous attendiez-vous à ce résultat ? Probablement pas. Avez-vous lu l’aide de la fonction ? L’argument pattern= (second argument) utilise une expression régulière et pas une simple chaîne de caractères. Les expressions régulières sont des outils puissants lorsqu’ils sont judicieusement employés. Nous les étudierons plus loin. Afin de remplacer les . par des _, il aurait fallu indiquer :

id %>.%
  str_to_lower(.) %>.%
  str_replace_all(., "\\.", "_") %->%
  id_new
id_new
# [1] "nitrate_1100_version1_2019-10-21" "nitrate_1101_version2_2019-10-21"
# [3] "nitrate_1109_version1_2019-10-24" "nitrate_1110_version2_2019-10-24"
# [5] "nitrate_1114_version1_2019-10-26" "nitrate_1115_version2_2019-10-26"

En effet le . a une signification particulière dans les expressions régulières que nous allons voir dans la section suivante (il signifie : “n’importe quel caractère”).

Pour en savoir plus

7.2.3 Expression régulière

Les expressions régulières servent à manipuler du texte. Elles permettent de rechercher et/ou remplacer des parties de texte dans les chaînes de caractères. Il s’agit pratiquement d’un petit langage en tant que tel pour spécifier ces recherches et remplacements. La syntaxe de base des expressions régulières est très riche, mais en voici quelques cas fréquents pour vous faire une première idée. Pour trouver une chaîne de caractères…

  • qui débute par nitrate : “^nitrate”
  • qui termine par nitrate : “nitrate$”
  • qui contient soit nitrate, soit nitrite : “(nitrate|nitrite)”, ou “nitr[ai]te” (ce qui est entre crochet représente les caractères permis à cet emplacement… donc, “a” ou “i”)
  • qui se répète plusieurs fois : + ? de 0 à 1 fois + + 1 ou plusieurs + * 0 ou plusieurs + {2,6} répétitions comprises entre 2 et 6 fois
  • ., n’importe quels caractères
  • .* n’importe quels caractères après de longueur quelconque
  • [0-9] un chiffre de 0 à 9, on peut également utiliser \d
  • [a-z] une lettre minuscule – [A-Z] une lettre majuscule
  • [a-zA-Z0-9_] une lettre minuscule ou majuscule ou un chiffre, ou le trait souligné _
  • [^0-9] tout caractère sauf un chiffre (le ^ après le crochet ouvrant indique n’importe quoi sauf ce qui est précisé à l’intérieur des crochets)

Si je veux rechercher un point je dois écrire \\., car sinon, j’indique que je veux n’importe quel caractère à cet emplacement-là.

Exercez-vous
Il n’est possible de véritablement comprendre les expressions régulières qu’en réalisant des exercices. Nous vous proposons donc de réaliser les exercices proposés sur le site RegexOne. Les exercices interactifs offrent un niveau de difficulté croissant et balayent les notions principales à connaitre pour manipuler les expressions régulières.

Dans R, le package {regexplain} propose des addins pour vous aider à utiliser les expressions régulières dans R. Ces expressions régulières sont souvent utilisables dans toutes les fonctions {stringr} que nous avons vues précédemment.

Pour en savoir plus

7.2.4 Variables facteurs

Partons d’une expérience sur la croissance de coraux scléractiniaires. Nous avons un vecteur ci-dessous qui correspond au nom abrégé des espèces étudiées. Le nombre d’espèces est fini. Chaque espèce correspond donc à un groupe d’individus d’une même espèce.

species <- c("s.hystrix", "p.damicornis", "a.millepora", "p.damicornis",
  "a.millepora", "s.hystrix", "s.hystrix", "p.damicornis", "a.millepora",
  "p.damicornis", "a.millepora", "s.hystrix", "a.millepora")
species
#  [1] "s.hystrix"    "p.damicornis" "a.millepora"  "p.damicornis" "a.millepora" 
#  [6] "s.hystrix"    "s.hystrix"    "p.damicornis" "a.millepora"  "p.damicornis"
# [11] "a.millepora"  "s.hystrix"    "a.millepora"
class(species)
# [1] "character"

Afin de gérer ce type de variable, nous pouvons aussi utiliser les objets factor dans R. La gestion correcte des variables facteurs dans R est particulière et peut mener à des erreurs. Nous vous conseillons la lecture de cet article sur le sujet Wrangling categorical data in R.

En R de base, la fonction qui permet de transformer une variable en une variable facteur est factor(). On retrouve des variantes à ces fonctions comme as.factor(), as.ordered() (ou encore as_factor() du package {forcats}). Nous vous conseillons d’utiliser factor() car cette fonction permet une gestion simple des variables facteurs. Il est important de comprendre la logique de gestion des facteurs dans R. Afin de gagner de la mémoire, R va attribuer à chaque niveau d’une variable facteur un nombre entier. Les niveaux vont être attribués par ordre alphabétique par défaut (mais vous pouvez proposer un autre ordre avec l’argument levels=).

species_f <- factor(species)
species_f
#  [1] s.hystrix    p.damicornis a.millepora  p.damicornis a.millepora 
#  [6] s.hystrix    s.hystrix    p.damicornis a.millepora  p.damicornis
# [11] a.millepora  s.hystrix    a.millepora 
# Levels: a.millepora p.damicornis s.hystrix
# Niveau de la variable species_f
levels(species_f)
# [1] "a.millepora"  "p.damicornis" "s.hystrix"
# Entiers associé à ces niveaux
as.integer(species_f)
#  [1] 3 2 1 2 1 3 3 2 1 2 1 3 1

Pour une variable d’une taille conséquente, on s’aperçoit qu’une variable facteur utilise moins d’espace en mémoire qu’une variable caractère.

species_length <- sample(species, size = 10000, replace = TRUE)

object.size(species_length)
# 80240 bytes
object.size(as.factor(species_length))
# 40656 bytes

Dans notre exemple, l’espace nécessaire est moitié moins important. Rappelez-vous que la première version de R date de 1993. La préoccupation de la mémoire était très importante à l’époque. De nombreuses fonctions plus anciennes vont utiliser de préférence des variables facteurs. Par exemple, la fonction read.csv() va par défaut tenter de transformer une variable caractère en une variable facteur alors que la fonction read_csv() du package {readr} va laisser la variable en caractère. Nous vous conseillons d’importer vos données en caractère et de les transformer en variable facteur si cela est nécessaire.

Afin de permettre de travailler plus facilement avec une variable facteur, de nombreuses fonctions sont à votre disposition comme recode() du package {dplyr} ou encore des fonctions disponibles dans le package {forcats}.

species_f2 <- recode(species_f,
  "a.millepora"  = "Acropora millepora",
  "p.damicornis" = "Pocillopora damicornis",
  "s.hystrix"    = "Seriatopora hystrix")
species_f2
#  [1] Seriatopora hystrix    Pocillopora damicornis Acropora millepora    
#  [4] Pocillopora damicornis Acropora millepora     Seriatopora hystrix   
#  [7] Seriatopora hystrix    Pocillopora damicornis Acropora millepora    
# [10] Pocillopora damicornis Acropora millepora     Seriatopora hystrix   
# [13] Acropora millepora    
# Levels: Acropora millepora Pocillopora damicornis Seriatopora hystrix
Les variables facteurs sont utiles, mais il faut toujours prendre le temps de bien comprendre chaque niveau de la variable. Soyons encore plus vigilant si les données sont amenées à évoluer au cours du temps.
Pour en savoir plus