Über unsMediaKontaktImpressum
Joachim Zuckarelli 27. März 2020

R-Packages entwickeln

Die Statistik-Programmiersprache R verdankt ihre zunehmende Popularität vor allem den tausenden von Erweiterungspaketen (Packages), die sie zum Schweizer Taschenmesser für Statistik und Data Science machen. In diesem Beitrag gehen wir anhand eines praktischen Beispiels der Frage nach, wie Sie als R-Nutzer mit wenig Aufwand selbst Packages entwickeln und der R-Community zur Verfügung stellen können.

Packages im R-Ökosystem

R ist eine weit verbreitete Programmiersprache, die vor allem für die Auswertung und Visualisierung von Daten entwickelt worden ist. Seine kommerziellen Konkurrenten wie SPSS, SAS oder Stata hat es in Hinblick auf Beliebtheit und Funktionsumfang längst überflügelt [1]. Im TIOBE-Ranking der populären Programmiersprachen rangiert R derzeit auf Platz 13. Alle Sprachen mit besseren Platzierungen im TIOBE-Ranking (mit Ausnahme von SQL) sind General Purpose Languages, darunter auch Python, das häufig als Konkurrent zu R betrachtet wird. Tatsächlich aber ist der Anwendungsfokus von Python weitaus breiter als der von R; seine Popularität im Bereich Data Science und künstliche Intelligenz – Feldern, die natürlich auch für R große Bedeutung haben – verdankt Python nicht zuletzt Erweiterungsmodulen wie NumPy, die die Sprache u. a. um Datenstrukturen für die statistische Arbeit ergänzen. Als originäre Statistik-Sprache wurden R solche Datenkonstrukte – wie Vektoren und Dataframes – bereits in die Wiege gelegt. Tatsächlich haben Python und R zahlreiche Verknüpfungspunkte: So sind zum Beispiel beliebte Python-Bibliotheken für Machine Learning wie Keras und Tensorflow auch in R verfügbar, mit dem Reticulate-Package kann zudem aus R heraus beliebiger Python-Code ausgeführt werden.

R erfährt heute nicht nur in der akademischen Welt breite Anwendung, sondern auch im Business-Umfeld, wovon unter anderem die tiefe Integration von R in Microsoft Power BI zeugt, das als Business-Intelligence-Werkzeug bekanntlich bereits große Verbreitung gefunden hat.

Seine Beliebtheit verdankt R sicherlich dem Umstand, dass es – anders als seine ebenfalls spezialisierten, aber kommerziellen Konkurrenten – als Open-Source-Werkzeug frei verfügbar ist. Nicht minder bedeutsam für den Erfolg von R dürfte aber sein immenser Funktionsumfang sein. Nur einen kleinen Teil davon bringt R von Haus aus mit. Den weitaus größeren steuert die überaus aktive R-Community in Form sogenannter Packages bei. Mehr als 15.400 dieser Erweiterungsbibliotheken sind Mitte Februar 2020, als dieser Artikel geschrieben wird, über die zentrale CRAN-Plattform (Comprehensive R Archive Network) [2] verfügbar – eine beinahe unfassbar große Palette von Funktionalitäten für die unterschiedlichsten Zwecke und Bedürfnisse, und mögen diese noch so ausgefallen sein. Die rasante Entwicklung zeigt Abb. 1. Es gibt Schätzungen, denen zufolge für R alleine im Jahr 2015 mehr Funktionen entwickelt worden sind, als das kommerzielle Statistik-Schwergewicht SAS insgesamt besitzt [3]. Diese Entwicklungsfreude der R-Community ist es, die R zum Schweizer Taschenmesser für Statistik und Data Science macht. Zugleich entsteht jedoch das Luxusproblem, sich im unübersichtlichen Dickicht der R-Packages zurechtfinden zu müssen. Dazu gibt es neben anderen Hilfsmitteln – Sie haben es sicher schon geahnt – wiederum spezielle Packages wie etwa pkgsearch oder packagefinder (letzteres vom Autor dieses Artikels).

Erste Schritte

In diesem Artikel wollen wir uns damit beschäftigen, wie man selbst Packages entwickelt und auf der zentralen Bibliotheksplattform CRAN veröffentlicht. Dabei werden wir einen sehr pragmatischen, anwendungsorientierten Weg einschlagen: Anstatt jeden Detailaspekt der Package-Entwicklung auszuleuchten, jede Variante, jeden Sonderfall, jeden Kniff und jedes Werkzeug zu betrachten, werden wir das tun, was dem Grundverständnis der Package-Entwicklung am meisten hilft: Ein Package entwickeln.

Konkret soll hier ein einfaches Package geschrieben werden, das eine Funktion zur Temperatur-Umrechnung zwischen Grad Celsius, Grad Fahrenheit und Kelvin zur Verfügung stellt. Anhand dieses Beispiel-Packages werden wir dabei den gesamten Entwicklungsprozess vom Schreiben des Codes bis zur Einreichung auf CRAN kennenlernen. Am Ende dieses Beitrags sollte man ohne weiteres in der Lage sein, selbst ein Package zu entwerfen und zu publizieren. Denn Package-Entwicklung ist viel leichter, als man denkt und keineswegs einem erlauchten Kreis erfahrener Experten vorbehalten. Tatsächlich entstehen gerade aus der täglichen Arbeit der R-Anwender viele nützliche Funktionen, die sich als "Nebenprodukt" mit überschaubarem Aufwand in ein Package einbetten und so der R-Community zur Verfügung stellen lassen. Zahlreiche R-Nutzer, die auf der Suche nach genau einer solchen Funktionalität sind, werden es Ihnen danken!

Für diesen Artikel sind keine Vorkenntnisse notwendig als allein einige Grundlagen der Programmiersprache R; möchten Sie diese noch einmal rasch auffrischen, sei auf den Grundlagen-Artikel verwiesen.

Bevor wir nun damit beginnen, ein Package zu entwickeln, zunächst eine ganz kurze Wiederholung der Verwendung von Packages in R. Vor der ersten Benutzung eines Packages muss dieses zunächst installiert werden; das geschieht mit Hilfe der R-Standardfunktion install.packages():

> install.packages("keras", dependencies = TRUE)
> install.packages(c("tidyr", "tibble", "dplyr", "ggplot2"), dependencies = TRUE)

