11  Truyền đạt

11.1 Giới thiệu

Trong Chương 10, bạn đã học cách sử dụng biểu đồ như công cụ khám phá. Khi tạo biểu đồ khám phá, bạn biết—ngay trước khi nhìn—biểu đồ sẽ hiển thị biến nào. Bạn tạo mỗi biểu đồ cho một mục đích, xem nhanh, rồi chuyển sang biểu đồ tiếp theo. Trong hầu hết phân tích, bạn sẽ tạo row chục hoặc row trăm biểu đồ, phần lớn bị bỏ ngay.

Giờ bạn đã hiểu dữ liệu, bạn cần truyền đạt sự hiểu biết đến người khác. Khán giả có thể không có kiến thức nền tảng như bạn và không đầu tư sâu vào dữ liệu. Để giúp người khác nhanh chóng xây dựng mô hình tinh thần tốt về dữ liệu, bạn cần đầu tư nỗ lực đáng kể để biểu đồ tự giải thích nhất có thể. Trong chương này, bạn sẽ học một số công cụ ggplot2 cung cấp cho việc đó.

Chương này tập trung vào công cụ bạn cần để tạo đồ họa tốt. Chúng tôi giả sử bạn biết mình muốn gì, và chỉ cần biết cách thực hiện. Vì vậy, chúng tôi rất khuyến nghị kết hợp chương này với sách visualization tổng quát. Chúng tôi đặc biệt thích The Truthful Art của Albert Cairo. Cuốn sách đó không dạy cơ chế tạo visualization, mà tập trung vào những gì cần suy nghĩ để tạo đồ họa hiệu quả.

11.1.1 Điều kiện tiên quyết

