Menu

Najczęstsze błędy w R i jak je debugować

Słownik klasycznych komunikatów o błędach w R (object not found, could not find function, non-numeric argument i inne) oraz tryCatch, traceback() i uczciwe debugowanie przez wypisywanie.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Jak czytać komunikat o błędzie w R

Błąd w R składa się z dwóch części i obie są przydatne:

Error in "10" + 5 : non-numeric argument to binary operator

Po Error in pojawia się wywołanie, czyli dokładny fragment kodu, który zawiódł ("10" + 5). Po dwukropku pojawia się warunek, czyli co poszło nie tak (non-numeric argument to binary operator). Najpierw przeczytaj wywołanie: mówi gdzie, a bardzo często problem widać od razu w cytowanym kodzie. Potem przeczytaj warunek, żeby poznać dlaczego.

Dwa nawyki odróżniają osoby, które debugują szybko, od tych, które się męczą. Po pierwsze, naprawdę czytaj komunikat: komunikaty R są zwykle precyzyjne, tylko zwięźle sformułowane. Po drugie, debuguj pierwszy błąd, nie ostatni: jedna awaria na początku skryptu wywołuje lawinę późniejszych błędów "object not found", które znikają wszystkie naraz, gdy naprawisz ten pierwszy. Reszta tej strony to słownik komunikatów, które spotkasz najczęściej, a potem narzędzia na sytuacje, gdy samo czytanie nie wystarcza.

Błędy nazw: not found

Error: object 'total' not found: R przeszukał każde znane środowisko i żadna zmienna nie ma tej nazwy. Trzy przyczyny wyjaśniają prawie każdy przypadek:

  • Literówka, także w wielkości liter. R rozróżnia wielkość liter: Total, total i TOTAL to trzy różne nazwy, a R nie zgadnie, o którą chodzi.
  • Linia definiująca nie została uruchomiona. W skrypcie jest total <- sum(x), ale ta linia nie została wykonana w tej sesji. To częste po restarcie R: plik skryptu wciąż pokazuje linię, ale sesja nigdy jej nie widziała. Uruchom skrypt od początku.
  • Złe środowisko. Zmienne utworzone wewnątrz funkcji żyją i giną razem z tym wywołaniem. Użycie takiej zmiennej poza funkcją to prośba o coś, co już nie istnieje; zamiast tego zwróć wartość.

Error: could not find function "read_excel": ten sam pomysł, ale dla nazwy funkcji. W dziewięciu przypadkach na dziesięć funkcja należy do pakietu, który jest zainstalowany, ale nie został wczytany w tej sesji:

library(readxl)          # the fix: loading is per-session, installing is per-machine
df <- read_excel("data.xlsx")

Jeśli samo library(readxl) zgłasza błąd, pakiet nie jest zainstalowany: najpierw install.packages("readxl"). A jeśli funkcja pochodzi z bazowego R, to literówka (lenght() to rytuał przejścia każdego programisty R).

Błędy składni: unexpected symbol

Error: unexpected symbol in "..." (i jego kuzyni unexpected ')', unexpected string constant) oznacza, że R nie potrafił nawet sparsować kodu. Komunikat wskazuje miejsce, w którym R zauważył problem, a to często leży za miejscem faktycznego błędu. Typowi podejrzani:

mean(x na.rm = TRUE)        # missing comma - should be mean(x, na.rm = TRUE)