Die zu installierenden Packages werden durch einen Vektor von Packagenamen identifiziert. Im zweiten Beispiel enthält dieser Vektor eine Reihe von Packages aus Hadley Wickhams bekannter tidyverse-Sammlung [4]. Durch dependencies = TRUE werden nicht nur die Packages selbst heruntergeladen und installiert, sondern gegebenenfalls auch weitere Packages, von denen die zu installierenden abhängen. Auf diese Weise wird sichergestellt, dass die Packages später problemlos verwendet werden können.

Nach der Installation stehen die Funktionalitäten, die die Packages bereitstellen, grundsätzlich zur Verfügung. Um sie verwenden zu können, muss das jeweilige Package aber zunächst geladen werden:

library(tibble)

Statt das Package zu laden, können seine Funktionen auch direkt in der Notation package::funktion() aufgerufen werden, eine Methode, die besonders dann interessant ist, wenn bei der Package-Entwicklung auf Funktionen anderer Packages zurückgegriffen werden soll. Ein solcher Zugriff könnte dann also so aussehen:

tibble::as_tibble(mein_dataframe)

Werkzeuge zur Package-Entwicklung

R-Packages lassen sich auf drei unterschiedlichen Wegen entwickeln:

  • Technisch betrachtet ist ein R-Package nichts weiter als eine Sammlung von Textdateien mit Code, Meta-Informationen und Dokumentation. Deshalb genügt ein einfacher Editor (und natürlich R selbst [5]), um ein Package zu entwickeln.
  • Wer es etwas bequemer haben will, nutzt eine Entwicklungsumgebung wie das überaus populäre (und als Community-Version kostenfreie) RStudio [6] des gleichnamigen Anbieters, das die Package-Entwicklung mit einigen praktischen Features unterstützt.
  • Viele in der Package-Entwicklung auftretende Aufgaben wie das Anlegen der Grundstruktur des Packages, das Testen des Codes und das Erzeugen der Dokumentation werden durch das von Hadley Wickham und anderen entwickelte devtools-Package erleichtert. Es erlaubt, diese Aufgaben direkt aus der R-Konsole (dem interaktiven R-Modus) heraus durch den Aufruf von Funktionen dieses Packages zu erledigen. Das devtools-Package besteht eigentlich aus einer ganzen Reihe unterschiedlicher Packages, die auch eigenständig verwendet werden können, wie etwa usethis (für das Package-Setup), testthat (für das Testen) und roxygen2 (für die Dokumentation). Es empfiehlt sich aber, einfach das devtools-Package mit install.packages("devtools", dependencies = TRUE) zu installieren, wodurch zugleich alle diese Spezialpackages mit verfügbar gemacht werden. Das devtools-Package ist übrigens auch nützlich, um Packages aus anderen Quellen als CRAN zu installieren. Mit seinen install_...-Funktionen erlaubt es beispielsweise, Packages direkt von GitHub (install_github()) oder von einer beliebigen URL (install_url()) zu installieren.

Wir werden im Weiteren die beiden letztgenannten Vorgehensweisen anwenden und sehen, wie man die Package-Entwicklung einmal mit den RStudio-Funktionalitäten und alternativ – und ergänzend, denn beide Ansätze schließen sich keineswegs aus! – mit den devtools-Werkzeugen bewältigt.

Das Package entwickeln

Ein Package-Projekt aufsetzen

Die bequemste Art, ein neues Package zu erzeugen, ist, in RStudio über den Menü-Eintrag File | New Project… ein neues Projekt anzulegen, woraufhin man zunächst gefragt wird, ob das Projekt in einem neuem oder einem bestehenden Verzeichnis platziert werden soll. Im nächsten Schritt ist der Typ des Projekts zu wählen; hier wählen wir natürlich Package und können dann in der abschließenden Seite des Assistenten, die auch in Abb. 2 dargestellt ist, festlegen, in welchem Verzeichnis genau das Package erzeugt werden und welchen Namen es tragen soll. Bei der Wahl des Namens sollte natürlich darauf geachtet werden, eine möglichst aussagekräftige Bezeichnung zu wählen, die zudem die Gefahr von Verwechslungen mit bereits existierenden Packages ausschließt (hierfür bietet sich eine Recherche auf CRAN an [7]). Package-Namen müssen mit einem Buchstaben beginnen und dürfen neben Buchstaben lediglich Zahlen und den Punkt enthalten (der in R ja ein normales Zeichen und nicht, wie in vielen anderen Sprachen, ein spezieller Operator in der objektorientierten Programmierung ist). Der Einfachheit halber sollte man Package-Namen vollständig in Kleinbuchstaben schreiben. Einen besonderen Twist kann man seinem Package geben, indem man eine Bezeichnung, die eigentlich auf -er oder -or enden würde, auf -r verkürzt. Unser Package, das ja der Temperatur-Umrechnung dient, werden wir in diesem Sinne tempconvertr nennen.

RStudio erzeugt nun automatisch ein Minimal-Package, bestehend aus einem (Unter-)Verzeichnis /R, in dem der R-Code liegt (standardmäßig eine einigermaßen sinnfreie Datei namens hello.r), ein Verzeichnis /man, das die Heimat der Package-Dokumentation werden wird (zu Beginn bewohnt von einer Template-Datei hello.rd), und den Templates für die beiden Meta-Dateien DESCRIPTION und NAMESPACE, die, wie wir gleich sehen werden, eine wichtige Rolle spielen. Außerdem wird eine .Rproj-Datei generiert, die in lesbarem Textformat die Projekteinstellungen abspeichert, die man in RStudio über Tools | Project options… bequem mit Hilfe eines Einstellungsidalogs bearbeiten kann. Schließlich wird eine Datei namens .Rbuildignore angelegt, in der man mit Hilfe regulärer Ausdrücke Verzeichnisse und Dateien angeben kann, die zwar im Package-Verzeichnis liegen, aber beim Zusammenbau des Packages am Ende nicht in das fertige Package-Archiv mit einbezogen werden sollen; mit der Funktion devtools::use_build_ignore() lassen sich Dateien und Verzeichnisse leicht zu .Rbuildignore hinzufügen.

