La Concaténation de chaines

Dans cet article nous allons revenir sur les chaines et les vecteurs de chaines de caractères et pour illustrer nous allons écrire quelques fonctions utilitaires.

Générer une chaine de caractères à partir d’autres

Nous avons déjà eu l’occasion d’utiliser la fonction paste() et de façon très fugace la fonction sprintf() pour composer des chaines de caractères à partir d’autres données.

Je vais détailler un peu plus.

paste()

paste() permet de coller l’une à la suite de l’autre des valeurs. Le prototype d’appel est :

paste(...,sep=" ",collapse=NULL,recycle0=FALSE)

tous les paramètres non reconnus comme paramètres nommés seront traités comme des vecteurs de charactères qui serviront de source pour la concaténation des chaines.

sep = ” “

Par défaut, la valeur de sep= est ” ” (une chaine de caractères contenant uniquement une espace).

Toutes les valeurs des autres paramètres (...) vont être collées bout à bout, séparées par la valeur de la chaine sep=, à chaque rang du vecteur.

Les valeurs sont “recyclées”, c’est à dire que le résultat est un vecteur de la longueur du vecteur le plus long reçu en entrée et les vecteurs plus courts sont répétés en bouclant.

> paste(c("a", "b", "c"),c(1, 2, 3, 4, 5), sep = "-")
[1] "a-1" "b-2" "c-3" "a-4" "b-5"
# Ici le 1er vecteur est prolongé de 2 et devient automatiquement c("a","b","c","a","b")
# Le résultat est un vecteur de 5 chaines de caractères 

collapse = NULL

collapse= quant à lui contrôle si la réponse doit être un vecteur de chaines ou une chaine unique.

Si le paramètre a une valeur différente de NULL, la chaine est utilisée pour concaténer les résultats de paste(...,sep= ) :

> paste(c("a", "b", "c"),c(1, 2, 3, 4, 5), sep="-", collapse = "/")
[1] "a-1/b-2/c-3/a-4/b-5"

Comme vous pouvez le voir, on peut faire les 2 étapes en un seul appel

recycle0= FALSE/TRUE

Enfin, recycle0= précise si une chaine vide (character(0)) doit être générée si l’évaluation échoue car la chaine nulle (et non la chaine vide “”) est rencontrée.

> paste(c("a","","c",NULL,"e"),collapse="/",recycle0=FALSE)
[1] "a//c/e"
> paste(c("a","","c",NULL,"e"),collapse="/",recycle0=TRUE)
[1] "a//c/e"
# recycle0 n'a pas d'action sur un vecteur unique
# Notez que NULL est simplement ignoré par la fonction c(), pas par paste() en effet
> c("a", "", "c", NULL, "e")
[1] "a" ""  "c" "e" # le NULL a disparu

# si par contre, on travaille sur plusieurs vecteurs et que l'un est à NULL recycle0= change le fonctionnement :
> paste("a","","c",NULL,"e",sep="/",recycle0=TRUE)
character(0)
> paste("a","","c",NULL,"e",sep="/",recycle0=FALSE)
[1] "a//c//e"

sprintf()

Cette fonction est une vétérante, elle est un portage de la fonction du même nom de la librairie C datant des années 70 (elle a été définitivement incluse dans la librairie standard de ce langage en 1989). Le principe est l’utilisation d’un format et d’une liste de variable à insérer. Il ne s’agit pas de concaténation mais d’une substitution. Cela implique par exemple que le format doit être connu et contraint. Il n’est pas possible d’utiliser sprintf() pour concaténer un nombre variable de valeurs

Elle s’appelle via le prototype : sprintf(fmt,...)

> sprintf("chaine: %s, nombre flottant: %f, hexadécimal: %x","abc",pi,254)
[1] "chaine: abc, nombre flottant: 3.141593, hexadécimal: fe"

Tous les paramètres sont là aussi recyclés c’est à dire répétés en cas de besoin et de possibilité. La règle est un vecteur unitaire est répété, tous les autres vecteurs doivent avoir la même taille sinon c’est l’erreur d’exécution assurée.

Par ailleurs, le format doit compter strictement autant de motifs (%<caractère>) que de vecteurs et vice-versa.

sprintf() ne digère pas très bien les types de données non comptatibles, il convient donc de faire les mises aux bons types des variables en amont par sécurité.

Les paramètres

Il peut s’agir de tout type de variable cependant celles-ci doivent être congruentes avec le type demandé par le motif correspondant dans la chaine de format.