name <- "Ada                # unclosed quote - swallows the following lines
total <- sum(c(1, 2, 3)     # unclosed paren - the error fires lines later

Gdy wskazana linia wygląda niewinnie, błąd prawie zawsze leży powyżej: niezamknięty cudzysłów, nawias okrągły lub klamrowy wcześniej w pliku. Edytor kodu, który podświetla pasujące pary, znajdzie to w kilka sekund.

Błędy typów i indeksowania

non-numeric argument to binary operator: wykonujesz obliczenia na czymś, co nie jest liczbą, zwykle na liczbie, która przyszła jako tekst (klasycznym źródłem są importy: kolumna z jednym zabłąkanym słowem wczytuje się jako character, co opisują typy danych). Wersja z błędem:

x <- "10"
x + 5
# Error in x + 5 : non-numeric argument to binary operator

I poprawka: najpierw konwertuj, potem licz:

subscript out of bounds: prosisz przez [[ ]] o pozycję n w czymś, co ma mniej niż n elementów:

scores <- list(ada = 92, grace = 88)
scores[[3]]
# Error in scores[[3]] : subscript out of bounds

Sprawdź length() przed indeksowaniem albo, jeszcze lepiej, pytaj po nazwie (scores[["grace"]]), żeby zmiana kolejności niczego nie zepsuła. Zwróć uwagę na asymetrię: pojedyncze nawiasy są bardziej wyrozumiałe, bo [ ] poza zakresem wektora po cichu zwraca NA zamiast błędu. To zamiana głośnego błędu na cichy.

$ operator is invalid for atomic vectors: $ należy do list i ramek danych. Na nazwanym wektorze używaj nawiasów:

([[ ]] daje samą wartość; [ ] zachowuje dołączoną nazwę.) Ten błąd często oznacza, że coś wcześniej zwróciło wektor, a spodziewana była ramka danych. Sprawdź to założenie, zamiast po prostu zmieniać operator.

argument is of length zero: if () dostał pusty warunek, prawie zawsze NULL, który wkradł się z brakującego elementu listy albo z funkcji, która nic nie zwróciła:

threshold <- NULL
if (threshold > 5) print("big")
# Error in if (threshold > 5) print("big") : argument is of length zero

Zabezpiecz sprawdzenie. Zwróć uwagę, że && przestaje obliczać, gdy tylko odpowiedź jest znana, więc porównanie nigdy nie wykona się na NULL:

(Pokrewny błąd, missing value where TRUE/FALSE needed, to ta sama awaria z NA zamiast NULL. Zabezpieczeniem jest tam is.na(), opisane w brakujących wartościach.)

replacement has length zero: wersja tej samej choroby przy przypisaniu: x[2] <- numeric(0) próbuje wypełnić jedno miejsce zerem wartości. To, co wyprodukowało prawą stronę, wróciło puste; debuguj to, a nie przypisanie.

Ostrzeżenia to nie błędy i właśnie w tym tkwi niebezpieczeństwo

Błąd zatrzymuje wykonanie; ostrzeżenie nie. R kończy obliczenia, oddaje wynik i dopiero potem wspomina o swoich zastrzeżeniach. Taki wynik bywa w porządku, a bywa po cichu błędny:

Obie linie się wykonują. Pierwsza powiela krótszy wektor i ostrzega longer object length is not a multiple of shorter object length, a powielanie wektora o długości 2 względem wektora o długości 3 prawie nigdy nie jest zamierzone. Druga ostrzega NAs introduced by coercion i zwraca wektor z dziurą, przez którą każde późniejsze mean() zwróci NA. Traktuj oba ostrzeżenia jak błędy do zbadania, a nie szum do przewinięcia. W skryptach możesz wymusić takie podejście przez options(warn = 2), co zamienia każde ostrzeżenie w błąd, więc nic się nie prześlizgnie.

Obsługa awarii przez tryCatch()

Czasem błąd jest spodziewany (jeden uszkodzony plik w folderze setek plików, jeden zły wiersz) i chcesz go obsłużyć i iść dalej, zamiast przerywać działanie. tryCatch() owija ryzykowne wyrażenie funkcjami obsługi:

Mechanika: jeśli główny blok się powiedzie, jego wartość jest wynikiem. Jeśli zgłosi błąd, zamiast niego uruchamia się funkcja obsługi error = i to jej wartość zwracana (tutaj NA) staje się wynikiem, a skrypt działa dalej. conditionMessage(e) odzyskuje oryginalny komunikat do logów. finally = uruchamia się w każdym przypadku, przy sukcesie i porażce, więc to miejsce na sprzątanie, np. zamykanie połączeń. Widać, jak wypisuje się przed każdym wynikiem powyżej.

Jest też funkcja obsługi warning =: tryCatch(as.numeric(x), warning = function(w) NA) przechwytuje ostrzeżenie o koercji z poprzedniej sekcji, zamiast je przepuścić. Jedna przestroga: funkcja obsługi, która zwraca wartość zastępczą bez zapisania czegokolwiek w logach, ukrywa awarie, a nie je obsługuje. Zawsze zapisuj conditionMessage(), bo ty z przyszłości będziesz tego potrzebować.

Lokalizowanie awarii: traceback(), browser() i uczciwe wypisywanie

Gdy błąd pochodzi z głębi zagnieżdżonych wywołań funkcji, sam komunikat nie mówi, który łańcuch wywołań do niego doprowadził. Uruchom traceback() zaraz po błędzie:

f <- function(x) g(x)
g <- function(x) stop("boom")

f(1)
# Error in g(x) : boom
traceback()
# 2: g(x)
# 1: f(1)

Wypisuje stos wywołań w chwili awarii: twoje wywołanie na jednym końcu, wywołanie, które zawiodło, na drugim. Musi to być następna uruchomiona rzecz; stos jest odrzucany, gdy wystąpi kolejny błąd.

Żeby zajrzeć na żywo, browser() wstrzymuje wykonanie w miejscu, gdzie go umieścisz, i otwiera interaktywny wiersz poleceń wewnątrz funkcji: sprawdzaj zmienne, przechodź krokami przez n, kontynuuj przez c, wyjdź przez Q. debug(f) robi to samo bez edytowania kodu: oznacza f, więc jej następne wywołanie otwiera się w przeglądarce debugera (cofniesz to przez undebug(f)).

I jest jeszcze technika, której nikt nie pokazuje na slajdach konferencyjnych, a której używa każdy: wypisywanie. Rozsiej print() albo cat() w punktach kontrolnych, uruchom i zobacz, gdzie rzeczywistość przestaje zgadzać się z oczekiwaniami. To uczciwe, szybkie i w skryptach często najbardziej praktyczne narzędzie. Jego najlepszym przyjacielem jest str(), które odpowiada na pytanie stojące za może połową wszystkich błędów w R: "czym ten obiekt właściwie jest?":

Jeden zwarty odczyt: to lista, dwa elementy, jeden to wektor liczb całkowitych, drugi to ramka danych z takimi kolumnami i typami. Gdy $ zawodzi albo obliczenia zachowują się dziwnie, wywołaj str() na obiekcie, zanim zaczniesz teoretyzować. Odpowiedź zwykle jest od razu widoczna ("...aha, to lista o długości 1, która zawiera moją ramkę danych").

Co warto zapamiętać

  • Czytaj komunikat: część po Error in mówi gdzie, część po dwukropku mówi dlaczego. Naprawiaj pierwszy błąd, nie najgłośniejszy.
  • object not found = literówka, kod jeszcze nieuruchomiony albo zmienna, która istniała tylko wewnątrz funkcji. could not find function = prawie zawsze brakujące wywołanie library().
  • unexpected symbol oznacza gramatykę, której nie da się sparsować, a prawdziwym błędem często jest niezamknięty cudzysłów lub nawias przed wskazanym miejscem.
  • Klasyki typów i indeksowania (non-numeric argument, subscript out of bounds, $ on atomic vectors, argument is of length zero) wskazują na błędne założenie o tym, czym jest obiekt; str() sprawdza to założenie jednym wywołaniem.
  • Ostrzeżenia nie zatrzymują wykonania i właśnie dlatego zasługują na uwagę: wynik może być po cichu błędny.
  • tryCatch(error =, warning =, finally =) obsługuje spodziewane awarie bez przerywania (zawsze zapisuj conditionMessage()); traceback() zaraz po błędzie pokazuje łańcuch wywołań; browser()/debug() zatrzymują się wewnątrz; debugowanie przez print() to uczciwa praca.

Dalej: wiele "błędów", które wcale nie są błędami, sprowadza się do jednej trzyznakowej wartości, czyli NA, i tego, jak brakujące wartości przepływają przez wszystko, co liczy R.

Najczęściej zadawane pytania

Co oznacza "object 'x' not found" w R?

R szukał zmiennej o nazwie x i takiej nazwy nie ma w żadnym przeszukanym środowisku. Przyczyny, od najbardziej prawdopodobnej: literówka w nazwie (R rozróżnia wielkość liter, Total to nie total), linia tworząca x nie została jeszcze uruchomiona w tej sesji albo x powstało wewnątrz funkcji, a próbujesz użyć go poza nią.

Co oznacza "could not find function" w R?

Funkcja istnieje w pakiecie, który nie został wczytany w tej sesji. Pakiet instaluje się raz na komputer; library() wywołuje się raz na sesję, a zapomniane wywołanie library() to typowa przyczyna. Jeśli samo library() się nie udaje, pakiet nie jest zainstalowany. Literówka w nazwie funkcji daje ten sam błąd.

Jak obsługiwać błędy w R przez tryCatch?

Owiń ryzykowne wyrażenie: tryCatch(expr, error = function(e) fallback, warning = function(w) fallback, finally = cleanup). Jeśli expr się nie powiedzie, zamiast przerwania skryptu uruchomi się pasująca funkcja obsługi, a to, co zwróci, stanie się wynikiem. conditionMessage(e) wewnątrz funkcji obsługi daje oryginalny komunikat do zapisania w logach.

Co robi traceback() w R?

Uruchomione zaraz po błędzie traceback() wypisuje łańcuch wywołań funkcji aktywny w chwili błędu: twoje wywołanie na jednym końcu, wywołanie, które zawiodło, na drugim. Niczego nie naprawia; mówi, gdzie szukać, a to połowa sukcesu, gdy błąd pochodzi z głębi zagnieżdżonych funkcji.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