Statt mit der RStudio-Funktionalität zu arbeiten, könnten wir auch mit den devtools arbeiten, um den Rohbau eines neuen Packages zu erzeugen. Dazu wird die Funktion devtools::create() aufgerufen, deren erstes Argument der Pfad zu dem neuem Package ist. Das letzte Segment des Pfads wird automatisch als Package-Name verwendet. Demnach können wir also mit create("c:/packages/tempconvertr") unser Package erzeugen (Verzeichnispfade werden in R in Linux-Schreibweise angegeben; alternativ muss der Backslash escaped werden: \\). Dabei wird anders als bei Verwendung der RStudio-Funktionalität kein /man-Verzeichnis erzeugt und auch keine R-Dummy-Datei in /R. Jedoch wird, wenn man es nicht durch Setzen des Arguments rstudio auf FALSE explizit abschaltet, auch von devtools::create() eine RStudio-Projektdatei angelegt.

Struktur und Bestandteile eines Packages

Der Aufbau eines R-Packages folgt einem einfachen Schema. In den folgenden Abschnitten werden wir jede Komponente einzeln anhand unseres Beispielpackages durchgehen. Hier zunächst die Übersicht:

  • Verzeichnis /R: Hier liegt der R-Quellcode, unter Umständen auch mehrere Dateien. Keine von ihnen muss zwingend den Namen des Packages tragen. Kleinere Packages (wie das, das wir hier entwickeln) kommen allerdings regelmäßig mit nur einer Code-Datei daher, die dann meist den Namen des Packages trägt.
  • Verzeichnis /man: Hier liegt der Quellcode der Dokumentation in Form von .Rd-Dateien. Aus diesen wird die Hilfe generiert, die man präsentiert bekommt, wenn man ?meine.funktion oder help(meine.funktion) in der R-Konsole ausführt.
  • Datei DESCRIPTION: Enthält eine Reihe von Meta-Daten zu einem Package, wie etwa eine Beschreibung der Funktionalität, die Version und Informationen zum Autor.
  • Datei README: Optionale, aber sinnvolle Datei im Markdown-Format, die im besten Fall ein kleines Getting-Started-Tutorial zu dem Package enthält.
  • Datei NEWS: Optionale Datei mit einer Änderungshistorie über die unterschiedlichen Versionen des Packages.

Der R-Code

Der R-Code ist natürlich die wichtigste Komponente des Packages. In unserem Beispiel der Temperatur-Umrechnung besteht der Code nur aus einer einzigen Funktion, nämlich convert.temp(from.temp, from.unit = "Celsius", to.unit = "Kelvin", as.json = FALSE). Deren Argument from.temp ist die umzurechnende Temperatur als Zahl, from.unit und to.unit die Einheit von der bzw. in die umgerechnet wird, vorbelegt mit "Celsius" und "Kelvin" als Standardwerten. Mit dem logical-Argument as.json kann festgelegt werden, dass die Funktion nicht einfach nur die umgerechnete Temperatur als Zahl zurückgibt, sondern eine Dokumentation des Umrechnungsvorgangs als JSON-Objekt liefern soll. Der Code der Funktion convert.temp() lautet vollständig:

convert.temp <- function(from.temp, from.unit = "Celsius", to.unit = "Kelvin", as.json = FALSE) {
  if(tolower(from.unit) %in% c("celsius", "kelvin", "fahrenheit") &
    tolower(to.unit) %in% c("celsius", "kelvin", "fahrenheit")) {
            conv.add1 <- data.frame(list(celsius = c(0,0,-32), kelvin = c(0,0,-32), fahrenheit = c(0,-273.15,0)))
            conv.mult <- data.frame(list(celsius = c(1,1,5/9), kelvin = c(1,1,5/9), fahrenheit = c(9/5,9/5,1)))
            conv.add2 <- data.frame(list(celsius = c(0,-273.15,0), kelvin = c(273.15,0,273.15), fahrenheit = c(32,32,0)))
            from.index <- which(c("celsius", "kelvin", "fahrenheit") == tolower(from.unit))[1]
            to.index <- which(c("celsius", "kelvin", "fahrenheit") == tolower(to.unit))[1]
            to.temp <- (from.temp + conv.add1[from.index, to.index])* conv.mult[from.index, to.index] + conv.add2[from.index, to.index]
    
        if(as.json) res <- jsonlite::toJSON(list(from=list(temp=from.temp, unit=from.unit)), to=list(temp=to.temp, unit=to.unit))
            else res <- to.temp
            return(res)  
  }
  else {
    stop("Arguments from.unit and to.unit must be \"Celsius\",
        \"Kelvin\" or \"Fahrenheit\".")
  }
}

Die Idee hinter dieser Funktion ist, dass sich jede Temperatur-Umrechnung in drei Schritte zerlegen lässt: Die Addition eines Summanden zum Ausgangswert (also der umzurechnenden Temperatur), die Multiplikation dieses neuen Werts mit einem Faktor und eine weitere Addition eines Werts. Im Fall der Umrechnung von Celsius in Fahrenheit zum Beispiel lautet die Umrechnungsvorschrift: Fahrenheit = (Celsius * 9/5)+32. Damit wäre der erste Summand 0 (es wird zum Celsius-Wert zunächst nichts addiert), der Faktor wäre 9/5 und der zweite Summand 32. Die Summanden für alle möglichen kreuzweisen Umrechnungen zwischen den von unserer Funktion verarbeiteten Temperaturskalen Celsius, Fahrenheit und Kelvin werden in den Dataframes conv.add1, conv.add2 gespeichert, die Faktoren im Dataframe conv.mult.

Um eine Zusammenfassung der Umrechnung als JSON-Objekt zurückzugeben, wird die Funktion toJSON aus dem jsonlite-Package aufgerufen. Beachten Sie dabei die Notation package::funktion(), die es uns erlaubt, einfach eine Funktion aus einem anderen Package aufzurufen. Das ist das übliche Vorgehen, wenn aus einem Package heraus Funktionalität eines anderen Packages verwendet wird. Wir könnten unsere Funktion nun beispielsweise so aufrufen:

tmp <- convert.temp(300.7, from.unit = "Kelvin", to.unit = "Celsius", as.json = TRUE)

Dann würde wir in tmp folgendes JSON-Objekt erhalten:

{
  "from": {
    "temp": [300.7],
    "unit": ["Kelvin"]
  },
  "to": {
    "temp": [27.55],
    "unit": ["Celsius"]
  }
}

 

Die Dokumentation

