Nous avons vu il y a longtemps dans l’article Le typage des variables : les types de base que R possède des types de données de base (et j’avais déjà abordé alors rapidement NA et NULL). Mais quand on manipule des données réelles, elles sont parfois/souvent parcellaires. C’est là qu’il est important de bon connaitre la différence entre ces trois acteurs incontournables.
La brute : NULL
NULL est particulier car il est à la fois un type/classe du langage et un objet instancié de ce type. Pour la facilité de lecture, je vais écrire [NULL] pour le type et NULL pour l’objet en lecture
> typeof(NULL) # L'objet NULL est de type [NULL]
[1] "NULL"
> class(NULL) # idem pour sa classe
[1] "NULL"
RNULL, tout d’abord, est un mot réservé. On ne peut pas en modifier le contenu (c’est donc une constante du langage) :
> NULL
NULL
> NULL <- 1
Error in NULL <- 1 : invalid (do_set) left-hand side to assignment
Rmais on peut l’affecter à une variable :
> vide <- NULL
> vide
NULL
> typeof(vide)
[1] "NULL"
> class(vide)
[1] "NULL"
ROn peut faire le parallèle par exemple avec TRUE, objet constant de type [logical] (booléen, vrai/faux) réservé et de valeur positive, et son instanciation par défaut T :
> TRUE # comme NULL, TRUE est une instanciation mais d'un type [logical] (booléen)
[1] TRUE
> T # T est aussi une instanciation d'un [logical] qui par défaut vaut TRUE
[1] TRUE
> TRUE <- 1 # on ne peut pas modifier la valeur de TRUE, comme NULL
Error in TRUE <- 1 : invalid (do_set) left-hand side to assignment
> T <- FALSE # par contre T n'est pas réservé, donc on peut en modifier la valeur
> T # comme vous vous en doutez, cela peut être TRES dangereux !
[1] FALSE
RUne autre des particularités de NULL est que c’est un vecteur de longueur 0 (zéro). Tant est si bien que la concaténation c() de valeurs de type [NULL] renvoie… NULL, vecteur de longueur toujours 0 :
> c(NULL, NULL, NULL)
NULL
> c(1, 2, NULL, 3)
[1] 1 2 3
# NULL en tant que vecteur de longueur 0 est ignoré par la concaténation
# au sein d'un vecteur
# Par contre dans une liste, l'entrée est crée mais
# est, elle-même, de type NULL et de longueur 0 :
> list(1, "2", NULL, 3)
[[1]]
[1] 1
[[2]]
[1] "2"
[[3]]
NULL
[[4]]
[1] 3
> lapply(list(1,2,NULL,3), \(x){length(x)})
[[1]]
[1] 1
[[2]]
[1] 1
[[3]]
[1] 0
[[4]]
[1] 1
R[NULL] possède comme tous les autres types des fonctions pour tester la nature d’un objet ou pour transformer depuis ou vers un autre type :
is.null()
Retourne TRUE (vrai) si la valeur passée est de type [NULL] et FALSE (faux) dans tous les autres cas.
> is.null(NULL)
[1] TRUE
> is.null(NA)
[1] FALSE
> is.null(0)
[1] FALSE
> is.null("")
[1] FALSE
Ras.null()
Comme toutes les fonctions as()
, celle-ci transforme le paramètre passé en un objet du type pour [NULL], vu qu’il n’y a qu’une représentation de cette classe, c’est particulièrement simple car cette fonction renvoie toujours l’objet NULL.
> as.null("n'importe quoi")
NULL
> as.null(NULL)
NULL
> as.null(100)
NULL
> as.null(Inf)
NULL
RAvouez que c’est la fonction la plus simple du monde !
Pour résumer, NULL représente l’absence (dans le sens non-existence) d’une donnée.
Enfin NULL ne peut rien contenir (donc pas NA) ainsi is.na()
renverra un vecteur [logical] de longueur 0.
le bon : NA
NA n’est pas un type, en fait c’est une valeur standard en R qui possède une représentation et un sens pour tous les types simples (hors [NULL]). C’est une différence majeure par rapport aux autres langages :
- NA n’est pas NULL, n’est pas vide, n’est pas 0.
- Le comportement de NA dépend du type de l’objet ainsi initialisé.
Que veut dire NA ?
« NA » signifie « Not Available », littéralement « indisponible » ou « donnée manquante ».
Cela signifie non pas que la donnée n’existe pas mais qu’elle n’est pas dans le jeu de données.
Pour prendre un exemple de la spécificité de cette notion, imaginons que nous traitons un RSS. L’un des champ est le poids de naissance qui n’est pertinent que pour les nouveaux nés. Nous n’allons pas recueillir la donnée pour Mme Michu 98 ans (qui ne s’en souvient probablement pas…). Dans un tel cas, nous devons donc marquer que cette valeur est absente sans saisir une valeur bidon qui pourrait perturber les traitements et allons donc mettre NA.
Peu de langages ont cette notion, et encore moins la différencient de NULL.
Qu’est ce que NA ?
Pour qu’on puisse l’utiliser, il faut bien que NA existe. En fait, c’est à nouveau une constante du langage définie comme un type [logical] dont la valeur est NA.
R> typeof(NA) "logical"
Au passage , cela nous permet de voir qu’un type [logical] au final n’est pas binaire vu qu’il peut prendre 3 valeurs (TRUE
, FALSE
… et NA
).
Cependant, il y a comme une incohérence. Nous avions dit qu’un vecteur est d’un type unique et il est tout de même possible d’écrire :
> a <- c(1, 2, 3, NA)
> a
[1] 1 2 3 NA
> typeof(a)
[1] "double"
> class(a)
[1] "numeric"
RLa réalité est que la sortie graphique (textuelle en fait) de la 4ème donnée est bien le texte « NA » mais en fait la valeur est NA_real_
.
> NA_real_
[1] NA
> typeof(NA_real_)
[1] "double"
# La réalité est que :
> a <- c(1, 2, 3, NA)
# est réellement une conversion implicite de type :
> a <- c(1, 2, 3, as.double(NA))
# NA[logical] étant transformé en NA_real_ de type [double]
# lors de la constitution du vecteur.
RAinsi, il existe plusieurs constantes NA
: NA_integer_
, NA_real_
, NA_complex_
, NA_character_
. Le seul type de données de base n’ayant pas de NA est [raw]. Tous ces types sont interconvertibles, c’est ce qui fait que NA
[logical] sera converti à la demande en [integer], [double], [complex], [character] en fonction des besoins.
Pour les listes, il n’existe pas de valeur NA
, mais le vecteur contiendra list(NA)
:
> typeof(list(1,"2",NA) [3])
[1] "list"
# Le 3ème élément est un conteneur [list]
> typeof(list(1,"2",NA) [[3]])
[1] "logical"
# Le contenu de ce 3ème élément de la liste est NA de type [logical]
# Si vous voulez vraiment forcer le type, utilisez
# les constantes typées, par exemple :
> typeof(list(1,"2",NA_real_) [[3]])
[1] "double"
Ris.na()
est une fonction qui vérifie quelles valeurs de l’objet passé en paramètre sont coercible à NA
[logical] (ou un des types primitifs). Le sens dépend donc du type de l’objet passé.
Par exemple, si l’objet est un vecteur de type primitif, is.na()
retourne un vecteur de même longueur où chaque valeur individuelle vaudra TRUE
si la valeur d’origine est NA
(ou la saveur correspondante) et FALSE
dans le cas contraire.
Si l’objet est un data.frame ou une matrice, la valeur en retour est une matrice.
NULL ne peut pas être NA et vu que NULL est de longueur 0, la valeur de retour est un vecteur [logical] de longueur 0.
Si vous définissez votre propre classe d’objet, vous pourrez écrire une fonction is.na() pour gérer spécifiquement cette classe.
is.na()<-
Rarement utilisée, cette fonction attribue NA
comme valeur aux index (passés comme membre de droite) de l’objet en premier paramètre de la fonction :
> is.na(a)<- c(1, 3)
> a
[1] NA 2 NA NA
# C'est l'équivalent de
> a[c(1, 3)] <- NA
Rx[is.na(x)]<-
Cette construction n’a rien de particulier en fait. Il s’agit de l’indexation de x afin de lui attribuer une ou des valeurs. Cependant, c’est un gimmick lorsqu’on a un jeu incomplet que l’on veut compléter par des valeurs par défaut. Je vous la mets donc pour ne pas que vous la cherchiez trop longtemps :
> a <- c(1, NA, 3, NA, NA, 6)
> a
[1] 1 NA 3 NA NA 6
> a[is.na(a)] <- c(2, 4, 5)
> a
[1] 1 2 3 4 5 6
# La donnée est recyclée si le vecteur passé est trop court
# ce qui est bien pratique.
> a <- c(1, NA, 3, NA, NA, 6)
> a[is.na(a)] <- -1
> a
[1] 1 -1 3 -1 -1 6
# Mais risqué d'où l'apparition d'un warning en cas de
# vecteur de taille inadaptée :
> a<-c(1, NA, 3, NA, NA, 6)
> a[is.na(a)] <- c(-1,-2)
Warning message:
In a[is.na(a)] <- c(-1, -2) :
number of items to replace is not a multiple of replacement length
> a
[1] 1 -1 3 -2 -1 6
RLe truand : « » (la chaine vide ou des caractères [espace] non significatifs)
Le chaine vide est le truand car son sens va varier selon votre jeu de données. Parfois significative, parfois à considérer comme un NA ou comme un NULL.
Typiquement, ce truand doit être éliminé au plus vide en connaissance de cause si non significatif pour la propreté de vos données !
Prenons un exemple avec à nouveau le format d’un fichier RSS :
Au niveau « macro », un fichier RSS est un ensemble de lignes se terminant par un retour à la ligne qui définit la fin d’un enregistrement sauf en fin de fichier. Selon les logiciels (ou qu’on ait retouché à la main le fichier), il persiste parfois (ou est inséré involontairement) en fin de fichier un retour à la ligne surnuméraire et donc une dernière ligne vide. Typiquement, cette ligne doit être ignorée donc éventuellement traitée comme un NULL, donc sortie du jeu de données.
Au niveau d’un champ du RSS, selon l’interprétation que vous voulez en faire les champs de mode d’entrée/sortie « provenance » ou « destination » (que j’appelle CENT et CSORT lorsque je manipule mon format de données) peuvent contenir une valeur « vide » représentée dans notre cas par un espace. Dans mon cas, j’ai pris le parti de remplacer ces espaces par NA
.
Comment faire ?
Outre « » et » « , certaines valeurs textuelles peuvent explicitement représenter NA
. Typiquement « NA », « N/A », « <> », « -« , « Empty », « Null », « null » voire « nil ». Ce n’est qu’en connaissant bien votre jeu de données que vous pourrez les gérer.
La plupart des fonctions de chargement externe de données ont pour rôle de convertir des données textuelles plus ou moins typées en des objets de données manipulables par R. Certains de ces formats externes incluent la notion de NA, mais des classiques reposant sur du texte brut comme CSV, TSV ne l’ont pas.
Typiquement nous les chargeons avec des fonctions read*()
de la librairie de base, la librairie readr, etc…
La plupart de ces fonctions possèdent généralement un mécanisme qui vous permet de spécifier comment est représenté NA :
> library(utils)
> read.table(nomDuFichier, na.strings = c("", " ", "NA", "NULL", "EMPTY"),...)
# spécifie que les valeurs passées en paramètre na.strings seront converties en NA
> library(readr)
> read_delim(nomDuFichier, na = c(""," ","NA","NULL","EMPTY"),...)
# idem mais le paramètre s'appelle seulement na
RL’autre possibilité est de le faire immédiatement après avoir chargé les données surtout si ces règles de substitution varient selon les colonnes :
> donnees <- read_csv(nomDuFichier) %>%
mutate(Champ1 = ifelse(Champ1 %in% c(""," ","NA","NULL","EMPTY"), NA, Champ1),
Champ2 = ifelse(Champ2 %in% c("","NA"), NA, Champ2)
# Ici dans sa plus simple expression donc attention à la
# découverte automatique des types de colonnes.
# Il vaut souvent mieux charger explicitement en format texte puis les
# convertir avec une fonction des familles as.*() ou parse_*()
# une fois la substitution des NA réalisée
REt les autres de la bande
Les nombres
Dans le cas des vecteurs numériques, il existe 3 valeurs non numériques en plus de NA
: NaN
, (+/-)Inf
.
NaN
Signifie « Not A Number », typiquement c’est le résultat de certaines opérations arithmétiques indéterminées : la division 0/0 ou ∞/∞, la soustraction ∞-∞ ou le produit ∞ x 0 .
A noter, la racine carrée d’un nombre négatif dans un vecteur numérique donne NaN
en R sauf si le vecteur est déjà de type complexe ‘il n’y a pas de conversion automatique du type de vecteur) :
> sqrt(-10)
[1] NaN
Warning message:
In sqrt(-10) : NaNs produced
> sqrt(as.complex(-10))
[1] 0+3.162278i
RInf et -Inf
Ils correspondent à « l’Infini » et « moins-l’Infini ». Des concepts mathématiques tout à fait licites que R sait donc représenter mais uniquement pour le type [double] et son collègue [complex] (ou des matrices [matrix] et tableaux [array] qui sont des spécialisations de vecteurs [double]) :
> typeof(Inf)
[1] "double"
> typeof(as.integer(Inf))
[1] "integer"
Warning message:
In typeof(as.integer(Inf)) : NAs introduced by coercion to integer range
# Le type integer est borné dans le langage.
# Il ne peut représenter que les nombres entre
# -2.147.483.647 et 2.147.483.647
# Cette limite dépend de la méthode interne de stockage de
# la donnée qui est une structure sur 32 bits avec 1 bit de signe
> as.complex(Inf)
[1] Inf+0i
> typeof(as.complex(Inf))
[1] "complex"
# et bien sûr pour faire bonne mesure :
> as.complex(0 + (1i * Inf) )
[1] NaN+Infi
RIl faut cependant bien dire que vous ne les utiliserez probablement pas pour gérer du PMSI.
La valeur de NA
Il est important de connaitre les particularités de NA quand des opérations sont réalisées dessus.
Les maths de NA
En effet, la non résolution de NA rend la plupart des opérations impossibles et NA agit alors souvent comme élément absorbant avec la possibilité parfois de contourner. Ainsi tout calcul arithmétique contenant NA retournera NA :
> 1 + NA
[1] NA
> 1 / NA
[1] NA
> 3 ^ NA
[1] NA
RC’est vrait aussi des fonctions du type sum()
, mean()
, prod()
qui cependant possèdent un paramètre na.rm=
qui s’il est réglé à TRUE va ignorer les NA :
> sum(c(1, 2, 3, NA))
[1] NA
> sum(c(1, 2, 3, NA), na.rm = TRUE)
[1] 6
RUne autre solution est de d’abord supprimer ces valeurs NA par exemple via un subsetting :
> a <- c(1, 2, 3, NA)
> sum(a[!is.na(a)])
[1] 6
#ou dans un dataframe (ou ici un tibble) avec dplyr, par exemple
> a <- tibble(valeurs = c(1, 2, 3, NA))
> a %>% filter(!is.na(valeurs)) %>% pull(valeurs) %>% sum
[1] 6
Rla valeur textuelle de NA[character]
En cas de manipulation de chaînes de caractères avec paste()
par exemple, NA
sera transformé par défaut en la chaîne « NA ».
Si cela ne vous va pas, parfois les fonctions ont un paramètre pour gérer les NA mais souvent ce n’est pas le cas (c’est normal, R est écrit sur le principe que le développeur doit maîtriser la cohérence de ses données) il faut alors remplacer préalablement les NA
par la chaîne désirée (par exemple la chaîne vide « », un espace, un point, un « ? », ou bien encore un ensemble vide « ∅ », bref ce que vous voulez).
a <- c("un", "deux", NA, "quatre")
paste(a, sep=" - ")
a[is.na(a)] <- "∅"
paste(a, sep=" - ")
RConclusion
J’espère que cet article vous a aidé à comprendre les différentes facettes du concept d’absence et leurs implications tant dans la gestion de l’import de données que leur traitement.
La prochaine fois, on va revenir au PMSI pour changer un peu des dernières discussions qui étaient très techniques et « fondamentales ».