R avancé et introduction à Git

Raphaël Nedellec

Les packages

Un package

Un ‘package’ ou une ‘librairie’ rassemble du code, des données, de la documentation et des tests ensemble et est facile à partager.

  • Pourquoi faire un package ?
  • Qu’est-ce qu’un package et comment développer un package ?
  • Tester et documenter son package.

Pourquoi faire un package ?

Motivation

Pour le développeur :

  • Se faciliter la vie, s’organiser ;
  • mutualiser et maintenir des fonctions en un seul endroit ;
  • monter en compétence ;
  • documenter et partager avec la communauté sous un format standardisé.

Motivation

Pour l’utilisateur :

  • Utiliser des outils testés et validés par la communauté ;
  • gagner en efficacité et optimalité ;
  • accélérer les différents processus de data science.

CRAN

Github

Github

Github et Git

Plus sur ce sujet lors du cours 6

Installer, charger

Installer un package :

install.packages("tidyverse") # base R
remotes::install_github("r-lib/conflicted") # using remotes
pak::pkg_install("tibble") # using pak
  • install.packages en R base, depuis le CRAN ou des sources
  • remotes : librairie permettant l’installation depuis le CRAN et différents dépôt de code programmaticalement, tels que github, gitlab, bitbucket, ainsi que Bioconductor.
  • pak : librairie pour l’installation interactive de packages

Structure d’un package

Arborescence : randomForest

Code R/

Contient le code R du package.

  • Ce code est exécuté au build time ;
  • différent d’un script/ d’une analyse interactive dans RStudio où le code est exécuté au run time.

Pas de sous-dossier

Utilisez des préfixes “smthing-*.R” pour organiser les fichiers par thème

Code R/

Contient le code R du package.

  • Ce code est exécuté au build time ;
  • différent d’un script/ d’une analyse interactive dans RStudio où le code est exécuté au run time.

Points de vigilance

  • Ne modifiez pas le système de l’utilisateur ! Pas d’appels aux fonctions source, library dans le code d’un package
  • Utilisez avec vigilance par, options, setwd

Exemple R/

Contenu des fichiers

Rester cohérent et clair sur le contenu des fichiers - ni trop, ni pas assez. Dépend du contexte.

data/

  • Partage de dataset utiles au package pour la documentation, les exemples, le fonctionnement interne du package ;
  • data packages : nycflights13, babynames ;
  • organisation différentes en fonction des données et de leur utilité.

usethis

Voir usethis::use_data()

data/

  • Stocker des objets R pour qu’ils soient disponibles pour les utilisateurs : data/ au format .rda/.RData
  • Stocker des objets à des fins de développement : R/sysdata.rda
  • Stocker des objets au format brut (xls, xlsx, json, ..) : inst/extdata/

Préserver la reproducibilité des datasets

Voir usethis::use_data_raw()

man/

man/

vignettes/

vignettes/

man/ et vignettes/

  • Il est possible d’écrire les documents .Rd à la main. Mais il recommandé d’utiliser roxygen2 pour les générer automatiquement !
  • roxygen2 est bien intégré au workflow devtools
  • Les vignettes sont des guides d’utilisation détaillés pour le package ou certaines de ses fonctionnalités.
  • À écrire en Rmarkdown

Autres dossiers

  • inst/ : pour les fichiers additionnels à inclure dans la librairie ;
  • src/: pour stocker des fichiers sources et “header” C/C++/… et utiliser du code compilé dans un package R ;
  • demo/, exec/, po/, tools/, …

Plus d’informations

Voir le chapitre dédié dans R packages

Metadata : DESCRIPTION

Package: mynewpackage
Title: What the Package Does (One Line, Title Case)
Version: 0.0.0.9000
Authors@R: 
    person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
           comment = c(ORCID = "YOUR-ORCID-ID"))
Description: What the package does (one paragraph).
License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
    license
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.1
Suggests: 
    testthat (>= 3.0.0)
Config/testthat/edition: 3

Metadata : DESCRIPTION

  • Title : Décrire en une phrase la librairie
  • Description : Décrire de manière plus détaillée l’intérêt et les fonctionnalités de la librairie
  • Authors@R : Les auteurs. Voir ?person().
  • Version : Version du package. Important de le maintenir à jour, voir chapitre dédié dans R avancé.
  • License : Quelle licence et quels droits d’utilisation ?
  • Suggests, Imports, and others : Gestion des dépendances avec des librairies tierces.

Quand utiliser des dépendances ?

Plusieurs critères et questions à se poser :

  • Type de dépendance
  • Nombre de dépendances indirectes induites
  • Coût d’installation des dépendances (dépendances systèmes, code compilé, taille des packages)
  • Maintenance et suivi des librairies
  • Criticité des fonctionnalités