Die Dokumentation des Packages und seiner Funktionen ist das, was der Benutzer sieht, wenn er ?tempconvertr (für das Package) oder ?convert.temp (für die Funktion) in die R-Konsole eingibt (oder alternativ die Hilfe mit help() aufruft). Im Fall unserer Funktion convert.temp() könnte die Hilfeseite aussehen wie in Abb. 3 dargestellt. Der Quellcode der Dokumentation liegt im Package-Verzeichnis /man. Er besteht aus Dateien des Typs .Rd (R documentation) und wird in einer Sprache verfasst, die an LaTex angelehnt, aber ungleich einfacher zu handhaben ist. Neben der Darstellung für die Online-(Konsolen-)Hilfe lässt sich diese auch zu HTML und PDF rendern.

Der Rd-Quellcode kann entweder direkt geschrieben werden, oder er wird mit Hilfe von roxygen (das auch Bestandteil des devtools-Package ist) aus speziellen Kommentaren erzeugt, die in den R-Quellcode des Packages eingebaut werden. Das hat den Vorteil, dass Code und Dokumentation immer in derselben Datei vorliegen; notwendige Updates der Dokumentation nach Änderungen am Code geraten so weniger schnell aus dem Blickfeld. In unserem Beispiel könnte folgendes als roxygen-Kommentare im Code unseres Packages stehen:

#' @title Package 'tempconvertr'
#'
#' @description Comfortable conversion of temperatures
#'
#'  This package provides functionality to convert temperatures #'  between degrees Celsius, degrees Fahrenheit and Kelvin in any #'  direction. Its main function is \code{\link{convert.temp}()}.
#'
#'@name tempconvertr
NULL


#' @title Converting Temperatures
#' @description Converts temperatures temperatures between degrees #'   Celsius, degrees Fahrenheit and Kelvin.
#'
#' @param from.temp Temperature value to be converted.
#' @param from.unit Scale of the temperature value to be converted. #'   Either \code{"Celsius"} (default), \code{"Fahrenheit"} or #'   \code{"Kelvin"}.
#' @param to.unit Target temperature scale. Either \code{"Celsius"} #'   (default), \code{"Fahrenheit"} or \code{"Kelvin"}.
#' @param as.json Indicates if function returns JSON object #'   describing the conversion instead of just the converted #'   temperature value.
#'
#' @return Either the converted temperature value or a JSON object #'   (see Details section).
#'
#' @details If \code{as.json==TRUE} then \code{convert.temp()} #'   returns a JSON object containing two sub-objects \code{from} and #'   \code{to} each of which has a \code{temp} and a \code{unit} #'   field to describe the temperature. The \code{from.unit} and #'   \code{to.unit} arguments are case-insensitive.
#'
#' @examples
#' result.fahrh.json <- convert.temp(24.5, from.unit="celsius", #'   to.unit="fahrenheit", as.json=TRUE)
#' result.kelv <- convert.temp(24.5)
#'
#' @export
convert.temp <- function(from.temp, from.unit = "Celsius", to.unit = "Kelvin", as.json = FALSE) {
# Hier folgt der Code der Funktion…
}

Jeder Block von roxygen-Kommentaren dokumentiert das, was unmittelbar hinter ihm folgt; der erste kommentiert das Package, deshalb ist er dem Wert NULL vorangestellt (das Package als solches hat ja im Code keine echte Entsprechung), der zweite die Funktion convert.temp(). roxygen-Kommentare sind echte R-Kommentare, werden vom Interpreter bei der Ausführung des Codes also ignoriert; das Apostroph hinter dem Kommentarsymbol # weist sie aber als roxygen-Kommentare aus. Die mit @ eingeleiteten Schlüsselwörter (tags) haben eine spezielle Bedeutung für roxygen:

  • @title: der Titel des Hilfeartikels.
  • @description: eine kurze Beschreibung des Packages bzw. der Funktion.
  • @param: Erläuterungen zu einem Funktionsargument.
  • @return: Erläuterungen zum Rückgabewert der Funktion.
  • @details: Details zur Arbeitsweise der Funktion oder zum implementierten Algorithmus.
  • @examples: lauffähige Beispiele, die die Anwendung der Funktion demonstrieren.

Das @export-Tag spielt keine Rolle für die Dokumentation, sondern hat eine andere Bedeutung, die wir uns gleich genauer ansehen werden, wenn wir uns mit dem Namespace von R-Packages beschäftigen. Zwischendurch sieht man im roxygen-Code LaTex-artige Anweisungen der Form \keyword{…}; hierbei handelt es sich um originäre Rd-Formatierungsanweisungen, die eins zu eins in den Rd-Code übernommen werden, wenn dieser aus den roxygen-Kommentaren erzeugt wird.

Genau das geschieht entweder, indem man devtools::document() aufruft, oder, indem man über das Menü Build | Build Source Package das Package vollständig zusammenbauen lässt (das schauen wir uns weiter unten noch genauer an). Im letzteren Fall ist es wichtig, zuvor in den Projektoptionen (Menü Tools | Project Options) unter der Rubrik Build Tools die Option Generate Documentation with Roxygen und im sich dann öffnenden Dialog die Option Automatically roxygenize when running: Build and Reload markiert zu haben, wie es in Abb. 4 zu sehen ist, ansonsten wird der Rd-Code der R-Dokumentation nicht automatisch aus den roxygen-Kommentaren erzeugt, wenn man das Paket vollständig zusammenbaut. Welchen Weg auch immer man wählt, es entstehen zwei .Rd-Dateien im /man-Verzeichnis des Packages, eine für das Package, eine für die Funktion temp.conv(). Letztere sieht folgendermaßen aus:

% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/tempconv.r

\name{convert.temp}
\alias{convert.temp}
\title{Converting Temperatures}
\usage{convert.temp(from.temp, from.unit = "Celsius", to.unit = "Kelvin", as.json = FALSE)
}
\arguments{
\item{from.temp}{Temperature value to be converted.}
\item{from.unit}{Scale of the temperature value to be converted. Either \code{"Celsius"} (default), \code{"Fahrenheit"} or \code{"Kelvin"}.}
\item{to.unit}{Target temperature scale. Either \code{"Celsius"} (default), \code{"Fahrenheit"} or \code{"Kelvin"}.}
\item{as.json}{Indicates if function returns JSON object describing the conversion instead of just the converted temperature value.}
}

\value{Either the converted temperature value or a JSON object (see details).
}

\description{Converts temperatures temperatures between degrees Celsius, degrees Fahrenheit and Kelvin.
}

\details{If \code{as.json==TRUE} then \code{convert.temp()}  returns a JSON object containing two sub-object \code{from} and \code{to} each of    which has a \code{temp} and a \code{unit} field to describe the temperature. The \code{from.unit} and \code{to.unit} arguments are    case-insensitive.
}

\examples{result.fahrh.json <- convert.temp(24.5, from.unit="celsius", to.unit="fahrenheit", as.json=TRUE) result.kelv <- covert.temp(24.5)
}

Man erkennt leicht, wie die roxygen-Kommentare und der aus ihnen erzeugte Rd-Code zusammenhängen. Zunächst einmal sieht man im Rd-Code Anweisungen, die die einzelnen Abschnitte (Sections) der Hilfe-Seite repräsentieren, wie etwa \details{…} für die Detailbeschreibung der Funktion. Eine Besonderheit dabei ist der Usage-Abschnitt. Er muss nur dann separat angelegt werden, wenn man den Rd-Code von Hand schreibt, nicht aber, wenn man mit roxygen arbeitet, das diese Aufgabe von selbst übernimmt. Weitere Abschnitte können in Rd mit \section{…} leicht hinzugefügt werden, bzw. mittels des Tags @section, wenn man die Dokumentation mit roxygen entwickelt.

Beispiele, die im Examples-Abschnitt stehen, müssen lauffähig sein; beim Upload Ihres Packages auf CRAN wird genau das getestet. Manchmal hat man zwar lauffähige Beispiele, möchte aber nicht, dass diese tatsächlich ausgeführt werden, etwa, wenn das Beispiel eine lange Laufzeit hat, die zu unschönen Warnungen beim Upload auf CRAN führt, oder wenn das Beispiel etwas tun müsste, was gemäß den CRAN-Richtlinien in Beispielen nicht auftreten darf, wie zum Beispiel das Öffnen des Webbrowsers. In diesen Fällen kann das Beispiel in die Rd-Anweisung \dontrun{…} eingebettet und sein Test beim Upload auf CRAN so verhindert werden.

Neben den Anweisungen zum Erzeugen der Abschnitte finden sich eine Reihe von Formatierungs-Anweisungen; die wichtigsten sind:

  • \code{…}: Stellt einen Text als Quellcode dar.
  • \item{…}: Erzeugt einen Bullet-Punkt.

Daneben existieren noch etliche weitere Rd-Anweisungen, wie zum Beispiel \strong{…} und \emph{…} für das Fett- bzw. Kursivsetzen von Text,  \link{…}, das es erlaubt, auf die Hilfeseiten anderer Funktionen zu verweisen (zum Beispiel mit \link{convert.temp}) oder \url{…}, das einen Link auf eine Webseite erzeugt. Alle diese Rd-Anweisungen können auch in den roxygen-Kommentaren verwendet werden, sodass Sie den von roxygen erzeugten Rd-Code gar nicht mehr anpassen müssen (er würde beim nächsten Lauf von roxygen ohnehin wieder überschrieben werden).

R-Packages und die von ihnen bereitgestellten Funktionen müssen über diese Form der Dokumentation verfügen. Daneben gibt es mit den sogenannten Vignettes noch eine weitere, optionale Möglichkeit der Dokumentation. Vignettes sind Dokumente, die dazu gedacht sind, systematisch in ein Package einzuführen, seinen Hintergrund, Zweck, Herangehensweise und Anwendung im Zusammenhang zu erläutern und zu demonstrieren. Sie werden in R Markdown geschrieben, einer einfachen Markup-Sprache, deren Formatierungsanweisungen im "Code" nicht weiter auffallen, sodass ein solches Dokument auch im unkompilierten "Rohzustand" gelesen werden kann. So werden Top-Level-Überschriften in Markdown zum Beispiel mit # markiert, die beiden darunter liegenden Überschriftenebenen mit ## und ###. Während Markdown in vielen Kontexten Anwendung findet, ist das Besondere an R Markdown (Dateinamenserweiterung .Rmd), dass es erlaubt, R-Code zu integrieren, der dann einschließlich der Outputs seiner Ausführung mit in das Dokument hineingeschrieben wird. Vignettes können erzeugt werden, indem devtools::use_vignette() aufgerufen wird. Dadurch wird ein Verzeichnis /vignettes im Package-Ordner sowie ein einfaches Vignette-Template generiert. Vignettes können als HTML- und PDF-Output gerendert werden. Einen Überblick über die durch die installierten Packages mitgebrachten Vignettes verschafft man sich als Anwender mit vignette(), zur Anzeige (PDF-Format) führt die Eingabe von vignette(vignette.name) in die R-Konsole.

Zur Dokumentation im weiteren Sinne dient auch die (ebenfalls Markdown-)Datei README, die wir uns einige Abschnitte weiter unten anschauen werden. Mit Hilfe des Packages pkgdown kann sogar als weitere Form der Dokumentation auf einfache Art und Weise eine Website für das Package aufgesetzt werden.

Die DESCRIPTION-Datei

Eine wichtige Komponente eines jeden Packages ist seine DESCRIPTION-Datei. Wie der Name bereits andeutet, enthält diese Datei Meta-Informationen über das Package. Für unser Package könnte eine schlanke Version dieser Beschreibung, die (neben einer Importangabe) nur die sieben Pflichtfelder enthält, die jedes Packages bereitstellen muss, so aussehen:

Package: tempconvertr
Title: Converting Temperature Values Easily
Version: 0.1.1
Author: Joachim Zuckarelli
Maintainer: Joachim Zuckarelli <joachim@nevereverreply.com>
Description: Convert temperatures between degrees Celsius, degrees Fahrenheit and Kelvin.
License: GPL-3
Imports: jsonlite

Die Informationen der DESCRIPTION-Datei sind eine wichtige Grundlage für die CRAN-Package-Seite, die auf dem CRAN-Server verfügbar wäre, würden wir das Package tatsächlich auf CRAN hochladen. Die Abb. 5 zeigt ein Beispiel einer solchen Übersichtsseite, nämlich die des devtools-Packages.

