data(mtcars)

Opis zbioru


Wykresy {base} & {graphics}

Wprowadzenie

System graficzny w R składa się z kilku pakietów. Dziś skupimy się na podstawowym systemie graficznym dostępnym w pakietach {base} i {graphics}, które są automatycznie ładowane przy starcie R.


Dlaczego Base Graphics?

  • Proste w użyciu dla podstawowych wykresów
  • Nie wymagają dodatkowych pakietów
  • Świetne do szybkiej eksploracji danych
  • Podstawa dla zrozumienia innych systemów graficznych

Podstawowe Typy Wykresów

Wykres punktowy - plot()

?base::plot
?graphics::plot

Najprostszy i najczęściej używany wykres

# Podstawowy wykres punktowy
base::plot(x = mtcars$wt, # Oś odciętych OX
           y = mtcars$mpg # Oś rzędnych OY
           )

graphics::plot(x = mtcars$wt, y = mtcars$mpg)

base::plot czy graphics::plot?

Od R 4.0.0 (2020) to jedna i ta sama funkcja. Wcześniej plot() należała do pakietu {graphics}, a od wersji 4.0.0 jej definicja jest w {base}. Pakiet {graphics} nadal ją eksportuje, żeby starszy kod i pakiety importujące ją z {graphics} działały dalej. R Core zastrzega jednak w NEWS do wersji 4.0.0, że może się to kiedyś zmienić.

Gdy wpisujemy samo plot(), R przeszukuje ścieżkę wyszukiwania i pierwszy trafia na package:graphics, bo jest on wyżej na liście. Znajduje tam jednak tylko „przekierowanie” do obiektu z {base}:

identical(base::plot, graphics::plot)  # TRUE  – ten sam obiekt
environment(plot)                      # <environment: namespace:base>
find("plot")                           # "package:graphics" "package:base"
plot                                   # function (x, y, ...) UseMethod("plot")

Sama plot() niczego nie rysuje. To funkcja generyczna: jej jedyne zadanie (UseMethod("plot")) to sprawdzić klasę obiektu i przekazać go odpowiedniej metodzie. Metody, które naprawdę rysują, są w {graphics}: plot.default() dla wektorów, plot.data.frame() dla całej ramki (dlatego plot(mtcars) daje macierz wykresów), plot.formula() dla zapisu y ~ x. Pakiety dokładają własne metody, np. {eulerr} dla obiektu z euler() w dalszej części zajęć.

environment(plot.default)              # <environment: namespace:graphics>
methods(plot)                          # lista wszystkich dostępnych metod


Dodajmy opisy osi, tytuł i kolory

plot(x = mtcars$wt, 
     y = mtcars$mpg,
     main = "Wydajność paliwowa a waga samochodu", # tytuł wykresu 
     xlab = "Waga (1000 lbs)", # podpis osi ox
     ylab = "Mile na galon (mpg)", # podpis osi oy
     pch = 19,           # kształt punktu (19 = wypełnione koło)
     col = "steelblue",  # kolor punktów
     cex = 1.5          # rozmiar punktów
     )

Uwaga na pułapkę językową: mpg to nie „zużycie paliwa”. Skrót mpg (miles per gallon) mówi, ile mil samochód przejedzie na jednym galonie paliwa. Im więcej mpg, tym auto oszczędniejsze, czyli zużywa mniej paliwa. My w Polsce jesteśmy przyzwyczajeni do miary odwrotnej: litrów na 100 km, gdzie mniejsza liczba oznacza lepszy wynik. Dlatego w tytułach piszemy o wydajności paliwowej. Ma to znaczenie przy czytaniu wykresu: cięższe auta mają niższe mpg, czyli palą więcej.

Dla ciekawskich: przeliczenie to w przybliżeniu 235.2 / mpg litrów na 100 km (auto z 21 mpg pali ok. 11,2 l/100 km).


Można też cały data.frame wrzucić do funkcji plot()

# plot() na ramce predefiniowanej 
plot(mtcars)

Taki wykres to macierz wykresów rozrzutu: każda zmienna z każdą. Czytamy go jak tabliczkę mnożenia – kafelek w wierszu mpg i kolumnie wt pokazuje mpg (oś Y) względem wt (oś X). Przy 11 zmiennych mamy 110 małych wykresów, więc to narzędzie do „rzutu oka” na cały zbiór, a nie do prezentacji. Do tej samej idei wrócimy niżej przy funkcji pairs().



Wykres liniowy

Dane: wzrost kurcząt w czasie

data(ChickWeight)


Wybieramy jedno kurczę

chick1 <- subset(ChickWeight, Chick == 1)


I śledzimy jego rozwój

plot(chick1$Time, chick1$weight,
     type = "b",  # "b" = both (punkty i linie)
     main = "Wzrost kurczęcia #1",
     xlab = "Czas (dni)",
     ylab = "Waga (g)",
     col = "darkgreen",
     pch = 16,
     lwd = 2)  # grubość linii


Różne typy linii:

  • type = "p" - tylko punkty (domyślnie)

  • type = "l" - tylko linie

  • type = "b" - punkty i linie

  • type = "o" - linie przez punkty

  • type = "h" - pionowe słupki

  • type = "s" - schodki (przydatne np. dla dystrybuanty empirycznej)

  • type = "n" - nic nie rysuje, tylko przygotowuje osie i „płótno”. Brzmi bezużytecznie, ale wykorzystamy to niżej w przykładzie z warstwami.



Histogram - hist()


Rozkład normalny

r <- rnorm(1000) # losowanie
hist(r) # rysowanie


Rozkład logarytmiczno-normalny (log-normalny)

r <- rlnorm(1000) # losowanie
hist(r) # rysowanie

Nazwa bywa myląca. Zmienna ma rozkład log-normalny, jeśli jej logarytm ma rozkład normalny. Czyli rlnorm() to w praktyce exp(rnorm()), a nie „logarytm z rozkładu normalnego”. Widać to po kształcie: wszystkie wartości są dodatnie, a prawy „ogon” jest długi.

Dlaczego to potencjalnie ważne dla kognitywistów? Bo podobny kształt mają na przykład czasy reakcji w eksperymentach: są zawsze dodatnie, większość odpowiedzi skupia się w jednym miejscu, ale zdarzają się pojedyncze bardzo wolne (zamyślenie, mrugnięcie, telefon). Rozkład log-normalny to popularne, choć uproszczone przybliżenie takich danych.


Częstość

hist(mtcars$mpg,
     main = "Rozkład wydajności paliwowej",
     xlab = "Mile na galon",
     ylab = "Liczebność",
     col = "lightblue",
     border = "white",
     freq = T, # TRUE: representation of frequencies (częstość), FALSE: probability densities (gęstość prawdopodobieństwa)
     breaks = 15)  # "sugerowana" liczba przedziałów 

Gęstość

hist(mtcars$mpg,
     main = "Rozkład wydajności paliwowej", 
     xlab = "Mile na galon",
     ylab = "'Gęstość' prawdopodobieństwa",
     col = "lightblue",
     border = "white",
     freq = F, # TRUE: representation of frequencies (częstość), FALSE: probability densities (gęstość prawdopodobieństwa)
     breaks = 10)


Majstrowanie przy parametrze breaks daje zaskakująco mało – i nie chodzi tylko o to, że mtcars jest małym zbiorem. Pojedyncza liczba w breaks to dla R tylko sugestia. R i tak dobiera „ładne” granice przedziałów (okrągłe liczby: 10, 12, 14…) za pomocą funkcji pretty().

plot = FALSE – hist() niczego nie rysuje, tylko liczy i zwraca wynik.

Sprawdźmy, ile słupków faktycznie powstaje?

length(hist(mtcars$mpg,              plot = FALSE)$counts) # domyślnie
## [1] 5
length(hist(mtcars$mpg, breaks = 10, plot = FALSE)$counts) # "prosimy" o 10
## [1] 12
length(hist(mtcars$mpg, breaks = 15, plot = FALSE)$counts) # "prosimy" o 15
## [1] 12

Prośba o 10 i o 15 przedziałów daje identyczny histogram! Jeśli naprawdę chcemy mieć kontrolę, podajemy wektor granic:

hist(mtcars$mpg,
     breaks = seq(from = 10, to = 34, by = 1), # granice co 1 mpg – teraz R robi dokładnie to, co mu każemy
     main = "Granice przedziałów podane ręcznie",
     xlab = "Mile na galon",
     ylab = "Liczebność",
     col = "lightblue",
     border = "white")

Przy 32 obserwacjach i 24 przedziałach wychodzi „szczerbaty grzebień” z pustymi miejscami. Za mało danych na tyle przedziałów.


Inne sposoby wyboru przedziałów:

# Różne metody wyboru przedziałów
par(mfrow = c(1, 3))
hist(mtcars$mpg, breaks = "Sturges", main = "Sturges (domyślna)")
hist(mtcars$mpg, breaks = "Scott",  main = "Scott")
hist(mtcars$mpg, breaks = "FD",      main = "Freedman-Diaconis")

par(mfrow = c(1, 1))

Reguła Sturgesa (domyślna) proponuje \(\lceil \log_2(n) + 1 \rceil\) przedziałów, czyli dla 32 obserwacji – 6. Wymuszanie znacznie większej liczby przedziałów tylko „poszarpie” histogram, bo nie ma wystarczająco dużo danych, żeby sensownie wypełnić więcej słupków. Nie ma jednej „poprawnej” liczby przedziałów; warto narysować kilka wersji i zobaczyć, czy wniosek o kształcie rozkładu się nie zmienia.

