R · 기본
데이터로 시작하는 R
문자열과 날짜
paste·sprintf·substr·gsub, as.Date·format·seq.Date, 요일·월별 집계
개발자KR · 원고 갱신
이 장에서 배우는 것
판매 기록은 숫자만으로 이루어져 있지 않다. 날짜는 "2025.01.30" 같은 글자로 들어오고, 금액에는 "4,500원"처럼 쉼표와 단위가 붙어 있으며, 상품명 앞뒤에는 공백이 섞여 있기도 한다. 이 장에서는 이런 글자 자료를 계산 가능한 값으로 바꾸는 방법과, 날짜에서 요일과 월을 뽑아 집계하는 방법을 다룬다. 앞 장에서 데이터 프레임을 걸러내고 묶는 방법을 익혔으므로, 여기서는 그 데이터 프레임의 열을 깨끗하게 정리하는 일에 집중한다.
- paste, sprintf 로 여러 값을 한 줄의 글자로 조합한다.
- substr, trimws, gsub 로 글자의 일부를 자르고 지우고 바꾼다.
- as.Date 와 format 으로 날짜 글자를 날짜형으로 바꾸고, 원하는 모양의 글자로 되돌린다.
- seq 로 날짜 나열을 만들어 판매 기록이 없는 날을 찾는다.
- 요일별·월별 매출을 tapply 로 집계한다.
문제 상황
편의점 사장님이 계산대 프로그램에서 내려받은 기록을 보내 주었다. 열 개의 판매 기록인데 그대로는 계산할 수 없다. 날짜 열은 "2025.01.30" 처럼 점으로 구분되어 있고, 금액 열은 "10,000원" 같은 글자다. 글자 상태의 금액은 더할 수 없다. 엑셀이라면 셀 서식을 바꾸거나 찾기·바꾸기를 쓰면 되지만, R 에서는 코드로 같은 일을 하고 그 코드를 다음 달 자료에도 그대로 쓸 수 있다.
사장님이 알고 싶은 것은 세 가지다. 요일별로 얼마나 팔렸는지, 월별 매출은 얼마인지, 주말 매출이 전체에서 얼마나 되는지다. 이 셋에 답하려면 먼저 글자를 숫자와 날짜로 바꿔야 한다.
문자열 다루기
R 에서 따옴표로 감싼 값은 문자열(string)이다. 문자열을 다루는 함수 가운데 이 장에서 쓰는 것은 다섯 가지다.
paste 와 sprintf
paste 는 여러 값을 이어 붙인다. 값 사이에 넣을 글자는 sep 로, 벡터 전체를 하나로 합칠 때 쓸 구분자는 collapse 로 정한다. sep 를 생략하면 공백이 들어가고, paste0 은 sep 가 빈 글자인 paste 다.
sprintf 는 틀에 값을 끼워 넣는다. 틀 안의 %s 는 글자, %d 는 정수, %.1f 는 소수점 아래 한 자리 실수가 들어갈 자리다. 퍼센트 기호 자체를 출력하려면 %% 라고 쓴다. 엑셀의 TEXT 함수나 & 연결과 비슷한 일을 하되, 벡터를 통째로 넘기면 한 번에 여러 줄이 만들어진다.
substr, trimws, gsub
substr(x, 시작, 끝) 은 글자 위치로 일부를 잘라 낸다. 위치는 1부터 센다. trimws 는 앞뒤 공백을 지운다. 눈에는 같아 보이는 " 삼각김밥 " 과 "삼각김밥" 은 R 에게 다른 값이므로, 집계 전에 반드시 정리해야 한다.
gsub(찾을 것, 바꿀 것, 대상) 은 찾은 글자를 모두 바꾼다. 찾을 것에는 정규 표현식(regular expression)이라는 글자 패턴 표기를 쓴다. 이 장에서 쓰는 것은 하나뿐이다. [0-9] 는 숫자 한 글자를, [^0-9] 는 숫자가 아닌 글자 한 개를 뜻한다. 따라서 gsub("[^0-9]", "", "4,500원") 은 숫자가 아닌 글자를 모두 지워 "4500" 을 돌려준다. 결과는 아직 글자이므로 as.numeric 으로 숫자로 바꾼다.
날짜 다루기
글자에서 날짜형으로
as.Date 는 글자를 날짜형(Date)으로 바꾼다. 날짜형은 겉으로는 "2025-01-30" 처럼 보이지만 안에서는 1970년 1월 1일부터 지난 날수로 저장되어 있어서, 빼기로 기간을 구하거나 더하기로 며칠 뒤를 구할 수 있다. 글자가 "년-월-일" 또는 "년/월/일" 모양이면 as.Date 가 알아서 읽는다. 그 밖의 모양은 format 인자로 읽는 방식을 알려 주어야 한다. 예를 들어 "2025.01.30" 은 format = "%Y.%m.%d" 라고 쓴다.
날짜형을 다시 글자로 만들 때는 format 함수를 쓴다. 같은 기호를 이 방향으로도 쓰는 것이다. 자주 쓰는 기호는 다음과 같다.
| 기호 | 뜻 | 2025-01-30 의 결과 | 주의 |
|---|---|---|---|
| %Y | 네 자리 연도 | 2025 | %y 는 두 자리 연도 |
| %m | 두 자리 월 | 01 | %M 은 분이다 |
| %d | 두 자리 일 | 30 | - |
| %u | 요일 번호(월 1 ~ 일 7) | 4 | 언어 설정과 무관하다 |
| %a | 요일 약칭 | 환경에 따라 다름 | 언어 설정을 따른다 |
그림 1 은 이 장 데이터의 날짜 하나가 거치는 상태 변화다. 가운데 상태가 되어야 요일을 뽑거나 날짜끼리 계산할 수 있다.
seq 로 날짜 나열 만들기
seq 에 시작 날짜와 by 를 주면 날짜 나열이 만들어진다. by 에는 "day", "week", "month" 를 쓸 수 있고, 끝 날짜를 to 로 주거나 개수를 length.out 으로 줄 수 있다. 판매 기록에는 장사를 한 날만 남아 있으므로, 전체 기간의 달력을 seq 로 만들어 대조하면 기록이 없는 날을 찾을 수 있다. 월 단위 나열은 1일을 시작점으로 잡는 것이 안전하다. 31일에서 시작하면 달마다 날수가 달라 결과가 기대와 어긋날 수 있다.
요일별·월별 집계
요일은 format(날짜, "%u") 로 번호를 얻은 뒤 요일 이름표에서 골라 쓴다. %a 를 그대로 쓰면 컴퓨터의 언어 설정에 따라 "Thu" 가 나오기도 하고 "목" 이 나오기도 해서 실행 결과가 달라진다. 번호와 이름표를 쓰면 어느 컴퓨터에서나 같은 결과를 얻는다.
이름표를 factor 로 만들 때 levels 에 월요일부터 일요일까지 일곱 개를 모두 적어 두면, 기록이 없는 요일도 칸이 남는다. 이렇게 하면 집계 결과가 항상 일곱 칸이고 순서도 월요일부터다. 월별 집계는 format(날짜, "%Y-%m") 으로 "2025-01" 같은 글자를 만들어 묶는 방식이다. 연도가 앞에 오는 이 모양은 글자순으로 정렬해도 시간순과 같다.
tapply(값, 묶는 기준, 함수) 는 기준별로 값에 함수를 적용한다. 기록이 없는 칸은 NA 가 되므로 0 으로 바꿔 주는 한 줄이 더 필요하다.
이 장의 함수를 tidyverse 로 옮길 때는 아래 표를 참고한다. 이 책은 외부 패키지를 쓰지 않으므로 대응 관계만 소개한다.
| 하려는 일 | base R | tidyverse |
|---|---|---|
| 글자 이어 붙이기 | paste0 | stringr 의 str_c |
| 글자 일부 자르기 | substr | stringr 의 str_sub |
| 글자 바꾸기 | gsub | stringr 의 str_replace_all |
| 글자를 날짜로 | as.Date(format=) | lubridate 의 ymd |
완성 코드
아래 코드를 main.R 로 저장한다. 파일은 UTF-8 로 저장해야 한글이 깨지지 않는다. 데이터는 코드 안에서 직접 만든다.
# main.R - 편의점 판매 기록의 문자열과 날짜 다루기
# 1. 계산대에서 내려받은 그대로의 기록
raw <- data.frame(
day = c("2025.01.30", "2025.01.31", "2025.02.01", "2025.02.02", "2025.02.03",
"2025.02.08", "2025.02.09", "2025.02.28", "2025.03.01", "2025.03.02"),
item = c("삼각김밥", "우유", " 삼각김밥 ", "커피", "우유",
"커피", "삼각김밥", "우유", "커피", "삼각김밥"),
qty = c(3L, 2L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 2L),
amount = c("4,500원", "5,000원", "6,000원", "3,000원", "2,500원",
"4,500원", "7,500원", "10,000원", "3,000원", "3,000원"),
stringsAsFactors = FALSE
)
# 2. 문자열 정리
cat(sprintf("원본 %s 에서 월 부분은 %s\n", raw$day[1], substr(raw$day[1], 6, 7)))
sales <- raw
cat(sprintf("정리 전 상품명: [%s]\n", sales$item[3]))
sales$item <- trimws(sales$item)
cat(sprintf("정리 후 상품명: [%s]\n", sales$item[3]))
sales$amount <- as.numeric(gsub("[^0-9]", "", raw$amount))
cat(sprintf("금액 문자열 %s → 숫자 %d\n", raw$amount[1], sales$amount[1]))
# 3. 날짜형으로 변환
sales$day <- as.Date(raw$day, format = "%Y.%m.%d")
cat(sprintf("날짜 형식: 변환 전 %s, 변환 후 %s\n", class(raw$day), class(sales$day)))
span <- as.numeric(max(sales$day) - min(sales$day))
cat(sprintf("기간: %s ~ %s (%d일 차이)\n",
format(min(sales$day)), format(max(sales$day)), span))
# 4. 월과 요일 열 만들기
sales$month <- format(sales$day, "%Y-%m")
wd <- c("월", "화", "수", "목", "금", "토", "일")
sales$wday <- factor(wd[as.integer(format(sales$day, "%u"))], levels = wd)
# 5. paste 와 sprintf 로 조합하기
cat("판매 상품: ", paste(unique(sales$item), collapse = ", "), "\n", sep = "")
h <- head(sales, 4)
cat(sprintf("%s %s: %d개, %s원\n", format(h$day, "%m/%d"), h$item, h$qty,
formatC(h$amount, format = "d", big.mark = ",")), sep = "")
# 6. seq 로 달력을 만들어 판매 없는 날 찾기
cal <- seq(as.Date("2025-01-30"), as.Date("2025-03-02"), by = "day")
by_day <- tapply(sales$amount, format(sales$day), sum)
full <- as.numeric(by_day[format(cal)])
full[is.na(full)] <- 0
cat(sprintf("달력 %d일 중 판매 없는 날 %d일\n", length(cal), sum(full == 0)))
print(seq(as.Date("2025-02-03"), by = "week", length.out = 3))
print(seq(as.Date("2025-01-01"), by = "month", length.out = 3))
# 7. 요일별·월별 집계
by_wday <- tapply(sales$amount, sales$wday, sum)
by_wday[is.na(by_wday)] <- 0
cat("요일별 매출\n")
cat(sprintf("%s요일 %6s원\n", names(by_wday),
formatC(by_wday, format = "d", big.mark = ",")), sep = "")
by_month <- tapply(sales$amount, sales$month, sum)
cat("월별 매출\n")
cat(sprintf("%s %7s원\n", names(by_month),
formatC(by_month, format = "d", big.mark = ",")), sep = "")
weekend <- sales$wday %in% c("토", "일")
share <- 100 * sum(sales$amount[weekend]) / sum(sales$amount)
cat(sprintf("주말 매출 비중: %.1f%%\n", share))
줄별 해설
1번 블록. data.frame 으로 원본을 만든다. qty 는 뒤에 L 을 붙여 정수로 만들었다. sprintf 의 %d 는 정수를 기대하기 때문이다. amount 는 일부러 글자로 두었다. stringsAsFactors = FALSE 는 글자 열이 factor 로 바뀌지 않게 하는 인자다.
2번 블록. substr(raw$day[1], 6, 7) 은 "2025.01.30" 의 여섯 번째와 일곱 번째 글자인 "01" 을 잘라 낸다. sales 는 raw 의 복사본이므로 원본은 그대로 남는다. trimws 로 세 번째 행의 공백을 지우고, gsub 로 숫자가 아닌 글자를 지운 뒤 as.numeric 으로 숫자로 바꾼다. 출력 틀에서 %d 는 4500 을 그대로 찍으므로 쉼표가 없다.
3번 블록. as.Date 에 format 을 주어 점 구분 날짜를 읽는다. class 는 열의 자료형 이름을 돌려준다. 날짜형끼리 빼면 기간이 되고, as.numeric 으로 숫자만 꺼낸다. 1월 30일부터 3월 2일까지는 31일 차이다. 출력에 날짜형을 넣을 때는 format 으로 먼저 글자로 바꿔 %s 에 맞춘다.
4번 블록. "%Y-%m" 으로 월 글자를 만든다. "%u" 는 월요일 1 ~ 일요일 7 이므로 wd 에서 그 번호의 이름을 고른다. format 의 결과는 글자이므로 as.integer 로 바꾸어 위치 번호로 쓴다. levels = wd 가 일곱 요일을 모두 칸으로 고정한다.
5번 블록. paste 의 collapse 는 벡터를 한 글자로 합친다. unique 는 중복을 제거하므로 공백을 지운 뒤에는 상품이 세 가지만 남는다. sprintf 에 벡터를 넘기면 줄이 여러 개 만들어지고, cat 의 sep = "" 는 줄 사이에 공백이 끼어들지 않게 한다. formatC 의 format = "d" 와 big.mark = "," 는 세 자리마다 쉼표를 넣는다.
6번 블록. seq 로 1월 30일부터 3월 2일까지 하루 간격 달력 32개를 만든다. tapply 로 날짜별 매출 합을 구하고, 그 결과에서 달력의 날짜 이름으로 값을 꺼낸다. 기록이 없는 날은 NA 가 되므로 0 으로 바꾼다. 마지막 두 줄은 주 단위와 월 단위 나열이다.
7번 블록. 요일 factor 로 매출을 묶으면 화요일과 수요일이 NA 가 되고, 이를 0 으로 바꾼다. %6s 는 여섯 칸 오른쪽 정렬이다. 월별 집계도 같은 모양이다. 주말 비중은 토·일 매출의 합을 전체 매출로 나눈 값이다. 토요일과 일요일이 각각 13,500원이므로 27,000원이고, 전체 49,000원 가운데 55.1%다.
실행 결과
터미널에서 main.R 이 있는 폴더로 이동해 다음 명령을 실행한다.
$ Rscript main.R
원본 2025.01.30 에서 월 부분은 01
정리 전 상품명: [ 삼각김밥 ]
정리 후 상품명: [삼각김밥]
금액 문자열 4,500원 → 숫자 4500
날짜 형식: 변환 전 character, 변환 후 Date
기간: 2025-01-30 ~ 2025-03-02 (31일 차이)
판매 상품: 삼각김밥, 우유, 커피
01/30 삼각김밥: 3개, 4,500원
01/31 우유: 2개, 5,000원
02/01 삼각김밥: 4개, 6,000원
02/02 커피: 2개, 3,000원
달력 32일 중 판매 없는 날 22일
[1] "2025-02-03" "2025-02-10" "2025-02-17"
[1] "2025-01-01" "2025-02-01" "2025-03-01"
요일별 매출
월요일 2,500원
화요일 0원
수요일 0원
목요일 4,500원
금요일 15,000원
토요일 13,500원
일요일 13,500원
월별 매출
2025-01 9,500원
2025-02 33,500원
2025-03 6,000원
주말 매출 비중: 55.1%
월별 합계를 더하면 9,500 + 33,500 + 6,000 = 49,000원이고, 요일별 합계의 합도 같다. 두 방향으로 집계한 합이 같은지 확인하는 습관은 집계 코드의 실수를 잡는 가장 값싼 방법이다.
실무에서 자주 틀리는 것
날짜 모양을 알려 주지 않는다
점으로 구분된 날짜를 as.Date 에 그대로 넣으면 오류가 난다.
as.Date("2025.01.30")
# Error in charToDate(x) :
# character string is not in a standard unambiguous format
읽는 모양을 format 으로 알려 준다.
as.Date("2025.01.30", format = "%Y.%m.%d")
gsub 의 점은 "모든 글자"를 뜻한다
정규 표현식에서 점은 아무 글자 하나와 맞는다. 그래서 점을 지우려고 쓴 코드가 전부 지워 버린다.
gsub(".", "-", "2025.01.30")
# [1] "----------"
글자 그대로 찾으려면 fixed = TRUE 를 준다.
gsub(".", "-", "2025.01.30", fixed = TRUE)
# [1] "2025-01-30"
요일 이름을 언어 설정에 맡긴다
format(x, "%a") 나 weekdays(x) 는 컴퓨터의 언어 설정에 따라 결과가 다르다. 같은 코드가 내 컴퓨터에서는 "목", 서버에서는 "Thu" 를 돌려주면 집계 열의 이름이 달라져 뒤의 코드가 깨진다. 번호로 받아 이름표에서 고른다.
x <- as.Date("2025-01-30")
wd <- c("월", "화", "수", "목", "금", "토", "일")
wd[as.integer(format(x, "%u"))]
# [1] "목"
NA 가 글자 "NA" 가 된다
paste 는 결측값을 그대로 "NA" 라는 글자로 바꿔 이어 붙인다. 오류도 경고도 없어서 보고서에 "금액 NA원" 이 그대로 실릴 수 있다.
paste("금액", NA)
# [1] "금액 NA"
이어 붙이기 전에 is.na 로 결측을 확인하고, 필요하면 값을 채운다.
x <- c(4500, NA)
sum(is.na(x))
# [1] 1
한눈에 보기
| 함수 | 하는 일 | 예 | 결과 |
|---|---|---|---|
| paste0(a, b) | 이어 붙이기 | paste0("A", 1) | "A1" |
| sprintf | 틀에 값 끼우기 | sprintf("%d개", 3L) | "3개" |
| substr(x, 6, 7) | 위치로 자르기 | "2025.01.30" 에서 월 | "01" |
| trimws | 앞뒤 공백 지우기 | " 우유 " | "우유" |
| gsub | 찾아서 모두 바꾸기 | gsub("[^0-9]", "", "4,500원") | "4500" |
| as.Date(format=) | 글자를 날짜형으로 | "%Y.%m.%d" | 날짜형 |
| format(날짜, 서식) | 날짜를 글자로 | format(x, "%Y-%m") | "2025-01" |
| seq(날짜, by=) | 날짜 나열 | by = "week" | 7일 간격 |
| tapply | 기준별 집계 | tapply(금액, 월, sum) | 월별 합계 |
연습 문제
- 글자 "3,200원" 을 gsub 와 as.numeric 으로 숫자 3200 으로 바꾸는 코드를 쓰시오.
- as.Date("2025-03-15") 의 45일 뒤 날짜와, 그 날짜의 요일 번호(월 1 ~ 일 7)를 구하는 코드를 쓰고 결과를 답하시오.
- 2025년 3월의 일요일 날짜만 seq 로 만드시오. 3월 2일이 일요일이다.
- 본문의 sales 에서 월별 판매 수량(qty)의 합을 구하시오.
정답과 해설
-
as.numeric(gsub("[^0-9]", "", "3,200원")) # [1] 3200숫자가 아닌 글자를 지우면 "3200" 이라는 글자가 남고, as.numeric 이 이를 숫자로 바꾼다. 쉼표만 지우는 방식은 "원" 이 남아 as.numeric 이 NA 와 경고를 내므로 부족하다.
-
x <- as.Date("2025-03-15") + 45 format(x) # [1] "2025-04-29" format(x, "%u") # [1] "2"날짜형에 숫자를 더하면 그만큼의 날수가 지난 날짜가 된다. 3월 15일은 토요일이고 45일은 6주에 3일이 더해진 값이므로, 화요일인 번호 2가 나온다.
-
seq(as.Date("2025-03-02"), as.Date("2025-03-31"), by = "week") # [1] "2025-03-02" "2025-03-09" "2025-03-16" "2025-03-23" "2025-03-30"시작을 첫 일요일로 잡고 7일씩 건너뛴다. 끝 날짜를 넘는 값은 만들어지지 않는다.
-
tapply(sales$qty, sales$month, sum) # 2025-01 2025-02 2025-03 # 5 19 4월 글자를 기준으로 수량을 묶어 더한 결과다. 세 값의 합은 28이고 이는 열 개 기록의 수량 합과 같다. 실제 출력 줄 끝에는 공백이 하나 붙는다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.