Die wichtigsten Felder einer DESCRIPTION-Datei sind die folgenden:

  • Package (Pflichtfeld): Der Name des Packages.
  • Title (Pflichtfeld): Eine Kurzbeschreibung (idealerweise nur ein Satz). Zentrale Begriffe müssen großgeschrieben sein (title case). Die Funktion tools::toTitelCase() hilft dabei, die Groß- und Kleinschreibung richtig einzusetzen.
  • Version (Pflichtfeld): Üblicherweise dreigliedrige Versionsnummer der Form H.K.B, zum Beispiel 1.0.7. H wird bei großen Updates inkrementiert, K bei kleineren, B bei Bugfixes/Patches.
  • Author (Pflichtfeld): Der Autor des Packages. Kann ersetzt werden durch Authors@R.
  • Maintainer (Pflichtfeld): Hauptansprechpartner des Packages; oft, aber nicht notwendigerweise identisch mit dem Author. Muss über eine E-Mail-Adresse verfügen, wie im Beispiel oben (eingeschlossen in Größer-/Kleiner-Zeichen). Kann ersetzt werden durch Authors@R.
  • Authors@R: Alternative Möglichkeit, Personen und ihre Rolle mit Hilfe der R-Funktion person() anzugeben, zum Beispiel: person("Joachim", "Zuckarelli", role = c("aut", "cre"), email = "joachim@nevereverreply.com"). Der Argument-Vektor role kann mehrere Rollen beschreiben; die wichtigsten sind aut (Autor), crea (Creator = Maintainer), ctb (Contributor). Die Person, die als Maintainer (crea) ausgewiesen ist, muss über eine E-Mail-Adresse verfügen.
  • Description (Pflichtfeld): Eine Beschreibung des Packages, die detaillierter ist als der Title. Sie sollte erläutern, wozu genau das Package nützlich ist und was seine besonderen Features sind (die es möglicherweise von anderen Packages, die einen ähnlichen Zweck erfüllen, abheben).
  • License (Pflichtfeld): Die Lizenz, unter der das Package verfügbar sein soll. Meistens eine der bekannteren Standardlizenzen, GNU Public License (GPL-2, GPL-3), MIT (MIT) oder Creative Commons (CC). Die Website Choose a License hilft bei der Auswahl einer geeigneten Lizenz [8]. Möchte  man eine eigene Lizenz einsetzen, muss hier auf eine Datei LICENSE verwiesen werden, die dann ebenfalls Bestandteil des Packages wird und den Lizenztext beinhaltet. Eine LICENSE-Datei benötigt man auch, wenn man die MIT-Lizenz verwendet, in die als Template das Jahr und der Name des Rechteinhabers eingetragen werden muss.
  • BugReports: Link zu (genau) einer Seite, über die Bugs berichtet werden können; hier bietet sich die Issues-Seite auf GitHub an, wenn das Package in einem GitHub-Repository abgelegt wird.
  • URL: Eine oder mehrere weitere URLs zu Webseiten, die sich mit dem Package beschäftigen, zum GitHub-Repository, Tutorials oder anderen Materialien; mehrere URLs werden durch Kommata separiert.
  • Imports: Packages, von denen das eigene Package abhängig ist; in unserem Beispiel ist das das Package jsonlite, aus dem wir die Funktion toJSON() verwenden. Packages, die hier als Abhängigkeiten (dependencies) angegeben sind, werden automatisch mitinstalliert, wenn der Benutzer unser Package mit install.packages("tempconvertr", dependencies=TRUE) installiert. In Klammern kann optional eine mindestens erforderliche Versionsnummer angegeben sein, z. B. jsonlite (>= 1.6.0).
  • Depends: Wurde früher generell statt Imports verwendet. Wird heute noch dazu benutzt, Abhängigkeiten von einer R-Mindestversion anzugeben, z. B. R (>= 3.4.0). Gibt man ein Package hier statt in Imports an, wird es vollständig geladen (also dem Benutzer verfügbar gemacht), sobald unser Package geladen wird; das ist aber oft gar nicht nötig, wenn nicht davon auszugehen ist, dass der Benutzer unseres Packages mit dem importierten Package selbst arbeiten will.

Neben diesen am häufigsten verwendeten Feldern gibt es noch einige weitere Standardfelder. Außerdem kann man auch selbst Felder hinzufügen, die dann ebenfalls auf der CRAN-Seite des Packages erscheinen.

Die NAMESPACE-Datei

Beim Namespace des Packages geht es darum, welche Funktionen das Package exportiert (also für den Anwender bereitstellt) und welche es importiert (also aus anderen Packages verwendet).

Oftmals will man nicht alle Funktionen, die in einem Package enthalten sind, für den Benutzer des Packages sichtbar machen; das gilt zum Beispiel für Hilfsfunktionen, die man nur zur Arbeitserleichterung verwendet, und die keine Kernfunktionalität des Packages darstellen. Funktionen, die tatsächlich exportiert, also dem Anwender des Packages zur Verfügung gestellt werden sollen, benötigen einen export-Eintrag in der NAMESPACE-Datei. Der könnte für unsere Funktion temp.convert() so aussehen: export(temp.convert). Anstatt die Datei NAMESPACE direkt zu bearbeiten, kann man aber auch einfach (wie wir es ja weiter oben auch getan haben) das roxygen-Tag @export in den roxygen-Kommentar-Block, der der Funktion vorangestellt ist, einfügen und dann roxygen das Erzeugen einer sauberen und syntaktisch korrekten NAMEPACE-Datei überlassen. Das geschieht automatisch, wenn wir roxygen::document() aufrufen oder das Package über das Menü Build | Build Source Package zusammenbauen lassen, ebenso wie bei der Erzeugung der Dokumentation.

Importe aus anderen Packages werden normalerweise in zwei Schritten realisiert: Zum einen dadurch, dass das Package, aus dem Funktionen importiert werden sollen, in der DESCRIPTION-Datei entweder unter Imports oder (seltener) unter Depends verzeichnet wird. Dadurch ist sichergestellt, dass das Package auch verfügbar ist, wenn unser Package installiert wird. Zum anderen signalisieren wir in unserem Code mit Hilfe des ::-Operators, dass wir explizit auf eine Funktion aus einem anderen Package zugreifen wollen. Genauso geschieht es in unserem Code, wo mit jsonlite::toJSON() eine Funktion aus dem importierten jsonlite-Package aufgerufen wird. Die Notation mit dem ::-Operator stellt sicher, dass das Package auch geladen ist, wenn wir es benötigen und macht zudem transparent, woher die Funktion kommt, die wir hier verwenden (was erfahrungsgemäß hilfreich sein kann, wenn man seinen Code nach einiger Zeit nochmal anschaut!).

Die README-Datei