Dlaczego tak wychodzi, że wszystkie wyglądają identycznie? To ten sam mechanizm co przy breaks = 10 i breaks = 15. Każda reguła liczy tylko sugerowaną liczbę przedziałów: Sturges proponuje 6, Scott 4, Freedman–Diaconis 6. Potem R przepuszcza tę liczbę przez pretty(), która dobiera „ładne”, okrągłe granice. Dla danych od 10,4 do 33,9 przy 4–6 przedziałach pretty() za każdym razem wybiera krok co 5. W efekcie wszystkie trzy reguły dają te same granice 10, 15, …, 35.


10000 losowań z rozkładu normalnego zamiast mtcars

set.seed(2026)
y <- rnorm(10000)

par(mfrow = c(1, 3))
hist(y, breaks = "Sturges", main = "Sturges (domyślna)", col = "lightblue", border = "white")
hist(y, breaks = "Scott",   main = "Scott",              col = "lightblue", border = "white")
hist(y, breaks = "FD",      main = "Freedman-Diaconis",  col = "lightblue", border = "white")

par(mfrow = c(1, 1))


Można pobawić się innymi parametrami

# To zmieni wygląd bardziej niż breaks:
hist(mtcars$mpg,
     main = "Rozkład wydajności paliwowej",
     xlab = "Mile na galon",
     ylab = "Liczebność",         
     col = rainbow(15),           # kolory słupków (gdy słupków jest mniej niż kolorów, nadmiarowe są pomijane)
     border = "black",            # kolor obramowania
     density = 20,                # linie wypełnienia
     angle = 45,                  # kąt linii
     xlim = c(5, 40),             # zakres osi X
     ylim = c(0, 15),             # zakres osi Y
     las = 1                     # orientacja etykiet
     )



Wykres pudełkowy - boxplot()


Standardowe prostokątne pudełka bez wcięć - klasyczny boxplot.

boxplot(mpg ~ cyl, 
        data = mtcars,
        main = "Wydajność paliwowa według liczby cylindrów",
        xlab = "Liczba cylindrów",
        ylab = "Mile na galon",
        col = c("lightpink", "lightblue", "lightgreen"),
        notch = FALSE) # <- 


boxplot(mpg ~ cyl, 
        data = mtcars,
        main = "Wydajność paliwowa według liczby cylindrów",
        xlab = "Liczba cylindrów",
        ylab = "Mile na galon",
        col = c("lightpink", "lightblue", "lightgreen"),
        notch = TRUE)  # wcięcie ≈ przedział niepewności dla mediany (szczegóły niżej)
## Warning in (function (z, notch = FALSE, width = NULL, varwidth = FALSE, : some
## notches went outside hinges ('box'): maybe set notch=FALSE

notch = TRUE Tworzy wcięcia (notches) po bokach pudełek - to charakterystyczne “zwężenia” wokół mediany, które tworzą kształt przypominający klepsydrę lub wazę.

Co to daje?

  • Wcięcie rozciąga się od mediany o \(\pm 1{,}58 \cdot \mathrm{IQR} / \sqrt{n}\) (IQR to rozstęp międzykwartylowy – wysokość pudełka, \(n\) – liczebność grupy). Ten mnożnik dobrano tak, żeby porównywanie dwóch pudełek odpowiadało z grubsza testowi na poziomie 5%. To więc nie jest dokładnie „95% przedział ufności dla mediany” jednej grupy, tylko narzędzie do porównań.

  • Jeśli wcięcia dwóch pudełek nie zachodzą na siebie, to mamy mocną przesłankę, że mediany się różnią. To reguła kciuka, a nie formalny test.

  • Wzór zawiera \(\sqrt{n}\), więc przy małych grupach wcięcia robią się bardzo szerokie i mogą „wystawać” poza pudełko. Tak jest tutaj: aut 6-cylindrowych jest tylko 7, stąd dziwny kształt środkowego pudełka. R ostrzega wtedy komunikatem „some notches went outside hinges”.

Ciekawostka: autorzy metody (McGill, Tukey i Larsen, 1978) proponowali, żeby w takiej sytuacji rysować zwykłe pudełko bez wcięć, linią przerywaną – jako sygnał „za mało danych, nie ufaj temu porównaniu”. R tego nie robi.


Ile obserwacji jest w każdej grupie?

table(mtcars$cyl)
## 
##  4  6  8 
## 11  7 14


Oryginał mówił o „95% przedziale ufności dla mediany” i „istotności na poziomie ~95%”. Źródło wzoru: McGill, R., Tukey, J. W., & Larsen, W. A. (1978). Variations of box plots. The American Statistician, 32(1), 12–16. W artykule wcięcie to M ± 1,7 · (1,25 · R / (1,35 · √N)), co daje ≈ 1,57 · IQR/√N; R używa zaokrąglenia 1,58 (?boxplot.stats).



Wizualizacja różnych sytuacji. Trzy typy wcięć w wykresie pudełkowym.

## bA 
##  wcięcia: 9.77 10.59 11.52 12.2 
##  pudełka (Q1,Q3): 8.75 11.33 10.8 12.94 
## bB 
##  wcięcia: 9.51 10.39 9.92 10.69 
##  pudełka (Q1,Q3): 8.56 11.34 9.32 11.76 
## bC 
##  wcięcia: 8.28 10.66 9.93 13.44 
##  pudełka (Q1,Q3): 8.26 10.11 10.6 13.31



A: wcięcia się nie nakładają (po 100 obserwacji, prawdziwe średnie 10 i 12). Wcięcie G1 kończy się na 10,59, a wcięcie G2 zaczyna na 11,52. Między niebieskimi a różowymi liniami jest wyraźna przerwa. Duże grupy dają wąskie wcięcia, a różnica median jest duża, więc mamy mocną, ale tylko przesłankę, że mediany się różnią.

B: wcięcia się nakładają (po 100 obserwacji, prawdziwe średnie 10 i 10,4). Wcięcia to 9,51–10,39 i 9,92–10,69, więc linie się przeplatają. To nie dowodzi, że mediany są równe – w populacji różnica 0,4 naprawdę istnieje. Mówi tylko, że przy tej ilości danych nie da się jej odróżnić od przypadku.

C: wcięcia wychodzą poza pudełko (po 6 obserwacji, prawdziwe średnie 10 i 12). Powstają charakterystyczne „klepsydry” z rogami wystającymi poza pudełko. Wcięcie G2 (9,93–13,44) jest szersze niż samo pudełko (10,6–13,31), a wcięcie G1 wystaje górą. Przy n = 6 niepewność mediany jest większa niż rozrzut środkowej połowy danych.

Panel C kryje dodatkową lekcję. Różnica w populacji jest tu taka sama jak w A (10 kontra 12), a jednak wcięcia się nakładają. Te same efekty przy sześciu obserwacjach na grupę są niewykrywalne. Pomyślmy o tym w przyszłych rozważaniach o mocy testu i liczebności próby.



Wykres słupkowy - barplot()


Zliczamy samochody według liczby cylindrów i przypisujemy do obiektu

(cylinder_counts <- table(mtcars$cyl))
## 
##  4  6  8 
## 11  7 14
barplot(cylinder_counts, # <- inaczej niż poprzednio, nie mtcars czy któraś ze zmiennych
        main = "Liczba samochodów według cylindrów",
        xlab = "Liczba cylindrów",
        ylab = "Liczba samochodów",
        col = "coral",
        border = "white")



Wykres pie chart - pie()


Najpierw przygotujmy dane: tygodniowa sprzedaż samochodów w salonie (od poniedziałku do piątku).

auta <- c(seq(from = 1, 
              to = 5, 
              by = 2)
          , 4, 9) # seq() daje 1, 3, 5; dokładamy 4 i 9
auta
## [1] 1 3 5 4 9
pie(auta) # bez "fajerwerków"

WYZWANIE

Czy potrafiliby Państwo oszacować proporcje? Błąd może wynieść max 2%.

pie(auta,
    main = "Samochody",
    col = hcl.colors(length(auta), 
                     palette = "viridis"),
    labels = c("Pon","Wt","Śr","Czw","Pt"))

Pie Chart z etykietami (rozwiązanie zgadywanki)
paleta2 <- c("white","grey70","grey90","grey50","black") # odcienie nie idą po kolei – czy to pomaga, czy przeszkadza w czytaniu?

# Obliczamy udziały
auta_labels <- round(auta/sum(auta) * 100, 1) # przypominając sobie również funkcję round()

# robimy labelki 
auta_labels <- paste(auta_labels, "%", sep = "") # sklejamy wyliczenia ze znakiem procent (%)


pie(auta, 
    main = "Samochody - sprzedaż tygodniowa", 
    col = paleta2, # używamy wcześniej zdefiniowanych
    labels = auta_labels, # używamy wcześniej zdefiniowanych
    cex = 0.8
    )

# legenda
dniTygodnia <- c("Pon","Wt","Śr","Czw","Pt")

# dodanie do rysunku
legend("topright",
       dniTygodnia,
       fill = paleta2,
       bty = "n") # bez ramki

