{base}, {graphics} and
dplyrdata(mtcars)
{base} & {graphics}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?
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().
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.
hist()r <- rnorm(1000) # losowanie
hist(r) # rysowanie
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.
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
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
)
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.
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")
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"))
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?
moje_A i
moje_B i policz średni błąd bezwzględny: o
ile punktów procentowych średnio się pomyliłeś(-aś).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))
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.
# 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)
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")
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.
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)
)
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"
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
# 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)
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
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ą
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)
R rysuje? Logika urządzenia graficznegoDo 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 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))
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
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)”.
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:
otwórz urządzenie plikowe
(png(...)) – od tej chwili wszystko trafia do pliku, na
ekranie nic się nie pojawia;
narysuj wykres (dowolne funkcje wysokiego i niskiego poziomu);
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()
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.).
Pamiętamy z poprzedniej sekcji: png() i
pdf() to urządzenia. Schemat jest zawsze
ten sam – otwórz, narysuj, zamknij.
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 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.
Base i graphics w
R to potężne narzędzie do:
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().
?gplotCechy 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 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 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 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 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 liczby cylindrów
qplot(mpg,
wt,
data = mtcars,
color = factor(cyl))
# 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
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 liczby koni mechanicznych (hp)
qplot(mpg,
wt,
data = mtcars,
size = hp)
# 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 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)
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)")
dplyrPo 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)
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)
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))
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.
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_")
)
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
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.
|>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:
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.
Dodaj kolumnę bmi i pokaż 5 zawodników z
najwyższym BMI (kolumny name,
height, weight, bmi).
Narysuj wykres rozrzutu wagi względem wzrostu. Zawodników starszych niż 25 lat zaznacz innym kolorem i dodaj legendę.
# 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")
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
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.