Les valeurs spécifiques aux variables de R ne sont pas traitées. Ainsi, +Inf et -Inf ne sont pas interprétables dans le cadre d’un motif numérique (les valeurs infinies n’existent pas en C standard). De même NA a un traitement spécifique, il est intégré en tant que chaine de caractère “NA”

fmt=

Je ne vous ferai pas un cours sur la chaine de format qui nécessiterait plusieurs articles à elle toute seule mais nous allons évoquer les principales possibilités.

fmt= est une chaine de caractères ou un vecteur de chaine. La substitution se fait par défaut de gauche à droite. Tout caractère “%” introduit un motif de substitution.

Pour insérer le symbole de pourcentage directement dans le format, il faut le doubler :

> sprintf("%s %% %s.", "le symbole :", "est intégré")
[1] "le symbole : % est intégré."

Les principaux types de motifs :

  • Le type chaine de caractères : %s (string)
  • Les types numériques entiers : surtout %d (décimal), %o (octal), %x/%X (hexadécimal),
  • Les types numériques flottants et scientifiques : %f (float), %e/%E (notation scientifique), %g/%G (notation scientifique “si besoin”)

Il est possible d’ajouter un préfixe modifiant le motif juste après le % et avant la lettre de type :

  • un indicateur de position <n>$ : %1$d par exemple prend la 1ère variable et applique le type décimal ; %5$s prend la 5ème variable et applique le motif chaine de caractère.
    ATTENTION, si vous mixez les indicateurs de position et des motifs sans, les appels par position risquent de ne pas donnez ce que vous escomptiez. Evitez de mélanger les deux.
  • une taille de mantisse et de reste avec le format <nombre>.<nombre> (assez logiquement uniquement pour les flottants) : %3.5f
  • une taille de padding pour les entiers en préfixant un <nombre> : %5d
  • un padding avec un alignement à gauche en préfixant d’un : %5d
  • le remplissage par des “0” à gauche en préfixant celle-ci par 0 : %05d
  • On peut forcer la présence d’un signe en rajoutant + : %+05d
  • Ou au contraire ne pas le rajouter mais garder la place en le remplaçant par une <espace> : % 05d
# Indicateur de position
> sprintf(c("%2$s %1$s"),"mot 1","mot 2")
[1] "mot 2 mot 1"

# ATTENTION, si on mixe position et sans position : 
# l'appel du "mot 2" par position fait qu'il est appelé 2 fois
# et "mot 3" est en trop 
> sprintf("%2$s %s %s","mot 1","mot 2","mot 3")
[1] "mot 2 mot 1 mot 2"
Message d'avis :
Dans sprintf("%2$s %s %s", "mot 1", "mot 2", "mot 3") :
  un argument est inutilisé par le format '%2$s %s %s'
  
# mantisse et reste
# Ici une mantisse de 10 chiffres significatifs et 20 chiffres après la virgule.
> sprintf("%10.20f",pi*25)
[1] "78.53981633974483145266"
> sprintf("%010.20f",pi*25)

# padding (alignement à droite)
>  sprintf("%10d",25)
[1] "        25"
# avec des 0
>  sprintf("%010d",25)
[1] "0000000025"

# et un signe obligatoire
>  sprintf("%+10d",25)
[1] "       +25"
>  sprintf("%+010d",25)
[1] "+000000025"
# ou facultatif mais avec espace réservé
>  sprintf("% 010d",25)
[1] " 000000025"

# padding à gauche
>  sprintf("%-10d",25)
[1] "25      "

Il existe d’autres fonctions un peu plus “de niche”, on le verra en cas de besoin. Gardez surtout à l’esprit que paste() est plus adapté à du traitement dans un flux de regroupement, tandis que sprintf() a plus d’intérêt en vue d’une présentation d’un résultat à l’écran sous un format texte.

Exemples de mise en pratique

Je vous propose quelques fonctions de manipulation des chaines mais aussi de noms de fichiers. Cela va nous permettre de jouer entre autre avec ces fonctions de base.

Nous allons utiliser la librairie dplyr pour la lisibilité et vous pouvez utiliser si besoin l’échantillon de RSA déjà utilisé dans de précédents articles (il nous faut donc readr)

Comme d’habitude, essayez de résoudre vous-même les questions avant de regarder le code.

Générer des champs mode d’entrée ou sortie “étendus”