Dlaczego z wykresu kołowego tak trudno odczytać proporcje? W 1984 roku William Cleveland i Robert McGill opisali, jak „dekodujemy” liczby z wykresów, i ułożyli hierarchię zadań percepcyjnych – od najdokładniejszych do najmniej dokładnych: położenie na wspólnej skali; położenie na skalach niewyrównanych; długość, kierunek i kąt (jeden poziom); pole; objętość i krzywizna; na końcu cieniowanie i nasycenie koloru. Część tej hierarchii sprawdzili eksperymentalnie: badani oceniali, jaką częścią jednej wartości jest druga. Porównania na wykresie słupkowym (wysokości słupków odczytujemy względem wspólnej osi, czyli to zadanie typu „położenie”) wypadły wyraźnie lepiej niż te same porównania na wykresie kołowym (kąt).

To przykład, jak psychologia percepcji przekłada się na praktykę: dobry wykres koduje najważniejszą informację tym kanałem wzrokowym, w którym jesteśmy najdokładniejsi.

Źródło: Cleveland, W. S., & McGill, R. (1984). Graphical perception: Theory, experimentation, and application to the development of graphical methods. Journal of the American Statistical Association, 79(387), 531–554. https://doi.org/10.2307/2288400

Mini-eksperyment w grupie: koło czy słupki?

  1. Uruchom kod poniżej, ale nie wypisuj prawdziwych wartości.
  2. Dla każdego dnia zapisz na kartce, jaki procent sprzedaży Twoim zdaniem przypada na ten dzień – osobno dla zestawu A (koło) i zestawu B (słupki). Słupki są bez osi, więc nie da się „podejrzeć” liczb.
  3. Wpisz swoje szacunki do wektorów moje_A i moje_B i policz średni błąd bezwzględny: o ile punktów procentowych średnio się pomyliłeś(-aś).
  4. Zbierzcie wyniki całej grupy na tablicy. Który typ wykresu wygrał?
zestaw_A <- c(18, 22, 20, 25, 15) # suma = 100, więc wartości to od razu procenty
zestaw_B <- c(21, 17, 24, 19, 19)
dni <- c("Pon", "Wt", "Śr", "Czw", "Pt")

par(mfrow = c(1, 2))

pie(zestaw_A, 
    labels = dni, 
    main = "Zestaw A", 
    col = "grey85")
barplot(zestaw_B, 
        names.arg = dni, 
        main = "Zestaw B", 
        col = "grey85", 
        axes = FALSE)

par(mfrow = c(1, 1))
Rozwiązanie (kod do policzenia błędu)
moje_A <- c(20, 20, 20, 25, 15) # <- tu wpisz swoje szacunki
moje_B <- c(20, 15, 25, 20, 20) # <- i tu

mean(abs(moje_A - zestaw_A)) # średni błąd dla wykresu kołowego
mean(abs(moje_B - zestaw_B)) # średni błąd dla wykresu słupkowego

abs() to wartość bezwzględna – nie interesuje nas, czy przeszacowaliśmy, czy niedoszacowaliśmy, tylko o ile się pomyliliśmy.

W klasycznych badaniach błąd dla słupków jest zwykle wyraźnie mniejszy. Uwaga metodologiczna: każdy zestaw oglądamy tylko w jednej formie, a dane są różne – czy to uczciwe porównanie? Jak można by to poprawić (podpowiedź: losowe przydzielanie, równoważenie)?



Dotcharts


# Prosty nazwany wektor z ocenami
oceny <- c(3.5, 4.2, 3.8, 4.5, 3.0)
names(oceny) <- c("Jan", "Anna", "Piotr", "Ewa", "Marek")

dotchart(oceny, 
         main = "Oceny studentów",
         xlab = "Ocena")


# Posortujmy samochody według mocy (hp) i zobaczmy ranking
mtcars_sorted <- mtcars[order(mtcars$hp),]

dotchart(mtcars_sorted$hp,
         labels = rownames(mtcars_sorted),
         cex = 0.7,  # rozmiar etykiet
         main = "Ranking samochodów według mocy silnika",
         xlab = "Moc (KM)",
         col = ifelse(mtcars_sorted$cyl == 4, "lightblue",
                     ifelse(mtcars_sorted$cyl == 6, "lightgreen", "lightpink")), # zagnieżdżony ifelse() - o tym później
         pch = 19,  # pełne kółka
         pt.cex = 1.2)  # rozmiar punktów

# Dodajmy legendę
legend("bottomright", 
       legend = c("4 cylindry", "6 cylindrów", "8 cylindrów"),
       col = c("lightblue", "lightgreen", "lightpink"),
       pch = 19,
       cex = 0.8)




Dostosowywanie Wykresów

Kolory

Kolory pojawiały się już kilka razy: col = "steelblue", col = "coral"… W R ten sam kolor można zapisać na kilka sposobów – nazwą (R zna ich 657, pełna lista w colors()), kodem szesnastkowym HEX albo funkcją, np. rgb(). Wszystkie trzy słupki poniżej mają ten sam kolor:

barplot(c(1, 1, 1),
        
        col = c(
          "tomato",                               # <- 
          
          "#FF6347",                              # <-
          
          rgb(255, 99, 71, maxColorValue = 255)), # <- 
        
        names.arg = c(
          '"tomato"',
          '"#FF6347"',
          'rgb(255, 99, 71, ...)'),
        axes = FALSE,
        main = "Trzy zapisy, jeden kolor")

Symbole punktów (pch)

plot(1:25, rep(1, 25), 
     pch = 1:25, 
     cex = 2, 
     bg = "gold", # działa tylko dla symboli 21-25
     ylim = c(0.5, 1.5),
     main = "Dostępne symbole (pch = 1:25)",
     xlab = "Numer symbolu", 
     ylab = "")
text(1:25, # wektor x dłuższy niż y i labels;
     rep(0.7, 25), # R po cichu "zawijałby" krótsze wektory (recycling)
     labels = 1:25, 
     cex = 0.8)

Symbole 21–25 są wyjątkowe: mają osobno obrys (col) i wypełnienie (bg). Dzięki temu da się np. zrobić kółka z czarną obwódką i kolorowym środkiem – wykorzystamy to w wyzwaniu na końcu tej części.

Przy okazji: błąd z 1:26 jest bardzo pouczający. R nie zgłosił żadnego problemu, tylko „dopasował” długości, powtarzając krótsze wektory. To tzw. recycling – wygodny, ale czasem ukrywa pomyłki.



Osie i Siatka


plot(mtcars$wt,
     mtcars$mpg,
     main = "Dostosowane osie",
     xlab = "Waga", 
     ylab = "MPG",
     pch = 19,           # typ punktu (19 = wypełnione kółko)
     col = "navy",       # kolor punktów (granatowy)
     xlim = c(1, 6),     # zakres osi X
     ylim = c(10, 35),   # zakres osi Y
     las = 1)            # etykiety poziomo

# Dodajemy siatkę
grid(col = "gray", # kolor siatki - szary 
     lty = "dotted") # typ linii - kropkowana
# ciekawostka: siatka jest rysowana PO punktach, więc leży na nich.
# Aby była pod spodem: plot(..., panel.first = grid())

# Dodajemy linię trendu
abline(                        # abline -> rysuje linię 
  lm(mpg ~ wt, data = mtcars), # na podstawie modelu
  col = "red", 
  lwd = 2                      # grubość linii (line width = 2)
  ) 



Legendy


plot(iris$Sepal.Length, iris$Sepal.Width,
     pch = 19,
     col = c("red", "blue", "green")[iris$Species],
     main = "Wymiary działek kielicha",
     xlab = "Długość", ylab = "Szerokość")

legend("topright",                              # pozycja
       legend = levels(iris$Species),           # etykiety
       col = c("red", "blue", "green"),         # kolory
       pch = 19,                                # symbol
       title = "Gatunek")                       # tytuł legendy

Jak działa col = c("red", "blue", "green")[iris$Species]? iris$Species to factor, a factor „pod spodem” przechowuje liczby: 1 dla setosa, 2 dla versicolor, 3 dla virginica.

Gdy użyjemy go jako indeksu, R wybiera z wektora kolorów odpowiednio pierwszy, drugi lub trzeci element. Dostajemy wektor 150 kolorów – po jednym na każdy kwiat.

# Podejrzyjmy, co się dzieje
head(as.integer(iris$Species))
## [1] 1 1 1 1 1 1
head(c("red", "blue", "green")[iris$Species])
## [1] "red" "red" "red" "red" "red" "red"




Funkcja par(mfrow)


Wiele Wykresów na Jednym Rysunku

par(mfrow = c(2, 2))  # 2 wiersze, 2 kolumny

# Wykres 1
plot(mtcars$wt, 
     mtcars$mpg, 
     main = "Scatter plot", 
     pch = 19, 
     col = "blue")

# Wykres 2
hist(mtcars$mpg, 
     main = "Histogram", 
     col = "lightgreen", 
     border = "white")

# Wykres 3
boxplot(mpg ~ cyl, 
        data = mtcars,
        main = "Boxplot", 
        col = "coral")

# Wykres 4
barplot(table(mtcars$cyl), 
        main = "Barplot", 
        col = "steelblue")

par(mfrow = c(1, 1))  # Reset do jednego wykresu


Dodawanie Elementów do Istniejącego Wykresu

# Podstawowy wykres
plot(mtcars$wt, 
     mtcars$mpg, 
     pch = 19, 
     col = "gray",
     main = "Dodawanie elementów",
     xlab = "Waga", 
     ylab = "MPG")