Trong chương này, chúng ta sẽ tập trung vào ggplot2 một lần nữa. Chúng ta cũng sẽ dùng một chút dplyr để xử lý dữ liệu, scales để ghi đè mặc định về break, nhãn, phép biến đổi và bảng màu, và một số package ggplot2, bao gồm ggrepel (https://ggrepel.slowkow.com) của Kamil Slowikowski và patchwork (https://patchwork.data-imaginist.com) của Thomas Lin Pedersen. Đừng quên cài đặt các package đó bằng install.packages() nếu bạn chưa có.

11.2 Nhãn

Nơi dễ nhất để bắt đầu khi chuyển đồ họa khám phá thành đồ họa trình bày là với nhãn tốt. Bạn thêm nhãn bằng function labs().

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  labs(
    x = "Engine displacement (L)",
    y = "Highway fuel economy (mpg)",
    color = "Car type",
    title = "Fuel efficiency generally decreases with engine size",
    subtitle = "Two seaters (sports cars) are an exception because of their light weight",
    caption = "Data from fueleconomy.gov"
  )

Biểu đồ phân tán hiệu suất nhiên liệu cao tốc theo dung tích động cơ, điểm tô màu theo loại xe. Đường cong trơn được phủ lên. Trục x gắn nhãn "Engine displacement (L)", trục y gắn nhãn "Highway fuel economy (mpg)". Chú giải gắn nhãn "Car type". Tiêu đề, phụ đề và comment nguồn dữ liệu.

Mục đích của tiêu đề biểu đồ là tóm tắt phát hiện chính. Tránh tiêu đề chỉ mô tả biểu đồ là gì, ví dụ “Biểu đồ phân tán dung tích động cơ theo hiệu suất nhiên liệu”.

Nếu bạn cần thêm văn bản, có hai nhãn hữu ích khác: subtitle thêm chi tiết bổ sung bằng phông chữ nhỏ hơn dưới tiêu đề và caption thêm văn bản ở góc dưới bên phải, thường dùng để mô tả nguồn dữ liệu. Bạn cũng có thể dùng labs() để thay tiêu đề trục và chú giải. Thường nên thay tên biến ngắn bằng mô tả chi tiết hơn, và bao gồm đơn vị.

Có thể dùng phương trình toán thay vì string văn bản. Chỉ cần thay "" bằng quote() và đọc về các tùy chọn trong ?plotmath:

df <- tibble(
  x = 1:10,
  y = cumsum(x^2)
)

ggplot(df, aes(x, y)) +
  geom_point() +
  labs(
    x = quote(x[i]),
    y = quote(sum(x[i] ^ 2, i == 1, n))
  )

Biểu đồ phân tán với công thức toán trên nhãn trục x và y.

11.2.1 Bài tập

  1. Tạo biểu đồ dữ liệu nhiên liệu với title, subtitle, caption, x, y, và nhãn color tùy chỉnh.

  2. Tái tạo biểu đồ sau dùng dữ liệu nhiên liệu. Lưu ý cả màu và hình dạng điểm thay đổi theo loại hệ dẫn động.

    Biểu đồ phân tán quãng đường cao tốc theo thành phố. Hình dạng và màu sắc điểm phụ thuộc loại hệ dẫn động.

  3. Lấy một biểu đồ khám phá bạn đã tạo trong tháng qua, và thêm tiêu đề mang tính mô tả để người khác dễ hiểu hơn.

11.3 Chú thích

Ngoài gắn nhãn cho các thành phần chính của biểu đồ, thường hữu ích khi gắn nhãn từng quan sát hoặc nhóm quan sát riêng lẻ. Công cụ đầu tiên là geom_text(). geom_text() tương tự geom_point(), nhưng có thêm aesthetic label. Điều này cho phép thêm nhãn văn bản vào biểu đồ.

Có hai nguồn nhãn. Thứ nhất, bạn có thể có tibble cung cấp nhãn. Trong biểu đồ sau chúng ta trích xuất xe có dung tích động cơ lớn nhất trong mỗi loại hệ dẫn động và lưu thành data frame mới label_info.

label_info <- mpg |>
  group_by(drv) |>
  arrange(desc(displ)) |>
  slice_head(n = 1) |>
  mutate(
    drive_type = case_when(
      drv == "f" ~ "front-wheel drive",
      drv == "r" ~ "rear-wheel drive",
      drv == "4" ~ "4-wheel drive"
    )
  ) |>
  select(displ, hwy, drv, drive_type)

label_info
#> # A tibble: 3 × 4
#> # Groups:   drv [3]
#>   displ   hwy drv   drive_type       
#>   <dbl> <int> <chr> <chr>            
#> 1   6.5    17 4     4-wheel drive    
#> 2   5.3    25 f     front-wheel drive
#> 3   7      24 r     rear-wheel drive

Rồi chúng ta dùng data frame mới để gắn nhãn trực tiếp ba nhóm, thay chú giải bằng nhãn đặt trên biểu đồ. Dùng argument fontfacesize để tùy chỉnh giao diện nhãn. (theme(legend.position = "none") tắt tất cả chú giải — chúng ta sẽ nói thêm ngay.)

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point(alpha = 0.3) +
  geom_smooth(se = FALSE) +
  geom_text(
    data = label_info,
    aes(x = displ, y = hwy, label = drive_type),
    fontface = "bold", size = 5, hjust = "right", vjust = "bottom"
  ) +
  theme(legend.position = "none")
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Biểu đồ phân tán quãng đường cao tốc theo dung tích động cơ, tô màu theo hệ dẫn động. Đường cong trơn cho mỗi loại được phủ lên. Nhãn văn bản xác định các đường cong.

Lưu ý cách dùng hjust (căn ngang) và vjust (căn dọc) để kiểm soát vị trí nhãn.

Tuy nhiên, biểu đồ comment ở trên khó đọc vì nhãn chồng chéo nhau và với điểm. Chúng ta có thể dùng function geom_label_repel() từ package ggrepel để giải quyết cả hai vấn đề. Gói mở rộng này tự động điều chỉnh nhãn để không chồng chéo:

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point(alpha = 0.3) +
  geom_smooth(se = FALSE) +
  geom_label_repel(
    data = label_info,
    aes(x = displ, y = hwy, label = drive_type),
    fontface = "bold", size = 5, nudge_y = 2
  ) +
  theme(legend.position = "none")
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Biểu đồ phân tán quãng đường cao tốc theo dung tích động cơ, tô màu theo hệ dẫn động. Đường cong trơn cho mỗi loại được phủ lên. Nhãn hộp trắng xác định các đường cong, không chồng chéo.

Bạn cũng có thể dùng cùng ý tưởng để đánh dấu điểm nhất định bằng geom_text_repel() từ ggrepel. Lưu ý kỹ thuật tiện dụng khác: chúng ta thêm lớp thứ hai gồm điểm lớn, rỗng để đánh dấu rõ hơn.

potential_outliers <- mpg |>
  filter(hwy > 40 | (hwy > 20 & displ > 5))

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  geom_text_repel(data = potential_outliers, aes(label = model)) +
  geom_point(data = potential_outliers, color = "red") +
  geom_point(
    data = potential_outliers,
    color = "red", size = 3, shape = "circle open"
  )

Biểu đồ phân tán hiệu suất nhiên liệu cao tốc theo dung tích động cơ. Các điểm có quãng đường trên 40 hoặc trên 20 với dung tích trên 5 được tô đỏ, khoanh tròn rỗng đỏ, và gắn nhãn tên model.

Ngoài geom_text()geom_label(), bạn có nhiều geom khác trong ggplot2 để comment biểu đồ. Một vài ý tưởng:

  • Dùng geom_hline()geom_vline() để thêm đường tham chiếu. Chúng tôi thường làm chúng dày (linewidth = 2) và trắng (color = white), vẽ dưới lớp dữ liệu chính. Điều này giúp dễ thấy mà không thu hút sự chú ý khỏi dữ liệu.

  • Dùng geom_rect() để vẽ hình chữ nhật quanh điểm quan tâm. Ranh giới được định nghĩa bởi aesthetic xmin, xmax, ymin, ymax. Hoặc xem package ggforce, cụ thể geom_mark_hull().

  • Dùng geom_segment() với argument arrow để vẽ mũi tên chỉ đến điểm. Dùng aesthetic xy cho vị trí bắt đầu, xendyend cho vị trí kết thúc.

Function tiện dụng khác để thêm comment là annotate(). Theo nguyên tắc chung, geom hữu ích cho đánh dấu tập con dữ liệu trong khi annotate() hữu ích cho thêm một hoặc vài phần tử comment.

Để minh họa annotate(), hãy tạo văn bản để thêm vào biểu đồ. Văn bản hơi dài nên dùng stringr::str_wrap() để tự động xuống dòng:

trend_text <- "Larger engine sizes tend to have lower fuel economy." |>
  str_wrap(width = 30)
trend_text
#> [1] "Larger engine sizes tend to\nhave lower fuel economy."

Rồi thêm hai lớp comment: một với label geom và một với segment geom. Aesthetic xy trong cả hai định nghĩa nơi comment bắt đầu, và xendyend trong segment định nghĩa vị trí kết thúc. Lưu ý segment được tạo kiểu mũi tên.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  annotate(
    geom = "label", x = 3.5, y = 38,
    label = trend_text,
    hjust = "left", color = "red"
  ) +
  annotate(
    geom = "segment",
    x = 3, y = 35, xend = 5, yend = 25, color = "red",
    arrow = arrow(type = "closed")
  )

Biểu đồ phân tán hiệu suất nhiên liệu cao tốc theo dung tích động cơ. Mũi tên đỏ hướng xuống theo xu hướng điểm, với comment đỏ bên cạnh.

Chú thích là công cụ mạnh mẽ để truyền đạt phát hiện chính và đặc điểm thú vị. Giới hạn duy nhất là trí tưởng tượng (và sự kiên nhẫn định vị comment cho đẹp)!

11.3.1 Bài tập

  1. Dùng geom_text() với vị trí vô cực để đặt văn bản ở bốn góc biểu đồ.

  2. Dùng annotate() để thêm point geom ở giữa biểu đồ cuối mà không cần tạo tibble. Tùy chỉnh hình dạng, kích thước, hoặc màu sắc.

  3. Nhãn geom_text() tương tác với facet thế nào? Làm sao thêm nhãn vào một facet? Làm sao đặt nhãn khác nhau cho mỗi facet? (Gợi ý: Nghĩ về tập dữ liệu được truyền vào geom_text().)

  4. Đối số nào của geom_label() kiểm soát giao diện hộp nền?

  5. Bốn argument của arrow() là gì? Chúng hoạt động thế nào? Tạo string biểu đồ minh họa các tùy chọn quan trọng nhất.

11.4 Thang đo

Cách thứ ba để cải thiện biểu đồ cho truyền đạt là điều chỉnh scale (scale). Thang đo kiểm soát cách mapping aesthetic hiển thị trực quan.

11.4.1 Thang đo mặc định

Thông thường, ggplot2 tự động thêm scale. Ví dụ, khi bạn gõ:

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class))

ggplot2 tự động thêm scale mặc định phía sau:

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  scale_x_continuous() +
  scale_y_continuous() +
  scale_color_discrete()

Lưu ý quy ước đặt tên scale: scale_ theo sau bởi tên aesthetic, rồi _, rồi tên scale. Thang đo mặc định được đặt tên theo loại biến: continuous, discrete, datetime, hoặc date. scale_x_continuous() đặt giá trị số displ trên trục x liên tục, scale_color_discrete() chọn màu cho mỗi class, v.v. Có nhiều scale không mặc định bạn sẽ học dưới đây.

Thang đo mặc định được chọn kỹ để hoạt động tốt với nhiều đầu vào. Tuy nhiên, bạn có thể muốn ghi đè mặc định vì hai lý do:

  • Bạn muốn chỉnh một số parameter của scale mặc định, như thay đổi break trên trục hoặc nhãn chú giải.

  • Bạn muốn thay thế hoàn toàn scale, dùng thuật toán khác. Thường bạn làm tốt hơn mặc định vì hiểu dữ liệu hơn.

11.4.2 Vạch trục và khóa chú giải

Gộp lại, trục và chú giải được gọi là hướng dẫn (guide). Trục dùng cho aesthetic x và y; chú giải dùng cho mọi thứ khác.

Có hai argument chính ảnh hưởng giao diện vạch trục và khóa chú giải: breakslabels. Breaks kiểm soát vị trí vạch, hoặc giá trị liên kết với khóa. Labels kiểm soát nhãn văn bản của mỗi vạch/khóa. Cách dùng phổ biến nhất của breaks là ghi đè lựa chọn mặc định:

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  scale_y_continuous(breaks = seq(15, 40, by = 5))

Biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo hệ dẫn động. Trục y có break từ 15 đến 40, mỗi 5.

Bạn có thể dùng labels tương tự (vector ký tự cùng độ dài với breaks), nhưng cũng có thể đặt thành NULL để ẩn nhãn. Điều này hữu ích cho bản đồ, hoặc khi không thể chia sẻ số tuyệt đối. Bạn cũng có thể dùng breakslabels cho chú giải. Với scale rời rạc cho biến phân loại, labels có thể là list đặt tên.

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  scale_x_continuous(labels = NULL) +
  scale_y_continuous(labels = NULL) +
  scale_color_discrete(labels = c("4" = "4-wheel", "f" = "front", "r" = "rear"))

Biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo hệ dẫn động. Trục x và y không có nhãn. Chú giải có nhãn tùy chỉnh: 4-wheel, front, rear.

Đối số labels kết hợp với function gắn nhãn từ package scales cũng hữu ích để định dạng số thành tiền tệ, phần trăm, v.v. Biểu đồ bên trái hiển thị gắn nhãn mặc định với label_dollar(), thêm ký hiệu đô la và dấu phẩy phân cách row nghìn. Biểu đồ bên phải tùy chỉnh thêm bằng cách chia giá trị cho 1.000 và thêm hậu tố “K” cùng break tùy chỉnh. Lưu ý breaks dùng scale gốc của dữ liệu.

# Trái
ggplot(diamonds, aes(x = price, y = cut)) +
  geom_boxplot(alpha = 0.05) +
  scale_x_continuous(labels = label_dollar())

# Phải
ggplot(diamonds, aes(x = price, y = cut)) +
  geom_boxplot(alpha = 0.05) +
  scale_x_continuous(
    labels = label_dollar(scale = 1/1000, suffix = "K"),
    breaks = seq(1000, 19000, by = 6000)
  )

Hai biểu đồ hộp giá theo cut kim cương. Ngoại lệ trong suốt. Cả hai biểu đồ có nhãn trục x định dạng tiền tệ.

Hai biểu đồ hộp giá theo cut kim cương. Ngoại lệ trong suốt. Cả hai biểu đồ có nhãn trục x định dạng tiền tệ.

Function gắn nhãn tiện dụng khác là label_percent():

ggplot(diamonds, aes(x = cut, fill = clarity)) +
  geom_bar(position = "fill") +
  scale_y_continuous(name = "Percentage", labels = label_percent())

Biểu đồ column phân đoạn cut, tô theo clarity. Nhãn trục y từ 0% đến 100%, tên trục y là "Percentage".

Một cách dùng breaks khác là khi có ít điểm dữ liệu và muốn đánh dấu chính xác nơi quan sát. Ví dụ, biểu đồ sau cho thấy mỗi tổng thống Mỹ bắt đầu và kết thúc nhiệm kỳ khi nào.

presidential |>
  mutate(id = 33 + row_number()) |>
  ggplot(aes(x = start, y = id)) +
  geom_point() +
  geom_segment(aes(xend = end, yend = id)) +
  scale_x_date(name = NULL, breaks = presidential$start, date_labels = "'%y")

Biểu đồ đường id tổng thống theo năm bắt đầu nhiệm kỳ. Năm bắt đầu được đánh dấu bằng điểm và đoạn thẳng kéo đến cuối nhiệm kỳ. Nhãn trục x định dạng năm hai chữ số với dấu nháy đơn.

Lưu ý rằng chúng ta trích xuất biến start làm vector với presidential$start vì không thể dùng mapping aesthetic cho argument này. Cũng lưu ý rằng đặc tả break và label cho scale ngày và ngày-giờ hơi khác:

  • date_labels nhận đặc tả định dạng, cùng dạng với parse_datetime().

  • date_breaks (không hiển thị ở đây), nhận string như “2 days” hoặc “1 month”.

11.4.3 Bố cục chú giải

Bạn sẽ thường dùng breakslabels để chỉnh trục. Dù chúng cũng hoạt động cho chú giải, có vài kỹ thuật khác hay dùng hơn.

Để kiểm soát vị trí tổng thể của chú giải, dùng thiết lập theme(). Chúng ta sẽ quay lại theme (theme) cuối chương, nhưng tóm tắt, chúng kiểm soát phần không phải dữ liệu. Thiết lập legend.position kiểm soát nơi chú giải được vẽ:

base <- ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class))

base + theme(legend.position = "right") # mặc định
base + theme(legend.position = "left")
base +
  theme(legend.position = "top") +
  guides(color = guide_legend(nrow = 3))
base +
  theme(legend.position = "bottom") +
  guides(color = guide_legend(nrow = 3))

Bốn biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo loại xe. Theo chiều kim đồng hồ, chú giải ở phải, trái, dưới, trên.

Bốn biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo loại xe. Theo chiều kim đồng hồ, chú giải ở phải, trái, dưới, trên.

Bốn biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo loại xe. Theo chiều kim đồng hồ, chú giải ở phải, trái, dưới, trên.

Bốn biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo loại xe. Theo chiều kim đồng hồ, chú giải ở phải, trái, dưới, trên.

Nếu biểu đồ ngắn và rộng, đặt chú giải ở trên hoặc dưới; nếu cao và hẹp, đặt ở trái hoặc phải. Bạn cũng có thể dùng legend.position = "none" để ẩn chú giải hoàn toàn.

Để kiểm soát hiển thị từng chú giải, dùng guides() cùng guide_legend() hoặc guide_colorbar(). Ví dụ sau cho thấy hai thiết lập quan trọng: kiểm soát số row với nrow, và ghi đè aesthetic để điểm lớn hơn. Điều này đặc biệt hữu ích nếu dùng alpha thấp cho nhiều điểm.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  theme(legend.position = "bottom") +
  guides(color = guide_legend(nrow = 2, override.aes = list(size = 4)))
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Biểu đồ phân tán hiệu suất nhiên liệu theo dung tích động cơ, tô màu theo loại xe. Đường cong trơn được phủ lên. Chú giải ở dưới, hai row, điểm chú giải lớn hơn điểm biểu đồ.

Lưu ý tên argument trong guides() khớp với tên aesthetic, giống như trong labs().

11.4.4 Thay thế scale

Thay vì chỉ chỉnh chi tiết, bạn có thể thay thế hoàn toàn scale. Hai loại scale hay muốn đổi nhất: scale vị trí liên tục và scale màu. Nguyên tắc tương tự áp dụng cho tất cả aesthetic khác.

Rất hữu ích khi vẽ phép biến đổi biến. Ví dụ, dễ thấy mối quan hệ chính xác giữa caratprice hơn nếu biến đổi log:

# Trái
ggplot(diamonds, aes(x = carat, y = price)) +
  geom_bin2d()
#> `stat_bin2d()` using `bins = 30`. Pick better value `binwidth`.

# Phải
ggplot(diamonds, aes(x = log10(carat), y = log10(price))) +
  geom_bin2d()
#> `stat_bin2d()` using `bins = 30`. Pick better value `binwidth`.

Hai biểu đồ giá theo carat kim cương. Biểu đồ phải có giá trị log trên trục.

Hai biểu đồ giá theo carat kim cương. Biểu đồ phải có giá trị log trên trục.

Tuy nhiên, nhược điểm là trục gắn nhãn giá trị đã biến đổi, khó đọc. Thay vì biến đổi trong mapping aesthetic, chúng ta có thể biến đổi bằng scale. Trực quan giống hệt, trừ việc trục gắn nhãn scale gốc.

ggplot(diamonds, aes(x = carat, y = price)) +
  geom_bin2d() +
  scale_x_log10() +
  scale_y_log10()
#> `stat_bin2d()` using `bins = 30`. Pick better value `binwidth`.

Biểu đồ giá theo carat kim cương. Nhãn trục trên scale gốc.

Thang đo khác thường tùy chỉnh là màu. Thang đo phân loại mặc định chọn màu cách đều trên vòng màu. Thay thế hữu ích là scale ColorBrewer, được chỉnh tay để tốt hơn cho người mù màu. Hai biểu đồ dưới trông tương tự, nhưng đủ khác biệt để phân biệt được ngay cả với người mù màu đỏ-xanh.1

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv))

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv)) +
  scale_color_brewer(palette = "Set1")

Hai biểu đồ phân tán quãng đường cao tốc theo dung tích động cơ, tô màu theo hệ dẫn động. Trái dùng bảng màu mặc định, phải dùng bảng màu khác.

Hai biểu đồ phân tán quãng đường cao tốc theo dung tích động cơ, tô màu theo hệ dẫn động. Trái dùng bảng màu mặc định, phải dùng bảng màu khác.

Đừng quên kỹ thuật đơn giản hơn để cải thiện tính tiếp cận. Nếu chỉ có vài màu, thêm mapping hình dạng dư thừa. Điều này cũng giúp biểu đồ đọc được khi in đen trắng.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv, shape = drv)) +
  scale_color_brewer(palette = "Set1")

Biểu đồ phân tán quãng đường cao tốc theo dung tích động cơ, cả màu và hình dạng theo hệ dẫn động. Bảng màu không phải mặc định.

Thang đo ColorBrewer được tài liệu tại https://colorbrewer2.org/ và có sẵn trong R qua package RColorBrewer. Hình 11.1 hiển thị tất cả bảng màu. Bảng màu tuần tự (trên) và phân kỳ (dưới) đặc biệt hữu ích nếu biến phân loại có thứ tự hoặc có “giữa”.

Tất cả scale ColorBrewer. Một nhóm đi từ sáng đến tối. Nhóm khác là màu không có thứ tự. Nhóm cuối có scale phân kỳ.
Hình 11.1: Tất cả scale ColorBrewer.

Khi có mapping giá trị-màu được định nghĩa sẵn, dùng scale_color_manual(). Ví dụ, nếu mapping đảng phái tổng thống đến màu, chúng ta muốn đỏ cho Cộng hòa và xanh cho Dân chủ:

presidential |>
  mutate(id = 33 + row_number()) |>
  ggplot(aes(x = start, y = id, color = party)) +
  geom_point() +
  geom_segment(aes(xend = end, yend = id)) +
  scale_color_manual(values = c(Republican = "#E81B23", Democratic = "#00AEF3"))

Biểu đồ đường id tổng thống theo năm bắt đầu. Dân chủ màu xanh, Cộng hòa màu đỏ.

Với màu liên tục, dùng scale_color_gradient() hoặc scale_fill_gradient() tích hợp sẵn. Nếu có scale phân kỳ, dùng scale_color_gradient2() cho phép gán màu khác nhau cho giá trị dương và âm.

Tùy chọn khác là scale màu viridis. Các nhà thiết kế Nathaniel Smith và Stéfan van der Walt đã tạo ra sơ đồ màu liên tục dễ nhận biết cho người mù màu và đồng nhất tri giác trong cả màu và đen trắng. Chúng có sẵn dạng liên tục (c), rời rạc (d), và phân bin (b) trong ggplot2.

df <- tibble(
  x = rnorm(10000),
  y = rnorm(10000)
)

ggplot(df, aes(x, y)) +
  geom_hex() +
  coord_fixed() +
  labs(title = "Default, continuous", x = NULL, y = NULL)

ggplot(df, aes(x, y)) +
  geom_hex() +
  coord_fixed() +
  scale_fill_viridis_c() +
  labs(title = "Viridis, continuous", x = NULL, y = NULL)

ggplot(df, aes(x, y)) +
  geom_hex() +
  coord_fixed() +
  scale_fill_viridis_b() +
  labs(title = "Viridis, binned", x = NULL, y = NULL)

Ba biểu đồ hex trong đó màu hex cho thấy số quan sát. Biểu đồ đầu dùng scale mặc định liên tục, thứ hai dùng viridis liên tục, thứ ba dùng viridis phân bin.

Ba biểu đồ hex trong đó màu hex cho thấy số quan sát. Biểu đồ đầu dùng scale mặc định liên tục, thứ hai dùng viridis liên tục, thứ ba dùng viridis phân bin.

Ba biểu đồ hex trong đó màu hex cho thấy số quan sát. Biểu đồ đầu dùng scale mặc định liên tục, thứ hai dùng viridis liên tục, thứ ba dùng viridis phân bin.

Lưu ý tất cả scale màu có hai biến thể: scale_color_*()scale_fill_*() cho aesthetic colorfill tương ứng.

11.4.5 Phóng to

Có ba cách kiểm soát giới hạn biểu đồ:

  1. Điều chỉnh dữ liệu được vẽ.
  2. Đặt giới hạn trong mỗi scale.
  3. Đặt xlimylim trong coord_cartesian().

Chúng ta sẽ minh họa các tùy chọn này. Biểu đồ trái hiển thị mối quan hệ giữa dung tích động cơ và hiệu suất nhiên liệu, tô màu theo hệ dẫn động. Biểu đồ phải hiển thị cùng biến, nhưng lọc dữ liệu. Lọc dữ liệu đã ảnh hưởng đến scale x và y cũng như đường cong trơn.

# Trái
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv)) +
  geom_smooth()

# Phải
mpg |>
  filter(displ >= 5 & displ <= 6 & hwy >= 10 & hwy <= 25) |>
  ggplot(aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv)) +
  geom_smooth()

Bên trái, biểu đồ phân tán quãng đường cao tốc theo dung tích, tô màu theo hệ dẫn động, đường cong trơn giảm rồi tăng. Bên phải, cùng biến nhưng dung tích từ 5-6, quãng đường từ 10-25, đường cong trơn hơi tăng rồi giảm.

Bên trái, biểu đồ phân tán quãng đường cao tốc theo dung tích, tô màu theo hệ dẫn động, đường cong trơn giảm rồi tăng. Bên phải, cùng biến nhưng dung tích từ 5-6, quãng đường từ 10-25, đường cong trơn hơi tăng rồi giảm.

So sánh với hai biểu đồ dưới: trái đặt limits trên scale, phải đặt trong coord_cartesian(). Thu hẹp limits tương đương lọc dữ liệu. Vì vậy, để phóng to vùng biểu đồ, tốt nhất dùng coord_cartesian().

# Trái
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv)) +
  geom_smooth() +
  scale_x_continuous(limits = c(5, 6)) +
  scale_y_continuous(limits = c(10, 25))

# Phải
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv)) +
  geom_smooth() +
  coord_cartesian(xlim = c(5, 6), ylim = c(10, 25))

Bên trái, biểu đồ phân tán với limits trên scale, đường cong hơi tăng rồi giảm. Bên phải, cùng limits nhưng qua coord_cartesian, đường cong gần phẳng hơi tăng cuối.

Bên trái, biểu đồ phân tán với limits trên scale, đường cong hơi tăng rồi giảm. Bên phải, cùng limits nhưng qua coord_cartesian, đường cong gần phẳng hơi tăng cuối.

Mặt khác, đặt limits trên scale riêng lẻ hữu ích hơn khi muốn mở rộng giới hạn, ví dụ để đồng bộ scale giữa biểu đồ. Nếu trích xuất hai loại xe và vẽ riêng, khó so sánh vì cả ba scale khác nhau.

suv <- mpg |> filter(class == "suv")
compact <- mpg |> filter(class == "compact")

# Trái
ggplot(suv, aes(x = displ, y = hwy, color = drv)) +
  geom_point()

# Phải
ggplot(compact, aes(x = displ, y = hwy, color = drv)) +
  geom_point()

Bên trái, biểu đồ phân tán quãng đường cao tốc theo dung tích cho SUV. Bên phải, biểu đồ tương tự cho xe compact.

Bên trái, biểu đồ phân tán quãng đường cao tốc theo dung tích cho SUV. Bên phải, biểu đồ tương tự cho xe compact.

Cách khắc phục là chia sẻ scale, huấn luyện với limits của toàn bộ dữ liệu.

x_scale <- scale_x_continuous(limits = range(mpg$displ))
y_scale <- scale_y_continuous(limits = range(mpg$hwy))
col_scale <- scale_color_discrete(limits = unique(mpg$drv))

# Trái
ggplot(suv, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  x_scale +
  y_scale +
  col_scale

# Phải
ggplot(compact, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  x_scale +
  y_scale +
  col_scale

Hai biểu đồ phân tán cùng scale cho x, y, và màu, giúp so sánh trực tiếp.

Hai biểu đồ phân tán cùng scale cho x, y, và màu, giúp so sánh trực tiếp.

Trong trường hợp này, bạn có thể dùng facet, nhưng kỹ thuật này hữu ích hơn khi muốn rải biểu đồ qua nhiều trang.

11.4.6 Bài tập

  1. Tại sao mã sau không ghi đè scale mặc định?

    df <- tibble(
      x = rnorm(10000),
      y = rnorm(10000)
    )
    
    ggplot(df, aes(x, y)) +
      geom_hex() +
      scale_color_gradient(low = "white", high = "red") +
      coord_fixed()
  2. Đối số đầu tiên của mọi scale là gì? So sánh với labs()?

  3. Thay đổi hiển thị nhiệm kỳ tổng thống bằng cách:

    1. Kết hợp hai biến thể tùy chỉnh màu và break trục x.
    2. Cải thiện hiển thị trục y.
    3. Gắn nhãn mỗi nhiệm kỳ bằng tên tổng thống.
    4. Thêm nhãn biểu đồ mang tính mô tả.
    5. Đặt break mỗi 4 năm (khó hơn tưởng!).
  4. Đầu tiên, tạo biểu đồ sau. Rồi, sửa mã dùng override.aes để chú giải dễ thấy hơn.

    ggplot(diamonds, aes(x = carat, y = price)) +
      geom_point(aes(color = cut), alpha = 1/20)

11.5 Chủ đề

Cuối cùng, bạn có thể tùy chỉnh phần không phải dữ liệu với theme (theme):

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  theme_bw()

Biểu đồ phân tán quãng đường cao tốc theo dung tích, tô màu theo loại xe. Nền trắng, đường lưới xám.

ggplot2 bao gồm tám theme trong Hình 11.2, với theme_gray() là mặc định.2 Nhiều theme khác có trong package bổ trợ như ggthemes (https://jrnold.github.io/ggthemes). Bạn cũng có thể tạo theme riêng để phù hợp với phong cách công ty hoặc tạp chí.

Tám biểu đồ column, mỗi cái với một trong tám theme tích hợp.
Hình 11.2: Tám theme tích hợp trong ggplot2.

Cũng có thể kiểm soát từng thành phần của theme, như kích thước và màu phông chữ trục y. Chúng ta đã thấy legend.position kiểm soát vị trí chú giải. Nhiều khía cạnh khác có thể tùy chỉnh bằng theme(). Ví dụ, trong biểu đồ dưới chúng ta thay đổi hướng chú giải và thêm viền đen. Tùy chỉnh hộp chú giải và phần tử tiêu đề được thực hiện với function element_*(). Các phần tử theme kiểm soát vị trí tiêu đề và comment là plot.title.positionplot.caption.position, được đặt thành "plot" để căn chỉnh với toàn bộ vùng biểu đồ.

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  labs(
    title = "Larger engine sizes tend to have lower fuel economy",
    caption = "Source: https://fueleconomy.gov."
  ) +
  theme(
    legend.position = c(0.6, 0.7),
    legend.direction = "horizontal",
    legend.box.background = element_rect(color = "black"),
    plot.title = element_text(face = "bold"),
    plot.title.position = "plot",
    plot.caption.position = "plot",
    plot.caption = element_text(hjust = 0)
  )

Biểu đồ phân tán hiệu suất nhiên liệu theo dung tích, tô màu theo hệ dẫn động. Tiêu đề và comment căn trái, chú giải nằm trong biểu đồ với viền đen.

Để xem tổng quan tất cả thành phần theme(), xem trợ giúp ?theme. Cuốn sách ggplot2 cũng là nơi tuyệt vời cho chi tiết đầy đủ về theme.

11.5.1 Bài tập

  1. Chọn một theme từ package ggthemes và áp dụng cho biểu đồ cuối bạn đã tạo.
  2. Làm nhãn trục của biểu đồ thành màu xanh và in đậm.

11.6 Bố cục

Cho đến nay chúng ta nói về cách tạo và sửa một biểu đồ. Nếu bạn có nhiều biểu đồ muốn bố trí theo cách nhất định thì sao? Gói mở rộng patchwork cho phép kết hợp biểu đồ riêng lẻ thành cùng đồ họa. Chúng ta đã tải package này ở đầu chương.

Để đặt hai biểu đồ cạnh nhau, đơn giản cộng chúng. Lưu ý bạn cần tạo biểu đồ và lưu làm đối tượng (ví dụ p1p2). Rồi đặt cạnh nhau bằng +.

p1 <- ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  labs(title = "Plot 1")
p2 <- ggplot(mpg, aes(x = drv, y = hwy)) +
  geom_boxplot() +
  labs(title = "Plot 2")
p1 + p2

Hai biểu đồ (biểu đồ phân tán và biểu đồ hộp) đặt cạnh nhau.

Lưu ý rằng chúng ta không dùng function mới từ patchwork. Thay vào đó, package thêm chức năng mới cho toán tử +.

Bạn cũng có thể tạo bố cục phức tạp. Dưới đây, | đặt p1p3 cạnh nhau và / đưa p2 xuống dòng tiếp theo.

p3 <- ggplot(mpg, aes(x = cty, y = hwy)) +
  geom_point() +
  labs(title = "Plot 3")
(p1 | p3) / p2

Ba biểu đồ: biểu đồ 1 và 3 cạnh nhau ở trên, biểu đồ 2 trải rộng phía dưới.

Ngoài ra, patchwork cho phép gom chú giải từ nhiều biểu đồ, tùy chỉnh vị trí chú giải cùng kích thước biểu đồ, và thêm tiêu đề, phụ đề, comment chung. Dưới đây chúng ta tạo 5 biểu đồ. Chúng ta tắt chú giải trên biểu đồ hộp và phân tán, gom chú giải cho biểu đồ mật độ lên trên bằng & theme(legend.position = "top"). Lưu ý dùng & thay vì + vì chúng ta sửa theme cho biểu đồ patchwork, không phải từng ggplot. Chú giải được đặt trong guide_area(). Cuối cùng, chúng ta tùy chỉnh chiều cao các thành phần – hướng dẫn cao 1, biểu đồ hộp 3, mật độ 2, và phân tán có facet 4.

p1 <- ggplot(mpg, aes(x = drv, y = cty, color = drv)) +
  geom_boxplot(show.legend = FALSE) +
  labs(title = "Plot 1")

p2 <- ggplot(mpg, aes(x = drv, y = hwy, color = drv)) +
  geom_boxplot(show.legend = FALSE) +
  labs(title = "Plot 2")

p3 <- ggplot(mpg, aes(x = cty, color = drv, fill = drv)) +
  geom_density(alpha = 0.5) +
  labs(title = "Plot 3")

p4 <- ggplot(mpg, aes(x = hwy, color = drv, fill = drv)) +
  geom_density(alpha = 0.5) +
  labs(title = "Plot 4")

p5 <- ggplot(mpg, aes(x = cty, y = hwy, color = drv)) +
  geom_point(show.legend = FALSE) +
  facet_wrap(~drv) +
  labs(title = "Plot 5")

(guide_area() / (p1 + p2) / (p3 + p4) / p5) +
  plot_annotation(
    title = "City and highway mileage for cars with different drive trains",
    caption = "Source: https://fueleconomy.gov."
  ) +
  plot_layout(
    guides = "collect",
    heights = c(1, 3, 2, 4)
    ) &
  theme(legend.position = "top")

Năm biểu đồ bố trí: hai biểu đồ hộp cạnh nhau, hai biểu đồ mật độ phía dưới, và biểu đồ phân tán có facet trải rộng dưới cùng. Tất cả tô màu theo hệ dẫn động với một chú giải chung ở trên.

Nếu bạn muốn tìm hiểu thêm về kết hợp và bố trí biểu đồ với patchwork, xem hướng dẫn trên trang web package: https://patchwork.data-imaginist.com.

11.6.1 Bài tập

  1. Điều gì xảy ra nếu bỏ dấu ngoặc đơn trong bố cục sau? Bạn có thể giải thích tại sao?

    p1 <- ggplot(mpg, aes(x = displ, y = hwy)) +
      geom_point() +
      labs(title = "Plot 1")
    p2 <- ggplot(mpg, aes(x = drv, y = hwy)) +
      geom_boxplot() +
      labs(title = "Plot 2")
    p3 <- ggplot(mpg, aes(x = cty, y = hwy)) +
      geom_point() +
      labs(title = "Plot 3")
    
    (p1 | p2) / p3
  2. Dùng ba biểu đồ từ bài tập trước, tái tạo patchwork sau.

    Ba biểu đồ: Plot 1 ở row đầu, Plot 2 và 3 ở row dưới. Plot 1 gắn nhãn "Fig. A", Plot 2 gắn nhãn "Fig. B", Plot 3 gắn nhãn "Fig. C".

11.7 Tóm tắt

Trong chương này bạn đã học về thêm nhãn biểu đồ như tiêu đề, phụ đề, comment cũng như sửa nhãn trục mặc định, dùng comment để thêm văn bản hoặc đánh dấu điểm dữ liệu cụ thể, tùy chỉnh scale trục, và thay đổi theme biểu đồ. Bạn cũng đã học về kết hợp nhiều biểu đồ trong một đồ họa dùng cả bố cục đơn giản và phức tạp.

Dù bạn đã học cách tạo nhiều loại biểu đồ và tùy chỉnh bằng nhiều kỹ thuật, chúng ta mới chỉ chạm bề mặt những gì bạn có thể tạo với ggplot2. Nếu muốn hiểu toàn diện ggplot2, chúng tôi khuyến nghị đọc cuốn sách ggplot2: Elegant Graphics for Data Analysis. Tài nguyên hữu ích khác là R Graphics Cookbook của Winston Chang và Fundamentals of Data Visualization của Claus Wilke.


  1. Bạn có thể dùng công cụ như SimDaltonism để mô phỏng mù màu.↩︎

  2. Nhiều người thắc mắc tại sao theme mặc định có nền xám. Đây là lựa chọn có chủ đích vì nó đẩy dữ liệu lên phía trước trong khi vẫn hiển thị đường lưới. Đường lưới trắng dễ thấy (quan trọng vì chúng hỗ trợ đánh giá vị trí), nhưng ít ảnh hưởng thị giác. Nền xám tạo màu tương tự văn bản, đảm bảo đồ họa hòa hợp với tài liệu. Cuối cùng, nền xám tạo trường màu liên tục đảm bảo biểu đồ được nhận biết như một thực thể trực quan duy nhất.↩︎