Devin.KR

R · 기본

데이터로 시작하는 R

그래프 그리기 - base graphics

plot·hist·boxplot·barplot, 축·제목·색, png 로 저장, 그래프 고르는 기준, ggplot2 소개

개발자KR · 원고 갱신

이 장에서 배우는 것

앞 장에서 평균과 중앙값, 흩어진 정도를 이용해 편의점 판매 기록을 요약했다. 숫자로 요약하면 비교하기는 쉽지만, 판매액이 어느 날부터 늘었는지나 유난히 큰 거래가 몇 건인지까지 바로 보이지는 않는다. 그래프는 여러 숫자의 관계를 위치와 길이로 나타내어 이런 질문에 답하도록 돕는다. 다만 같은 자료라도 그래프를 고르는 방법과 축의 범위에 따라 다른 인상을 줄 수 있다. 그림을 만들기 전에 무엇을 비교하려는지 먼저 정해야 한다.

이 장에서는 R에 기본으로 포함된 그래프 기능인 기본 그래픽스(base graphics)를 사용한다. 별도의 패키지, 즉 기능을 묶어 배포하는 확장 도구를 설치하지 않는다. 편의점의 2주 판매 기록을 코드 안에서 만들고, 네 가지 그래프를 PNG 파일로 저장한다. PNG는 그림을 점의 격자로 기록하는 이미지 파일 형식이다. 화면에서 그래프 창을 열지 않아도 스크립트를 실행하면 결과 파일이 남도록 구성한다.

  • 질문에 맞게 꺾은선 그래프, 히스토그램, 상자 그림, 막대그래프를 고른다.
  • plot(), hist(), boxplot(), barplot()의 입력 자료와 결과를 구별한다.
  • 축, 제목, 색을 조정하여 그래프가 무엇을 보여 주는지 분명하게 만든다.
  • png()와 dev.off()를 짝지어 그림을 파일로 저장한다.
  • 기본 그래픽스와 ggplot2의 표현 방식이 어떻게 다른지 이해한다.

문제 상황

동네 편의점 점주는 최근 2주 동안 음료와 간식 판매가 늘었다고 느낀다. 엑셀 표에는 날짜, 상품, 분류, 수량, 금액이 있지만, 표를 훑어보는 것만으로는 증가가 꾸준했는지 특정 날짜에 집중됐는지 판단하기 어렵다. 음료가 간식보다 많이 팔렸다는 말도 수량을 뜻하는지 금액을 뜻하는지 불분명하다.

점주에게 보여 줄 자료에는 네 가지 질문을 담기로 한다. 날짜별 전체 판매액은 어떻게 변했는가. 하루 판매액은 어느 구간에 많이 모였는가. 음료와 간식의 기록별 판매액은 어떻게 분포하는가. 두 분류의 전체 판매 수량은 얼마인가. 질문마다 비교 대상이 다르므로 같은 모양의 그래프를 반복해서 쓰지 않는다.

이 예제의 한 행은 한 날짜에 한 상품이 팔린 수량과 금액을 합친 기록이다. 영수증 한 장이나 고객 한 명의 구매 기록이 아니다. 매일 생수와 과자 기록을 하나씩 만들므로 14일 동안 28행이 생긴다. 판매 기록의 단위를 먼저 밝히면 상자 그림에 나타난 금액을 고객별 구매액으로 잘못 읽는 일을 줄일 수 있다.

데이터는 분석 과정을 익히기 위해 만든 예시이며 실제 매장의 실적이 아니다. 생수 단가는 1,000원, 과자 단가는 1,500원으로 고정한다. 할인과 반품은 포함하지 않는다. 난수를 사용하지 않으므로 언제 실행하든 같은 수량과 금액을 얻는다. 결과 파일은 실행한 폴더에 저장한다.

질문에 맞는 그래프를 고른다

그래프를 고를 때는 먼저 가로축에 놓을 대상이 무엇인지 생각한다. 날짜처럼 순서와 간격에 의미가 있으면 시간에 따른 변화를 보여 줄 수 있다. 음료와 간식처럼 종류를 나눈 이름이라면 범주별 크기를 비교한다. 하나의 수치가 어느 범위에 모였는지 궁금하다면 그 수치의 분포를 살핀다. 분포는 값이 어느 곳에 얼마나 모이고 얼마나 넓게 퍼지는지를 뜻한다.

질문과 자료의 형태에 따라 그래프를 선택한다
질문입력 자료함수읽을 부분
날짜별 판매액이 변했는가날짜와 하루 합계plot()점의 높이와 이어진 흐름
하루 판매액이 어디에 모였는가하루 합계 벡터hist()금액 구간별 날짜 수
분류별 기록 금액이 어떻게 다른가금액과 분류boxplot()중앙값과 상자의 범위
어느 분류가 더 많이 팔렸는가분류별 수량 합계barplot()막대의 길이

plot()은 두 변수의 관계를 그리는 기본 함수다. 숫자 두 묶음을 넣으면 보통 점으로 나타낸다. 이 장에서는 날짜와 판매액을 넣고 type = "b"로 점과 선을 함께 그린다. 선은 날짜 순서에 따른 흐름을 읽게 한다. 자료가 날짜 순으로 정렬되어 있어야 선이 앞뒤로 오가지 않는다. 날짜가 없는 상품 이름을 임의의 순서로 이어 놓으면 선의 기울기에 의미를 부여하기 어렵다.

히스토그램(histogram)은 수치의 범위를 여러 구간으로 나누고 각 구간에 들어가는 관측값의 개수를 나타낸다. 여기서 관측값은 하루 판매액 하나다. 막대가 높으면 해당 금액 구간에 속하는 날짜가 많다는 뜻이다. 원래 날짜 순서는 남지 않는다. 따라서 판매액이 증가했는지를 판단하는 데에는 날짜별 그래프가 필요하고, 보통 하루 판매액이 얼마인지를 살피는 데에는 히스토그램이 어울린다.

hist()의 breaks는 구간 경계다. 경계를 자동으로 정하게 할 수도 있지만, 비교하려는 보고서에서는 경계를 직접 지정하는 편이 해석하기 쉽다. 예제에서는 15,000원부터 45,000원까지 5,000원 간격으로 나눈다. 구간을 너무 넓게 잡으면 차이가 가려지고, 너무 좁게 잡으면 날짜가 14개뿐인 자료에서 빈 구간이 많아진다. 막대 모양 하나만 보고 판매 패턴이 확정됐다고 판단하지 않는다.

날짜별 그래프는 시간의 흐름을 남기고 히스토그램은 금액 구간별 날짜 수를 남긴다

상자 그림(box plot)은 여러 집단의 수치 분포를 간결하게 비교한다. 상자 안의 선은 중앙값이다. 상자의 아래쪽과 위쪽은 하위 절반과 상위 절반을 나누는 기준인 사분위수에 해당하며, 상자에는 자료의 가운데 부분이 담긴다. 같은 금액 축에서 상자가 높은 곳에 있으면 그 집단의 기록 금액이 대체로 더 크다고 읽을 수 있다.

상자에서 뻗어 나온 선을 수염이라고 부른다. R의 기본 상자 그림에서 수염은 항상 최솟값과 최댓값을 뜻하지 않는다. 기본 설정에서는 상자 길이의 1.5배를 기준으로 정한 범위 안에서 가장 바깥쪽 관측값까지 뻗는다. 그 범위 밖의 값은 별도 점으로 표시한다. 점이 따로 나왔다고 입력 오류라는 뜻은 아니다. 큰 주문이나 특별한 판매일이었는지 원래 기록을 확인할 단서다.

이 예제의 상자 그림은 분류마다 14개의 날짜·상품별 금액을 비교한다. 상품 가격이 서로 다르므로 상자의 높이만으로 수량을 비교할 수 없다. 또 같은 날의 음료와 간식이 어떻게 함께 변했는지도 이 그림만으로는 알기 어렵다. 상자 그림이 무엇을 요약하면서 무엇을 생략하는지 함께 이해해야 한다.

막대그래프(bar chart)는 이름으로 구분되는 대상의 크기를 비교한다. barplot()에 수량 합계를 넣으면 막대 높이가 수량을 나타낸다. 원자료를 넣는다고 분류별 합계가 자동으로 계산되지는 않는다. 이 장에서는 앞에서 익힌 tapply()로 분류별 합계를 구한 뒤 그 결과를 전달한다. 히스토그램의 막대는 수치 구간의 개수이고, 이 막대그래프의 막대는 분류별 판매 수량이다.

축과 제목이 해석의 기준이 된다

그래프 제목에는 비교 대상과 기간을 담는다. 축에는 값의 이름과 단위를 붙인다. 판매액 축에 단위가 없으면 40이 40원인지 4만 원인지 알 수 없다. 예제에서는 계산에 사용하는 금액은 원 단위로 두고, 그래프를 그릴 때만 1,000으로 나누어 천 원 단위로 표시한다. 제목의 main, 가로축 이름의 xlab, 세로축 이름의 ylab을 서로 구별한다.

날짜 눈금을 모두 표시하면 좁은 그림에서 글자가 겹칠 수 있다. xaxt = "n"으로 기본 가로축 눈금 표시를 생략하고 axis()로 일부 날짜만 표시한다. 관측값 14개를 모두 그리면서 눈금 글자만 다섯 개로 줄이는 방식이다. 점을 줄인 것이 아니라는 점을 구별해야 한다. 날짜 형식은 월과 일을 하이픈으로 잇도록 지정한다.

ylim은 세로축에 표시할 범위다. 막대의 길이로 크기를 비교할 때는 보통 0에서 시작하는 축이 적절하다. 이 예제에서는 날짜별 판매액도 0에서 시작하여 금액의 크기를 함께 읽게 한다. 작은 변화만 자세히 살피려고 축을 좁히는 경우도 있지만, 그러면 같은 변화가 더 크게 보인다. 축 범위를 바꾸었는지 독자가 알아볼 수 있게 해야 한다.

색은 강조할 대상을 구별하는 데 사용한다. 예제에서는 푸른색 계열을 기본으로 쓰고 간식 막대에 붉은색을 사용한다. 이 색은 분류를 구별할 뿐, 붉은색이 손실이나 위험을 뜻하지 않는다. 제목과 축 이름, 분류 이름을 함께 표시해야 색을 구별하기 어려운 독자도 내용을 읽을 수 있다. 색을 추가하는 것보다 비교 기준을 정확히 적는 것이 먼저다.

완성 코드의 그래프 글자는 운영체제의 한글 글꼴 설정에 따른 차이를 줄이기 위해 영어로 작성한다. Drink는 음료, Snack은 간식, Sales (thousand KRW)는 천 원 단위 판매액이라는 뜻이다. 실제 보고서에서 한글을 사용할 때는 저장된 파일을 열어 글꼴과 글자 겹침을 확인해야 한다.

그림을 파일로 남기고 다른 표현 방식도 알아둔다

R이 그림을 보내는 출력 대상을 그래픽 장치라고 부른다. 화면의 그래프 창도 장치이고 PNG 파일도 장치다. png()로 파일 장치를 열면 이후의 그래프 명령은 그 파일에 그림을 그린다. dev.off()는 현재 장치를 닫아 파일 기록을 마무리한다. 파일을 여는 명령, 그림을 그리는 명령, 파일을 닫는 명령의 순서를 지킨다.

PNG 파일 저장은 장치를 열고 그림을 그린 뒤 장치를 닫는 순서로 진행한다

완성 코드에서는 이 순서를 draw_png()라는 함수로 묶는다. 파일 이름과 그림을 그릴 코드를 받아서 장치를 열고 실행한다. on.exit()로 종료 시 실행할 명령을 등록하면 그림을 그리다가 오류가 나도 장치를 닫도록 할 수 있다. 다만 오류가 난 그림이 올바르게 완성되었다는 뜻은 아니다. 실행이 끝난 뒤 결과 파일의 내용도 살펴야 한다.

이미지의 가로와 세로 크기는 각각 960과 640픽셀로 지정한다. 픽셀은 이미지의 작은 점 하나다. res = 120은 해상도를 지정하며 글자와 선의 크기를 결정하는 데에도 영향을 준다. 파일 이름은 네 그림의 역할을 알 수 있도록 정한다. 같은 폴더에서 다시 실행하면 같은 이름의 파일을 덮어쓰므로, 이전 결과를 보관하려면 먼저 다른 이름이나 폴더로 옮긴다.

ggplot2는 기본 R에 포함되지 않은 그래프 패키지다. 자료의 열을 축이나 색에 연결하는 규칙과, 점·선·막대 같은 표현 요소를 조합하여 그래프를 만든다. 기본 그래픽스에서는 plot()으로 그림을 시작하고 axis()처럼 필요한 요소를 순서대로 더한다. 두 방식 모두 질문에 맞는 자료와 단위를 먼저 준비해야 한다.

기본 그래픽스에서 배운 작업을 ggplot2의 표현과 연결한다
작업기본 그래픽스ggplot2의 대응 요소
점과 선으로 날짜별 변화 표시plot(type = "b")geom_point(), geom_line()
수치 구간별 빈도 표시hist()geom_histogram()
집단별 분포 요약boxplot()geom_boxplot()
이미 계산한 합계를 막대로 표시barplot()geom_col()
제목과 축 이름 지정main, xlab, ylablabs()

이 대응표는 표현 방식의 연결을 보여 주는 안내다. 완성 코드에서는 해당 패키지를 불러오거나 설치하지 않는다. 특히 계산한 합계를 그리는 막대 표현과 원자료의 행 개수를 세는 막대 표현을 혼동하지 않아야 한다. 어떤 도구를 쓰든 판매 수량과 기록 개수는 서로 다른 값이다. 함수의 인수와 기본 동작은 R 그래픽스 공식 도움말과 PNG 장치 공식 도움말에서 확인할 수 있다.

완성 코드

다음 내용을 main.R로 저장한다. macOS 또는 Linux의 R 4.5 이상에서 PNG 출력 기능을 사용할 수 있는 환경을 전제로 한다. 외부 파일을 읽거나 패키지를 설치할 필요가 없다. 실행한 폴더에 네 개의 PNG 파일을 저장한다.

dates <- as.Date("2026-09-01") + 0:13
drink_qty <- c(10, 12, 9, 11, 13, 18, 20,
               11, 10, 12, 14, 15, 22, 24)
snack_qty <- c(6, 7, 5, 6, 8, 10, 11,
               6, 7, 6, 8, 9, 12, 13)

sales <- data.frame(
  date = rep(dates, each = 2),
  product = rep(c("Water", "Crackers"), times = 14),
  category = factor(
    rep(c("Drink", "Snack"), times = 14),
    levels = c("Drink", "Snack")
  ),
  quantity = as.vector(rbind(drink_qty, snack_qty))
)
sales$amount <- sales$quantity *
  rep(c(1000, 1500), times = 14)

daily <- aggregate(amount ~ date, data = sales, FUN = sum)
daily <- daily[order(daily$date), ]
category_qty <- tapply(sales$quantity, sales$category, sum)

draw_png <- function(filename, drawing) {
  png(filename = filename, width = 960, height = 640,
      units = "px", res = 120, bg = "white")
  on.exit(dev.off(), add = TRUE)
  par(mar = c(5, 5, 4, 2) + 0.1)
  force(drawing)
  invisible(NULL)
}

files <- c(
  "01_daily_sales.png",
  "02_daily_histogram.png",
  "03_category_boxplot.png",
  "04_category_quantity.png"
)

draw_png(files[1], {
  plot(daily$date, daily$amount / 1000,
       type = "b", pch = 16, lwd = 2,
       col = "#315675", xaxt = "n",
       ylim = c(0, 50),
       main = "Daily sales: Sep 1-14",
       xlab = "Date",
       ylab = "Sales (thousand KRW)")
  ticks <- c(1, 4, 7, 10, 14)
  axis(1, at = daily$date[ticks],
       labels = format(daily$date[ticks], "%m-%d"))
})

draw_png(files[2], {
  hist(daily$amount / 1000,
       breaks = seq(15, 45, by = 5),
       right = FALSE, include.lowest = TRUE,
       freq = TRUE, ylim = c(0, 5),
       col = "#e1edf7", border = "#315675",
       main = "Distribution of daily sales",
       xlab = "Daily sales (thousand KRW)",
       ylab = "Number of days")
})

draw_png(files[3], {
  boxplot(I(amount / 1000) ~ category, data = sales,
          col = c("#e1edf7", "#f7fafc"),
          border = "#315675", ylim = c(0, 30),
          main = "Record sales by category",
          xlab = "Category",
          ylab = "Record sales (thousand KRW)")
})

draw_png(files[4], {
  barplot(category_qty,
          col = c("#315675", "#c0392b"),
          border = NA, ylim = c(0, 250),
          main = "Total quantity: Sep 1-14",
          xlab = "Category",
          ylab = "Quantity (items)")
})

cat(sprintf("Rows: %d\n", nrow(sales)))
cat(sprintf("Days: %d\n", nrow(daily)))
cat(sprintf("Total sales (KRW): %.0f\n", sum(sales$amount)))
for (filename in files) {
  cat(sprintf("Saved: %s\n", filename))
}

줄별 해설

첫 줄은 시작 날짜에 0부터 13까지를 더하여 연속된 14일을 만든다. 이어지는 두 수량 벡터의 첫 값은 9월 1일 수량이고 마지막 값은 9월 14일 수량이다. 두 벡터의 위치가 같은 날짜를 가리키므로, 값을 추가하거나 바꿀 때에도 날짜와 대응하는 위치를 유지해야 한다.

data.frame() 안의 date는 날짜를 두 번씩 반복한다. 상품과 분류는 음료, 간식 순서로 반복한다. rbind()는 두 수량 벡터를 위아래로 붙이고, as.vector()는 그 결과를 열 순서로 펼친다. 따라서 첫 두 수량은 첫날의 음료 10개와 간식 6개가 된다. 분류의 수준을 지정하여 상자와 막대가 언제나 음료, 간식 순서로 나타나게 한다.

sales$amount를 만드는 줄은 각 수량에 해당 상품 단가를 곱한다. 첫날 금액은 음료 10,000원, 간식 9,000원이다. aggregate()는 같은 날짜의 금액을 더해 하루 한 행인 표를 만든다. order()로 날짜 순서를 명시한 뒤 그래프에 전달한다. category_qty는 음료 221개와 간식 114개라는 이름 붙은 합계 벡터다.

draw_png()의 첫 인수는 파일 이름이고 둘째 인수는 실행할 그림 코드다. R은 함수에 전달된 식을 필요할 때 계산한다. force(drawing)은 그 식을 이 위치에서 계산하게 한다. 따라서 PNG 장치를 연 다음에 중괄호 안의 그래프 코드가 실행된다. on.exit()는 함수가 끝날 때 장치를 닫고, invisible(NULL)은 함수 반환값을 화면에 표시하지 않는다.

par(mar = ...)는 그림 바깥 여백을 아래, 왼쪽, 위, 오른쪽 순서로 조정한다. 축 이름을 넣을 공간을 확보하기 위한 설정이다. 이 설정은 열린 PNG 장치에 적용된다. 각 그림을 새 장치에 그리므로 이전 그림의 축이나 점이 다음 파일에 섞이지 않는다.

첫 번째 호출은 날짜별 판매액을 그린다. pch = 16은 채운 원 모양의 점을, lwd = 2는 선 두께를 지정한다. col에는 색을 나타내는 코드를 전달한다. 기본 날짜 눈금을 생략했으므로 axis(1, ...)으로 아래쪽 축을 추가한다. 숫자 1은 아래쪽 축이라는 뜻이다.

두 번째 호출은 하루 판매액 14개를 히스토그램으로 바꾼다. 그래프 값이 천 원 단위이므로 경계도 15부터 45까지 지정한다. right = FALSE에서는 보통 왼쪽 경계를 포함하고 오른쪽 경계를 제외한다. 예를 들어 20은 20 이상 25 미만 구간에 들어간다. include.lowest = TRUE는 이 설정에서 마지막 경계인 45도 마지막 구간에 포함하도록 한다. freq = TRUE로 세로축이 날짜 수를 나타내게 한다.

세 번째 호출의 ~는 왼쪽의 금액을 오른쪽의 분류에 따라 나누라는 관계를 나타낸다. 식 안의 I(amount / 1000)는 나눗셈을 일반 계산으로 처리하도록 한다. 식을 사용하는 문맥에서는 일부 기호에 별도 의미가 있으므로 계산 의도를 분명하게 적는다. 여기서는 원 단위 금액을 천 원 단위로 바꾼 뒤 상자를 그린다.

네 번째 호출은 이미 계산한 분류별 수량을 막대로 그린다. border = NA는 막대 테두리를 생략한다. 마지막 출력 부분은 행 수, 날짜 수, 전체 판매액과 파일 이름을 차례로 알린다. sprintf()의 %d는 정수 형태, %.0f는 소수점 아래를 표시하지 않는 숫자 형태를 지정한다. 그림 장치를 닫은 뒤 파일 이름을 출력하도록 구성했다.

실행 결과

터미널에서 main.R을 저장한 폴더로 이동한 뒤 다음 명령을 실행한다. 아래 출력의 파일 이름은 그 폴더를 기준으로 한 이름이다.

Rscript main.R
Rows: 28
Days: 14
Total sales (KRW): 372000
Saved: 01_daily_sales.png
Saved: 02_daily_histogram.png
Saved: 03_category_boxplot.png
Saved: 04_category_quantity.png

날짜별 그림에서는 첫날 판매액이 19천 원, 마지막 날이 43.5천 원으로 표시된다. 중간에 내려가는 날도 있으므로 매일 증가했다고 설명하면 안 된다. 마지막 이틀은 각각 40천 원과 43.5천 원으로, 앞선 날짜들보다 높은 곳에 나타난다. 이 사실만으로 증가 원인을 알 수는 없다. 가격 변화, 방문객 수, 행사 여부는 이 예제에 들어 있지 않다.

히스토그램의 여섯 구간에 들어가는 날짜 수는 왼쪽부터 2, 5, 3, 1, 1, 2다. 가장 높은 막대는 20천 원 이상 25천 원 미만 구간이다. 상자 그림에서 음료 기록의 중앙값은 12.5천 원이고 간식 기록의 중앙값은 11.25천 원이다. 수량 막대는 음료 221개, 간식 114개를 보여 준다. 이처럼 그림을 읽을 때는 무엇을 합쳤는지와 무엇을 그대로 관측값으로 사용했는지를 함께 확인한다.

그래프의 숫자와 파일 이름은 같은 코드에서 결정된다. 다만 운영체제의 글꼴과 PNG 장치 구현에 따라 글자 모양이나 가장자리 표현은 조금 달라질 수 있다. 그림을 보고서에 넣기 전에 네 파일을 열어 제목과 눈금이 잘리지 않았는지 확인한다.

실무에서 자주 틀리는 것

파일 장치를 닫지 않고 저장을 끝냈다고 생각한다

그림 명령이 실행되었어도 파일 장치를 닫지 않으면 기록이 마무리되지 않은 상태일 수 있다. 다른 그림까지 같은 장치로 보내는 실수도 생긴다. 아래처럼 여는 명령과 닫는 명령을 한 흐름에 둔다. 반복해서 저장할 때는 완성 코드의 함수를 사용한다.

틀린 코드다.

png("daily.png")
plot(daily$date, daily$amount)

고친 코드다.

png("daily.png")
plot(daily$date, daily$amount)
dev.off()

막대그래프가 분류별 수량을 합쳐 줄 것으로 기대한다

barplot(sales$quantity)는 기록 28개의 수량을 각각 막대로 그린다. 그 자체로 실행 가능한 코드지만 분류별 전체 수량을 비교하려는 질문에는 맞지 않는다. 먼저 합계를 계산하고 두 막대를 그린다. 합계와 평균 중 어느 값을 썼는지도 축이나 제목에 적는다.

틀린 코드다.

barplot(sales$quantity,
        main = "Total quantity by category")

고친 코드다.

totals <- tapply(sales$quantity, sales$category, sum)
barplot(totals,
        main = "Total quantity by category",
        ylab = "Quantity (items)")

값의 단위와 히스토그램 경계의 단위를 다르게 쓴다

값만 천 원으로 바꾸고 경계는 원 단위로 남겨 두면 의도한 구간을 만들 수 없다. 다음의 틀린 코드는 관측값이 지정한 범위 밖에 있어서 오류가 난다. 값과 경계, 축 이름을 함께 맞춘다. 다른 데이터로 바꾸었다면 경계가 모든 값을 포함하는지도 확인한다.

틀린 코드다.

hist(daily$amount / 1000,
     breaks = seq(15000, 45000, by = 5000))

고친 코드다.

hist(daily$amount / 1000,
     breaks = seq(15, 45, by = 5),
     right = FALSE, include.lowest = TRUE,
     xlab = "Daily sales (thousand KRW)")

막대의 시작을 잘라 차이를 크게 보이게 한다

아래 틀린 코드는 판매 수량 축을 100에서 시작한다. 화면에 남는 막대 길이는 실제 수량에 비례하지 않아 음료와 간식의 차이를 과하게 읽기 쉽다. 전체 수량을 막대 길이로 비교하려면 0을 포함한다. 보고서에서 여러 기간의 그림을 나란히 비교한다면 축 범위도 함께 맞추는 편이 좋다.

틀린 코드다.

barplot(category_qty,
        ylim = c(100, 250),
        ylab = "Quantity (items)")

고친 코드다.

barplot(category_qty,
        ylim = c(0, 250),
        ylab = "Quantity (items)")

한눈에 보기

그림을 만들고 저장할 때 확인할 핵심 항목이다
항목코드 또는 인수확인할 내용
날짜별 변화plot(type = "b")날짜 순서와 판매액 단위를 확인한다.
수치의 분포hist(breaks = ...)구간 경계와 포함 방향을 확인한다.
집단별 분포boxplot(값 ~ 분류)한 관측값의 단위와 중앙값을 확인한다.
분류별 합계barplot()먼저 합계를 구하고 축에 0을 포함한다.
제목과 축main, xlab, ylab대상, 기간, 단위를 적는다.
색과 범위col, ylim색의 의미와 축 범위를 일관되게 둔다.
파일 저장png(), dev.off()열기, 그리기, 닫기 순서를 지킨다.

그림을 고르기 전에 질문을 한 문장으로 쓰고, 그 질문에 필요한 표를 준비한다. 그림을 만든 뒤에는 제목과 축만 읽어도 비교 대상이 드러나는지 확인한다. 다음 장에서는 이렇게 만든 표와 그림을 함께 사용하여 편의점 판매 분석 보고서를 작성한다.

연습 문제

  1. 분류별 판매 수량 대신 분류별 판매액 합계를 비교하려 한다. 완성 코드의 sales와 draw_png()를 사용하여 05_category_amount.png를 저장하고 파일 이름을 출력하라. 세로축은 천 원 단위로 표시하라.
  2. 하루 판매액이 25,000원인 날짜는 완성 코드의 히스토그램에서 어느 구간에 들어가는가. right = FALSE의 의미와 함께 설명하라. 날짜 순서에 따른 증가 여부도 이 그림에서 알 수 있는지 답하라.
  3. 날짜별 판매액 그림에 전체 기간의 하루 평균을 가로선으로 추가하여 06_daily_mean.png로 저장하라. abline(h = ...)은 지정한 높이에 가로선을 그린다. 금액 단위를 맞추고 파일 이름을 출력하라.
  4. 점주가 “상자 그림 밖의 점은 모두 잘못 입력한 금액이므로 지우자”라고 말했다. 바로 삭제하면 안 되는 이유를 설명하고, 먼저 확인할 기록 정보를 두 가지 제시하라.

정답과 해설

첫 번째 문제는 금액을 분류별로 합친다. 결과는 음료 221천 원, 간식 171천 원이다. 수량이 더 많은 분류가 여기서는 금액도 더 크지만, 상품 가격이 다르면 두 비교의 차이도 달라진다. 다음 코드는 완성 코드 뒤에 덧붙여 실행한다.

category_amount <- tapply(sales$amount, sales$category, sum)
draw_png("05_category_amount.png", {
  barplot(category_amount / 1000,
          col = c("#315675", "#c0392b"),
          ylim = c(0, 250),
          main = "Total sales by category",
          xlab = "Category",
          ylab = "Sales (thousand KRW)")
})
cat("Saved: 05_category_amount.png\n")

두 번째 문제의 25,000원은 천 원 단위로 25다. 왼쪽 경계를 포함하므로 25 이상 30 미만 구간에 들어간다. 20 이상 25 미만 구간에는 들어가지 않는다. 히스토그램은 날짜를 금액 구간별 개수로 요약하여 날짜 순서를 보여 주지 않는다. 증가 여부를 살피려면 날짜별 그래프를 함께 읽어야 한다.

세 번째 문제는 평균도 1,000으로 나누어 점과 같은 축에 놓는다. 하루 평균 판매액은 28천 원이다. lty = 2는 점선 형태의 선을 지정한다. 그래프 제목에 선의 의미를 적어 붉은 선이 무엇인지 알 수 있게 한다.

draw_png("06_daily_mean.png", {
  plot(daily$date, daily$amount / 1000,
       type = "b", pch = 16, col = "#315675",
       ylim = c(0, 50),
       main = "Daily sales: dashed line = mean",
       xlab = "Date",
       ylab = "Sales (thousand KRW)")
  abline(h = mean(daily$amount) / 1000,
         col = "#c0392b", lty = 2, lwd = 2)
})
cat("Saved: 06_daily_mean.png\n")

네 번째 문제의 별도 점은 상자와 수염의 기본 기준에서 멀리 떨어진 값이라는 뜻이다. 입력 오류를 판정한 결과가 아니다. 해당 날짜와 상품의 원래 판매 수량을 확인하고, 적용 단가나 할인·반품 기록을 확인한다. 실제 큰 판매였다면 지우는 것이 분석 대상을 바꾸게 된다. 오류가 확인된 경우에만 근거를 남기고 수정한다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.