# Dodaj linię trendu
abline(lm(mpg ~ wt, data = mtcars), col = "red", lwd = 2)

# Dodaj linie poziomą i pionową
abline(h = mean(mtcars$mpg), 
       col = "blue", 
       lty = 2)

abline(v = mean(mtcars$wt), 
       col = "blue", 
       lty = 2)

# Dodaj tekst
text(5, 30, "Średnie zaznaczone\n niebieskimi liniami", col = "blue")

# Dodaj punkty
points(mtcars$wt[mtcars$mpg > 30], 
       mtcars$mpg[mtcars$mpg > 30], 
       pch = 19, 
       col = "green", 
       cex = 2)


Macierz Wykresów - pairs()

# pairs() - tworzy macierz wykresów rozproszenia (scatterplot matrix)

pairs(iris[, 1:4], # pierwsze 4 kolumny ze zbioru iris (pomiary kwiatów)
      col = iris$Species, # kolor punktów, iris$Species przypisuje kolor według gatunku
      pch = 19, # pch=19 - wypełnione kółka jako symbole punktów
      main = "Macierz wykresów dla zbioru Iris") # tytuł całej macierzy wykresów


# pairs() - tworzy macierz wykresów rozproszenia (scatterplot matrix)
pairs(mtcars,
      col = mtcars$cyl, # kolor punktów według liczby cylindrów
      pch = 19, # pch=19 - wypełnione kółka jako symbole punktów
      main = "Macierz wykresów dla zbioru mtcars") # tytuł całej macierzy wykresów



Wykres z Marginesami

Idea: obok wykresu rozrzutu dokładamy dwa histogramy – nad nim rozkład wagi (wt), z prawej rozkład mil na galon (mpg). Widzimy wtedy jednocześnie zależność między zmiennymi i rozkład każdej z nich osobno.

Kluczowa jest funkcja layout(). Dostaje macierz, która jest „mapą” okna graficznego: liczba w komórce mówi, który z kolejnych wykresów ma tam trafić, a 0 oznacza puste miejsce. Nasza mapa:

2  0      <- górny wiersz: wykres nr 2 (histogram wagi), obok pusto
1  3      <- dolny wiersz: wykres nr 1 (główny), obok wykres nr 3 (histogram mpg)

layout()

┌─────────────┬────┐
│      2      │ 0  │   ← niski wiersz (heights: 1)
├─────────────┼────┤
│             │    │
│      1      │ 3  │   ← wysoki wiersz (heights: 3)
│             │    │
└─────────────┴────┘
  szeroka (3)  wąska (1)

Wykresy trafiają na miejsca w kolejności rysowania: pierwszy narysowany idzie w pole „1”, drugi w pole „2” itd. Dlatego najpierw ustawiamy layout(), a dopiero potem rysujemy – i to w odpowiedniej kolejności.

# zapisujemy ustawienia graficzne, żeby na końcu je przywrócić
stary_par <- par(no.readonly = TRUE)

# liczymy histogramy BEZ rysowania – potrzebujemy ich granic, żeby wyrównać osie
h_wt  <- hist(mtcars$wt,  plot = FALSE)
h_mpg <- hist(mtcars$mpg, plot = FALSE)
zakres_x <- range(h_wt$breaks)   # wspólny zakres osi X (wykres główny + górny histogram)
zakres_y <- range(h_mpg$breaks)  # wspólny zakres osi Y (wykres główny + prawy histogram)

# 1) najpierw "mapa" okna
layout(matrix(c(2, 0,
                1, 3),
              nrow = 2, 
              byrow = TRUE),
       widths  = c(3, 1),   # lewa kolumna 3 razy szersza od prawej
       heights = c(1, 3))   # dolny wiersz 3 razy wyższy od górnego

# 2) pole nr 1: wykres główny
par(mar = c(4, 4, 0.5, 0.5))   # marginesy: dół, lewo, góra, prawo
plot(mtcars$wt, 
     mtcars$mpg,
     pch = 19, 
     col = "steelblue",
     xlab = "Waga (1000 lbs)",
     ylab = "Mile na galon (mpg)",
     xlim = zakres_x, 
     ylim = zakres_y)

# 3) pole nr 2: histogram wagi – rysujemy słupki "ręcznie" funkcją rect()
par(mar = c(0.5, 4, 3, 0.5))
plot.new()                                       # puste płótno

plot.window(xlim = zakres_x, 
            ylim = c(0,
                     max(h_wt$counts)))
rect(xleft  = head(h_wt$breaks, -1), 
     ybottom = 0,         # lewe krawędzie słupków
     xright = tail(h_wt$breaks, -1), 
     ytop = h_wt$counts,  # prawe krawędzie i wysokości
     col = "lightblue", 
     border = "white")

title(main = "Wykres z histogramami brzegowymi")

# 4) pole nr 3: histogram mpg, obrócony – zamieniamy role osi X i Y
par(mar = c(4, 0.5, 0.5, 1))

plot.new()

plot.window(xlim = c(0, 
                     max(h_mpg$counts)), 
            ylim = zakres_y)

rect(xleft  = 0,
     ybottom = head(h_mpg$breaks, -1), #  „wszystko oprócz ostatniego”
     xright = h_mpg$counts,            
     ytop    = tail(h_mpg$breaks, -1), # „wszystko oprócz pierwszego”
     col = "lightblue", 
     border = "white") # `head(x, -1)` to „wszystko oprócz ostatniego”, a `tail(x, -1)` – „wszystko oprócz pierwszego”. Z wektora granic `10, 12, 14, 16` dostajemy więc lewe krawędzie `10, 12, 14` i prawe `12, 14, 16`.

# przywracamy ustawienia (w tym układ jednego wykresu)
par(stary_par)




Podstawowe funkcje:

  • Funkcje TWORZĄCE nowy wykres: plot(), hist(), barplot(), boxplot()

  • Funkcje DODAJĄCE do wykresu: points(), lines(), text(), abline(), legend(), rect(), polygon()


# Przykład z warstwami
plot(1:10, type = "n", main = "Wykres warstwowy")  # pusty canvas
rect(2, 2, 8, 8, col = "lightblue")                # warstwa 1
points(1:10, 1:10, col = "red", pch = 19, cex = 2) # warstwa 2
lines(1:10, 1:10, col = "blue", lwd = 3)           # warstwa 3


# To tworzy NOWY wykres (i zastępuje poprzedni)
plot(1:10)

# To też tworzy NOWY wykres (poprzedni zaorany)
plot(1:5, col = "red")

# Stwórzmy bazowy wykres (NOWY - kolejny, analogicznie jak wcześniej)
plot(1:10, type = "l", col = "blue")

# Dodajmy do niego (nie zastępując)
points(1:10, col = "red", pch = 19)    # dodaj punkty
lines(1:10, lwd = 2, col = "green")    # dodaj linię
text(5, 5, "Tekst", col = "purple")    # dodaj tekst
abline(h = 5, lty = 2)                 # dodaj linię poziomą




Przydatne Parametry Graficzne


Najważniejsze argumenty funkcji par():

# Wyświetl bieżące ustawienia (tylko kilka)
par()[c("mar", "mfrow", "bg")]
## $mar
## [1] 5.1 4.1 4.1 2.1
## 
## $mfrow
## [1] 1 1
## 
## $bg
## [1] "white"
Parametr Opis Przykład
col Kolor col = "red"
pch Kształt punktu pch = 19
cex Rozmiar cex = 1.5
lwd Grubość linii lwd = 2
lty Typ linii lty = "dashed"
main Tytuł główny main = "Mój wykres"
xlab, ylab Etykiety osi xlab = "Oś X"
las Orientacja etykiet las = 1 (poziomo)
xlim, ylim Zakresy osi xlim = c(0, 10)



Czym są parametry graficzne?

To ustawienia, które kontrolują jak wyglądają wszystkie kolejne wykresy, np.:

  • rozmiary marginesów

  • liczba paneli na jednym wykresie

  • kolory, czcionki, grubości linii (domyślne)

  • orientacja tekstu, itd.


Podstawowe przykłady:

# Zobaczmy wszystkie aktualne parametry
# par()

# Ustawmy 4 wykresy w siatce 2x2
par(mfrow = c(2, 2))
plot(1:100)
plot(rlnorm(1000))
hist(rnorm(1000))
boxplot(rnorm(500))

# Wróć do 1 wykresu
par(mfrow = c(1, 1))


Najczęściej używane parametry:

# MARGINESY (bottom, left, top, right)
par(mar = c(5, 4, 4, 2))  # domyślne
par(mar = c(2, 2, 2, 2))  # mniejsze

# UKŁAD WYKRESÓW
par(mfrow = c(2, 3))  # 2 rzędy, 3 kolumny (wypełnianie po wierszach)
par(mfcol = c(2, 3))  # 2 rzędy, 3 kolumny (wypełnianie po kolumnach)

# KOLORY I STYLE
par(col = "blue")      # domyślny kolor linii/punktów
par(bg = "lightgray")  # kolor tła
par(lwd = 2)           # grubość linii

# CZCIONKI
par(cex = 1.5)         # rozmiar tekstu (1 = normalny)
par(family = "serif")  # rodzina czcionek




Ważne: zachowywanie i przywracanie ustawień wykresów

# ZAPISZ obecne ustawienia PRZED zmianą
old_par <- par(no.readonly = TRUE)

# Zmień co chcesz
par(mfrow = c(2, 2), mar = c(2, 2, 2, 2))
# ... twoje wykresy ...

# PRZYWRÓĆ stare ustawienia
par(old_par)




Jak R rysuje? Logika urządzenia graficznego

Do tej pory wpisywaliśmy polecenia i „jakoś” pojawiał się obrazek. Warto zrozumieć, co się dzieje pod spodem – wtedy większość dziwnych zachowań (zniknął wykres, legenda się nie mieści, plik PNG jest pusty) przestaje być zagadką.

Urządzenie to kartka, R maluje farbą

Urządzenie graficzne (graphics device) to „kartka”, na której R rysuje: okno Plots w RStudio, ale też plik PNG albo PDF. W danej chwili rysujemy zawsze na jednym, aktywnym urządzeniu.

System {graphics} działa według tzw. modelu malarza (painters model): każde polecenie nakłada na kartkę kolejną warstwę farby, a to, co narysowane później, przykrywa to, co było wcześniej. Niczego nie da się zmazać ani przesunąć – żeby coś poprawić, trzeba narysować wszystko od nowa. Stąd podział, który już znamy:

  • funkcje wysokiego poziomu (plot(), hist(), boxplot(), barplot()…) biorą nową kartkę i rysują cały wykres razem z osiami;

  • funkcje niskiego poziomu (points(), lines(), abline(), text(), legend(), rect()…) dokładają farbę na kartkę, która już leży.

Kolejność ma znaczenie:

par(mfrow = c(1, 2))

plot(1:10, pch = 19, main = "Punkty, potem prostokąt")
rect(3, 3, 8, 8, col = "lightblue")        # zamalowuje punkty, które już były

plot(1:10, type = "n", main = "Prostokąt, potem punkty")  # type = "n": kartka z osiami, bez punktów
rect(3, 3, 8, 8, col = "lightblue")
points(1:10, pch = 19)                     # punkty leżą na prostokącie

par(mfrow = c(1, 1))

Anatomia kartki: trzy prostokąty

Kartka nie jest jednolita. R dzieli ją na zagnieżdżone obszary:

  • region urządzenia – cała kartka; wokół brzegu może mieć margines zewnętrzny (oma, domyślnie zero);

  • region rysunku (figure) – miejsce na jeden wykres razem z jego opisami; przy par(mfrow = c(2, 2)) na jednej kartce mamy cztery takie regiony;

  • region wykresu (plot) – wnętrze osi, gdzie lądują dane. Między nim a brzegiem regionu rysunku jest margines (mar) – tam mieszkają tytuł, podpisy osi i liczby na osiach.

Poniższy rysunek niechaj będzie ilustracją opisu

## [1]  0.64 10.36  0.64 10.36

par("usr") pokazuje, że oś X biegnie nie od 1 do 10, tylko trochę szerzej. R domyślnie dokłada po 4% zakresu z każdej strony, żeby punkty nie leżały na krawędzi.


Domyślnie wszystko, co wystaje poza region wykresu, jest obcinane (clipping). Argument xpd = TRUE pozwala rysować także na marginesach – dlatego używaliśmy go przy legendzie w wyzwaniu:

plot(1:10, main = "Co wystaje poza region wykresu?")
points(10.8, 4, pch = 19, cex = 2, col = "red")               # obcięty – nie widać go
points(10.8, 7, pch = 19, cex = 2, col = "blue", xpd = TRUE)  # widoczny na marginesie


Kilka kartek naraz

Urządzeń może być otwartych kilka – każde ma swój numer, a rysujemy na aktywnym. Ten kod najlepiej wykonać samodzielnie w konsoli:

# Sprawdźmy aktywne urządzenie graficzne
dev.cur() # <- null device o numerze 1 to domyślny stan - oznacza, że nie mamy otwartego żadnego aktywnego urządzenia graficznego.

# otwórzmy nowe urządzenie (w RStudio zwykle osobne okno) i przełączajmy się między nimi
dev.new()
dev.set(2)  # uaktywnij urządzenie nr 2

# Lista wszystkich otwartych urządzeń
dev.list()

# Zamknij bieżące urządzenie (resetuje canvas)
dev.off()

# Zamknij wszystkie
graphics.off()

Wywołanie dev.off(), gdy nic nie jest otwarte, kończy się błędem „cannot shut down device 1 (the null device)”.


Plik też jest urządzeniem

Otwórz –> Narysuj –> Zamknij

png() i pdf(), które zobaczymy za chwilę przy eksporcie, nie „zapisują wykresu” – one otwierają nowe urządzenie, tyle że kartką jest plik na dysku. Cykl jest zawsze ten sam:

  1. otwórz urządzenie plikowe (png(...)) – od tej chwili wszystko trafia do pliku, na ekranie nic się nie pojawia;

  2. narysuj wykres (dowolne funkcje wysokiego i niskiego poziomu);

  3. zamknij urządzenie (dev.off()) – dopiero teraz plik zostaje dokończony i zapisany.

Najczęstszy błąd: zapomniane dev.off(). Plik jest wtedy pusty albo niedokończony, a kolejne wykresy „znikają” – bo dalej lecą do pliku zamiast na ekran.

Przewidź, zanim uruchomisz. Dla każdego fragmentu najpierw zapisz, co Twoim zdaniem się stanie, a dopiero potem sprawdź w konsoli (każdy fragment w świeżej sesji albo po graphics.off()).

# A
points(1:5, 1:5)
# B
plot(1:10)
hist(rnorm(100))
abline(v = 0, col = "red")
# C
plot(1:10, 
     ylim = c(0, 10))

abline(h = 15, col = "red")
# D
png("test.png")
plot(1, main = "PIERWSZY")
plot(1, main = "DRUGI")
dev.off()
Rozwiązanie
points(1:5, 1:5)
## Error in `plot.xy()`:
## ! plot.new has not been called yet

A – błąd „plot.new has not been called yet”. points() to funkcja niskiego poziomu: chce dokładać farbę, a nie ma jeszcze kartki z osiami.


# B
plot(1:10)

hist(rnorm(100))
abline(v = 0, col = "red")

B – na ekranie zostaje tylko histogram z czerwoną linią w zerze. hist() to funkcja wysokiego poziomu – wzięła nową kartkę, więc wykres punktowy przepadł, a abline() dorysowało linię do histogramu.


plot(1:10, 
     ylim = c(0, 10))

abline(h = 15, col = "red")

C – nie widać żadnej linii. Oś Y kończy się w okolicach 10, a linia na wysokości 15 leży poza regionem wykresu – została obcięta. R nie zgłasza przy tym żadnego błędu.


png("test.png")
plot(1, main = "PIERWSZY")
plot(1, main = "DRUGI")
dev.off()
## png 
##   2
D – na ekranie nic się nie pojawia, a w pliku test.png jest tylko wykres „DRUGI”. Każde nowe wywołanie funkcji wysokiego poziomu to nowa strona, a plik PNG mieści jedną – kolejna strona nadpisuje poprzednią. Żeby dostać dwa pliki, można użyć wzorca z numerem: png("wykres_%02d.png") utworzy wykres_01.png i wykres_02.png.

Model malarza i podział na regiony za: Murrell, P. (2018). R Graphics (3rd ed.).




Eksport Wykresów


Pamiętamy z poprzedniej sekcji: png() i pdf() to urządzenia. Schemat jest zawsze ten sam – otwórz, narysuj, zamknij.


Plik rastrowy

Plik rastrowy to obraz zapisany jako siatka pikseli: dla każdego kwadracika zapamiętany jest jego kolor. Działa jak mozaika z płytek. Ma z góry ustaloną rozdzielczość, więc przy powiększaniu widać „schodki” i rozmycie. Formaty w R: png(), jpeg(), tiff(), bmp(). Rozmiar pliku zależy od liczby pikseli, a nie od tego, co jest na rysunku.

# Zapisz jako PNG
png( # przekierowuje wszystkie kolejne polecenia rysujące do pliku PNG zamiast na ekran.
  "moj_wykres.png", # nazwa pliku, który zostanie utworzony w aktualnym folderze roboczym
  width = 800, 
  height = 600 # rozmiary grafiki w pikselach, bo png jest rastrowy (piksele —> bitmapa)
  # res = DPI (dots per inch), czyli rozdzielczość
  ) 

# tworzenie wykresu
plot(mtcars$wt, 
     mtcars$mpg, 
     pch = 19, 
     col = "blue")

dev.off() # Kończy zapis do pliku, „zamyka” grafikę i finalizuje plik PNG. Po tej linijce obraz jest gotowy i zapisany


Do druku (praca zaliczeniowa, plakat) piksele bez podanej rozdzielczości to kiepski pomysł. Lepiej podać rozmiar w calach i rozdzielczość:

# wersja "do druku" – 8 x 6 cali przy 300 DPI = 2400 x 1800 pikseli
png("moj_wykres_druk.png",
    width = 8, 
    height = 6, 
    units = "in", 
    res = 300)

plot(mtcars$wt, 
     mtcars$mpg, 
     pch = 19, 
     col = "blue")
dev.off()


Plik wektorowy

Plik wektorowy to obraz zapisany jako przepis na rysowanie: „linia od punktu A do B”, „okrąg o środku tu i promieniu tyle”, „napis taką czcionką”. Program odtwarza go przy każdym wyświetleniu, więc obraz jest ostry w każdym powiększeniu – na ekranie, wydruku i plakacie. Formaty w R: pdf(), svg(), postscript(). Rozmiar pliku zależy od liczby elementów: wykres z 50 000 punktów będzie ciężki, bo każdy punkt to osobna instrukcja.


Analogicznie do poprzedniego kodu można zapisać grafikę do pdf.

pdf(# Zapisz jako PDF (wysoka jakość)
  "moj_wykres.pdf", 
  width = 8, 
  height = 6 # 8×6 cali, nie pikseli, 
  )          # bo pdf jest wektorowy (kształty — nieskończona rozdzielczość)
plot(mtcars$wt, mtcars$mpg, pch = 19, col = "blue")
dev.off() # j.w.




WYZWANIE

Odtwórz ten wykres używając base i graphics!

Dataset: mtcars




Kwartet Anscombe’a – dlaczego w ogóle rysujemy dane?

W R jest wbudowany zbiór anscombe. Są to cztery pary zmiennych (x1–y1, …, x4–y4). Układ tych danych został stworzony w 1973 roku przez brytyjskiego statystyka Francisa Anscombe’a, żeby pokazać znaczenie graficznej reprezentacji danych przy okazji ich analizy statystycznej. Kwartet Anscombe’a w Wikipedii.


Policzmy dla każdej pary średnią x, średnią y i korelację cor(x, y). Co zauważamy?

# statystyki – sapply() wykonuje funkcję dla i = 1, 2, 3, 4 i skleja wyniki w tabelę
sapply(1:4, function(i) {
  x <- anscombe[[paste0("x", i)]]   # [[ ]] z nazwą kolumny: "x1", "x2", ...
  y <- anscombe[[paste0("y", i)]]
  round(c(srednia_x = mean(x), 
          srednia_y = mean(y), 
          korelacja = cor(x, y)), 2)
})
##           [,1] [,2] [,3] [,4]
## srednia_x 9.00 9.00 9.00 9.00
## srednia_y 7.50 7.50 7.50 7.50
## korelacja 0.82 0.82 0.82 0.82


Narysujmy cztery wykresy rozrzutu w układzie 2 × 2 (par(mfrow = ...)), na każdym dodajmy linię regresji (abline(lm(...))). Ustaw te same zakresy osi na wszystkich czterech wykresach.

# wykresy
par(mfrow = c(2, 2), 
    mar = c(4, 4, 2, 1))

for (i in 1:4) {
  x <- anscombe[[paste0("x", i)]]
  y <- anscombe[[paste0("y", i)]]
  plot(x, y, pch = 19, col = "darkorange",
       xlim = c(3, 20), ylim = c(2, 14),
       main = paste("Zestaw", i))
  abline(lm(y ~ x), col = "grey40")
}

par(mfrow = c(1, 1))

Statystyki są praktycznie identyczne, a dane – zupełnie różne: zależność liniowa, krzywoliniowa, jedna obserwacja odstająca, która „robi” całą korelację… Wniosek: zanim policzysz, narysuj.




Podsumowanie

Base i graphics w R to potężne narzędzie do:

  • szybkiej eksploracji danych
  • Tworzenia prostych i czytelnych wizualizacji
  • Pełnej kontroli nad każdym elementem wykresu
  • Publikacji gotowych wykresów




Tabele częstości.

Dla zmiennych o niewielkiej liczbie różnych wartości (w praktyce zmiennych jakościowych – tekstowych, czynnikowych albo liczbowych, jak liczba cylindrów w mtcars) możemy wyświetlić tabelę częstości za pomocą table()

data(mtcars)
table(mtcars$cyl)
## 
##  4  6  8 
## 11  7 14

Pokazuje ona jedynie liczebności poszczególnych kategorii.


Moglibyśmy, co już widzieliśmy wcześniej, zrobić tak

prop.table(table(mtcars$cyl)) * 100
## 
##      4      6      8 
## 34.375 21.875 43.750


Więcej informacji (i bez kombinowania) możemy zobaczyć używając funkcji tabyl() z pakietu {janitor}

# install.packages("janitor")
library(janitor)
tabyl(mtcars, cyl)
##  cyl  n percent
##    4 11 0.34375
##    6  7 0.21875
##    8 14 0.43750


I drobne zabiegi estetyczne

# "ręcznie"
podsumowanie <- data.frame(tabyl(mtcars, cyl))
names(podsumowanie) <- c("Cylindry", "Liczba aut", "Udział")
podsumowanie$Procent <- paste0(round(podsumowanie$`Udział` * 100, 1), "%") 
print(podsumowanie)
##   Cylindry Liczba aut  Udział Procent
## 1        4         11 0.34375   34.4%
## 2        6          7 0.21875   21.9%
## 3        8         14 0.43750   43.8%
# `janitor` ma do tego gotową funkcję
adorn_pct_formatting( # <- 
  tabyl(mtcars, cyl)
  , digits = 1)
##  cyl  n percent
##    4 11   34.4%
##    6  7   21.9%
##    8 14   43.8%




qplot


qplot (skrót od quick plot) to funkcja w języku R, która pochodzi z pakietu ggplot2 i służy do szybkiego tworzenia wykresów. Jest to prostsza i bardziej intuicyjna alternatywa dla funkcji ggplot(), choć mniej elastyczna i mniej rozbudowana. Pozwala w szybki sposób tworzyć różne typy wykresów, takie jak wykresy punktowe, liniowe, histogramy czy wykresy słupkowe, przy minimalnym nakładzie pracy. Bliżej samemu pakietowi ggplot2 przyjrzymy się na innych zajęciach. Teraz chodzi bardziej o poznanie szybkiego i prostego narzędzia do wizualizacji. Sprawdźmy ją zanim całkowicie zaniknie.


Od wersji 3.4.0 pakietu ggplot2 (listopad 2022) funkcja qplot() jest oficjalnie przestarzała (deprecated): działa, ale przy pierwszym użyciu wypisuje ostrzeżenie. Autorzy zapowiedzieli, że funkcja nie zniknie, ale nie będą jej już rozwijać ani dostosowywać do nowych możliwości pakietu. W tym dokumencie ostrzeżeń nie widać, bo są wyłączone w setup. Traktujmy ją jako „rozgrzewkę” przed pełnym ggplot().


?gplot
?gplot


Cechy qplot():

  • Automatyczne dopasowanie typu wykresu: W zależności od przekazanych danych, qplot() sam automatycznie dobiera odpowiedni typ wykresu (np. punktowy, liniowy, histogram).

  • Prostota: Argumenty funkcji qplot są łatwiejsze do zrozumienia dla początkujących. Przykładowo, do stworzenia podstawowego wykresu punktowego wystarczy podać zmienne x i y.

  • Mniej skomplikowana składnia: W porównaniu do bardziej rozbudowanej funkcji ggplot(), qplot() wymaga mniej kodu.

  • Funkcjonalność ggplot2: Mimo prostoty, qplot() dziedziczy wiele funkcji z pełnego systemu wykresów ggplot2, co daje użytkownikom możliwość łatwej rozbudowy wykresów.



Pakiet z którego weźmiemy funkcję qplot() to {ggplot2}.

library(ggplot2)


Wykres słupkowy (Bar Plot)

# Wykres słupkowy kategorii cyl z ramki danych mtcars
qplot(factor(cyl), # <- dlaczego musi być factor?
      data = mtcars, 
      geom = "bar")

# qplot(factor(cyl), data = mtcars) # sam dokmini typ wykresu

Formalnie nie musi. Bez factor() wykres też powstanie, ale cyl będzie traktowana jak zmienna ciągła: oś X dostanie wartości 4, 5, 6, 7, 8, a słupki staną nad 4, 6 i 8 z dziurami pomiędzy. factor() mówi: „to są kategorie, nie liczby na osi” – i każda kategoria dostaje własne, równo rozmieszczone miejsce.

qplot(cyl, data = mtcars, geom = "bar")


Histogram (Histogram)

# Histogram zmiennej mpg (miles per gallon)
qplot(mpg, 
      data = mtcars, 
      geom = "histogram", 
      bins = 20, 
      fill=I("light blue")
      ) # qplot(mpg, data = mtcars)


Wykres gęstości (Density Plot)

# Wykres gęstości dla zmiennej mpg
qplot(x = mpg, 
      data = mtcars, 
      geom = "density",
      fill=I("lightblue"),
      alpha=I(0.5)
      )


Ten sam rozkład, ale zmienna podana jako y – wykres gęstości „kładzie się” na bok:

qplot(y = mpg, 
      data = mtcars, 
      geom = "density",
      fill=I("lightblue"),
      alpha=I(0.5)
      )


Wykres pudełkowy (Box Plot)

# Wykres pudełkowy dla mpg w podziale na liczbę cylindrów
qplot(factor(cyl), 
      mpg,
      data = mtcars, 
      geom = "boxplot", 
      fill=I("lightgreen"))


Wykres punktowy z kolorami zależnymi od zmiennej (Scatter Plot with Coloring)

# Wykres punktowy z kolorami zależnymi od liczby cylindrów
qplot(mpg, 
      wt, 
      data = mtcars, 
      color = factor(cyl))


Wykres liniowy z wieloma liniami (Line Plot with Multiple Lines)

# Wykres liniowy dla mpg z grupowaniem według liczby cylindrów
qplot(x = hp, 
      y = mpg, 
      data = mtcars, 
      geom = "line", 
      color = factor(cyl))

Czy ten wykres ma sens? Linia sugeruje, że między punktami „coś się dzieje w sposób ciągły”, jak w czasie przy kurczętach. Tutaj każdy punkt to inny samochód, a linia łączy je tylko dlatego, że posortowaliśmy je po mocy. Kiedy wykres liniowy jest uprawniony, a kiedy wprowadza w błąd?


Wykres punktowy z linią regresji (Scatter Plot with Regression Line)

# Wykres punktowy z linią regresji
qplot(wt,
      mpg,
      data = mtcars, 
      geom = c("point", "smooth"), 
      method = "lm") # 1000 lbs ≈ 453,6 kg


Wykres punktowy z rozmiarem punktów zależnym od zmiennej (Bubble Plot)

# Wykres punktowy z rozmiarem punktów zależnym od liczby koni mechanicznych (hp)
qplot(mpg, 
      wt, 
      data = mtcars, 
      size = hp)


Wykres heksagonalny (Hexbin Plot)

# Wykres heksagonalny (hexbin)
# install.packages("hexbin")
library(hexbin)

qplot(carat, price, data = diamonds) # zwykłe punkty: jedna wielka czarna plama

qplot(carat, price, data = diamonds, geom = "hex") # kolor sześciokąta = ile diamentów w nim leży

Wykres heksagonalny dzieli płaszczyznę na sześciokąty i koloruje je według liczby punktów w środku. Pierwszy wykres sugeruje, że diamenty są rozłożone mniej więcej równo. Drugi pokazuje, że zdecydowana większość to małe, tanie kamienie w lewym dolnym rogu. To problem tzw. overplottingu – punkty zasłaniają się nawzajem i oko nie widzi, gdzie jest ich najwięcej.




Diagram Venna


Diagram Venna przedstawia wszystkie możliwe kombinacje i zależności między zbiorami, niezależnie od tego, czy są one puste. Każda część wykresu, która reprezentuje przecięcie, istnieje nawet wtedy, gdy faktycznie nie zawiera elementów.

# install.packages("VennDiagram")
library(VennDiagram)

futile.logger::flog.threshold(futile.logger::ERROR, 
                              name = "VennDiagramLogger") # wyłączenie tworzenia plików roboczych
## NULL
# Dane do stworzenia diagramu Venna (trzy zbiory)
set1 <- c("A", "B", "C", "D") # c("A", "B", "C", "D", "C", "D")
set2 <- c("B", "C", "D", "E", "F")
set3 <- c("C", "D", "G", "H")

# Tworzenie diagramu Venna
venn.plot <- venn.diagram(
  x = list(set1 = set1, 
           set2 = set2, 
           set3 = set3),
  category.names = c("Set 1", 
                     "Set 2", 
                     "Set 3"),
  filename = NULL
)

# Wyświetlenie diagramu Venna
grid.draw(venn.plot)


# Tworzenie diagramu Venna
venn.plot <- venn.diagram(
  x = list(Set1 = LETTERS[c(1:15)], 
           Set2 = LETTERS[10:20], 
           Set3 = LETTERS[c(1:3, 18:26)]),
  category.names = c("Set 1", 
                     "Set 2", 
                     "Set 3"),
  filename = NULL
)

# Wyświetlenie diagramu Venna
grid.draw(venn.plot)




Diagram Eulera


Wykres Eulera pokazuje tylko te części, które faktycznie istnieją. Jeśli dwa zbiory nie mają wspólnych elementów, nie będą miały wspólnej części na wykresie. Jest bardziej elastyczny i realistyczny, bo nie pokazuje obszarów, które nie mają faktycznego odpowiednika w danych zbiorach

# install.packages("eulerr")
library(eulerr)

# Dane do diagramu Eulera
set1 <- c("A", "B", "C", "D", "F", "G", "H", "I", "J", "K")
set2 <- c("B", "C", "E", "F", "D", "Z", "X")
set3 <- c("C", "D", "G", "H")

# Utworzenie listy zestawów danych
sets_list <- list(Set1 = set1, 
                  Set2 = set2, 
                  Set3 = set3)

# Utworzenie diagramu Eulera
euler_plot <- euler(sets_list)

# Wyświetlenie diagramu Eulera
plot(euler_plot, 
     quantities = TRUE, 
     main = "Diagram Eulera dla zestawów danych")

Trzech okręgów nie zawsze da się ułożyć tak, żeby pola dokładnie odpowiadały liczebnościom. Diagram Eulera ma większą ambicję niż diagram Venna: pole każdego kawałka ma być proporcjonalne do liczby elementów - szuka więc najlepszego przybliżenia. Jak dobre ono jest, pokazuje euler_plot wypisany w konsoli: kolumna original to prawdziwe liczebności, fitted – te odwzorowane na rysunku.

euler_plot
##                original fitted residuals regionError
## Set1                  4      4         0           0
## Set2                  3      3         0           0
## Set3                  0      0         0           0
## Set1&Set2             2      2         0           0
## Set1&Set3             2      2         0           0
## Set1&Set2&Set3        2      2         0           0
## 
## diagError: 0 
## stress:    0


Gdy dopasowanie jest słabe, można zamiast okręgów użyć elips, które mają więcej „swobody”: euler(..., shape = "ellipse")

sets_list <- list(Set1 = LETTERS[c(1:15)], 
                  Set2 = LETTERS[10:20], 
                  Set3 = LETTERS[c(1:3, 18:26)])

euler_plot <- euler(sets_list, 
                    shape = "ellipse") # <-
plot(euler_plot, 
     quantities = TRUE, 
     main = "Diagram Eulera dla zestawów danych (Krztałt: ellipse)")




dplyr


Po wczytaniu danych kolejnym etapem analizy jest czyszczenie zbioru danych, wykrywanie braków danych oraz przekształcanie zmiennych. Poznamy najpopularniejszy pakiet do przetwarzania danych dplyr, będący obecnie standardem w świecie R-owym i jednocześnie elementem projektu {tidyverse}, czyli uniwersum pakietów rozwijanych przez firmę Posit (do 2022 roku nazywała się RStudio)

Więcej informacji znajdziesz pod tym linkiem: https://www.tidyverse.org/.


Pakiet dplyr jest wzorowany na języku SQL - dlatego osoby, które pracowały kiedyś z bazami danych poczują się jak w domu.

Poznamy:

  • funkcję filter() (filtrowanie zbioru),

  • funkcję select() (wybór kolumn),

  • funkcję rename() (zmiana nazwy zmiennych),

  • funkcję arrange() (sortowanie),

  • funkcję mutate() (tworzenie nowych zmiennych).



# install.packages('dplyr')
library(dplyr)

# wczytanie danych
footballers <- read.csv('dane/footballers.csv', 
                        sep = ';', 
                        stringsAsFactors = F)


Filtrowanie obserwacji w zbiorze

Funkcja filter()

# Wybór osób powyżej 25 lat
filter(footballers, age > 25)


# Wybór osób  w wieku od 21 do 25
filter(footballers, age >= 21 & age <= 25) 

# To samo, tylko za pomocą funkcji between (uwaga: between() WŁĄCZA końce przedziału)
filter(footballers, between(age, 21, 25))

# Wybierzmy osoby o danym wieku i wzroście
filter(footballers, age == 23 & height <= 180)

# Można również:
filter(footballers, age == 23, height <= 180)


# Wybierzmy osoby w wieku 20 LUB 23 lata
filter(footballers, age == 23 | age == 20)

Zwróćmy uwagę, że w funkcji filter() możemy używać również operatora %in%

# zapisujemy listę do obiektu – wtedy nie trzeba jej przepisywać wielokrotnie
wybrani <- c("lionel messi", "sinama", "palladino", "rodallega", "ljuboja", "ebbers", "macheda")

# Tradycyjnie napisalibyśmy:
filter(footballers, name == 'lionel messi' | name == 'sinama' | name == 'palladino' | 
         name == 'rodallega'  | name == 'ljuboja'  | name == 'ebbers'  | name == 'macheda') 

# Strasznie długo. Spróbujmy wykorzystać operator `%in%`:
filter(footballers, name %in% wybrani)  # korzystamy z obiektu

# Wszyscy OPRÓCZ wybranych. `!` neguje cały warunek `name %in% wybrani`
filter(footballers, !name %in% wybrani)

Na koniec tej sekcji spróbujmy wybrać unikalne wartości zmiennych:

unique(footballers$age) # <- co zwróciło?

distinct(footballers, age) # <- co zwróciło?

# Można robić distinct po wielu zmiennych:
distinct(footballers, age, top_spa)


Sortowanie obserwacji

Funkcja arrange()

# Rosnąco po wieku:
arrange(footballers, age)


# Malejąco po wieku:
arrange(footballers, desc(age))

# Rosnąco po wieku, malejąco po wzroście:
arrange(footballers, age, desc(height))


Inne operacje na wierszach

dplyr oferuje wiele dodatkowych funkcji do pracy z wierszami, które są jednak wykorzystywane zdecydowanie rzadziej.

Poniżej kilka z nich:

set.seed(123) # losowe" wyniki będą u wszystkich takie same

# losowe wybranie frakcji zbioru (tu 50%), ze zwracaniem lub bez
slice_sample(footballers, 
             prop = 0.5, 
             replace = TRUE)  # dawniej: sample_frac(a, 0.5, replace = TRUE)

# losowe wybranie n obserwacji, ze zwracaniem lub bez
slice_sample(footballers, n = 10, replace = TRUE) # dawniej: sample_n(a, 10, replace = TRUE)

# Wybór wierszy w zależności od pozycji w bazie, tu wiersze od 10 do 15 włącznie
slice(footballers, 10:15)

# Wybranie n obserwacji z najwyższą wartością zmiennej (tu: najstarsi zawodnicy)
slice_max(footballers, age, n = 2) # dawniej: top_n(a, 2, age)

Pytanie do sprawdzenia: prosiliśmy o 2 wiersze, a czy na pewno dostaliśmy 2? Jeśli kilku zawodników ma ten sam, najwyższy wiek, slice_max() zwróci ich wszystkich (remisy). Aby dostać dokładnie n wierszy, dopisać należy with_ties = FALSE.


Wybór kolumn

Do wyboru kolumn służy funkcja select(). Pierwszym argumentem jest zawsze zbiór danych (jak we wszystkich funkcjach dplyr). W kolejnym wymieniamy nazwy kolumn (bez cudzysłowu) lub funkcje, które zwracają nam grupy kolumn (o tym za chwilę).

Przyjrzyjmy się podstawowej implementacji select().

# wybranie kolumn name, weight, height
select(footballers, name, weight, height) 


Za pomocą funkcji select() można również usuwać kolumny, poprzez wybieranie “wszystkiego oprócz”. Nazwę takiej “niewybieranej” kolumny poprzedzamy minusem.

# Wybranie wszystkich kolumn oprócz name, weight, height
select(footballers, -name, -weight, -height)

# Usunięcie kolumn z bazy i przypisanie do obiektu footballers2
footballers2 <- select(footballers, -name, -weight, -height)

# Można również usunąć zakres kolumn:
select(footballers,-name:-height)


Czasami opłaca nam się sformułować zapytanie w bardziej ogólny sposób.

Na przykład:

Wybierz kolumny zaczynające się od…,

Wybierz kolumny których nazwy kończą się/ zawierają frazę….

Takie zapytania są niesłychanie przydatne - zobaczmy, jak można je pisać w dplyr:

# Wybierz zmienne, których nazwy zawierają zwrot "goals"
select(footballers, c
       ontains("goals"))

# Podobnie jak contains(), ale wzorzec jest wyrażeniem regularnym (regex),
# więc można szukać np. matches("^goals") – nazw ZACZYNAJĄCYCH się od "goals"
select(footballers, 
       matches("als_"))


# Wybranie zmiennych kończących się na frazę "ght"
select(footballers, 
       ends_with("ght"))

# Wybranie kolumn, których nazwy powstają z połączenia goals_ i elementów wektora c()
# num_range() jest pomyślane dla zakresów LICZBOWYCH, np. num_range("x", 1:3) -> x1, x2, x3.
# Z tekstem działa, ale czytelniej jest skleić nazwy samemu:
select(footballers, all_of(
  paste0("goals_", c("CL", "UEFA"))
  ))  


# Wybranie kolumn, które są u nas na liście
# select(footballers, goals_CL, goals_UEFA, goals_PL)
#   all_of() - gdy którejś kolumny brak -> BŁĄD (bezpiecznie, od razu widać pomyłkę)
#   any_of() - gdy którejś kolumny brak -> po cichu ją pomija
select(footballers, 
       any_of(c("goals_CL", "goals_UEFA", "goals_PL"))
       )

# Wybranie kolumn zaczynających się od danej frazy i zmiennej name
select(footballers, 
       name, starts_with("goals_")
       )


Zmiana nazwy kolumny

Zmianę nazwy zmiennej umożliwia funkcja rename().

Zasada jest następująca - najpierw nowa nazwa, następnie nazwa zastępowana.

footballers <- rename(.data = footballers, 
                      goals_cl = goals_CL, # goals_cl to nie nazwa arumentu funkcji !
                      goals_uefa = goals_UEFA) # podobnie


Tworzenie nowych zmiennych

Funkcja mutate() dodaje nową zmienną na podstawie już istniejących.

Dodajmy zmienną BMI (Body Mass Index - Wskaźnik Masy Ciała).

Przepis: BMI = waga (kg) / wzrost (m)², a ponieważ wzrost mamy w centymetrach, dzielimy go najpierw przez 100.

# tylko podgląd – wynik wypisujemy, ale NIGDZIE go nie zapisujemy
head(
  mutate(footballers, bmi = weight / (height / 100)^2)
  , 5)

footballers <- mutate(footballers, 
                      bmi = weight / (height / 100)^2) # dodanie do zbioru (nadpisujemy obiekt footballers!)

footballers <- mutate(footballers, 
                      bmi = NULL) # wyrzucenie ze zbioru

footballers <- mutate(footballers, 
                      height2 = height^2, # dodanie zmiennej
                      age2   = age^2) # nowa zmienna zamiast "niszczenia" starej

# Zmianę wartości istniejącej zmiennej pokazujemy bezpiecznie – na kopii zbioru
footballers_m <- mutate(footballers, 
                        height = height / 100)  # wzrost w metrach zamiast centymetrów

head(
  select(footballers_m, name, height)
  , 3)

Operator ** też działa w R, ale jest rzadko spotykany. Standardem jest ^.

Mutacja age = age**2 zmieniałaby zbiór na stałe.




potok |>

Spójrzmy na zapis: head(filter(a, age > 25), 5). Czytamy go od środka: najpierw filter, potem head. Przy trzech, czterech krokach robi się z tego nieczytelna „cebula” nawiasów.

Potok |> (wbudowany w R od wersji 4.1) pozwala zapisać to samo od lewej do prawej, w kolejności wykonywania. x |> f(y) znaczy dokładnie to samo co f(x, y) – wynik z lewej strony trafia jako pierwszy argument funkcji z prawej. Czytamy go jako „a potem”:

footballers |>                  # Weź zbiór, 
  filter(age > 25) |>           # A POTEM wybierz starszych niż 25 lat, 
  arrange(desc(height)) |>      # A POTEM posortuj malejąco po wzroście,
  select(name, age, height) |>  # A POTEM zostaw trzy kolumny, 
  head(5)                       # A POTEM pokaż pierwsze 5 wierszy

W starszym kodzie i w wielu poradnikach spotkacie potok %>% z pakietu magrittr (ładowany razem z dplyr). Działa bardzo podobnie.




Na zbiorze a:

  1. Wybierz zawodników wyższych niż 185 cm i młodszych niż 23 lata; posortuj ich malejąco według wzrostu i pokaż kolumny name, age, height.

  2. Dodaj kolumnę bmi i pokaż 5 zawodników z najwyższym BMI (kolumny name, height, weight, bmi).

  3. Narysuj wykres rozrzutu wagi względem wzrostu. Zawodników starszych niż 25 lat zaznacz innym kolorem i dodaj legendę.

Rozwiązanie
# 1.
footballers |>
  filter(height > 185, age < 23) |>
  arrange(desc(height)) |>
  select(name, age, height) |>
  head(10)
##          name age height
## 1    di santo  21    193
## 2  arnautovic  21    192
## 3    bendtner  22    191
## 4     karroll  21    191
## 5    dedriyok  22    191
## 6        ngog  21    190
## 7   balotelli  20    189
## 8       costa  22    188
## 9     kalinic  22    187
## 10  hernandez  20    186
# 2.
footballers |>
  mutate(bmi = weight / (height / 100)^2) |>
  slice_max(bmi, n = 5) |>
  select(name, height, weight, bmi)
##           name height weight      bmi
## 1 ebanks-blake    173     91 30.40529
## 2    di natale    170     78 26.98962
## 3      asamoah    180     85 26.23457
## 4    gudjonsen    185     89 26.00438
## 5       erding    181     85 25.94548
# 3.
kolor <- ifelse(footballers$age > 25, "darkorange", "steelblue")

plot(x = footballers$height,
     y = footballers$weight,
     pch = 19, 
     col = kolor,
     xlab = "Wzrost (cm)",
     ylab = "Waga (kg)",
     main = "Waga a wzrost piłkarzy")
legend("topleft",
       legend = c("25 lat i mniej", "powyżej 25 lat"),
       col = c("steelblue", "darkorange"),
       pch = 19, 
       bty = "n")




Źródła i inspiracje:

  • Materiały z warsztatów Data Science w zastosowaniach biznesowych - Warsztaty z wykorzystaniem programu R Uniwersytet Warszawski, Wydział Nauk Ekonomicznych

  • Frank McCown, Ph.D. home page materials

  • R Graphics Cookbook

  • Dokumentacja: help(package = "graphics")

  • ?par - wszystkie parametry graficzne

  • colors() - lista 657 dostępnych kolorów w R

  • Anscombe, F. J. (1973). Graphs in statistical analysis. The American Statistician, 27(1), 17–21.

  • Cleveland, W. S., & McGill, R. (1984). Graphical perception: Theory, experimentation, and application to the development of graphical methods. Journal of the American Statistical Association, 79(387), 531–554. https://doi.org/10.2307/2288400

  • McGill, R., Tukey, J. W., & Larsen, W. A. (1978). Variations of box plots. The American Statistician, 32(1), 12–16.

  • Murrell, P. (2018). R Graphics (3rd ed.). Chapman & Hall/CRC.




Materiał przygotowano z pomocą asystenta AI Claude (Anthropic); treści i źródła zweryfikował autor.