Dépendances : DESCRIPTION

  • Imports : Les packages listés ici doivent être présents et installés pour que votre librairie fonctionne.
  • Suggests: Votre package peut utiliser ces dépendances, mais ne sont pas nécessaires à son fonctionnement. Par exemple, ces packages peuvent être utilisés dans les vignettes ou les tests.

DESCRIPTION

Les dépendances déclarées dans le fichier DESCRIPTION aident R à valider la présence des packages nécessaires et à les installer lors de l’installation de votre package.

Dépendances : NAMESPACE

Quelle fonction here() est disponible dans l’environnement global ?

library(lubridate)    |  library(here)
library(here)         |  library(lubridate)

here() # here::here() |  here() # lubridate::here()

Opérateur ::

lubridate::here() # always gets lubridate::here()
here::here()      # always gets here::here()

Dépendances : NAMESPACE

sd
#> function (x, na.rm = FALSE) 
#> sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
#>     na.rm = na.rm))
#> <bytecode: 0x563eba6f48a0>
#> <environment: namespace:stats>

Que se passe-t-il lorsque l’on déclare une nouvelle fonction var ?

var <- function(x) -5
var(1:5)
#> [1] -5
sd(1:5)

Dépendances : NAMESPACE

sd
#> function (x, na.rm = FALSE) 
#> sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
#>     na.rm = na.rm))
#> <bytecode: 0x563eba6f48a0>
#> <environment: namespace:stats>

Que se passe-t-il lorsque l’on déclare une nouvelle fonction var ?

var <- function(x) -5
var(1:5)
#> [1] -5
sd(1:5)
#> [1] 1.58

Cela est possible grâce au namespace du package stats où la fonction var du package stats est déjà référencée.

Dépendances : NAMESPACE

Exemple tiré du package remotes

# Generated by roxygen2: do not edit by hand

S3method(format,bioc_git2r_remote)
S3method(format,bioc_xgit_remote)
...
export(add_metadata)
export(available_packages)
...
importFrom(stats,update)
importFrom(tools,file_ext)

roxygen2

Une bonne pratique aujourd’hui est d’utiliser roxygen2 pour générer automatiquement le fichier NAMESPACE

Dépendances : NAMESPACE

  • importFrom : importe un objet (fonction) d’une dépendance
  • import : importe tous les objets exportés
  • S3method : pour exporter une méthode S3
  • export : pour exporter une fonction (rendre une fonction publique)

Autres instructions

Plus rares : useDynLib(), exportPattern(), exportClasses(), exportMethods(), importClassesFrom(), importMethodsFrom()

Développer un package

Un exemple de workflow avec Devtools

  • devtools, librairie “chapeau” pour tous les outils de développement
  • usethis, pour automatiser l’initialisation d’un nouveau package
  • roxygen2, pour la gestion de la documentation

Setup

Création d’un squelette de package :

# package name may only contain 
# letters, numbers
# dots are allowed but not recommended
usethis::create_package("mynewpackage")

RStudio initialise un nouveau projet.

Setup : nouveau projet

Nouveau projet

En utilisant l’interface graphique, il est possible de créer des projets avec des templates, dont des packages. Ceci utilise in fine usethis.

Écrire et documenter ses fonctions

  • les fonctions sont dans des fichiers .R au sein du répertoire R/
  • il faut documenter ses fonctions en utilisant les balises roxygen2

usethis

usethis::use_r pour créer un fichier .R dans le répertoire R/, ou naviguer dans celui-ci dans RStudio.

roxygen2

compute_stats_by_levels <- function(
    data, col, by, stats
) {
  if (stats %in% c("mean", "max", "min")) {
    res <- tapply(data[[col]], data[[by]], stats)
  } else {
    stop("wrong stats")
  }
  return(res)
}

Resultats ?

compute_stats_by_levels(mtcars, "mpg", "cyl", mean)
compute_stats_by_levels(mtcars, "mpg", "cyl", "mean")

roxygen2

compute_stats_by_levels <- function(
    data, col, by, stats
) {
  if (stats %in% c("mean", "max", "min")) {
    res <- tapply(data[[col]], data[[by]], stats)
  } else {
    stop("wrong stats")
  }
  return(res)
}

Résultats :

compute_stats_by_levels(mtcars, "mpg", "cyl", mean)
#> Error in match(x, table, nomatch = 0L): 'match' requires vector arguments
compute_stats_by_levels(mtcars, "mpg", "cyl", "mean")
#>        4        6        8 
#> 26.66364 19.74286 15.10000

roxygen2

#' @param data A data.frame
#' @param col A string, the column name for which we compute the stats
#' @param by A string, the column name by which we group
#' @param stats A string, the name of the function. Must be one of "mean",
#'   "min", "max".
compute_stats_by_levels <- function(
    data, col, by, stats
) {
  if (stats %in% c("mean", "max", "min")) {
    res <- tapply(data[[col]], data[[by]], stats)
  } else {
    stop("wrong stats")
  }
  return(res)
}

roxygen2

#' @param data A data.frame
#' @param col A string, the column name for which we compute the stats
#' @param by A string, the column name by which we group
#' @param stats A string, the name of the function. Must be one of "mean",
#'   "min", "max".
#' 
#' @export
compute_stats_by_levels <- function(
    data, col, by, stats
) {
  if (stats %in% c("mean", "max", "min")) {
    res <- tapply(data[[col]], data[[by]], stats)
  } else {
    stop("wrong stats")
  }
  return(res)
}

roxygen2

#' A Function to Compute Group Statistics
#' 
#' @description This function is designed for example only and is not really usefull..
#'
#' @param data A data.frame
#' @param col A string, the column name for which we compute the stats
#' @param by A string, the column name by which we group
#' @param stats A string, the name of the function. Must be one of "mean", "min", "max".
#' @return A named vector statistics, of size `length(unique(data[[col]]))`
#' 
#' @export
#' @examples
#' compute_stats_by_levels(mtcars, "mpg", "cyl", "mean")
compute_stats_by_levels <- function(
    data, col, by, stats
) {
  if (stats %in% c("mean", "max", "min")) {
    res <- tapply(data[[col]], data[[by]], stats)
  } else {
    stop("wrong stats")
  }
  return(res)
}

roxygen2 : balises

  • @title, @description, @detail : pour présenter et détailler sa fonction. Peuvent être implicites.
  • @param : pour documenter les paramètres de la fonction
  • @return : quel est l’output de la fonction
  • @examples : suite d’examples
  • @export : pour exposer la fonction dans le NAMESPACE
  • @importFrom : quand la fonction importe une fonction depuis une librairie tierce
  • @noRd : pour documenter la fonction mais ne pas exposer la documentation publiquement

roxygen2

  • Possibilité d’utiliser des balises Mardown dans la documentation.
  • Possibilité de réutiliser/mutualiser de la documentation entre fonctions, en utilisant les balises @describe, @rdname, @inheritParams, @inheritSection, @inherit
  • Pour documenter des fonctions, mais aussi des datasets.

Liste des balises

roxygen2 : en résumé

  • documentez vos fonctions avec les balises roxygen2
  • roxygen2 générera la documentation et le NAMESPACE de votre package
  • multiples balises disponibles, et il est possible de moduler la documentation ;
  • s’intègre au workflow de développement devtools

Testing

Comment testez-vous une fonction ?

Tester vos fonctions

En développement logiciel (donc quand vous développez une librairie / des fonctions en R) :

  • il faut tester vos fonctions pour s’assurer qu’elles se comportent comme attendu ;
  • de manière reproductible et non de manière informelle et ad’hoc ;
  • un bug : un nouveau test à ajouter ;
  • évaluer la couverture des tests et la robustesse du code.

testthat

Framework dominant pour l’écriture de tests en R.

# from dplyr test suite
# https://github.com/tidyverse/dplyr/blob/2af5c86112c697a8abf114eb5dc323e5116777bc/tests/testthat/test-bind-cols.R#L33
test_that("bind_cols() handles all-NULL values (#2303)", {
  expect_identical(bind_cols(list(a = NULL, b = NULL)), tibble())
  expect_identical(bind_cols(NULL), tibble())
})

testthat, exemple

Dans R/

# myfunction.R
my_function <- f(x, y) {
  stopifnot(!is.numeric(x), !is.numeric(y))
  return(x * y)
}

Dans tests/testthat/test-myfunction.R

# test-myfunction.R
test_that("my_function multiply works", {
  expect_identical(my_function(1, 2), 1 * 2)
  expect_identical(my_function(TRUE, FALSE), 0)
  expect_error(my_function("x", 1))
})

Quelques assertions

  • expect_equal() expect_identical()
  • expect_type() expect_s3_class()
  • expect_length()
  • expect_true() expect_false()
  • expect_error() expect_warning() expect_message() expect_condition()

Liste exhaustive ici

En résumé : processus de développement

Processus de développement d’une fonction

  • Écrire sa fonction
  • Documenter
  • Écrire les tests associés (voir usethis::use_testthat() et ?usethis::use_test())
  • Charger son package en émulant son installation avec ?devtools::load_all()
  • Itérer

Développement d’une librairie

  • Développez vos fonctions et ajoutez vos datasets.
  • Documenter. devtools::document() permet de générer et mettre à jour automatiquement les différents documents (.Rd, NAMESPACE).
  • Testez régulièrement vos fonctions en utilisant devtools::test() qui exécutera la suite de test de la librairie en développement.
  • Utilisez devtools::check() si vous voulez tester la compatibilité avec le CRAN.

Développement d’une librairie

  • Utilisez les bonnes pratiques et outils de développement logiciel : git, versioning, CI/CD, code coverage
  • Améliorer et maintenir la documentation, pourquoi pas avec pkgdown et des vignettes détaillées.

Ressources additionnelles

Ressources additionnelles

Questions ?