Es ist gute Praxis, aber keine Notwendigkeit, seinem Package eine README-Datei mitzugeben, in der eine Kurzanleitung zur Benutzung (und wenn notwendig, auch zur Installation/Konfiguration) des Packages enthalten ist. Eine gute README-Datei versetzt den Anwender mit Beispielen in die Lage, das Package rasch für die eigene Arbeit einzusetzen. Die README-Datei ist entweder eine Text-Datei oder eine Markdown-Datei (letzteres ist zu empfehlen). Liegt das Package in einem GitHub-Repository, fungiert die README-Datei zugleich als Startseite für das Repository und begrüßt so die (potentiellen) Anwender.

Die NEWS-Datei

Die NEWS-Datei dient v. a. dazu, eine Änderungshistorie bereitzustellen und über Neuerungen zu informieren. Genauso wie README ist sie optional und wird üblicherweise im Markdown-Format verfasst. Gute Praxis ist es, bei jedem neuen Versionseintrag zu unterscheiden zwischen größeren und kleineren Veränderungen sowie Bugfixes (also den Faktoren, die auch die Versionsnummerierung beeinflussen sollten). In unserem Beispiel könnte die NEWS-Datei so aussehen:

# tempconvertr 0.1.1
## Minor changes
* Error handling for non meaningful from.temp and to.temp arguments in function convertTemp() added.
## Bug fixes
* Error in conversion logic Kelvin -> Fahrenheit fixed.
# tempconvertr 0.1.0
First release.

Das Package testen

Tests auf Funktionsfähigkeit

Nachdem das Package entwickelt ist, geht es nun ans Testen. Ziel dabei ist zum einen natürlich, die Funktionsfähigkeit sicherzustellen, zum anderen aber auch die Einhaltung der CRAN-Richtlinien zu gewährleisten, denn nur, wenn diese gegeben ist, wird das Package am Ende auch zur Publikation auf CRAN akzeptiert werden.

Der Test der eigentlichen Funktionalität des Packages kann selbstverständlich "von Hand" erfolgen (ggf. unter Verwendung von Hilfsmitteln wie den Ausgabefunktionalitäten, die das debugr-Package bereitstellt). Nachdem Sie Änderungen am Package-Code vorgenommen haben, können Sie mit devtools::load_all() das Package laden, als hätte der Benutzer es mit library(tempconvertr) geladen. Auf diese Weise kann man den Effekt von Änderungen schnell überprüfen.

Mit Hilfe des ebenfalls zur devtools-Sammlung gehörenden testthat-Packages können aber auch automatisiert Tests durchgeführt werden. Die Grundidee dabei ist, einen Testcase zu definieren und das Ergebnis von dessen Durchführung automatisch mit einem erwarteten Ergebnis zu vergleichen.

Um testthat zu verwenden, genügt ein einfacher Aufruf von devtools::use_testthat(). Das erzeugt ein neues Verzeichnis /tests im Package-Ordner und darin eine Datei testthat.r sowie ein Unterverzeichnis /testthat. In diesem Unterverzeichnis werden die R-Dateien abgelegt, in denen die eigentlichen Tests enthalten sind. Deren Name muss zwingend mit test anfangen, anders benannte Dateien werden beim Ausführen der Tests ignoriert.

Ein Test besteht aus einem Aufruf der Funktion test_that(), die neben einer Bezeichnung des Tests als zweites Argument einen Code-Block mit dem Aufruf einer oder mehrerer Erwartungsfunktionen enthält. Jeder Erwartungsfunktion nimmt zwei Argumente, einen Aufruf der zu testenden Funktionen und eine Erwartung über das Ergebnis dieses Aufrufs:

test_that("Kelvin conversions", {
  expect_equal(convert.temp(from.temp=0, from.unit="Kelvin",     to.unit="Celsius"), -273.15)
  expect_equal(convert.temp(from.temp=-273.15, from.unit="Celsius",     to.unit="Kelvin"), 0)
})

In unserem Beispiel nehmen wir jeweils das Ergebnis der Temperatur-Umrechnung und vergleichen es mit einem gegebenen Wert. Neben expect_equal() existieren noch eine Reihe weiterer Erwartungsfunktionen, die zu Abprüfung anderer Konstellationen dienen, darunter auch zum Test der Ausgabe von (Fehler-, Warn- oder sonstigen) Meldungen.

In jeder Testdatei können mehrere solcher test_that()-Blöcke enthalten sein. Ausgeführt werden die Tests, indem man devtools::test() aufruft. Die Ergebnisse werden dann in der R-Konsole angezeigt.

Automatisierte CRAN-Tests

Neben der Funktionalität muss auch geprüft werden, ob das Package die automatisierten Eingangstests besteht, die es durchlaufen wird, wenn es auf CRAN hochgeladen wird. Diese Eingangstests prüfen eine Vielzahl unterschiedlicher Kriterien, beispielsweise, ob die erforderlichen DESCRIPTION-Felder vorhanden sind und ihr Format der Erwartung entspricht, ob der R-Code und die .Rd-Dateien frei von Syntaxfehlern sind, und ob die Beispiele in der Dokumentation lauffähig sind. Um hier ärgerliche Fehler zu vermeiden, besteht die Möglichkeit, das Package bereits vor dem Upload denselben Tests zu unterziehen, die auch von CRAN angewendet werden. Das geschieht entweder, indem man devtools::check() aufruft oder den Menüpunkt Build | Check Package in RStudio ansteuert. Das Ergebnis des Checks ist eine Meldung etwaiger Fehler (errors), Warnungen (warnings) und Hinweise (notes). Fehler und Warnungen müssen vor dem Upload auf CRAN beseitigt werden; gleiches sollte man, wenn irgend möglich, mit den Hinweisen tun, die regelmäßig den Upload-Prozess dadurch verlängern, dass sie einen manuellen Eingriff durch einen der CRAN-Betreuer notwendig machen, der sich das Problem anschaut, bevor er den Upload freigibt.