En partant d’un fichier RSS ou RSA contenant les “mode d’entrée” (dans le champ MENT), “provenance” (dans le champ CENT), “mode de sortie” (champ MSORT) et “destination” (champ CSORT). Créer un champ MCENT (ou MCSORT) contenant une concaténation des 2.

Par exemple si MENT=”7″ et CENT=”1″, MCENT sera égal à “71”

library(dplyr)

library(readr)
rsa <- read_csv2("extrait_RSA.csv")

# avec paste()
rsa %>% mutate(MCENT = paste(MENT, CENT, sep=""),
               MCSORT = paste(MSORT, CSORT, sep=""))
<...>

# ou avec sprintf()
rsa %>% mutate(MCENT = sprintf("%s%s", MENT, CENT),
               MCSORT = sprintf("%s%s", MENT, CENT)
<...>

Recréer un type d’entrée “8-5” (entrée par les urgences)

Depuis 2023, le passage par les urgences est porté par un champ “Passage Urgences” présent en sus dans le jeu de données, la valeur “5” dans le champ provenance (CENT) est désormais interdite. Dans mon format personnel, le nouveau champ s’appelle PURG. Pour ne pas devoir réécrire d’anciens traitements, j’aurais besoin que les passages urgences depuis le domicile qui désormais sont MENT=”8″, CENT=” ” et PURG=”5″ apparaissent à nouveau MCENT=”85″ sans modifier les autres types de mouvements

# avec paste()
rsa %>% mutate(MCENT= paste(MENT, ifelse(PURG == "5" & MENT == "8","5", CENT),sep="")

# ou avec sprintf()
rsa %>% mutate(MCENT= sprintf("%s%s",MENT, ifelse(PURG == "5" & MENT == "8","5", CENT))

Comme vous pouvez le voir, il est tout à fait possible de passer le résultat d’une évaluation comme paramètre. Ici on utilise ifelse() qui est la version vectorisée de if(){}else{} et qui donc s’exécute sur chaque ligne.

Générer des noms de fichiers PMSI

Les fichiers PMSI ont un nom caractéristique constitué d’au moins 4 éléments :

  • Le FINESS (9 caractères)
  • L’année (nombre entier 4 caractères jusqu’en l’an 10000 🙂 )
  • Le mois de l’envoi (nombre entier sans zéro à gauche)
  • le type (qui est en quelque sorte l’extension du nom de fichier, précisant le format de son contenu)

Pour les archives (in/out.zip), il peut aussi y avoir un numéro d’ordre unique entre le mois et le type.

Ainsi le RSS de l’établissement “123456789”, pour le 3ème mois 2024 porte le nom “123456789.2024.3.rss.ini” dans le fichier d’archive in.zip. Tandis que le nom de l’archive elle-même est “123456789.2024.3.XXXXXXXXXXXXXX.in.zip” où XXXXXXXXXXXXXX est un numéro unique permettant d’identifier des versions différentes produites à des temps différents. Dans le cas des in/out, il s’agit d’une version linéarisée de la date de production “20240328112909” = “produit le 28 mars 2024 à 11:29:09”. Il n’y a aucune garantie de pérennité de ce format, cependant il est strictement croissant en terme de comparaison de chaine de caractère en vu d’un requêtage éventuel.

Par ailleurs, si on veut accéder aux fichiers d’envois de DRUIDES eux-mêmes (les archives “basket”), leur nom est du type “123456789.2024.3.SEJOURS.SEJOURS.20240328112909.96f668ac-db14-49cd-9807-57cac0ddf6e5.zip”. (L’extension “96f668ac-db14-49cd-9807-57cac0ddf6e5” n’est pas calculable ou spontanément signifiante.)

Créons donc une fonction ayant le prototype nomPmsi(finess, annee, mois, type, id) et retournant le nom correspondant, id= pouvant être ignoré .

Spontanément on écrirait :

nomPmsi <- function(finess, annee, mois, type, id = NA){

  #On fait des conversions de types explicites pour être propre
  finess <- as.character(finess)
  annee <- as.integer(annee)
  mois <- as.integer(mois)
  type <- as.character(type)
  id <- as.character(id)
  
  # avec paste()
  paste(finess, annee, mois, id, type, sep=".")
  # ou avec sprintf()
  sprintf("%s.%d.%d.%s.%s",finess, annee, mois, id, type)
}

Mais il y a un problème si on ne passe pas de id= :

# avec paste() ou avec sprintf() : les lettres "NA" apparaissent si id=NA
[1] "1234567890123.2024.3.NA.rss.ini"

if faut donc décomposer le traitement en fonction de la valeur de id= :

nomPmsi <- function(finess, annee, mois, type, id = NA){

  #On fait des conversions de types explicites pour être propre
  finess <- as.character(finess)
  annee <- as.integer(annee)
  mois <- as.integer(mois)
  type <- as.character(type)
  id <- as.character(id)
  
  # avec paste() ; ce serait pareil pour sprintf()
  ifelse(is.na(id),
    paste(finess, annee, mois, type, sep="."),    # si id=NA
    paste(finess, annee, mois, id, type, sep=".") # sinon
  )
}

et là aussi il faut passer par un ifelse() pour garder le traitement vectorisé.

> nomPmsi("123456789",2024,3,id="SEJOURS.SEJOURS.20240328112909.96f668ac-db14-49cd-9807-57cac0ddf6e5","zip")
[1] "123456789.2024.3.SEJOURS.SEJOURS.20240328112909.96f668ac-db14-49cd-9807-57cac0ddf6e5.zip"

Pour la petite histoire, il existe une version “rapide” (mais bien sûr un peu plus limitée) de paste() servant à générer des chemins d’accès à des fichiers mais qui peut être détournée : file.path()

A titre personnel, j’aime bien surtout parce que le nom contient “file” et donc on voit sur quoi on travaille. Sur mon système, la différence de temps d’exécution est de… 0,8 microsecondes par ligne entre file.path() et paste(). Jamais cela n’aura d’impact significatif sur des fichiers PMSI même très nombreux.

L’appel en est similaire à part qu’il n’y a pas de collapse=, que sep= s’appelle fsep= et que sa valeur par défaut dépend du système d’exploitation (le plus souvent “/”, même sous Windows) :

> file.path("~","Documents","R")
[1] "~/Documents/R"

# qu'on peut détourner en :
> file.path("1234567890123",2024,3,"rss.ini",fsep = ".")
[1] "1234567890123.2024.3.rss.ini"

# et combiner par exemple en :
> file.path("~","Documents","R",
            file.path("1234567890123",2024,3,"rss.ini",fsep = ".")
            )
[1] "~/Documents/R/1234567890123.2024.3.rss.ini"

Enfin, on pourrait imaginer une fonction allant directement chercher dans une archive :

idarchive= devient alors non optionnel. Pour le retour, on sépare le nom de l’archive du sous-fichier archivé par “::” pour une meilleure lisibilité.

nomPmsiDansArchive <-function(finess, annee, mois, idarchive, type){

  #On fait des conversions de types explicites pour être propre
  finess <- as.character(finess)
  annee <- as.integer(annee)
  mois <- as.integer(mois)
  type <- as.character(type)
  idarchive <- as.character(idarchive)
  
  nomArchive <- paste(finess, annee, mois, idarchive, "zip", sep=".") 
  nomFichier <- paste(finess, annee, mois, type, sep=".")
  
  paste(nomArchive, nomFichier, sep=":")

}

> nomPmsiDansArchive("123456789",2024,3,idarchive ="20240328112909.in","rss.ini")
[1] "123456789.2024.3.20240328112909.in.zip::123456789.2024.3.rss.ini"

#Et en usage vectorisé pour par exemple récupérer les 3 rss des 3 dernières années :
annees <- bind_rows(list(annee = "2021", idarchive = "20220130102301"),
                    list(annee = "2022", idarchive = "20230201095606"),
                    list(annee = "2023", idarchive = "20240129173154"))

> nomPmsiDansArchive("123456789",
                     annees$annee,
                     12,
                     idarchive = paste(annees$idarchive,"in",sep="."),
                     "rss.ini")
[1] "123456789.2021.12.20220130102301.in.zip::123456789.2021.12.rss.ini"
[2] "123456789.2022.12.20230201095606.in.zip::123456789.2022.12.rss.ini"
[3] "123456789.2023.12.20240129173154.in.zip::123456789.2023.12.rss.ini"

(A noter que cette valeur n’est pas utilisable en l’état, les “::” ne sont pas reconnus par les moyens de dézippage de R, c’est juste à titre d’exercice. Pour une utilisation directe, il faudrait renvoyer un tableau de 2 colonnes : nom de l’archive et nom du fichier dans l’archive)

Conclusion

Ces quelques fonctions devraient vous permettre de manipuler plus facilement les chaines de caractères tout en gardant la vectorisation qui fait le charme et la puissance de R.

Pour le coup, une prochaine fois nous pourrons jouer avec les fichiers PMSI et les in/out directement.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *