Bei einem Data Science-Projekt besteht der häufigste Ansatz darin, eine Reihe von Skripten zu schreiben, um die Daten zu untersuchen, statistische Modelle zu entwickeln und die Ergebnisse anzuzeigen. Nach einer Weile kann unser Code leicht aus dem Ruder laufen, weil die Skripte zu zahlreich (und/oder zu lang) sind, und wir könnten mit Problemen wie diesen konfrontiert werden:
- es ist schwierig, unseren Code mit Mitarbeitern zu teilen, die möglicherweise nicht verstehen, was vor sich geht
- es ist leicht, gut versteckte Fehler zu machen, die möglicherweise erst nach Monaten ans Licht kommen, was selten angenehm ist
- für unser nächstes Projekt werden wir höchstwahrscheinlich wieder bei Null anfangen, auch wenn wir etwas Ähnliches machen werden
Die Paketierung unseres Codes adressiert all die oben genannten Punkte, denn es ist für Mitarbeiter einfach, ein Paket wie jede andere Bibliothek eines Drittanbieters zu installieren, wir können leicht ein Handbuch für unseren Code schreiben und wir werden in der Lage sein, viele Funktionen, die wir zuvor definiert haben, wiederzuverwenden, indem wir sie einfach mit library('mynewpackage') importieren.
In diesem praktischen Leitfaden werden daher die wichtigsten Tools und bewährten Verfahren vorgestellt, um ein R-Paket von Grund auf zu erstellen. Der folgende Inhalt wird klarer, wenn Sie zumindest ein wenig Erfahrung mit Konzepten der kontinuierlichen Integration / kontinuierlichen Entwicklung haben. Ein minimales Beispiel, aber vollständig in Bezug auf die Themen, die wir behandeln werden, könnte Lob oder sein wütender neugeborener kleiner Bruder Curser sein.
Erforderliche Pakete
Das Erstellen und Verwalten eines Pakets/einer Bibliothek kann über die R-Konsole erfolgen; alternativ können Sie kurze R-Skripte als Strings ausführen:
$ Rscript -e '<R code>'
Um ein neues Paket zu entwickeln, sind die folgenden Bibliotheken erforderlich oder zumindest nützlich.
- usethis: Es enthält verschiedene Funktionen für die Einrichtung und Entwicklung von Paketen
- devtools: Es enthält viele Funktionen von
usethisund fügt zusätzliche hinzu. - testthat: Framework für Unit-Tests für R-Pakete
- lintr: Code-Qualitätsprüfungen
- roxygen2: Dokumentation zum Paket hinzufügen
Mehr dazu später, aber zunächst können wir unsere Umgebung vorbereiten, indem wir sie mit dem folgenden Befehl in unserer R-Konsole installieren:
install.packages(c("usethis", "devtools", "testthat", "lintr", "roxygen2"))
Wenn Sie mit Docker vertraut sind, können Sie sich für die Entwicklung in einem Container entscheiden, um die Umgebung zu isolieren. Tolle Docker-Images finden Sie beim Projekt Rocker, und insbesondere das Image rocker/tidyverse enthält bereits die oben beschriebenen Abhängigkeiten, zusätzlich zum kompletten tidyverse-Ökosystem (es ist ein ziemlich großes Image). In Kombination mit der Möglichkeit, mit Visual Studio Code innerhalb von Containern zu entwickeln, halte ich dies für ein sehr gutes Setup, da es auf jedem Rechner in hohem Maße reproduzierbar ist und nicht spezifisch für R ist.
Das Paket erstellen
Beginnen wir damit, ein Paket zu erstellen, das wir in Ermangelung eines besseren Namens analytics nennen werden.
Der erste Schritt ist die Erstellung des Ordners, der ihn enthalten wird. Mit der Funktion
usethis::create_package("path/to/analytics")
erstellen wir einen Ordner namens analytics, der eine leere Paketstruktur enthält.
HINWEIS
Wenn Sie dies innerhalb eines git Projektarchivs tun, könnten Sie eine Beschwerde erhalten, da usethis erwartet, dass der Ordner ein git Projektarchiv ist. Ich bin jedoch der Meinung, dass diese Entscheidung den Entwicklern und dem Anwendungsfall überlassen werden sollte.
Zu diesem Zeitpunkt sollte der Ordner analytics den folgenden Inhalt haben:
ROrdner. Hier befinden sich die.RDateien mit Ihrem Code.DESCRIPTIONDatei. Metadaten über das Paket, wie Name, Version, Autor, erforderliche und vorgeschlagene Abhängigkeiten. Sie können auch die Lizenz angeben, und es gibt recht praktische Funktionen wieusethis::use_gpl3_license("path/to/analytics"), die diese Informationen für Sie ausfüllen können. Sie findenLICENSE.mdim Stammordner des Pakets.NAMESPACEDatei, die die importierten und exportierten Objekte beschreibt. Sie sollte nicht von Hand geändert werden und wird vonroxygen2verwaltet..RbuildignoreDatei. Wie der Name schon sagt, können Sie angeben, welche Dateien beim Erstellen des Pakets ignoriert werden sollen. Sie wird nicht automatisch von Anfang an erstellt, sondern ist möglicherweise ein Nebenprodukt einiger vorheriger Schritte, z. B. der Erstellung der Lizenzdatei.
Wir sind nun bereit, dem Paket Code hinzuzufügen. Alle .R Dateien mit dem Code müssen sich im Ordner R befinden, denn jede Datei innerhalb eines Unterordners von analytics/R wird ignoriert. Angenommen, wir benötigen eine Funktion, die die monatlichen Ausgaben jedes Kunden aus einem Datensatz mit Transaktionen zusammenfasst. Wir haben dann zum Beispiel:
calc_monthly_spend <- function(transactions) {
transactions %>%
dplyr::group_by(customer_id, month) %>%
dplyr::summarise(monthly_spend = sum(amount))
}
Dies könnte in einer Datei namens path/to/analytics/R/calc_monthly_spend.R geschehen. Wenn Sie der Datei den gleichen Namen wie der Funktion geben, ist es einfach zu finden, wo sie definiert ist, aber es sollte einen Kompromiss geben, um nicht Hunderte von Dateien zu haben.
Das Paket dokumentieren
Wir wollen auch nette Menschen sein und den Code, den wir schreiben, dokumentieren. Das Paket roxygen2 bietet einen Rahmen, um die Dokumentation von Objekten neben ihrer Definition zu verfassen und ein Handbuch zu erstellen, auf das Sie z.B. durch den Aufruf der Funktion help für das gewünschte Objekt zugreifen können.
Um auf das obige Beispiel zurückzukommen, vervollständigen wir es mit einer Beschreibung im Stil von roxygen2:
#' Calculate monthly spend
#'
#' A more extensive description
#'
#' @param transactions data.frame A data set with transactions indexed by customer and month
#'
#' @return data.frame A data set with aggregated monthly spend per customer
#'
#' @import dplyr
#'
#' @export
calc_monthly_spend <- function(transactions) {
...
}
Nach dem Titel und einer ausführlicheren Beschreibung (optional) sehen wir die Tags code>@param und code>@return zur Beschreibung der Ein- und Ausgabe. Abhängigkeiten sollten entweder mit code>@import angegeben werden, um ein komplettes Paket zu importieren, oder mit einer abgespeckten Version @importFrom packagename functionname, um nur eine bestimmte Funktion oder ein Objekt zu importieren. Schließlich teilt der code>@export Tag dem Paket mit, dass diese Funktion öffentlich sein wird und kann nach dem Import des Pakets mit import oder mit dem Doppelpunkt, wie in package::calc_monthly_spend, aufgerufen werden. Auf Funktionen/Objekte, die nicht exportiert werden, kann trotzdem auf ähnliche Weise zugegriffen werden, allerdings mit dem dreifachen Doppelpunkt :::.
Jetzt können wir die Dokumentation erstellen, indem wir
devtools::document("path/to/analytics")
Zwei Dinge werden passieren: ein Ordner path/to/analytics/man wird erscheinen, der eine R-markdown Datei mit der Dokumentation der Funktion enthält; die Datei NAMESPACE wird aktualisiert, um die Abhängigkeit von dplyr und der exportierten Funktion calc_monthly_spend zu spezifizieren.
Lassen Sie uns sehen, ob das funktioniert hat. Wir können unser Entwicklungspaket in einer R-Sitzung mit devtools::load_all("path/to/analytics") laden, ohne es zu erstellen, so dass es so importiert wird, als wäre es installiert und mit library(package) importiert worden. Wir sollten nun in der Lage sein, die Dokumentation der Funktion nachzuschlagen, die wir mit help(calc_monthly_spend) definiert haben.
Die Datei DESCRIPTION muss jedoch manuell aktualisiert werden, um dem Paket mitzuteilen, dass es von dplyr abhängt. Dazu müssen wir ein neues Tag Imports hinzufügen. Das Ergebnis sieht dann wie folgt aus (weitere Pakete als Beispiel importiert):
...
Imports:
dplyr (>= 1.0.0),
tibble (>= 1.0.0)
Sie können eine bestimmte Version einer Abhängigkeit korrigieren oder Ungleichungen verwenden, aber meines Wissens nach gibt es keine Möglichkeit, einen Versionsbereich anzugeben oder nur die Hauptversion zu korrigieren.
Schließlich können wir die Dokumentation auf Paketebene in einer separaten Datei hinzufügen, die möglicherweise nach dem Paket selbst benannt ist. Hier ist ein Beispiel:
#' Package
#'
#' Aggregate customer data and create amazing reports
#'
#' @docType package
#' @name package
"_PACKAGE"
Vergessen Sie nicht, devtools::document("path/to/analytics") aufzurufen, um die Dokumentation zu aktualisieren (ich vergesse das immer, was zu fehlgeschlagenen Builds führt).
Unit-Tests
Die Einbeziehung von Tests ist nie eine schlechte Idee. Mit Unit-Tests haben wir mehr Vertrauen, dass der Code, den wir geschrieben haben, das tut, was er tun soll, selbst wenn es sich nur um eine relativ kleine, aber repräsentative Teilmenge von Fällen handelt. Der schnellste Weg, die Testkomponente einzurichten, ist der Aufruf von
usethis::use_test("path/to/analytics")
aus dem Ordner, in dem die Tests erstellt werden sollen, in der Regel dem Paketordner. Sie werden einige neue Ordner und Dateien finden, wie z.B.:
analytics
├── ...
├── tests
│ ├── testthat.R
│ └── testthat
│ └── test-package.R
Im Ordner testthat müssen wir die Dateien .R mit den Tests ablegen, denen das Präfix test vorangestellt ist, während die Datei testthat.R automatisch generiert wird und die grundlegende Einrichtung des Test-Frameworks enthält.
Ein Test ist als Funktionsaufruf von testthat::test_that(description, code) definiert, dem eine Zeichenkette mit der Beschreibung des Tests und ein Codeblock als zweites Argument übergeben wird. Das Paket testthat bietet Funktionen zum Testen der Gleichheit auf mehreren Ebenen (auch mit Toleranz im Falle von Fließkommazahlen) oder Funktionen wie testthat::expect_true, die den Wahrheitsgehalt eines beliebigen booleschen Ausdrucks testen können.
Hier ein Beispiel für einen Test für unsere neue Funktion calc_monthly_spend, die Transaktionen pro Kunde und Monat aggregiert. Wir erstellen einen kleinen Datenrahmen mit einigen fiktiven Transaktionen, die wir von Hand aggregieren können, so dass wir die gewünschte Ausgabe erzeugen. Die Ausgabe der Funktion wird mit der gewünschten Ausgabe verglichen:
test_that("calc_monthly_spend works", {
transactions <- data.frame(
customer_id = c('A', 'A', 'B', 'B'),
month = c(202101L, 202101L, 202101L, 202101L),
amount = c(100.0, 200.0, 10000.0, 100.0)
)
desired_result <- data.frame(
customer_id = c('A', 'B'),
month = c(202101L, 202101L),
monthly_spend = c(300.0, 10100.0)
)
calculated_result <- as.data.frame(package::calc_monthly_spend(transactions))
test_that::expect_equal(desired_result, calculated_result)
})
Da wir nun einen grundlegenden Test für die einzige Funktion im Paket haben, können wir ihn mit
devtools::test("path/to/analytics")
Hoffentlich wird alles grün sein!
Linting
Unter Linting versteht man das Auffinden von stilistischen Fehlern im Code, potenziellen Bugs oder Entscheidungen, die gegen vordefinierte Konventionen verstoßen. Ein gängiges Beispiel ist das Erzwingen einer Begrenzung der Zeilenlänge, da begrenzte Zeilen die Lesbarkeit verbessern. Das Paket devtools kann auch viele Prüfungen im Code und in den Metadaten des Pakets automatisieren. Dies ist einfach eine Frage der Ausführung von
devtools::check("path/to/analytics")
der auch die Unit-Tests ausführt, so dass Sie devtools::test nicht separat aufrufen müssen.
Sie können die Codeanalyse auch mit dem Paket lintr erweitern. Um das vollständige Paket zu prüfen, können Sie
lintr::lint_package("path/to/analytics")
Wie zu erwarten, können Sie die Linting-Einstellungen anpassen, indem Sie eine .lintr Datei im Paketverzeichnis erstellen. Es gibt eine Vielzahl von Linting-Einstellungen, die Sie vornehmen können (siehe lintr auf github). Wir möchten zum Beispiel die maximale Zeilenlänge anpassen, um sicherzustellen, dass jeder Objektname in snake_case geschrieben wird, und wir möchten das Erfordernis der doppelten Anführungszeichen für Strings deaktivieren. Diese minimale .lintr Datei wird wie folgt aussehen:
linters: with_defaults(
line_length_linter(120),
object_name_linter(styles = "snake_case"),
single_quotes_linter = NULL
)
Ein letzter Hinweis: Es ist nicht unbedingt ein Problem für den nächsten Schritt, aber der .lintr Dateiname kann in .Rbuildignore aufgenommen werden, um mögliche Probleme (bei einigen Versionen Ihrer Abhängigkeiten) zu vermeiden.
Erstellen der Quelldatei
Der letzte Schritt dieses Tutorials zeigt, wie Sie den Quellcode eines Pakets erstellen. Das bedeutet, dass wir am Ende eine einzige .tar.gz Datei haben werden, mit der wir das von uns entwickelte Paket installieren können. Das Muster ist das gleiche wie immer:
devtools::build("path/to/analytics")
Sie können auch das optionale Argument path angeben, um festzulegen, wo der Quellcode gespeichert werden soll.
Das war's! Jetzt können Sie das erstellte Paket aus der Quelldatei installieren, indem Sie
install.packages("path/to/analytics/analytics_0.0.0.9000.tar.gz", type="source")
und es wird für den Standardimport verfügbar sein.
Schlussfolgerungen
In diesem Tutorial wird gezeigt, wie wir ein R-Paket erstellen, das wir weitergeben und für zukünftige Zwecke wiederverwenden können. Durch Tests und Codeanalyse erhalten wir einen Beweis dafür, dass sich unser Code wie erwartet verhält und ausreichend lesbar ist. Die Dokumentation hilft anderen Nutzern (oder Ihnen selbst), schnell zu verstehen, wie man die Funktionen und Objekte des Pakets verwendet. Im weiteren Verlauf möchten Sie vielleicht alle Ihre Prüfungen innerhalb einer CI/CD-Pipeline automatisieren. Das Paket usethis enthält Funktionen zur Erstellung von Vorlagen-Pipelines für die am häufigsten verwendeten Tools.
Referenzen
- Entwickeln von R-Paketen: HTML-Version des O'Reilly-Buches "Developing R Packages" (R-Pakete entwickeln)
- Jozef's Rblog: mehrere ausführlichere Artikel zu den von uns behandelten Themen
- verwenden: Benutzerhandbuch
- devtools: Benutzerhandbuch
- testthat: Benutzerhandbuch
- lintr: GitHub Repo
- roxygen2: Benutzerhandbuch
Unsere Ideen
Weitere Blogs
Contact