Damit sichergestellt ist, dass das Package die Tests nicht nur auf dem System, auf dem es entwickelt worden ist, besteht, sondern auch auf anderen Systemen, die man möglicherweise selbst nicht im Zugriff hat, gibt es den R-Hub-Service. Durch ihn wird ein Package automatisiert auf unterschiedlichen Plattformen geprüft und die Ergebnisse dieser Tests dann per E-Mail an Sie zurückgespielt. Benutzen lässt sich der R-Hub ganz bequem durch das gleichnamige Package rhub. Nach Installation des Packages lässt sich der Prüfprozess mit rhub::check_for_cran(path = "C:/packages/tempconvertr", email = "joachim@nevereverreply.com") starten. Das erste Argument von check_for_cran() ist dabei der Pfad zum Package-Ordner. Es dauert dann in der Regel um die 30 Minuten, bis alle E-Mails mit den Ergebnissen der Tests auf den unterschiedlichen Plattformen eingegangen sind.

Nicht wundern sollte man sich übrigens darüber, dass beim Test eines neuen Packages, das bislang noch nie erfolgreich auf CRAN hochgeladen wurde, immer eine Note entsteht. Das ist durchaus beabsichtigt, überhaupt kein Grund zur Sorge und führt lediglich dazu, dass der CRAN-Betreuer den Neuankömmling nochmal genauer unter die Lupe nimmt.

Das Package auf CRAN hochladen

Der letzte Schritt der Package-Entwicklung besteht im Upload auf CRAN. Zuvor sollte das Package alle Tests erfolgreich absolviert haben. Im Anschluss kann dann mit devtools::build() ein sogenanntes Bundled-Package in Form einer .tar.gz-Datei erzeugt werden. Es enthält die Quelldateien des Packages mit Ausnahme der RStudio-Projektdatei und aller Dateien, die durch den Inhalt von .RBuildignore (sofern vorhanden) erfasst sind; ansonsten unterscheidet es sich von der Entwicklungsversion, dem Source-Package, mit dem wir die ganze Zeit gearbeitet haben, nur dadurch, dass Vignettes bereits zu PDF und HTML kompiliert sind (sofern das Package über Vignettes verfügt).

Dieses Bundled-Package kann nun auf CRAN hochgeladen werden. Installiert man das Package später, so lädt man von CRAN allerdings nicht das Bundled-Package herunter, sondern ein plattformabhängiges, sogenanntes Binary-Package, das bzgl. der Verzeichnisse eine etwas andere Struktur aufweist und in dem der R-Code und die Dokumentation in ein binäres Format codiert wurden. Das Bundled-Package einschließlich des R-Codes kann natürlich weiterhin von der Package-Seite auf CRAN bezogen werden.

Der Upload des fertigen Bundled Package, das man sicherheitshalber nochmals mit devtools::build() oder über das Menü Build | Build Source Package erzeugen sollte, wird mit der Funktion devtools::release() bewerkstelligt, oder aber manuell über das Submission Form auf CRAN [9]. Bei beiden Vorgehensweisen muss man bestätigen, dass man die CRAN Policies [10] gelesen hat, und das Package diesen genügt.

Danach erhält man eine automatisch generierte Eingangsbestätigung per E-Mail. In den folgenden Tagen schauen sich die CRAN-Betreuer das Package an und geben – wenn alles in Ordnung ist – den Upload auf CRAN frei; ein Upload eines Updates für ein bereits auf CRAN vorhandenes Package, der keine Errors, Warnings oder Notes hervorruft, geht in der Regel deutlich schneller. Wenn es Probleme gibt, meldet sich der zuständige CRAN-Betreuer per E-Mail. In Abhängigkeit der handelnden Personen ist die Kommunikation in der Regel extrem knapp (was natürlich auch dem Umstand geschuldet ist, dass die CRAN-Betreuung ein Ehrenamt und "dank" der Popularität von R die Zahl der pro Woche neu eingereichten Packages durchaus nicht unerheblich ist). Diese knappe und auf alles Unnötige verzichtende Kommunikation ist aber in der Regel vollkommen in Ordnung. Wenn ein erneutes Hochladen des Packages notwendig ist, nachdem etwaige Probleme behoben wurden, empfiehlt es sich, über das Kommentarfeld der Webseite (oder über eine Datei cran-comments.md, wenn man mit den devtools arbeitet), darauf hinzuweisen, dass es sich um eine Resubmission handelt. Eine cran-comments.md-Markdown-Datei im Package-Verzeichnis zu haben, bietet sich übrigens auch dann an, wenn man Notes hat, die man nicht beheben kann; in diesem Fall sollte man den Sachverhalt kurz in cran-comments.md erläutern.

Wenn das Package dann schließlich auf CRAN verfügbar ist (Glückwunsch!), möchte man natürlich auch wissen, ob es auch tatsächlich heruntergeladen wird. Das ist leicht möglich anhand der Server-Logs des RStudio-CRAN-Mirrors, die mit Hilfe des cranlogs-Packages oder über eine REST-API abgefragt werden können. So würde die HTTP-Query tempconvertr ein JSON-Objekt mit den Downloadzahlen von Anfang Dezember 2019 bis Ende Januar 2020 liefern.

Zusammenfassung: Eine Checkliste

Hier nochmal die wichtigsten Schritte bei der Package-Entwicklung als Checkliste (der Einfachheit halber unter Verwendung der devtools):

  • Package-Grundstruktur anlegen mit devtools::create()
  • Code schreiben; dabei entscheiden, welche Funktionen exportiert werden sollen, diesen ein @export-Tag für roxygen mitgeben
  • Tests entwickeln und devtools::test() ausführen
  • Dokumentation schreiben (entweder als .Rd-Dateien oder als roxygen-Kommentare), ggf. Dokumentation mit devtools::document() aus roxygen-Kommentaren erzeugen
  • Meta-Daten in DESCRIPTION pflegen
  • README-Datei schreiben
  • NEWS-Datei schreiben
  • Bundled-Package mit devtools::build() erzeugen
  • Gesamt-Package mit devtools::check() auf eigenem System prüfen
  • Package mit rhub::check_cran() auf anderen Plattformen prüfen
  • Package mit devtools::release() auf CRAN hochladen.

Wer tiefer in die Entwicklung von R-Packages einsteigen möchte, dem sei das Buch R Packages von Hadley Wickham empfohlen [11]; noch detaillierter befasst sich die vom R-Core-Team bereitgestellte Dokumentation Writing R Extensions[12] mit der Thematik.

Autor

Joachim Zuckarelli

Joachim Zuckarelli beschäftigt sich seit über 12 Jahren mit R. Heute ist er als Leiter Business Intelligence für einen Tierklinik-Betreiber tätig.
>> Weiterlesen

Kommentare (0)

Neuen Kommentar schreiben