3 R 기초 배우기
이번 장에서는 R과 R 스튜디오의 설치, R 스튜디오의 화면 구성과 간단한 사용법에 대해서 배우도록 하겠습니다. 본 장은 R의 기초 중에서도 핵심만을 추린 것으로써, 기초에 대해 세세하게 다루지는 않습니다. 좀 더 탄탄한 기본기를 배우고 싶으신 분은 시중에 나와있는 훌륭한 R 기본 서적을 추가로 보실 것을 추천드립니다.
3.1 R과 R 스튜디오 설치하기
3.1.1 R 설치하기
먼저 R 프로젝트 공식 사이트인 https://cran.r-project.org/ 에 접속하여 본인의 OS에 맞는 설치 파일을 다운로드 합니다.
가장 상단의 [base]를 선택합니다.
[Download R x.x.x for Windows] 항목을 클릭하면 설치 파일이 다운로드 됩니다. 다운로드 받은 파일을 실행해 설치를 하며, 옵션은 수정하지 않아도 됩니다.
3.1.2 R 스튜디오 설치하기
위에서 설치한 R GUI를 그대로 쓰는 사용자는 거의 없습니다. 대부분의 경우 R을 사용하기 편리하게 만들어주는 IDE 소프트웨어인 R 스튜디오를 사용하므로, 해당 프로그램을 설치하도록 합니다. R 스튜디오를 사용하려면 R이 먼저 설치되어 있어야 하며, R과 마찬가지로 무료로 사용할 수 있습니다. 먼저 아래 사이트에 접속합니다.
- https://posit.co/download/rstudio-desktop/
하단의 [All Installers] 항목에서 본인의 OS에 해당하는 파일을 다운로드 받아 설치합니다.
윈도우 사용자의 경우 간혹 R 스튜디오를 실행하는데 있어 오류가 발생할 수 있습니다.
- R 스튜디오가 관리자 권한으로 실행되지 않으면 오류가 발생할 수 있으며, 이 경우 아래와 같은 방법으로 해결이 가능합니다.
- R 스튜디오 바로가기 아이콘을 마우스 우클릭으로 연 후, [속성] → [호환성]을 클릭합니다.
- [관리자 권한으로 이 프로그램 실행]에 체크한 후 [확인]을 누릅니다.
2. 윈도우 사용자 계정이 한글인 경우 기존 사용자 계정을 영문으로 변경하거나, 영문으로 된 사용자 계정을 새로 추가합니다.
3.2 R 스튜디오 화면 구성
처음으로 R 스튜디오를 실행하면 다음과 같은 화면으로 구성되어 있습니다. 이 중 소스 창을 열기 위해 네모 2개가 겹쳐 있는 모양()의 버튼을 클릭합니다.
소스 창이 활성화되면 총 4개의 창으로 화면이 구성되며, 각 창의 크기는 경계선 부분을 드래그하여 조절할 수 있습니다.
1. 콘솔 창
좌측 하단에 있는 콘솔 창은 코드를 입력하고 결과물을 출력하는 곳입니다.
콘솔 창의 > 기호 뒤에 1+1을 입력하면 그 결과값인 2가 출력됩니다.
이 외에도 [Terminal] 탭에서는 시스템 쉘을 이용해 운영 체제를 조작할 수 있습니다.
2. 소스 창
좌측 상단에 있는 소스 창은 코드를 기록할 수 있는 공간이며, 이를 저장한 파일을 스크립트라고 합니다. 콘솔 창과는 다르게 코드를 입력하여도 바로 실행이 되지 않으며, 엔터를 누르면 행이 바뀝니다. 실행하고자 하는 코드가 있는 행을 선택한 후, [Ctrl + Enter]키를 누르면 해당 코드가 실행됩니다.
3*7이란 코드가 있는 곳에 커서를 둔 후 [Ctrl + Enter]키를 누르면 해당 코드가 콘솔 창에서 실행됩니다. 만일 여러줄의 명령어를 한번에 실행하고 할 경우, 원하는 부분의 코드를 마우스로 드래그하여 선택한 후 [Ctrl + Enter]키를 누르면 코드가 순차적으로 콘솔 창에 입력되면서 실행됩니다.
위에서 작성한 코드를 저장해보도록 하겠습니다. 저장 버튼()을 클릭한 후 원하는 폴더 및 파일 이름을 입력 한 후 [Save] 버튼을 누릅니다.
Untitled1로 되어 있던 스크립트의 이름이 저장한 이름으로 바뀌며, 스크립트가 저장되어 있는 것이 확인됩니다. 이처럼 코딩을 한 후 스크립트를 저장할 경우, 나중에 해당 내역을 그대로 불러올 수 있습니다.
3. 환경 창
우측 상단에 있는 환경 창은 생성된 데이터를 보여주는 화면입니다.
스크립트 창에서 a = 1을 입력하면, 환경 창의 Values 목록에 a가 생기며 그 값은 1로 표시됩니다.
이 외에도 환경 창의 [History] 탭은 이제까지 실행했던 코드의 내역을 볼 수 있으며, [Connections] 탭은 SQL이나 Spark 등 데이터베이스와의 연결을 도와줍니다.
4. 파일 창
우측 하단에 있는 파일 창은 윈도우의 파일 탐색기와 비슷한 역할을 하며, 워킹 디렉터리 내의 파일을 보여줍니다. 이 외에도 [Plots] 탭은 그래프를 보여주며 [Packages] 탭은 설치된 패키지의 목록을 보여줍니다. [Help] 탭은 도움말을 보여주며, [Viewer] 탭은 분석 결과를 HTML 등 웹 문서로 출력한 모습을 보여줍니다.
3.3 R 스튜디오 설정하기
R 스튜디오는 기본적으로 흰색 바탕에 검은색 글씨로 설정되어 있습니다. 그러나 흰 화면에서 작업을 하게 되면 눈이 쉽게 피로해지며, 시력에도 좋지 않습니다. 이를 방지하기 위해 어두운 화면으로 설정을 변경해주는 것이 좋습니다.
상단 탭에서 [Tools] → [Global Options]를 선택한 후, Options의 [Appearance] 탭의 [Editor theme]을 통해 각종 테마를 적용할 수 있습니다. 이 중 본인의 마음에 드는 테마를 선택한 후, [OK] 버튼을 누릅니다.
화면의 배경이 어두워져 눈이 한결 편해졌습니다.
3.4 프로젝트 만들기
R 스튜디오에서 코딩을 하기 전에 프로젝트(Project)를 만들면 하나의 프로젝트에 사용되는 소스 코드, 이미지, 문서 등의 파일을 폴더별로 관리하여 효율적으로 관리할 수 있습니다.
먼저 R 스튜디오 상단의 육각형 모양 버튼()을 클릭하거나 [File → New Project]를 클릭합니다.
[Create Project] 화면에서 가장 상단의 [New Directory]를 클릭합니다. 참고로 Existing Directory는 기존 폴더에 새로운 프로젝트를 만들때, Version Control은 깃허브 등의 버전 관리 시스템을 이용할 때 사용됩니다.
[Project Type]에서 가장 상단의 [New Project]를 클릭합니다.
[Create New Project] 창에서 [Directory name] 항목에 새로 만들 프로젝트 이름을 입력합니다. [Create project as subdirectory of] 항목에는 프로젝트 폴더를 만들 위치를 선택하며, [Browse]를 클릭해 원하는 위치를 선택합니다. 그 후 하단의 [Creage Project]를 클릭합니다.
R 스튜디오가 재시작되면 우측 상단 부분이 프로젝트 이름으로 바뀌며, 파일 창의 윗부분도 프로젝트 폴더의 위치로 바뀝니다. 또한 폴더 내에 fin_ds.Rpoj 라는 파일이 생성됩니다. 스크립트 및 각종 파일들을 해당 프로젝트 폴더에 저장하여, 효율적으로 각종 작업을 관리할 수 있습니다.
프로젝트 이름과 폴더 경로에 한글이 들어가면 오류가 발생할 수 있으니, 영문으로 입력하는 것이 좋습니다.
3.5 데이터 타입별 다루기
R과 R 스튜디오 설치가 끝났으면 본격적으로 R의 기본적인 사용법에 대해 배워보겠으며, 먼저 데이터의 타입별로 다루는 법부터 시작하겠습니다.
R 뿐만 아니라 각종 프로그래밍에는 여러가지 데이터 타입이 있으며, 이를 다루는 방법은 각각 다릅니다. 예를 들어 같은 ’3’도 숫자 3인지 문자 3인지에 따라 다루는 방법이 다릅니다. 따라서 데이터 타입의 종류와 이들을 어떻게 다루어야 하는지를 아는 것이 프로그래밍의 기초라고 할 수 있습니다.
3.5.1 숫자 형태
R에서 숫자(Numbers) 형태는 크게 integer와 double로 나눌 수 있습니다. 이 중 integer는 정수를 의미하며, double은 부동소수점 실수를 의미합니다.
= c(1, 2.5, 4.5)
dbl_var
print(dbl_var)
[1] 1.0 2.5 4.5
위와 같이 입력하면 double, 즉 소수점 형태의 숫자가 만들어 집니다.
= c(1L, 6L, 5L)
int_var
print(int_var)
[1] 1 6 5
만일 숫자 뒤에 L을 붙이면, integer(정수) 형태의 숫자가 만들어 집니다.
double 형태를 integer 형태로 바꾸려면 할 경우, as.integer()
함수를 사용해 쉽게 변경할 수 있습니다. 이처럼 R에서는 as.*()
함수의 형태로 각 데이터의 형태를 바꿀 수 있습니다.
as.integer(dbl_var)
[1] 1 2 4
[1.0 2.5 4.5] 이던 dbl_var 값이 as.integer()
함수를 통해 소수점이 사라지고 정수 형태인 [1 2 4]로 변경되었습니다.
3.5.1.1 숫자 생성하기
R에서는 콜론(:)과 c()
함수를 통해 순서가 있는 숫자 벡터를 생성할 수 있습니다.
1:10
[1] 1 2 3 4 5 6 7 8 9 10
시작숫자:끝숫자의 형태로 입력하여 1에서 10까지 숫자가 생성됩니다.
c(1, 5, 10)
[1] 1 5 10
c()
함수 내부에 각각의 숫자를 입력할 경우, 이로 구성된 숫자 벡터가 생성됩니다.
seq()
함수를 이용할 경우 더욱 다양하게 숫자 벡터를 생성할 수 있습니다. seq는 Sequence 즉 ’순서’의 약어입니다. 이처럼 R이나 여타 프로그래밍에서는 함수의 이름을 통해 대략적인 기능을 추론할 수 있습니다.
seq(from = 1, to = 21, by = 2)
[1] 1 3 5 7 9 11 13 15 17 19 21
seq()
함수 내부에 from에는 시작 숫자, to에는 종료 숫자, by에는 간격을 입력합니다. 즉 1에서 21까지 2 단위로 숫자가 생성됩니다.
seq(0, 21, length.out = 15)
[1] 0.0 1.5 3.0 4.5 6.0 7.5 9.0 10.5 12.0 13.5 15.0 16.5 18.0 19.5 21.0
만일 입력값에 by 대신 length.out을 쓸 경우 from에서 to 까지 동일한 증가폭으로 length.out 만큼의 숫자를 생성하며, 해당 예제에서는 총 15개의 숫자가 만들어집니다.
rep()
함수 역시 숫자를 생성해주는 함수입니다.
rep(1:4, times = 2)
[1] 1 2 3 4 1 2 3 4
rep는 Replicate 즉 ’복제하다’의 약어 입니다. 해당 함수 내에 times라는 입력값을 추가해줄 경우, 해당 숫자만큼 반복되어 벡터가 생성됩니다.
rep(1:4, each = 2)
[1] 1 1 2 2 3 3 4 4
만일 each라는 입력값을 추가할 경우, 각각의 숫자를 n번 반복하여 벡터가 생성됩니다.
3.5.1.2 올림, 내림, 반올림
함수를 통해 간단하게 숫자의 올림, 내림, 반올림을 할 수도 있습니다. 먼저 다음과 같이 숫자를 입력합니다.
= c(1, 1.35, 1.7, 2.053, 2.4, 2.758, 3.1, 3.45,
x 3.8, 4.15, 4.5, 4.855, 5.2, 5.55, 5.9)
round(x)
[1] 1 1 2 2 2 3 3 3 4 4 4 5 5 6 6
round()
함수는 가장 가까운 정수로 반올림을 합니다.
round(x, digits = 2)
[1] 1.00 1.35 1.70 2.05 2.40 2.76 3.10 3.45 3.80 4.15 4.50 4.86 5.20 5.55 5.90
함수 내부에 digits 입력값을 추가해 줄 경우, 해당 자리수 만큼 반올림을 합니다. 위 예제에서는 소수 둘째자리 만큼 반올림을 하였습니다.
ceiling(x)
[1] 1 2 2 3 3 3 4 4 4 5 5 5 6 6 6
floor(x)
[1] 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5
ceiling()
함수는 올림을, floor()
함수는 내림을 실행합니다.
3.5.2 문자열 형태
일반적인 글자 혹은 텍스트를 문자열(Character Strings)이라고 합니다.
= 'learning to create'
a = 'character strings'
b paste(a, b)
[1] "learning to create character strings"
먼저 a와 b 변수에 각각의 문자를 입력한 후, R의 기본함수인 paste()
함수를 이용해 두 문자를 붙일 수 있습니다.
print(pi)
[1] 3.141593
paste('pi is', pi)
[1] "pi is 3.14159265358979"
원주율을 의미하는 pi는 원래 3.14159 라는 숫자가 입력되어 있습니다. 그러나 paste()
함수를 통해 문자열과 숫자를 합칠 경우, 그 결과값은 문자열이 됩니다.
paste('I', 'love', 'R', sep = ',')
[1] "I,love,R"
paste()
함수 내부에 sep 인자를 추가할 경우, 각 단어를 구분하는 문자를 입력할 수 있습니다. 기존에는 각 문자가 공백을 기준으로 합쳐졌다면, 이번에는 콤마(,)를 기준으로 합쳐졌습니다.
paste0('I', 'love', 'R')
[1] "IloveR"
paste0()
함수는 구분 문자가 없이 결합됩니다.
3.5.2.1 stringr
패키지를 이용한 문자열 다루기
R의 기본함수를 이용하여도 문자열을 다룰 수 있지만, stringr
패키지를 이용할 경우 더욱 다양한 작업을 수행할 수 있습니다. 패키지를 설치하는 방법은 다음과 같습니다.
install.packages('패키지이름')
stringr 패키지를 설치한 후 다음과 같이 입력합니다.
library(stringr)
str_c('Learning', 'to', 'use', 'the', 'stringr', 'package', sep = ' ')
[1] "Learning to use the stringr package"
str_c()
함수는 paste()
함수와 기능이 동일하며, sep 인자를 통해 구분자를 추가할 수 있습니다.
= c('Learning', 'to', NA, 'use', 'the', NA, 'stringr', 'package')
text str_length(text)
[1] 8 2 NA 3 3 NA 7 7
str_length()
함수는 문자열 각각의 텍스트 갯수를 세줍니다.
= 'Learning to use the stringr package'
x
str_sub(x, start = 1, end = 15)
[1] "Learning to use"
str_sub(x, start = -7, end = -1)
[1] "package"
str_sub()
함수는 start부터 end까지의 문자를 출력합니다. 만일 start 혹은 end에 음수를 입력하면, 문장의 뒤에서부터 start/end 지점이 계산됩니다. 즉, start와 end에 각각 -7와 -1을 입력하면 끝에서부터 일곱번째와 첫번째 지점이 시작점과 끝점이 됩니다.
텍스트 데이터를 다룰때는 빈 공백이 따라오는 경우가 많으며, 이는 대부분 제거해주어야 할 대상입니다.
= c('Text ', ' with', ' whitespace ', ' on', 'both ', 'sides ')
text print(text)
[1] "Text " " with" " whitespace " " on" "both "
[6] "sides "
각 단어를 자세히 살펴보면 좌/우 혹은 양쪽에 공백이 있습니다. 이를 제거하도록 하겠습니다.
str_trim(text, side = 'left')
[1] "Text " "with" "whitespace " "on" "both "
[6] "sides "
str_trim(text, side = 'right')
[1] "Text" " with" " whitespace" " on" "both"
[6] "sides"
str_trim(text, side = 'both')
[1] "Text" "with" "whitespace" "on" "both"
[6] "sides"
str_trim()
함수는 공백을 제거해주는 기능을 합니다. side 인자에 left를 입력할 경우 각 텍스트 왼쪽의 공백을, right를 입력할 경우 오른쪽의 공백을, both를 입력할 경우 양쪽의 공백을 제거해줍니다.
마지막으로 원하는 자리수를 채우기 위해 문자열에 공백 혹은 특정 문자를 입력할 수도 있으며, str_pad()
함수를 통해 손쉽게 작업을 할 수 있습니다.
str_pad('beer', width = 10, side = 'left')
[1] " beer"
width에 해당하는 10자리를 맞추기 위해 side의 입력값인 좌측에 공백이 추가되었습니다.
str_pad('beer', width = 10, side = 'left', pad = '!')
[1] "!!!!!!beer"
pad 인자를 추가할 경우, 공백이 아닌 입력한 문자가 추가됩니다
아래 페이지에는 stringr
패키지의 자세한 사용법이 나와 있습니다.
3.5.3 날짜 형태
시계열 작업을 위해서는 날짜(Date), 혹은 시간(Datetime) 형태를 다루어야 합니다.
Sys.timezone()
[1] "Asia/Seoul"
Sys.Date()
[1] "2023-01-19"
Sys.time()
[1] "2023-01-19 12:02:04 KST"
Sys.timezone()
함수는 현재 타임존을 출력합니다. Sys.Date()
함수는 현재 날짜를, Sys.time()
함수는 날짜와 시간을 출력합니다.
’2018-12-31’과 같이 사용자가 보기에는 날짜 형태이지만 문자열 형태로 데이터가 들어오는 경우, 이를 날짜 형태로 변경해야 할 경우가 있습니다.
= c('2021-07-01', '2021-08-01', '2021-09-01')
x = as.Date(x)
x_date
str(x_date)
Date[1:3], format: "2021-07-01" "2021-08-01" "2021-09-01"
as.Date()
함수를 이용하면 문자열을 손쉽게 날짜 형태로 변경할 수 있습니다. str()
함수는 데이터의 형태를 확인하는 함수로써, Date 형태임이 확인됩니다.
= c('07/01/2015', '08/01/2015', '09/01/2015')
y
as.Date(y, format = '%m/%d/%Y')
[1] "2015-07-01" "2015-08-01" "2015-09-01"
YYYY-MM-DD 형태가 아닌 다른 형태(MM/DD/YYYY)로 입력된 경우, format을 직접 입력하여 Date 형태로 변경할 수 있습니다.
YYYY는 연, MM은 월, DD는 일을 나타냅니다.
3.5.3.1 lubridate
패키지를 이용한 날짜 다루기
lubridate
패키지를 이용할 경우 날짜 형태와 관련된 다양한 작업을 수행할 수 있습니다.
library(lubridate)
Loading required package: timechange
Attaching package: 'lubridate'
The following objects are masked from 'package:base':
date, intersect, setdiff, union
= c('2021-07-01', '2021-08-01', '2021-09-01')
x = c('07/01/2015', '08/01/2015', '09/01/2015')
y
ymd(x)
[1] "2021-07-01" "2021-08-01" "2021-09-01"
mdy(y)
[1] "2015-07-01" "2015-08-01" "2015-09-01"
lubridate
패키지를 이용할 경우 YYYY-MM-DD 형태는 ymd()
, MM-DD-YYYY 형태는 mdy()
함수를 사용해 손쉽게 Date 형태로 변경할 수 있습니다. 이 외에도 lubridate
에는 Date 형태로 변경하기 위한 다양한 함수가 존재합니다.
순서 | 함수 |
---|---|
연, 월, 일 | ymd() |
연, 일, 월 | ydm() |
월, 일, 연 | mdy() |
일, 월, 연 | dmy() |
시, 분 | hm() |
시, 분, 초 | hms() |
연, 월, 일, 시, 분, 초 | ymd_hms() |
lubridate
패키지에는 날짜 관련 정보를 추출할 수 있는 다양한 함수가 존재합니다.
정보 | 함수 |
---|---|
연 | year() |
월 | month() |
주 | week() |
연도 내 일수 | yday() |
월 내 일수 | mday() |
주 내 일수 | wday() |
시 | hour() |
분 | minute() |
초 | second() |
타임존 | tz() |
= c('2021-07-01', '2021-08-01', '2021-09-01')
x
year(x)
[1] 2021 2021 2021
month(x)
[1] 7 8 9
week(x)
[1] 26 31 35
year()
, month()
, week()
함수를 통해 년도, 월, 주 정보를 확인할 수 있습니다.
= '2021-09-15'
z
yday(z)
[1] 258
mday(z)
[1] 15
wday(z)
[1] 4
yday()
, mday()
, wday()
함수는 각각 해당 년도에서 몇번째 일인지, 해당 월에서 몇번째 일인지, 해당 주에서 몇번째 일인지를 계산합니다.
= ymd('2021-07-01', '2021-08-01', '2021-09-01')
x
+ years(1) - days(c(2, 9, 21)) x
[1] "2022-06-29" "2022-07-23" "2022-08-11"
날짜에서 연과 월, 일자를 더하거나 빼는 계산 역시 가능합니다. 먼저 year()
함수를 통해 1년씩을 더 하였으며, days()
함수를 통해 각각의 일자 만큼을 뺍니다.
3.5.3.2 날짜 순서 생성하기
숫자와 마찬가지로 seq()
함수를 이용할 경우 날짜 벡터를 생성할 수 있습니다.
seq(ymd('2015-01-01'), ymd('2021-01-01'), by ='years')
[1] "2015-01-01" "2016-01-01" "2017-01-01" "2018-01-01" "2019-01-01"
[6] "2020-01-01" "2021-01-01"
2015년 1월 1일부터 2021년 1월 1일까지 1년을 기준으로 벡터가 생성됩니다.
seq(ymd('2021-09-01'), ymd('2021-09-30'), by ='2 days')
[1] "2021-09-01" "2021-09-03" "2021-09-05" "2021-09-07" "2021-09-09"
[6] "2021-09-11" "2021-09-13" "2021-09-15" "2021-09-17" "2021-09-19"
[11] "2021-09-21" "2021-09-23" "2021-09-25" "2021-09-27" "2021-09-29"
지정한 일수인 2일 단위로 날짜 벡터를 생성할 수도 있습니다. 이 외에도 by 인자를 통해 원하는 기간 단위의 벡터를 생성할 수 있습니다.
아래 페이지에는 lubridate 패키지의 자세한 사용법이 나와 있습니다.
3.6 데이터 구조 다루기
R에서 자주 사용되는 데이터구조는 벡터(Vector), 리스트(List), 데이터프레임(Dataframe) 입니다.
3.6.1 벡터 다루기
벡터는 R의 가장 기본적인 데이터 구조로써 integer, double, logical, character로 이루어져 있습니다. 벡터를 만드는 방법에 대해서는 앞서 다루었습니다.
= 8:17
vec_integer
vec_integer
[1] 8 9 10 11 12 13 14 15 16 17
= c(0.5, 0.6, 0.2)
vec_double
vec_double
[1] 0.5 0.6 0.2
= c('a', 'b', 'c')
vec_char
vec_char
[1] "a" "b" "c"
integer의 경우 start:end 형태를 통해서, 그 외에는 c()
함수를 통해 벡터를 만들 수 있습니다.
c('a', 'b', 'c', 1, 2, 3)
[1] "a" "b" "c" "1" "2" "3"
숫자와 문자가 같이 벡터로 묶일 경우, 숫자는 모두 문자 형태로 변경됩니다.
c(1, 2, 3, TRUE, FALSE)
[1] 1 2 3 1 0
TRUE와 FALSE는 참 혹은 거짓을 나타내는 논리값(logical) 입니다. 숫자와 논리값이 같이 묶일 경우 TRUE는 1, FALSE는 0으로 치환된 후 숫자 형태로 변경됩니다.
c('a', 'b', 'c', TRUE, FALSE)
[1] "a" "b" "c" "TRUE" "FALSE"
문자와 논리값이 같이 묶일 경우 모두 문자 형태로 변경됩니다. 이처럼 문자와 다른 형태가 묶일 경우엔 모든 데이터가 문자로 변경됩니다.
이번에는 기존의 벡터에 새로운 값을 추가해보겠습니다.
= 8:17
v1
c(v1, 18:22)
[1] 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
기존 8부터 17까지의 숫자로 이루어진 v1 벡터에, c()
함수를 이용하여 새로운 값을 추가할 수 있습니다.
만약 벡터에서 원하는 부분의 데이터를 추출하려면 대괄호([])를 이용하면 됩니다.
2] v1[
[1] 9
2:4] v1[
[1] 9 10 11
c(2, 4, 6)] v1[
[1] 9 11 13
대괄호 안에 숫자를 입력하면, 벡터에서 해당 순서의 데이터가 추출됩니다. c(2,4,6)과 같이 특정 위치를 지정하여 데이터를 추출할 수도 있습니다.
-1] v1[
[1] 9 10 11 12 13 14 15 16 17
-c(2, 4, 6, 8)] v1[
[1] 8 10 12 14 16 17
마이너스 기호를 입력하면, 해당 순서를 제외한 데이터가 추출됩니다.
< 12 v1
[1] TRUE TRUE TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
< 12] v1[v1
[1] 8 9 10 11
< 12 | v1 > 15] v1[v1
[1] 8 9 10 11 16 17
먼저 v1 < 12를 입력하면 해당 조건에 해당하는 부분은 TRUE, 그렇지 않은 부분은 FALSE를 반환합니다. 그 후 대괄호 안에 다시 결과를 입력하면 TRUE에 해당하는 순서의 데이터만 반환합니다. 이처럼 대괄호 내부에 조건을 설정하여 원하는 데이터를 추출할 수도 있습니다.
3.6.2 리스트 다루기
먼저 리스트를 생성합니다.
= list(1:3, 'a', c(TRUE, FALSE, TRUE), c(2.5, 4.2))
l
str(l)
List of 4
$ : int [1:3] 1 2 3
$ : chr "a"
$ : logi [1:3] TRUE FALSE TRUE
$ : num [1:2] 2.5 4.2
첫번째 원소는 정수(int), 두번째 원소는 문자(chr), 세번째 원소는 논리값(logi), 네번째 원소는 숫자(num)로 이루어져 있습니다. 이처럼 리스트는 각 원소간 타입이나 길이가 달라도 데이터가 결합할 수 특징이 있습니다.
= list(1:3, list(letters[1:5], c(TRUE, FALSE, TRUE)))
l2
str(l2)
List of 2
$ : int [1:3] 1 2 3
$ :List of 2
..$ : chr [1:5] "a" "b" "c" "d" ...
..$ : logi [1:3] TRUE FALSE TRUE
위 예제에서 두번째 원소는 리스트로 구성되어 있습니다. 이처럼 리스트 내에 또 다른 리스트를 생성하는 것 역시 가능합니다.
이번에는 기존 리스트에 새로운 원소를 추가하도록 하겠습니다.
= list(1:3, 'a', c(TRUE, FALSE, TRUE))
l3 = append(l3, list(c(2.5, 4.2)))
l4
print(l4)
[[1]]
[1] 1 2 3
[[2]]
[1] "a"
[[3]]
[1] TRUE FALSE TRUE
[[4]]
[1] 2.5 4.2
append()
함수를 이용하면 기존 리스트에 추가로 원소를 붙일 수 있습니다.
$item5 = 'new list item'
l4
print(l4)
[[1]]
[1] 1 2 3
[[2]]
[1] "a"
[[3]]
[1] TRUE FALSE TRUE
[[4]]
[1] 2.5 4.2
$item5
[1] "new list item"
또한 기존 리스트에 달러 사인($)을 입력할 경우, 원소에 이름이 생성되며 데이터가 추가됩니다.
리스트에서 원하는 데이터를 추출할 때는, 벡터와 동일하게 대괄호를 이용하면 됩니다.
1] l4[
[[1]]
[1] 1 2 3
c(1,3)] l4[
[[1]]
[1] 1 2 3
[[2]]
[1] TRUE FALSE TRUE
원소에 이름이 있을 경우, 이를 이용해 추출도 가능합니다.
'item5'] l4[
$item5
[1] "new list item"
원소의 이름인 item5를 입력하면 해당 원소만 반환합니다.
1]] l4[[
[1] 1 2 3
$item5 l4
[1] "new list item"
대괄호를 두번, 혹은 달러 사인($)을 이용해 데이터를 추출할 경우 원소 내의 형태가 반환되며, 위의 예제들과는 다르게 벡터 형태가 반환되었습니다.
1]] l4[[
[1] 1 2 3
1]][3] l4[[
[1] 3
특정 원소의 항목을 추출하기 위해서는 [[와 [를 함께 사용합니다. 위 예제는 l4 리스트의 첫번째 원소에서 3번째 항목을 추출하게 됩니다.
3.6.3 데이터프레임 다루기
데이터프레임은 R에서 가장 널리 사용되는 형식으로써, 각 컬럼이 다른 형태를 가질 수 있습니다.
= data.frame (col1 = 1:3,
df col2 = c ("this", "is", "text"),
col3 = c (TRUE, FALSE, TRUE),
col4 = c (2.5, 4.2, pi))
str(df)
'data.frame': 3 obs. of 4 variables:
$ col1: int 1 2 3
$ col2: chr "this" "is" "text"
$ col3: logi TRUE FALSE TRUE
$ col4: num 2.5 4.2 3.14
col1은 숫자(int), col2는 문자(chr), col3는 논리연산자(logi), col4는 숫자(num)로 구성되어 있습니다. 또한 벡터 혹은 리스트를 이용해 데이터프레임을 생성할 수도 있습니다.
= 1:3
v1 = c ("this", "is", "text")
v2 = c (TRUE, FALSE, TRUE)
v3
data.frame(col1 = v1, col2 = v2, col3 = v3)
col1 col2 col3
1 1 this TRUE
2 2 is FALSE
3 3 text TRUE
v1, v2, v3는 각각 숫자, 문자, 논리연산로 구성된 벡터입니다. 이를 data.frame()
함수 내에 입력할 경우 col1, col2, col3 열 이름에 해당 데이터들이 입력됩니다. 주의해야 할 점은 각 벡터의 길이가 동일해야 데이터프레임 형태를 만들 수 있습니다.
= list (item1 = 1:3,
l item2 = c ("this", "is", "text"),
item3 = c (2.5, 4.2, 5.1))
l
$item1
[1] 1 2 3
$item2
[1] "this" "is" "text"
$item3
[1] 2.5 4.2 5.1
data.frame(l)
item1 item2 item3
1 1 this 2.5
2 2 is 4.2
3 3 text 5.1
리스트 역시 data.frame()
함수를 이용할 경우, 각 원소명을 열이름으로 한 데이터프레임이 생성됩니다. 이 경우 역시 각 원소의 데이터 길이가 동일해야 합니다.
기존 데이터프레임에 행방향 혹은 열방향으로 데이터를 추가할 수 있습니다.
df
col1 col2 col3 col4
1 1 this TRUE 2.500000
2 2 is FALSE 4.200000
3 3 text TRUE 3.141593
= c ("A", "B", "C")
v4
cbind(df, v4)
col1 col2 col3 col4 v4
1 1 this TRUE 2.500000 A
2 2 is FALSE 4.200000 B
3 3 text TRUE 3.141593 C
cbind()
함수는 ’column bind’의 약어로써, 기존 데이터프레임에 새로운 열을 추가합니다.
= c (4, "R", F, 1.1)
v5
rbind(df, v5)
col1 col2 col3 col4
1 1 this TRUE 2.5
2 2 is FALSE 4.2
3 3 text TRUE 3.14159265358979
4 4 R FALSE 1.1
rbind()
함수는 ’row bind’의 약어로써, 기존 데이터프레임에 새로운 행을 추가합니다. 주의할 점은 각 행의 데이터 형태가 기존 데이터의 형태와 일치해야 합니다.
데이터프레임 역시 대괄호를 이용해 데이터를 추출할 수 있으며, 공백으로 두면 모든 행(열)을 선택하게 됩니다.
df
col1 col2 col3 col4
1 1 this TRUE 2.500000
2 2 is FALSE 4.200000
3 3 text TRUE 3.141593
2:3, ] df[
col1 col2 col3 col4
2 2 is FALSE 4.200000
3 3 text TRUE 3.141593
데이터프레임중 2:3, 즉 두번째부터 세번째까지의 행과 모든 열을 선택하게 됩니다.
c('col2', 'col4')] df[ ,
col2 col4
1 this 2.500000
2 is 4.200000
3 text 3.141593
행이름 혹은 열이름 직접 입력하여 해당값을 추출할 수도 있습니다. 위 예제에서는 열이름이 col2와 col4인 열을 추출합니다.
2] df[,
[1] "this" "is" "text"
2, drop = FALSE] df[,
col2
1 this
2 is
3 text
만일 하나의 열만 선택시 결과가 벡터 형태로 추출되며, drop = FALSE 인자를 추가해주면 데이터프레임의 형태가 유지되어 추출됩니다.
3.6.4 결측치 처리하기
결측치란 누락된 데이터를 의미하며, 데이터 분석에서 결측치를 처리하는 것은 매우 중요합니다. R에서 결측치는 NA
로 표시되며, is.na()
함수를 통해 결측치 여부를 확인할 수 있습니다.
= c(1:4, NA, 6:7, NA)
x x
[1] 1 2 3 4 NA 6 7 NA
is.na(x)
[1] FALSE FALSE FALSE FALSE TRUE FALSE FALSE TRUE
데이터가 NA인 경우에는 TRUE, 그렇지 않을 경우 FALSE를 반환합니다. 데이터프레임에도 해당 함수를 적용할 수 있습니다.
= data.frame (col1 = c (1:3, NA),
df col2 = c ("this", NA,"is", "text"),
col3 = c (TRUE, FALSE, TRUE, TRUE),
col4 = c (2.5, 4.2, 3.2, NA)
) df
col1 col2 col3 col4
1 1 this TRUE 2.5
2 2 <NA> FALSE 4.2
3 3 is TRUE 3.2
4 NA text TRUE NA
is.na(df)
col1 col2 col3 col4
[1,] FALSE FALSE FALSE FALSE
[2,] FALSE TRUE FALSE FALSE
[3,] FALSE FALSE FALSE FALSE
[4,] TRUE FALSE FALSE TRUE
데이터에 결측치가 있는 경우 계산이 불가능하다는 문제가 발생합니다.
= c(1, 3, NA, 4)
y mean(y)
[1] NA
데이터의 중간에 결측치인 NA가 존재하여 평균을 계산할 수 없으며, 이 외에도 모든 연산이 불가능하게 됩니다.
mean(y, na.rm = TRUE)
[1] 2.666667
na.rm에서 rm은 ’remove’의 약어입니다. 즉 해당 인자를 TRUE로 설정할 경우 NA를 제외하고 연산을 수행합니다. 따라서 1,3,4의 평균인 \(\frac{1+3+4}{3} = 2.6667\)이 계산됩니다.
일반적으로 결측치가 있는 경우 해당 데이터는 문제가 있다고 판단하여 제거하거나, 다른 데이터로 대체하여 채워넣기도 합니다. 먼저 결측치가 있는 데이터를 제거하는 법을 알아보겠습니다.
= data.frame (col1 = c (1:4),
df col2 = c ("this", NA,"is", "text"),
col3 = c (TRUE, FALSE, TRUE, TRUE),
col4 = c (2.5, 4.2, 3.2, 5.0)
) df
col1 col2 col3 col4
1 1 this TRUE 2.5
2 2 <NA> FALSE 4.2
3 3 is TRUE 3.2
4 4 text TRUE 5.0
두번째 행 col2 열에 결측치가 있으므로, 해당 부분을 제거해주도록 합니다.
na.omit(df)
col1 col2 col3 col4
1 1 this TRUE 2.5
3 3 is TRUE 3.2
4 4 text TRUE 5.0
na.omit()
함수를 이용하면 NA가 위치한 부분의 데이터가 제거됩니다.
= c(1:4, NA, 6:7, NA)
x x
[1] 1 2 3 4 NA 6 7 NA
이번에는 위와 같이 결측치가 있는 경우, 다른 데이터로 대체하도록 하겠습니다.
is.na(x)] = mean(x, na.rm = TRUE)
x[ x
[1] 1.000000 2.000000 3.000000 4.000000 3.833333 6.000000 7.000000 3.833333
mean()
함수 내부를 통해 값들의 평균을 구한 후, is.na()
함수를 통해 결측치가 위치한 부분의 데이터를 평균값인 3.833 으로 대체하였습니다. 이 외에도 결측치를 대체하는데는 다양한 방법이 존재합니다.
3.7 데이터 불러오기 및 내보내기
일반적으로 사람들은 csv 혹은 엑셀 파일로 저장된 데이터를 주고 받으며 데이터 분석을 하는 경우가 많습니다. 해당 형식의 파일을 R로 불러오는 법 그리고 저장하는 법에 대해 알아보겠습니다.
csv와 엑셀 샘플 파일의 주소는 다음과 같습니다.
- https://github.com/hyunyulhenry/dmmr_quant/blob/main/kospi.csv
- https://github.com/hyunyulhenry/dmmr_quant/blob/main/kospi.xlsx
아래의 코드를 실행하면 해당 파일을 PC에 다운로드 받을 수 있습니다.
download.file('https://raw.githubusercontent.com/hyunyulhenry/dmmr_quant/master/kospi.csv', 'kospi.csv')
download.file('https://github.com/hyunyulhenry/dmmr_quant/raw/main/kospi.xlsx', 'kospi.xlsx', mode = 'wb')
3.7.1 워킹 디렉터리
데이터를 불러오거나 내보낼 때 초보자들이 가장 많이 하는 실수가 워킹 디렉터리를 제대로 설정하지 않는 것입니다. 워킹 디렉터리(Working Directory)란 현재 사용중인 폴더를 의미하며, 현재 워킹 디렉터리는 콘솔창 상단 getwd()
함수를 통해 확인할 수 있습니다.
getwd()
# [1] "C:/Users/henry/Documents/R/fin_ds"
파일들이 A 폴더에 있는데 워킹 디렉터리가 B 폴더인 상태에서는 파일을 불러올 수 없으므로, 해당 파일들이 현재 워킹 디렉터리에 있어야 합니다.
워킹 디렉터리를 변경할때는 setwd()
함수를 통해 위치를 직접 지정해주어도 되지만, R 스튜디오의 파일 창을 이용하면 손쉽게 변경할 수 있습니다. 먼저 파일 창 상단의 […] 부분을 클릭합니다.
탐색기 화면에서 원하는 폴더를 선택한 후 하단의 [Open]을 클릭합니다.
탐색기 화면에 선택한 폴더의 파일들이 보입니다. 이제 [Move → Set As Working Directory]를 클릭하면 콘솔창에 해당 폴더를 워킹 디렉터리로 변경하는 코드가 입력되면서, 워킹 디렉터리 위치가 해당 폴더로 변경됩니다.
3.7.2 csv 파일 불러오기 및 저장하기
먼저 R의 기본함수인 read.csv()
함수를 이용하면 매우 손쉽게 csv 파일을 불러올 수 있습니다.
= read.csv('kospi.csv')
kospi # kospi = read.csv('kospi.csv', fileEncoding="UTF-8-BOM")
head(kospi)
Date Close Ret
1 2020-01-02 2175.17 -1.02
2 2020-01-03 2176.46 0.06
3 2020-01-06 2155.07 -0.98
4 2020-01-07 2175.54 0.95
5 2020-01-08 2151.31 -1.11
6 2020-01-09 2186.45 1.63
readr
패키지의 read._csv()
함수를 이용하면 기본 함수 대비 10배 정도 빠르게 데이터를 불러올 수 있으며, 훨씬 다양한 조건을 입력할 수도 있습니다.
library(readr)
= read_csv('kospi.csv') kospi2
Rows: 248 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): Close, Ret
date (1): Date
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(kospi2)
# A tibble: 6 × 3
Date Close Ret
<date> <dbl> <dbl>
1 2020-01-02 2175. -1.02
2 2020-01-03 2176. 0.06
3 2020-01-06 2155. -0.98
4 2020-01-07 2176. 0.95
5 2020-01-08 2151. -1.11
6 2020-01-09 2186. 1.63
R의 데이터를 csv로 저장하는 법은 기본함수의 write.csv()
혹은 readr
패키지의 write_csv()
함수를 이용하면 됩니다.
# using write.csv
write.csv(kospi, 'kospi2.csv', row.names = FALSE)
# using write_csv
write_csv(kospi2, 'kospi2.csv')
기본함수인 write.csv()
의 경우 행이름이 자동으로 새로운 열로 추가되어 저장되므로, 이를 원하지 않을 경우 row.names = FALSE
를 추가로 입력해주어야 합니다.
3.7.3 엑셀 파일 불러오기 및 저장하기
R의 기본함수 중에는 엑셀 파일을 불러오는 함수가 없지만, 해당 작업을 수행하는 다양한 패키지가 존재합니다. 그 중에서 readr
패키지와 쌍둥이 격인 readxl
패키지를 이용해보겠습니다.
먼저 해당 패키지의 read_excel()
함수를 이용해 엑셀 파일을 불러올 수 있습니다.
library(readxl)
= read_excel('kospi.xlsx', sheet = 'kospi')
kospi_excel head(kospi_excel)
# A tibble: 6 × 3
Date Close Ret
<dttm> <dbl> <dbl>
1 2020-01-02 00:00:00 2175. -1.02
2 2020-01-03 00:00:00 2176. 0.06
3 2020-01-06 00:00:00 2155. -0.98
4 2020-01-07 00:00:00 2176. 0.95
5 2020-01-08 00:00:00 2151. -1.11
6 2020-01-09 00:00:00 2186. 1.63
엑셀은 여러 시트로 구성된 경우가 많으며, sheet에 특정 시트명을 입력하면 해당 시트의 내용을 불러오게 됩니다. 만일 아무값도 입력하지 않을 경우 가장 첫번째 시트의 데이터를 불러옵니다.
반대로 R의 데이터를 엑셀로 저장하는 방법은 writexl
패키지의 write_xlsx()
함수를 이용하면 됩니다.
library(writexl)
write_xlsx(kospi_excel, 'kospi_excel.xlsx')
3.8 효율성과 가독성 높이기
이번에는 프로그래밍의 효율성을 높이기 위해 자주 사용되는 함수와 루프 구문, 그리고 가독성을 높이기 위한 파이프 오퍼레이터에 대해 알아보겠습니다.
3.8.1 함수
동일하거나 비슷한 작업을 반복해야 하는 경우 매번 실행하거나 복사-붙여넣기 하기 보다는 경우 함수를 작성하여 사용하면 매우 효율적으로 작업이 가능합니다.
함수는 크게 세가지 요소로 구성됩니다.
body()
: 함수 내부의 코드formals()
: 인자(argument) 내역environment()
: 함수의 변수에 대한 위치
예를 들어 금융 자산의 현재 가치는 다음과 같이 계산됩니다.
\[PV = FV / (1+r)^n\]
- PV: 현재 가치
- FV: 미래 가치
- r: 할인률
- n: 기간
즉, 1년 뒤에 110만원을 받는 돈의 현재가치는 \(110만원/(1+0.1)^1 = 100만원\) 이라 볼 수 있습니다. 이러한 값을 구하기 위해 매번 계산기를 사용하기 보다는 함수를 이용하면, 훨씬 효율적인 작업이 가능합니다. 위의 수식을 함수로 나타내면 다음과 같습니다.
= function(FV, r, n) {
PV = FV / (1+r)^n
PV return(round(PV, 2))
}
R에서는 함수명 = function(인자) {함수 내용}
의 형태로 함수를 만들수 있으며, 반환하고자 하는 결과를 return()
내부에 작성합니다. 함수의 구성요소 세가지를 한번 확인해보도록 하겠습니다.
body(PV)
{
PV = FV/(1 + r)^n
return(round(PV, 2))
}
먼저 body는 함수 내부의 코드를 의미합니다.
formals(PV)
$FV
$r
$n
formals에는 함수의 인자인 FV, r, n이 있습니다.
environment(PV)
<environment: R_GlobalEnv>
함수는 GlobalEnv에 위치하고 있습니다.
이제 해당함수를 이용해 현재가치를 계산해보도록 하겠습니다.
PV(FV = 1000, r = 0.08, n = 5)
[1] 680.58
\(1000 / (1.08)^5\)의 값인 680.58이 간단하게 계산됩니다.
PV(1000, 0.08, 5)
[1] 680.58
만약 인자의 리스트를 생략하면, 입력한 순서대로 값이 입력됩니다.
PV(r = 0.08, FV = 1000, n = 5)
[1] 680.58
인자의 내역을 정확하게 지정해준다면, 순서대로 입력하지 않아도 됩니다.
PV(1000, 0.08)
Error in PV(1000, 0.08): argument "n" is missing, with no default
PV()
함수에 필요한 인자는 3개인 반면, 2개만 입력하였으므로 에러가 발생합니다.
= function(FV = 1000, r = .08, n = 5) {
PV = FV / (1 + r)^n
PV return(round(PV, 2))
}PV(1000, 0.08)
[1] 680.58
만일 함수의 인자에 디폴트 값이 입력되어 있다면, 함수 실행시 이를 생략하여도 디폴트 값이 입력됩니다. 위 예제에서는 n 디폴트 값으로 5가 들어가있으며, PV()
함수 내에 입력값이 3개만 입력될 경우 마지막 인자는 디폴트 값인 5를 적용합니다.
3.8.2 루프 구문
루프 및 각종 구문을 이용하여 휴율적인 작업을 하는것도 가능합니다.
3.8.2.1 if
구문
먼저 if
구문은 다음과 같이 구성됩니다.
if (test_expression) {
statement }
괄호 안의 test_expression이 TRUE일 경우에만 statement 코드가 실행됩니다. 간단한 예제를 살펴보겠습니다.
= c(8, 3, -2, 5)
x if (any(x < 0)) {
print('x contains negative number')
}
[1] "x contains negative number"
any()
함수는 적어도 하나의 값이 참이면 참으로 출력하는 함수입니다. 즉, 위의 코드는 x중 하나라도 0보다 작은 값이 있으면 x contains negative number라는 문장을 출력하며, -2가 0보다 작으므로 해당 문장을 출력합니다.
= c (8, 3, 2, 5)
y if (any (y < 0)) {
print ("y contains negative numbers")
}
이번에는 y에 0보다 작은 값이 없으므로, 구문이 실행되지 않아 문장을 출력하지 않습니다. 이처럼 if
구문만 존재할 시 이를 만족하지 않는 경우 아무런 구문도 실행되지 않습니다. 만일 조건을 충족하지 않을 때 동작을 추가하고 싶을 경우 if else
구문을 사용하며, 이는 if의 조건을 만족하지 않을 경우 else에 해당하는 구문이 실행됩니다.
if (test_expression) {
1
statement else {
} 2
statement }
만일 test_expression 구문이 TRUE이면 statement 1이 실행되며, 그렇지 않을 경우 statement 2가 실행됩니다. 실제 예제를 살펴봅시다.
= c (8, 3, 2, 5)
y if (any (y < 0)) {
print ("y contains negative numbers")
else {
} print ("y contains all positive numbers")
}
[1] "y contains all positive numbers"
y에 음수가 존재하는 if
구문이 FALSE 이므로, else에 해당하는 메세지가 출력됩니다. ifelse
구문은 ifelse()
함수로 간단히 나타낼 수도 있습니다.
= c (8, 3, 2, 5)
x ifelse(any(x < 0), "x contains negative numbers", "x contains all positive numbers")
[1] "x contains all positive numbers"
해당 함수는 ifelse(test, yes, no)
형태로 입력되며, test를 충족하면 yes가 그렇지 않으면 no가 실행됩니다. 위의 예에서는 x에 0보다 작은 값이 없으므로, no에 해당하는 내용이 실행됩니다.
또한 if
와 else
사이에 else if
조건을 통해, 여러 조건을 추가할 수도 있습니다.
= 7
x if (x >= 10) {
print ("x exceeds acceptable tolerance levels")
else if(x >= 0 & x < 10) {
} print ("x is within acceptable tolerance levels")
else {
} print ("x is negative")
}
[1] "x is within acceptable tolerance levels"
위 조건은 다음과 같습니다.
- x가 10 이상일 경우 x exceeds acceptable tolerance levels을 출력합니다.
- 만일 x가 10 이상, 10 미만일경우 x is within acceptable tolerance levels을 출력합니다.
- 그렇지 않을 경우 x is negative을 출력합니다.
x는 7 이므로 else에 해당하는 내용이 출력됩니다.
3.8.2.2 for loop
구문
for loop
구문은 특정한 부분의 코드가 반복적으로 수행될 수 있도록 하며, 다음과 같이 구성됩니다.
for (i in 1:100) {
<code: do stuff here with i>
}
먼저 i에 1이 들어간 뒤 code에 해당하는 부분이 실행됩니다. 그 후, i에 2가 들어간 뒤 다시 code가 실행되며 이 작업이 100까지 반복됩니다. 실제 예제를 살펴보도록 하겠습니다.
for (i in 2016:2020) {
= paste("The year is", i)
output print(output)
}
[1] "The year is 2016"
[1] "The year is 2017"
[1] "The year is 2018"
[1] "The year is 2019"
[1] "The year is 2020"
i에 2010부터 2016 까지 대입되며, The year is라는 문자와 결합해 결과가 출력됩니다.
3.8.2.3 apply
계열 함수
apply
계열의 함수는 loop 구문과 비슷한 역할을 하며, 코드를 훨씬 간결하게 표현할 수 있습니다. 먼저 가장 기본이 되는 apply()
함수는 데이터프레임의 행 혹은 열단위 계산에 자주 사용됩니다. 해당 함수는 다음과 같이 구성됩니다.
apply(x, MARGIN, FUN, ...)
- x: 매트릭스, 데이터프레임, 혹은 어레이
- MARGIN: 함수가 적용될 벡. 1은 행을, 2는 열을, c(1, 2)는 행과 열을 의미
- FUN: 적용될 함수
- …: 기타
실제 사용 예제를 살펴보도록 하겠습니다.
head(mtcars)
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
먼저 위 데이터에서 각 열의 평균을 구하도록 합니다.
apply(mtcars, 2, mean)
mpg cyl disp hp drat wt qsec
20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
vs am gear carb
0.437500 0.406250 3.687500 2.812500
mtcars에서 2 즉 열의 방향으로 평균(mean)을 구합니다.
lapply()
함수는 리스트에 적용되며, 결과 또한 리스트로 반환됩니다. 해당 함수는 다음과 같이 구성됩니다.
lapply(x, FUN, ...)
- x: 리스트
- FUN: 적용될 함수
- …: 기타
실제 사용 예제를 살펴보도록 하겠습니다.
= list(item1 = 1:4,
data item2 = rnorm(10),
item3 = rnorm(20, 1),
item4 = rnorm(100, 5))
data
$item1
[1] 1 2 3 4
$item2
[1] -2.4277261 -0.9440400 -0.4566693 1.1261980 0.1302850 -0.4400706
[7] -1.3687153 -1.0160505 0.9574250 0.6030140
$item3
[1] -0.8280410 1.5877561 0.6519054 1.6851901 0.8160204 1.7310635
[7] 0.8194120 0.9896752 2.3098337 2.9746060 0.8184646 1.4615911
[13] 0.1959621 1.2887692 1.8622489 1.6840486 2.6746881 0.6979188
[19] 1.3292467 0.7115478
$item4
[1] 5.496185 4.652535 5.144889 3.531203 6.373730 5.242102 5.954658 5.874957
[9] 4.028542 3.955290 4.256409 6.755209 3.421948 5.473112 6.342920 4.894410
[17] 3.017986 5.433050 4.978889 6.814147 5.175712 6.290643 5.179915 4.574775
[25] 4.958425 4.585420 5.060215 3.755706 5.570188 5.335484 6.386616 5.269292
[33] 4.845014 2.812070 6.654766 5.893865 3.912794 5.530643 5.751659 4.616348
[41] 3.064464 6.182229 4.927253 5.132420 3.379126 6.907367 4.510491 4.616129
[49] 4.627057 5.373028 3.604680 5.107075 3.136585 6.013002 4.531478 5.323894
[57] 5.234609 4.919160 4.190791 5.588027 5.419336 5.653750 5.739616 6.203223
[65] 4.446966 5.224400 4.261675 3.204919 4.360111 4.002939 3.908668 6.625849
[73] 6.138562 4.977905 5.366792 3.848384 5.722863 5.915812 5.216841 6.838834
[81] 4.659914 4.031143 4.784598 5.163867 5.049546 4.963160 3.360283 4.972907
[89] 6.359261 4.081928 2.456386 4.685300 5.426302 3.964631 6.579156 5.004591
[97] 4.867578 4.034871 6.132982 5.082355
4개의 원소로 구성된 리스트에서 각 원소의 평균을 구하고자 할 경우, 리스트에 적용되는 apply인 lapply()
함수를 사용해야 합니다.
lapply(data, mean)
$item1
[1] 2.5
$item2
[1] -0.383635
$item3
[1] 1.273095
$item4
[1] 4.989088
lapply()
함수를 통해 각 항목의 평균을 구할 수 있으며, 결과 또한 리스트 형태로 반환됩니다. 해당 함수는 좀더 복잡한 형태로 응용도 가능합니다.
= list(beaver1 = beaver1, beaver2 = beaver2)
beaver_data lapply(beaver_data, head)
$beaver1
day time temp activ
1 346 840 36.33 0
2 346 850 36.34 0
3 346 900 36.35 0
4 346 910 36.42 0
5 346 920 36.55 0
6 346 930 36.69 0
$beaver2
day time temp activ
1 307 930 36.58 0
2 307 940 36.73 0
3 307 950 36.93 0
4 307 1000 37.15 0
5 307 1010 37.23 0
6 307 1020 37.24 0
위 데이터의 각 항목에서 열 별 평균을 구하고자 할 경우 lapply()
함수 만으로는 계산이 불가능합니다. 이러한 경우 해당 함수 내부에 새로운 함수인 function()
을 정의하여 복잡한 계산을 수행할 수 있습니다.
lapply(beaver_data, function(x) {
round(apply(x, 2, mean), 2)
})
$beaver1
day time temp activ
346.20 1312.02 36.86 0.05
$beaver2
day time temp activ
307.13 1446.20 37.60 0.62
function(x)를 통해 각 항목에 적용될 함수를 직접 정의할 수 있습니다. 우리가 정의한 함수는 apply()
함수를 통해 열의 방향으로 평균을 구한 뒤 소수 둘째 자리까지 반올림을 하는 것이며, 해당 함수가 리스트의 모든 원소에 적용됩니다.
마지막으로 살펴볼 sapply()
함수는 lapply()
함수와 거의 동일하며, 결과가 리스트가 아닌 벡터 혹은 매트릭스로 출력된다는 점만 차이가 있습니다.
lapply(beaver_data, function(x) {
round(apply(x, 2, mean), 2)
})
$beaver1
day time temp activ
346.20 1312.02 36.86 0.05
$beaver2
day time temp activ
307.13 1446.20 37.60 0.62
sapply(beaver_data, function(x) {
round(apply(x, 2, mean), 2)
})
beaver1 beaver2
day 346.20 307.13
time 1312.02 1446.20
temp 36.86 37.60
activ 0.05 0.62
sapply()
함수는 각 원소에 적용된 값을 벡터로 하는 매트릭스 형태로 결과값이 출력됩니다.
3.8.2.4 기타 함수
열과 행이 합계나 평균처럼 일반적으로 많이 사용되는 계산에는 apply()
함수보다 간단하게 표현할 수 있는 함수들이 있습니다.
rowSums(mtcars)
Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive
328.980 329.795 259.580 426.135
Hornet Sportabout Valiant Duster 360 Merc 240D
590.310 385.540 656.920 270.980
Merc 230 Merc 280 Merc 280C Merc 450SE
299.570 350.460 349.660 510.740
Merc 450SL Merc 450SLC Cadillac Fleetwood Lincoln Continental
511.500 509.850 728.560 726.644
Chrysler Imperial Fiat 128 Honda Civic Toyota Corolla
725.695 213.850 195.165 206.955
Toyota Corona Dodge Challenger AMC Javelin Camaro Z28
273.775 519.650 506.085 646.280
Pontiac Firebird Fiat X1-9 Porsche 914-2 Lotus Europa
631.175 208.215 272.570 273.683
Ford Pantera L Ferrari Dino Maserati Bora Volvo 142E
670.690 379.590 694.710 288.890
colSums(mtcars)
mpg cyl disp hp drat wt qsec vs
642.900 198.000 7383.100 4694.000 115.090 102.952 571.160 14.000
am gear carb
13.000 118.000 90.000
rowSums()
함수는 행의 합계를, colSums()
함수는 열의 합계는 구하며 이는 apply(mtcars, 1 or 2, sum)
과 동일합니다.
rowMeans(mtcars)
Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive
29.90727 29.98136 23.59818 38.73955
Hornet Sportabout Valiant Duster 360 Merc 240D
53.66455 35.04909 59.72000 24.63455
Merc 230 Merc 280 Merc 280C Merc 450SE
27.23364 31.86000 31.78727 46.43091
Merc 450SL Merc 450SLC Cadillac Fleetwood Lincoln Continental
46.50000 46.35000 66.23273 66.05855
Chrysler Imperial Fiat 128 Honda Civic Toyota Corolla
65.97227 19.44091 17.74227 18.81409
Toyota Corona Dodge Challenger AMC Javelin Camaro Z28
24.88864 47.24091 46.00773 58.75273
Pontiac Firebird Fiat X1-9 Porsche 914-2 Lotus Europa
57.37955 18.92864 24.77909 24.88027
Ford Pantera L Ferrari Dino Maserati Bora Volvo 142E
60.97182 34.50818 63.15545 26.26273
colMeans(mtcars)
mpg cyl disp hp drat wt qsec
20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750
vs am gear carb
0.437500 0.406250 3.687500 2.812500
rowMeans()
함수와 colMeans()
함수 역시 각각 행과 열의 평균을 구합니다.
3.8.3 파이프 오퍼레이터
파이프 오퍼레이터는 R에서 동일한 데이터를 대상으로 연속으로 작업하게 해주는 오퍼레이터(연산자)입니다.
흔히 프로그래밍에서 x라는 데이터를 F()
라는 함수에 넣어 결괏값을 확인하고 싶으면 F(x)
의 방법을 사용합니다. 예를 들어 3과 5라는 데이터 중 큰 값을 찾으려면 max(3,5)
를 통해 확인합니다. 이를 통해 나온 결괏값을 또 다시 G()
라는 함수에 넣어 결괏값을 확인하려면 비슷한 과정을 거칩니다. max(3,5)
를 통해 나온 값의 제곱근을 구하려면 result = max(3,5)
를 통해 첫 번째 결괏값을 저장하고, sqrt(result)
를 통해 두 번째 결괏값을 계산합니다. 물론 sqrt(max(3,5))
와 같은 표현법으로 한 번에 표현할 수 있습니다.
이러한 표현의 단점은 계산하는 함수가 많아질수록 저장하는 변수가 늘어나거나 괄호가 지나치게 길어진다는 것입니다. 그러나 파이프 오퍼레이터인 %>%
를 사용하면 함수 간의 관계를 매우 직관적으로 표현하고 이해할 수 있습니다. 이를 정리하면 아래 표와 같습니다.
내용 | 표현 방법 |
---|---|
F(x) | x %>% F |
G(F(x)) | x %>% F %>% G |
간단한 예제를 통해 파이프 오퍼레이터의 사용법을 살펴보겠습니다. 먼저 다음과 같은 10개의 숫자가 있다고 가정합니다.
= c(0.3078, 0.2577, 0.5523, 0.0564, 0.4685,
x 0.4838, 0.8124, 0.3703, 0.5466, 0.1703)
우리가 원하는 과정은 다음과 같습니다.
- 각 값들의 로그값을 구할 것
- 로그값들의 계차를 구할 것
- 구해진 계차의 지수값을 구할 것
- 소수 둘째 자리까지 반올림할 것
즉 log()
, diff()
, exp()
, round()
에 대한 값을 순차적으로 구하고자 합니다.
= log(x)
x1 = diff(x1)
x2 = exp(x2)
x3 round(x3, 2)
[1] 0.84 2.14 0.10 8.31 1.03 1.68 0.46 1.48 0.31
첫 번째 방법은 단계별 함수의 결괏값을 변수에 저장하고 저장된 변수를 다시 불러와 함수에 넣고 계산하는 방법입니다. 전반적인 계산 과정을 확인하기에는 좋지만 매번 변수에 저장하고 불러오는 과정이 매우 비효율적이며 코드도 불필요하게 길어집니다.
round(exp(diff(log(x))), 2)
[1] 0.84 2.14 0.10 8.31 1.03 1.68 0.46 1.48 0.31
두 번째는 괄호를 통해 감싸는 방법입니다. 앞선 방법에 비해 코드는 짧아졌지만, 계산 과정을 알아보기에는 매우 불편한 방법으로 코드가 짜여 있습니다.
library(magrittr)
%>% log() %>% diff() %>% exp() %>% round(., 2) x
[1] 0.84 2.14 0.10 8.31 1.03 1.68 0.46 1.48 0.31
마지막으로 파이프 오퍼레이터를 사용하는 방법입니다. 코드도 짧으며 계산 과정을 한눈에 파악하기도 좋습니다. 맨 왼쪽에는 원하는 변수를 입력하며, %>% 뒤에는 차례대로 계산하고자 하는 함수를 입력합니다. 변수의 입력값을 ()로 비워둘 경우, 오퍼레이터의 왼쪽에 있는 값이 입력 변수가 됩니다. 반면 round()
와 같이 입력값이 두 개 이상 필요하면 마침표(.)가 오퍼레이터의 왼쪽 값으로 입력됩니다.
파이프 오퍼레이터는 크롤링뿐만 아니라 모든 코드에 사용할 수 있습니다. 이를 통해 훨씬 깔끔하면서도 데이터 처리 과정을 직관적으로 이해할 수 있습니다.
3.9 데이터 구조 변형하기
기본적인 R 사용법을 익혔다면 데이터의 모양을 바꾸고 가공하여 내가 원하는 결과물을 출력해야 합니다. 해당 작업은 tidyr
패키지와 dplyr
패키지를 이용해 매우 효율적으로 수행할 수 있으며, dplyr 패키지의 함수 중 일부는 SQL 구문과 매우 유사합니다.
3.9.1 tidyr
패키지를 이용한 데이터 모양 바꾸기
깔끔한 데이터(tidy data)는 다음과 같이 구성되어 있습니다.
- 각 변수(variable)는 열로 구성됩니다.
- 각 관측값(observation)은 행으로 구성됩니다.
- 각 타입의 관측치는 테이블을 구성합니다.
tidyr
패키지에는 이러한 깔끔한 데이터를 만드는데 필요한 여러 함수가 있습니다.
3.9.1.1 pivot_longer()
: 세로로 긴 데이터 만들기
먼저 가로로 긴(Wide) 데이터를 세로로 길게 만드는데는 pivot_longer()
함수가 사용됩니다. 이 함수는 여러 열을 key-value 페어로 변형해줍니다.
library(tidyr)
Attaching package: 'tidyr'
The following object is masked from 'package:magrittr':
extract
table4a
# A tibble: 3 × 3
country `1999` `2000`
* <chr> <int> <int>
1 Afghanistan 745 2666
2 Brazil 37737 80488
3 China 212258 213766
위 예제에는 세 국가의 1999, 2000년 데이터가 있습니다. 이 중 country를 제외한 연도별 데이터를 세로로 길게 만들도록 하겠습니다.
= table4a %>% pivot_longer(names_to = 'years', values_to = 'cases', -country)
long print(long)
# A tibble: 6 × 3
country years cases
<chr> <chr> <int>
1 Afghanistan 1999 745
2 Afghanistan 2000 2666
3 Brazil 1999 37737
4 Brazil 2000 80488
5 China 1999 212258
6 China 2000 213766
열 이름에 해당하던 1999, 2000 데이터가 names_to에 입력한 years 열에 입력되었습니다. 또한 각 관측값이 values_to에 입력한 cases 열에 입력되었습니다. country 앞에는 마이너스(-) 기호를 붙여 해당 열은 그대로 유지됩니다.
|
|
3.9.1.2 pivot_wider()
: 가로로 긴 데이터 만들기
pivot_longer()
함수와 반대로, pivot_wider()
함수를 이용할 경우 세로로 긴 데이터를 가로로 길게 만들 수 있습니다. 위의 데이터에 year 열에 있는 항목들을 열 이름으로, cases 열에 있는 항목들을 가로로 길게 되돌려 보겠습니다.
= long %>% pivot_wider(names_from = 'years', values_from = 'cases')
back2wide print(back2wide)
# A tibble: 3 × 3
country `1999` `2000`
<chr> <int> <int>
1 Afghanistan 745 2666
2 Brazil 37737 80488
3 China 212258 213766
names_from와 values_from에 각각 열이름 및 관측값에 해당하는 값을 입력하면, 원래대로 가로로 긴 테이블 형태가 되었습니다.
3.9.1.3 separate()
: 하나의 열을 여러 열로 나누기
table3
# A tibble: 6 × 3
country year rate
* <chr> <int> <chr>
1 Afghanistan 1999 745/19987071
2 Afghanistan 2000 2666/20595360
3 Brazil 1999 37737/172006362
4 Brazil 2000 80488/174504898
5 China 1999 212258/1272915272
6 China 2000 213766/1280428583
rate 열에는 데이터가 ###/#### 형태로 들어가 있습니다. / 기호를 기준으로 앞과 뒤로 각각 나누어보도록 하겠습니다.
%>%
table3 separate(rate, into = c("cases", "population"))
# A tibble: 6 × 4
country year cases population
<chr> <int> <chr> <chr>
1 Afghanistan 1999 745 19987071
2 Afghanistan 2000 2666 20595360
3 Brazil 1999 37737 172006362
4 Brazil 2000 80488 174504898
5 China 1999 212258 1272915272
6 China 2000 213766 1280428583
separate()
함수를 사용할 경우 rate 열이 “/”를 기준으로 into에 입력한 cases와 population 열로 분리됩니다.
%>%
table3 separate(rate, into = c("cases", "population"), remove = FALSE)
# A tibble: 6 × 5
country year rate cases population
<chr> <int> <chr> <chr> <chr>
1 Afghanistan 1999 745/19987071 745 19987071
2 Afghanistan 2000 2666/20595360 2666 20595360
3 Brazil 1999 37737/172006362 37737 172006362
4 Brazil 2000 80488/174504898 80488 174504898
5 China 1999 212258/1272915272 212258 1272915272
6 China 2000 213766/1280428583 213766 1280428583
remove = FALSE 인자를 추가해주면 원래의 열이 사라지지 않고 유지됩니다.
3.9.1.4 unite()
: 여러 열을 하나로 합치기
separate()
함수와는 반대로, unite()
함수를 이용시 여러 열을 하나로 합칠 수 있습니다.
table5
# A tibble: 6 × 4
country century year rate
* <chr> <chr> <chr> <chr>
1 Afghanistan 19 99 745/19987071
2 Afghanistan 20 00 2666/20595360
3 Brazil 19 99 37737/172006362
4 Brazil 20 00 80488/174504898
5 China 19 99 212258/1272915272
6 China 20 00 213766/1280428583
이번에는 century와 year 열을 합친 후 새로운 열을 만들어보도록 하겠습니다.
%>%
table5 unite(new, century, year, sep = "_")
# A tibble: 6 × 3
country new rate
<chr> <chr> <chr>
1 Afghanistan 19_99 745/19987071
2 Afghanistan 20_00 2666/20595360
3 Brazil 19_99 37737/172006362
4 Brazil 20_00 80488/174504898
5 China 19_99 212258/1272915272
6 China 20_00 213766/1280428583
century 열과 year열이 합쳐진 후 new 열에 입력되었으며, sep 인자를 통해 구분자는 “_”로 설정하였습니다.
3.9.1.5 tidyr
패키지의 기타 함수
먼저 fill()
함수는 결측치를 채워주는 역할을 합니다.
= tribble(
score ~ person, ~ Math, ~ Computer,
"Henry", 1, 7,
NA, 2, 10,
NA, NA, 9,
"David", 1, 4
)
score
# A tibble: 4 × 3
person Math Computer
<chr> <dbl> <dbl>
1 Henry 1 7
2 <NA> 2 10
3 <NA> NA 9
4 David 1 4
score의 2번째와 3번째 행에 NA 데이터가 있어 이를 채워줄 필요가 있습니다.
%>%
score fill(person, Math)
# A tibble: 4 × 3
person Math Computer
<chr> <dbl> <dbl>
1 Henry 1 7
2 Henry 2 10
3 Henry 2 9
4 David 1 4
fill()
함수는 결측치가 있을 경우, 각 열의 이전 데이터를 이용해 채워줍니다. 반면에 NA 데이터를 특정 값으로 변경할 수도 있습니다.
%>% replace_na(replace = list(person = "unknown", Math = 0)) score
# A tibble: 4 × 3
person Math Computer
<chr> <dbl> <dbl>
1 Henry 1 7
2 unknown 2 10
3 unknown 0 9
4 David 1 4
replace_na()
함수를 이용해 person 열의 NA 데이터를 unknown으로, Math열의 NA 데이터를 0으로 변경하였습니다.
3.9.2 dplyr
패키지를 이용한 데이터 변형하기
데이터를 필터링 하거나, 요약하거나, 정렬하거나, 새로운 변수를 만드는 등 데이터 분석을 위해서는 데이터 변형하고 가공해야 하는 경우가 많습니다. R의 기본 함수도 이러한 기능을 제공하지만, dplyr
패키지를 이용할 경우 훨씬 빠르고 효율적으로 업무를 처리할 수 있습니다.
nycflights13 패키지의 flights 데이터셋을 예제로 사용하도록 하겠습니다.
library(dplyr)
Attaching package: 'dplyr'
The following object is masked from 'package:kableExtra':
group_rows
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(nycflights13)
flights
# A tibble: 336,776 × 19
year month day dep_time sched_de…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier
<int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr>
1 2013 1 1 517 515 2 830 819 11 UA
2 2013 1 1 533 529 4 850 830 20 UA
3 2013 1 1 542 540 2 923 850 33 AA
4 2013 1 1 544 545 -1 1004 1022 -18 B6
5 2013 1 1 554 600 -6 812 837 -25 DL
6 2013 1 1 554 558 -4 740 728 12 UA
7 2013 1 1 555 600 -5 913 854 19 B6
8 2013 1 1 557 600 -3 709 723 -14 EV
9 2013 1 1 557 600 -3 838 846 -8 B6
10 2013 1 1 558 600 -2 753 745 8 AA
# … with 336,766 more rows, 9 more variables: flight <int>, tailnum <chr>,
# origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>,
# minute <dbl>, time_hour <dttm>, and abbreviated variable names
# ¹sched_dep_time, ²dep_delay, ³arr_time, ⁴sched_arr_time, ⁵arr_delay
3.9.2.1 select()
: 원하는 열 선택하기
select()
함수를 이용해 특정 열만을 선택할 수 있습니다.
%>% select(year, month, day) %>% head() flights
# A tibble: 6 × 3
year month day
<int> <int> <int>
1 2013 1 1
2 2013 1 1
3 2013 1 1
4 2013 1 1
5 2013 1 1
6 2013 1 1
select()
함수 내에 선택하고자 하는 열을 입력하여 year, month, day 열을 선택했습니다.
%>% select(year:day) %>% head() flights
# A tibble: 6 × 3
year month day
<int> <int> <int>
1 2013 1 1
2 2013 1 1
3 2013 1 1
4 2013 1 1
5 2013 1 1
6 2013 1 1
콜론(:)을 이용해 year부터 day 까지의 열을 한번에 선택할 수도 있습니다.
%>% select(-(year:day)) %>% head() flights
# A tibble: 6 × 16
dep_time sched…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier flight tailnum origin
<int> <int> <dbl> <int> <int> <dbl> <chr> <int> <chr> <chr>
1 517 515 2 830 819 11 UA 1545 N14228 EWR
2 533 529 4 850 830 20 UA 1714 N24211 LGA
3 542 540 2 923 850 33 AA 1141 N619AA JFK
4 544 545 -1 1004 1022 -18 B6 725 N804JB JFK
5 554 600 -6 812 837 -25 DL 461 N668DN LGA
6 554 558 -4 740 728 12 UA 1696 N39463 EWR
# … with 6 more variables: dest <chr>, air_time <dbl>, distance <dbl>,
# hour <dbl>, minute <dbl>, time_hour <dttm>, and abbreviated variable names
# ¹sched_dep_time, ²dep_delay, ³arr_time, ⁴sched_arr_time, ⁵arr_delay
마이너스(-)를 이용할 경우 해당 열을 제외한 모든 열이 선택됩니다.
%>% select(starts_with("dep")) %>% head() flights
# A tibble: 6 × 2
dep_time dep_delay
<int> <dbl>
1 517 2
2 533 4
3 542 2
4 544 -1
5 554 -6
6 554 -4
select()
함수 내에 starts_with()
함수를 이용할 경우, 해당 문자로 시작하는 열을 모두 선택할 수 있습니다. 이 외에도 ends_with()
함수는 해당 문자로 끝나는 열울, contains()
함수는 해당 문자가 포함되는 열을 선택합니다.
3.9.2.2 rename()
: 열이름 바꾸기
%>% rename(tail_num = tailnum) %>% select(tail_num) %>% head() flights
# A tibble: 6 × 1
tail_num
<chr>
1 N14228
2 N24211
3 N619AA
4 N804JB
5 N668DN
6 N39463
rename()
함수를 이용해 tailnum 이던 열 이름을 tail_num 으로 변경하였습니다. 해당 함수는 rename(변경하고자 하는 이름 = 변경전 이름)
형태로 입력해야 합니다.
3.9.2.3 filter()
: 필터링
특정 열에 원하는 데이터가 있는 부분만 필터링을 해야하는 경우가 많으며, filter()
함수를 사용해 손쉽게 해결할 수 있습니다.
%>% filter(month == 3, day == 1) %>% head() flights
# A tibble: 6 × 19
year month day dep_time sched_dep…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier
<int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr>
1 2013 3 1 4 2159 125 318 56 142 B6
2 2013 3 1 50 2358 52 526 438 48 B6
3 2013 3 1 117 2245 152 223 2354 149 B6
4 2013 3 1 454 500 -6 633 648 -15 US
5 2013 3 1 505 515 -10 746 810 -24 UA
6 2013 3 1 521 530 -9 813 827 -14 UA
# … with 9 more variables: flight <int>, tailnum <chr>, origin <chr>,
# dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>, minute <dbl>,
# time_hour <dttm>, and abbreviated variable names ¹sched_dep_time,
# ²dep_delay, ³arr_time, ⁴sched_arr_time, ⁵arr_delay
filter()
함수 내에 필터링 하고자 하는 조건, 즉 month가 3이고 day가 1인 조건을 입력하면 해당 조건에 부합하는 행만 선택됩니다.
3.9.2.4 summarize()
: 요약값 계산하기
요약 통계값은 summarize()
함수를 통해 쉽게 계산할 수 있습니다.
%>% summarize(max_dep = max(dep_time, na.rm = TRUE),
flights min_dep = min(dep_time, na.rm = TRUE))
# A tibble: 1 × 2
max_dep min_dep
<int> <int>
1 2400 1
summarize()
함수를 통해 max_dep에는 dep_time의 최대값을, min_dep에는 dep_time의 최소값을 구해줍니다. na.rm 인자를 TRUE로 설정하여 NA 데이터는 제거해 줍니다.
3.9.2.5 group_by()
: 원하는 조건으로 그룹화
각 그룹별 통계량을 계산할 때는 group_by()
함수를 통해 그룹을 묶고, 계산하는 것이 편합니다.
= flights %>% group_by(year, month, day)
by_day %>% head() by_day
# A tibble: 6 × 19
# Groups: year, month, day [1]
year month day dep_time sched_dep…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier
<int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr>
1 2013 1 1 517 515 2 830 819 11 UA
2 2013 1 1 533 529 4 850 830 20 UA
3 2013 1 1 542 540 2 923 850 33 AA
4 2013 1 1 544 545 -1 1004 1022 -18 B6
5 2013 1 1 554 600 -6 812 837 -25 DL
6 2013 1 1 554 558 -4 740 728 12 UA
# … with 9 more variables: flight <int>, tailnum <chr>, origin <chr>,
# dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>, minute <dbl>,
# time_hour <dttm>, and abbreviated variable names ¹sched_dep_time,
# ²dep_delay, ³arr_time, ⁴sched_arr_time, ⁵arr_delay
year, month, day를 기준으로 그룹을 묶었습니다. 아직 계산을 하지 않았으므로 출력되는 데이터프레임 자체는 원래와 동일하며, Groups를 통해 어떠한 조건으로 그룹이 묶여있는지 확인됩니다.
%>%
by_day summarise(delay = mean(dep_delay, na.rm = TRUE)) %>%
head()
`summarise()` has grouped output by 'year', 'month'. You can override using the
`.groups` argument.
# A tibble: 6 × 4
# Groups: year, month [1]
year month day delay
<int> <int> <int> <dbl>
1 2013 1 1 11.5
2 2013 1 2 13.9
3 2013 1 3 11.0
4 2013 1 4 8.95
5 2013 1 5 5.73
6 2013 1 6 7.15
해당 데이터는 그룹별로 묶여 있으므로, summarise()
함수를 적용하면 각 그룹(year, month, day) 별로 dep_delay의 평균을 구합니다.
%>% group_by(dest) %>%
flights summarize(
count = n(),
dist = mean(distance, na.rm = TRUE),
delay = mean(arr_delay, na.rm = TRUE)
)
# A tibble: 105 × 4
dest count dist delay
<chr> <int> <dbl> <dbl>
1 ABQ 254 1826 4.38
2 ACK 265 199 4.85
3 ALB 439 143 14.4
4 ANC 8 3370 -2.5
5 ATL 17215 757. 11.3
6 AUS 2439 1514. 6.02
7 AVL 275 584. 8.00
8 BDL 443 116 7.05
9 BGR 375 378 8.03
10 BHM 297 866. 16.9
# … with 95 more rows
한 번에 여러 통계량을 계산할 수도 있으며, n()
은 데이터의 갯수를 의미합니다.
3.9.2.6 arrange()
: 데이터 정렬하기
arrange()
함수를 통해 원하는 열을 기준으로 데이터를 순서대로 정렬할 수 있으며, 오름차순을 기본으로 합니다.
%>% arrange(year, month, day) %>% head() flights
# A tibble: 6 × 19
year month day dep_time sched_dep…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier
<int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr>
1 2013 1 1 517 515 2 830 819 11 UA
2 2013 1 1 533 529 4 850 830 20 UA
3 2013 1 1 542 540 2 923 850 33 AA
4 2013 1 1 544 545 -1 1004 1022 -18 B6
5 2013 1 1 554 600 -6 812 837 -25 DL
6 2013 1 1 554 558 -4 740 728 12 UA
# … with 9 more variables: flight <int>, tailnum <chr>, origin <chr>,
# dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>, minute <dbl>,
# time_hour <dttm>, and abbreviated variable names ¹sched_dep_time,
# ²dep_delay, ³arr_time, ⁴sched_arr_time, ⁵arr_delay
arrange()
함수 내에 입력한 [year -> month -> day] 순으로 오름차순 정렬이 됩니다.
%>% arrange(desc(dep_delay)) %>% head() flights
# A tibble: 6 × 19
year month day dep_time sched_dep…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier
<int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr>
1 2013 1 9 641 900 1301 1242 1530 1272 HA
2 2013 6 15 1432 1935 1137 1607 2120 1127 MQ
3 2013 1 10 1121 1635 1126 1239 1810 1109 MQ
4 2013 9 20 1139 1845 1014 1457 2210 1007 AA
5 2013 7 22 845 1600 1005 1044 1815 989 MQ
6 2013 4 10 1100 1900 960 1342 2211 931 DL
# … with 9 more variables: flight <int>, tailnum <chr>, origin <chr>,
# dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>, minute <dbl>,
# time_hour <dttm>, and abbreviated variable names ¹sched_dep_time,
# ²dep_delay, ³arr_time, ⁴sched_arr_time, ⁵arr_delay
정렬하고자 하는 열에 desc()
함수를 추가할 경우, 오름차순이 아닌 내림차순으로 정렬됩니다.
3.9.2.7 *_join()
: 데이터 합치기
여러 테이블을 하나로 합치기 위해 *_join()
함수를 이용합니다. 합치는 방법은 그림 @ref(fig:joinimg2)과 표 @ref(tab:joindesc2) 같이 크게 네가지 종류가 있습니다.
함수 | 내용 |
---|---|
inner_join() | 교집합 |
full_join() | 합집합 |
left_join() | 좌측 기준 |
right_join() | 우측 기준 |
다음 두개의 데이터 테이블을 이용하도록 합니다.
= flights %>%
flights2 select(year:day, hour, tailnum, carrier)
%>% head() flights2
# A tibble: 6 × 6
year month day hour tailnum carrier
<int> <int> <int> <dbl> <chr> <chr>
1 2013 1 1 5 N14228 UA
2 2013 1 1 5 N24211 UA
3 2013 1 1 5 N619AA AA
4 2013 1 1 5 N804JB B6
5 2013 1 1 6 N668DN DL
6 2013 1 1 5 N39463 UA
%>% head() airlines
# A tibble: 6 × 2
carrier name
<chr> <chr>
1 9E Endeavor Air Inc.
2 AA American Airlines Inc.
3 AS Alaska Airlines Inc.
4 B6 JetBlue Airways
5 DL Delta Air Lines Inc.
6 EV ExpressJet Airlines Inc.
flights2 데이터에는 항공사 명의 약자인 carrier가 있으며, airlines 데이터는 해당 약자의 풀 네임이 적혀있습니다. left_join()
함수를 이용해 왼쪽 데이터프레임을 기준으로 데이터를 합치도록 하며, 두 데이터 모두 carrier 열이 있으므로 이를 기준으로 데이터가 합치도록 하겠습니다.
%>%
flights2 left_join(airlines, by = "carrier") %>%
head()
# A tibble: 6 × 7
year month day hour tailnum carrier name
<int> <int> <int> <dbl> <chr> <chr> <chr>
1 2013 1 1 5 N14228 UA United Air Lines Inc.
2 2013 1 1 5 N24211 UA United Air Lines Inc.
3 2013 1 1 5 N619AA AA American Airlines Inc.
4 2013 1 1 5 N804JB B6 JetBlue Airways
5 2013 1 1 6 N668DN DL Delta Air Lines Inc.
6 2013 1 1 5 N39463 UA United Air Lines Inc.
flights2에서 모든 데이터를 가져오며, airlines의 name 열이 기존 테이블에 추가됩니다. join 구문에 대한 더욱 상세한 예제 및 애니메이션은 다음 주소를 참조하시기 바랍니다.
- https://github.com/gadenbuie/tidyexplain
3.9.2.8 mutate()
: 새로운 열 생성하기
mutate()
함수를 사용해 기존 열끼리 계산을 하여 새로운 열을 생성할 수 있습니다.
= flights %>%
flights_sml select(
:day,
yearends_with("delay"),
distance,
air_time
)
%>%
flights_sml mutate(
gain = dep_delay - arr_delay,
speed = distance / air_time * 60
%>%
) head()
# A tibble: 6 × 9
year month day dep_delay arr_delay distance air_time gain speed
<int> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 2013 1 1 2 11 1400 227 -9 370.
2 2013 1 1 4 20 1416 227 -16 374.
3 2013 1 1 2 33 1089 160 -31 408.
4 2013 1 1 -1 -18 1576 183 17 517.
5 2013 1 1 -6 -25 762 116 19 394.
6 2013 1 1 -4 12 719 150 -16 288.
먼저 flights에서 일부 열을 선택하여 flights_sml에 저장합니다.그 후, mutate()
함수를 이용해 새로운 열을 만들어 줍니다. gain 열에는 dep_delay와 arr_delay의 차이가, speed 열에는 distance와 air_time 비에 60을 곱한 값이 새롭게 생성됩니다.
%>%
flights_sml mutate(
across(c('dep_delay', 'arr_delay'), ~ .x * 60)
%>%
) head()
# A tibble: 6 × 7
year month day dep_delay arr_delay distance air_time
<int> <int> <int> <dbl> <dbl> <dbl> <dbl>
1 2013 1 1 120 660 1400 227
2 2013 1 1 240 1200 1416 227
3 2013 1 1 120 1980 1089 160
4 2013 1 1 -60 -1080 1576 183
5 2013 1 1 -360 -1500 762 116
6 2013 1 1 -240 720 719 150
동일한 함수를 한번에 여러행에 적용해야 할 때는 mutate()
함수 내에 across()
함수를 입력합니다. 위 예제에서는 dep_delay과 arr_delay열의 데이터에 60을 곱해주었습니다. across()
함수의 자세한 사용법은 다소 생소하고 어려울 수 있으므로 아래 페이지의 내용을 따라가며 익히시는게 좋습니다.
- https://dplyr.tidyverse.org/reference/across.html
4 ggplot을 이용한 데이터 시각화
4.1 그래픽 문법
R에서 기본적으로 제공하는 plot()
함수를 이용해도 시각화가 충분히 가능합니다. 다음 사이트에는 기본 함수를 이용해 표현할 수 있는 다양한 그림이 나와 있습니다.
- http://www.sthda.com/english/wiki/r-base-graphs
그러나 기본 함수를 이용하여 시각화를 할 경우 다음과 같은 문제가 있습니다.
- 서로 다른 형태의 그림을 나타내기 위해 각각 다른 함수를 이용해야 함
- 표현할 수 있는 그림에 한계가 있음
- 원하는 형태로 꾸미기가 복잡함
ggplot2 패키지는 데이터 과학자들에게 가장 많이 사랑받는 패키지 중 하나이며, ggplot()
함수를 사용하면 그림을 훨씬 아름답게 표현할 수 있으며 다양한 기능들을 매우 쉽게 사용할 수도 있습니다. 처음에는 함수의 문법이 다소 어색하지만, 해당 패키지의 근본이 되는 철학인 그래픽 문법(The Grammar of Graphics)를 이해하고 조금만 연습해본다면, 충분히 손쉽게 사용이 가능합니다.
그래픽 문법(Grammar of Graphics)은 릴랜드 윌킨스(Leland Wilkinson)의 책 The Grammar of Graphics(Wilkinson 2012)에서 따온 것으로써, 데이터를 어떻게 표현할 것인지에 대한 내용입니다.
문법은 언어의 표현을 풍부하게 만든다. 단어만 있고 문법이 없는 언어가 있다면(단어 = 문장), 오직 단어의 갯수만큼만 생각을 표현할 수 있다. 문장 내에서 단어가 어떻게 구성되는 지를 규정함으로써, 문법은 언어의 범위를 확장한다.
— Leland Wilkinson, 《The Grammar of Graphics》 그래픽 문법에서 말하는 요소는 다음과 같습니다.
- Data: 시각화에 사용될 데이터
- Aesthetics: 데이터를 나타내는 시각적인 요소(x축, y축, 사이즈, 색깔, 모양 등)
- Geometrics: 데이터를 나타내는 도형
- Facets: 하위 집합으로 분할하여 시각화
- Statistics: 통계값을 표현
- Coordinates: 데이터를 표현 할 이차원 좌표계
- Theme: 그래프를 꾸밈
ggplot2 패키지의 앞글자가 gg인 것에서 알 수 있듯이, 해당 패키지는 그래픽 문법을 토대로 시각화를 표현하며, 전반적인 시각화의 순서는 그래픽 문법의 순서와 같습니다. ggplot2 패키지의 특징은 각 요소를 연결할 때 플러스(+) 기호를 사용한다는 점이며, 이는 그래픽 문법의 순서에 따라 요소들을 쌓아나간 후 최종적인 그래픽을 완성하는 패키지의 특성 때문입니다.
아래 사이트에는 R에서 ggplot2와 기타 패키지를 이용해 표현할 수 있는 그림이 정리되어 있습니다.
- https://www.r-graph-gallery.com/
4.2 데이터셋: 다이아몬드
ggplot2 패키지에는 데이터분석 및 시각화 연습을 위한 각종 데이터셋이 있으며, 그 중에서도 diamonds 데이터셋이 널리 사용됩니다. 먼저 해당 데이터를 불러오도록 하겠습니다.
library(ggplot2)
data(diamonds)
head(diamonds)
# A tibble: 6 × 10
carat cut color clarity depth table price x y z
<dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
4 0.29 Premium I VS2 62.4 58 334 4.2 4.23 2.63
5 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
6 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
데이터의 각 변수는 다음과 같습니다.
- carat: 다이아몬드 무게
- cut: 컷팅의 가치
- color: 다이아몬드 색상
- clarity: 깨끗한 정도
- depth: 깊이 비율, z / mean(x, y)
- table: 가장 넓은 부분의 너비 대비 다이아몬드 꼭대기의 너비
- price: 가격
- x: 길이
- y: 너비
- z: 깊이
이 외에도 R에서 제공하는 다양한 데이터셋은 다음의 함수를 통해 확인할 수 있습니다.
data()
[1] "band_instruments" "band_instruments2" "band_members"
[4] "starwars" "storms" "diamonds"
[7] "economics" "economics_long" "faithfuld"
[10] "luv_colours" "midwest" "mpg"
[13] "msleep" "presidential" "seals"
[16] "txhousing" "lakers" "airlines"
[19] "airports" "flights" "planes"
[22] "weather" "fruit" "sentences"
[25] "words" "billboard" "construction"
[28] "fish_encounters" "population" "relig_income"
[31] "smiths" "table1" "table2"
[34] "table3" "table4a" "table4b"
[37] "table5" "us_rent_income" "who"
[40] "world_bank_pop" "AirPassengers" "BJsales"
[43] "BJsales.lead (BJsales)" "BOD" "CO2"
[46] "ChickWeight" "DNase" "EuStockMarkets"
[49] "Formaldehyde" "HairEyeColor"
4.3 Data, Aesthetics, Geometrics
그래픽 문법의 순서에 맞춰 그림을 쌓아나가보도록 하겠습니다. 먼저 Data는 사용될 데이터이며, Aesthetics는 \(x\)축, \(y\)축, 사이즈 등 시각적인 요소를 의미합니다.
\(x\)축과 \(y\)축에 우리가 매핑한 carat과 price가 표현되었지만, 어떠한 모양(Geometrics)으로 시각화를 할지 정의하지 않았으므로 빈 그림이 생성됩니다. 다음으로 Geometrics을 통해 데이터를 그림으로 표현해주도록 하겠습니다.
사전에 정의된 Data와 Aesthetics 위에, 플러스(+) 기호를 통해 geom_point()
함수를 입력하여 산점도가 표현되었습니다. geom은 Geometrics의 약자이며, 이처럼 geom_*()
함수를 통해 원하는 형태로 시각화를 할 수 있습니다.
일반적으로 Data는 ggplot()
함수 내에서 정의하기 보다는, dplyr 패키지의 함수들을 이용하여 데이터를 가공한 후 파이프 오퍼레이터(%>%
)를 통해 연결합니다.
library(magrittr)
%>%
diamonds ggplot(aes(x = carat, y = price)) +
geom_point(aes(color = cut, shape = cut))
Warning: Using shapes for an ordinal variable is not advised
diamonds 데이터를 파이프 오퍼레이터로 이을 경우 그대로 시각화가 가능하며, ggplot()
함수 내에 데이터를 입력하지 않아도 됩니다.
geom_point()
내부에서 aes()를 통해 점의 색깔을 매핑해줄 수 있습니다. color = cut, shape = cut
을 지정하여 cut에 따라 점의 색깔과 형태를 다르게 표현하였습니다. 이 외에도 size 등을 통해 그룹별로 그래프를 각각 다르게 표현할 수 있습니다.
4.4 Facets
Facets은 여러 집합을 하나의 그림에 표현하기 보다 하위 집합으로 나누어 시각화하는 요소입니다.
facet_grid()
혹은 facet_wrap()
함수를 통해 그림을 분할할 수 있습니다. 물결 표시(~)를 통해 하위 집합으로 나누고자 하는 변수를 선택할 수 있으며, 위 예제에서는 cut에 따라 각기 다른 그림으로 표현되었습니다.
color를 추가해 facet_grid(color ~ cut)
를 입력하면 가로는 color, 세로는 cut으로 그림이 나누어집니다.
4.5 Statistics
Statistics는 통계값을 나타내는 요소입니다. 실무에서는 dplyr 패키지를 이용해 데이터의 통계값을 계산한 후 이를 그림으로 나타냅니다.
library(dplyr)
%>%
diamonds group_by(color) %>%
summarize(carat = mean(carat)) %>%
ggplot(aes(x = color, y = carat)) +
geom_col()
그룹 별 carat의 평균을 계산한 후 시각화를 하였습니다.
4.6 Coordinates
Coordinates는 좌표를 의미합니다. ggplot2에서는 `coord_*() 함수를 이용하여 \(x\)축 혹은 \(y\)축 정보를 변형할 수 있습니다.
%>%
diamonds ggplot(aes(x = carat, y = price)) +
geom_point(aes(color = cut)) +
coord_cartesian(xlim = c(0, 3), ylim = c(0, 20000))
coord_cartesian()
함수를 통해 \(x\)축과 \(y\)축 범위를 지정해 줄 수 있습니다. xlim과 ylim 내부에 범위의 최소 및 최댓값을 지정해주면, 해당 범위의 데이터만을 보여줍니다.
%>%
diamonds ggplot(aes(x = carat, y = price)) +
geom_boxplot(aes(group = cut))
%>%
diamonds ggplot(aes(x = carat, y = price)) +
geom_boxplot(aes(group = cut)) +
coord_flip()
coord_flip()
함수는 \(x\)축과 \(y\)축을 뒤집어 표현합니다. 위의 그림은 ggplot()
함수의 aes 내부에서 \(x\)축은 carat을, \(y\)축은 price를 지정해 주었지만, 아래 그림에서는 coord_flip()
함수를 통해 각 축이 서로 바뀌었습니다.
4.7 Theme
Theme은 그림의 제목, 축 제목, 축 단위, 범례, 디자인 등 그림을 꾸며주는 역할을 담당합니다.
%>%
diamonds ggplot(aes(x = carat, y = price)) +
geom_point(aes(color = cut)) +
theme_bw() +
labs(title = 'Relation between Carat & Price',
x = 'Carat', y = 'Price') +
theme(legend.position = 'bottom',
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
panel.grid.major.y = element_blank(),
panel.grid.minor.y = element_blank()
+
) scale_y_continuous(
labels = function(x) {
paste0('$',
format(x, big.mark = ','))
})
geom_point()
함수 이후 Theme에 해당하는 부분은 다음과 같습니다.
theme_bw()
함수를 통해 배경을 흰색으로 설정합니다.labs()
함수를 통해 그래프의 제목 및 \(x\)축, \(y\)축 제목을 변경합니다.theme()
함수 내 legend.position을 통해 범례를 하단으로 이동합니다.theme()
함수 내 panel.grid를 통해 격자를 제거합니다.scale_y_continuous()
함수를 통해 \(y\)축에서 천원 단위로 콤마(,)를 붙여주며, 이를 달러($) 표시와 합쳐줍니다.
이 외에도 각종 테마를 적용해 얼마든지 원하는 그림을 꾸밀 수 있습니다. R에서 적용가능한 그래프의 테마는 다음과 같습니다.
- https://ggplot2.tidyverse.org/reference/ggtheme.html
4.8 각종 팁
원하는 형태로 그래프를 가공하고자 할 경우, 구글에 검색을 하면 얼마든지 원하는 답을 얻을 수 있습니다. 만약 범례를 지우고 싶을 경우, 구글에서 ’remove legend ggplot’을 검색하도록 합니다.
이를 통해 우리가 원하는 답을 쉽게 얻을 수 있습니다.
또한 ggplot2 패키지 만으로도 나타낼 수 없는 그래프는 다양한 확장 패키지들을 통해 얼마든지 나타낼 수 있습니다.
- https://exts.ggplot2.tidyverse.org/gallery/
해당 패키지에 대한 더욱 자세한 설명은 패키지들의 책을 살펴보시기 바랍니다. 해당 책은 온라인에서 무료로 확인할 수 있습니다.
- https://ggplot2-book.org/
5 실제 금융데이터 분석하기
데이터 분석 및 시각화 실습을 위헤 국내 상장종목 티커 리스트 및 WICS 기준 섹터 지표를 다운로드 합니다.
download.file('https://raw.githubusercontent.com/hyunyulhenry/dmmr_quant/master/KOR_sector.csv', 'KOR_sector.csv')
download.file('https://raw.githubusercontent.com/hyunyulhenry/dmmr_quant/master/KOR_ticker.csv', 'KOR_ticker.csv')
해당 데이터를 불러옵니다.
library(stringr)
library(dplyr)
= read.csv('KOR_ticker.csv', fileEncoding = 'EUC-KR')
KOR_ticker = read.csv('KOR_sector.csv', fileEncoding = 'EUC-KR')
KOR_sector
= KOR_ticker %>% mutate(종목코드 = str_pad(종목코드, 6,'left', 0))
KOR_ticker = KOR_sector %>% mutate(CMP_CD = str_pad(CMP_CD, 6,'left', 0)) KOR_sector
각 파일을 불러온 후 티커에 해당하는 종목코드와 CMP_CD 열을 6자리 숫자로 만들어줍니다.
5.1 *_join()
먼저 두 테이블을 하나로 합쳐줍니다.
= left_join(KOR_ticker, KOR_sector,
data_market by = c('종목코드' = 'CMP_CD',
'종목명' = 'CMP_KOR'))
%>% head() data_market
종목코드 종목명 종가 대비 등락률 시장구분 업종명 시가총액
1 005930 삼성전자 70400 -1100 -1.54 KOSPI 전기전자 4.20273e+14
2 000660 SK하이닉스 105500 -2000 -1.86 KOSPI 전기전자 7.68042e+13
3 035420 NAVER 402500 -7500 -1.83 KOSPI 서비스업 6.61160e+13
4 207940 삼성바이오로직스 874000 0 0.00 KOSPI 의약품 5.78000e+13
5 035720 카카오 124500 -3500 -2.73 KOSPI 서비스업 5.54475e+13
6 051910 LG화학 784000 -47000 -5.66 KOSPI 화학 5.53444e+13
EPS PER BPS PBR 주당배당금 배당수익률 IDX_CD IDX_NM_KOR
1 3841 18.33 39406 1.79 2994 4.25 G45 WICS IT
2 6952 15.18 71275 1.48 1170 1.11 G45 WICS IT
3 6877 58.53 44850 8.97 402 0.10 G50 WICS 커뮤니케이션서비스
4 3642 239.98 69505 12.57 0 0.00 G35 WICS 건강관리
5 369 337.40 14286 8.71 30 0.02 G50 WICS 커뮤니케이션서비스
6 6666 117.61 230440 3.40 10000 1.28 G15 WICS 소재
ALL_MKT_VAL MKT_VAL WGT S_WGT CAL_WGT SEC_CD SEC_NM_KOR SEQ TOP60
1 551957421 315204519 57.11 57.11 1 G45 IT 1 2
2 551957421 56835145 10.30 67.40 1 G45 IT 2 2
3 180662521 51570493 28.55 28.55 1 G50 커뮤니케이션서비스 1 4
4 136785726 14457053 10.57 26.66 1 G35 건강관리 2 27
5 180662521 38258764 21.18 49.72 1 G50 커뮤니케이션서비스 2 4
6 122898119 35420414 28.82 28.82 1 G15 소재 1 8
APT_SHR_CNT
1 4477336913
2 538721750
3 128125448
4 16541250
5 307299311
6 45179100
left_join()
함수를 이용해 KOR_ticker와 KOR_sector 데이터를 합쳐줍니다. by 인자는 데이터를 합치는 기준점을 의미하며, x 데이터(KOR_ticker)의 종목코드와 y 데이터(KOR_sector)의 CMP_CD는 같음을, x 데이터의 종목명과 y 데이터의 CMP_KOR는 같음을 정의합니다.
= data_market %>% as_tibble() data_market
데이터 분석시에는 데이터프레임 보다는 티블 형태가 더욱 깔끔하므로 형태를 변경해줍니다.
5.2 glimpse()
glimpse(data_market)
Rows: 2,237
Columns: 26
$ 종목코드 <chr> "005930", "000660", "035420", "207940", "035720", "051910"…
$ 종목명 <chr> "삼성전자", "SK하이닉스", "NAVER", "삼성바이오로직스", "카…
$ 종가 <dbl> 70400, 105500, 402500, 874000, 124500, 784000, 732000, 208…
$ 대비 <int> -1100, -2000, -7500, 0, -3500, -47000, 1000, -1500, -1000,…
$ 등락률 <dbl> -1.54, -1.86, -1.83, 0.00, -2.73, -5.66, 0.14, -0.71, -1.1…
$ 시장구분 <chr> "KOSPI", "KOSPI", "KOSPI", "KOSPI", "KOSPI", "KOSPI", "KOS…
$ 업종명 <chr> "전기전자", "전기전자", "서비스업", "의약품", "서비스업", …
$ 시가총액 <dbl> 4.20273e+14, 7.68042e+13, 6.61160e+13, 5.78000e+13, 5.5447…
$ EPS <int> 3841, 6952, 6877, 3642, 369, 6666, 8593, 5454, 3710, 3756,…
$ PER <dbl> 18.33, 15.18, 58.53, 239.98, 337.40, 117.61, 85.19, 38.23,…
$ BPS <int> 39406, 71275, 44850, 69505, 14286, 230440, 184387, 250888,…
$ PBR <dbl> 1.79, 1.48, 8.97, 12.57, 8.71, 3.40, 3.97, 0.83, 1.15, 8.6…
$ 주당배당금 <int> 2994, 1170, 402, 0, 30, 10000, 1000, 3000, 1000, 0, 0, 800…
$ 배당수익률 <dbl> 4.25, 1.11, 0.10, 0.00, 0.02, 1.28, 0.14, 1.44, 1.18, 0.00…
$ IDX_CD <chr> "G45", "G45", "G50", "G35", "G50", "G15", "G45", "G25", "G…
$ IDX_NM_KOR <chr> "WICS IT", "WICS IT", "WICS 커뮤니케이션서비스", "WICS 건…
$ ALL_MKT_VAL <int> 551957421, 551957421, 180662521, 136785726, 180662521, 122…
$ MKT_VAL <int> 315204519, 56835145, 51570493, 14457053, 38258764, 3542041…
$ WGT <dbl> 57.11, 10.30, 28.55, 10.57, 21.18, 28.82, 6.75, 18.14, 13.…
$ S_WGT <dbl> 57.11, 67.40, 28.55, 26.66, 49.72, 28.82, 74.15, 18.14, 31…
$ CAL_WGT <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, NA, 1, 1, 1, 1, 1, 1, …
$ SEC_CD <chr> "G45", "G45", "G50", "G35", "G50", "G15", "G45", "G25", "G…
$ SEC_NM_KOR <chr> "IT", "IT", "커뮤니케이션서비스", "건강관리", "커뮤니케이…
$ SEQ <int> 1, 2, 1, 2, 2, 1, 3, 1, 2, 1, 4, 2, NA, 3, 3, 1, 1, 5, 1, …
$ TOP60 <int> 2, 2, 4, 27, 4, 8, 2, 9, 9, 27, 7, 8, NA, 9, 4, 7, 2, 4, 2…
$ APT_SHR_CNT <dbl> 4477336913, 538721750, 128125448, 16541250, 307299311, 451…
glimpse()
함수는 데이터 내용, 구조, 형식을 확인하는 함수입니다. 기본 함수인 str()
과 그 역할은 비슷하지만, tidy 형태로 결과물이 훨씬 깔끔하게 출력됩니다. 총 관측값 및 열의 개수, 각 열의 이름과 데이터 형식, 앞부분 데이터를 확인할 수 있습니다.
5.3 rename()
: 열 이름 바꾸기
names(data_market)
[1] "종목코드" "종목명" "종가" "대비" "등락률"
[6] "시장구분" "업종명" "시가총액" "EPS" "PER"
[11] "BPS" "PBR" "주당배당금" "배당수익률" "IDX_CD"
[16] "IDX_NM_KOR" "ALL_MKT_VAL" "MKT_VAL" "WGT" "S_WGT"
[21] "CAL_WGT" "SEC_CD" "SEC_NM_KOR" "SEQ" "TOP60"
[26] "APT_SHR_CNT"
열 이름 중 ’배당수익률’이 있습니다. 해당 이름을 변경해주도록 하겠습니다.
= data_market %>%
data_market rename(`배당수익률(%)` = `배당수익률`)
names(data_market)
[1] "종목코드" "종목명" "종가" "대비"
[5] "등락률" "시장구분" "업종명" "시가총액"
[9] "EPS" "PER" "BPS" "PBR"
[13] "주당배당금" "배당수익률(%)" "IDX_CD" "IDX_NM_KOR"
[17] "ALL_MKT_VAL" "MKT_VAL" "WGT" "S_WGT"
[21] "CAL_WGT" "SEC_CD" "SEC_NM_KOR" "SEQ"
[25] "TOP60" "APT_SHR_CNT"
rename()
함수는 열 이름을 바꾸는 함수로서, rename(tbl, new_name = old_name)
형태로 입력합니다. 위의 경우 배당수익률 열 이름이 배당수익률(%)로 변경되었습니다.
5.4 distinct()
%>%
data_market distinct(SEC_NM_KOR) %>% pull()
[1] "IT" "커뮤니케이션서비스" "건강관리"
[4] "소재" "경기관련소비재" "금융"
[7] NA "에너지" "산업재"
[10] "유틸리티" "필수소비재"
distinct()
함수는 고유한 값을 반환하며, 기본 함수 중 unique()
와 동일한 기능을 합니다. 데이터의 섹터 정보를 확인해보면, WICS 기준 10개 섹터 및 섹터 정보가 없는 종목인 NA 값이 있습니다. 또한 pull()
함수는 열을 벡터로 치환해줍니다.
5.5 select()
%>%
data_market select(종목명) %>% head()
# A tibble: 6 × 1
종목명
<chr>
1 삼성전자
2 SK하이닉스
3 NAVER
4 삼성바이오로직스
5 카카오
6 LG화학
%>%
data_market select(종목명, PBR, SEC_NM_KOR) %>% head()
# A tibble: 6 × 3
종목명 PBR SEC_NM_KOR
<chr> <dbl> <chr>
1 삼성전자 1.79 IT
2 SK하이닉스 1.48 IT
3 NAVER 8.97 커뮤니케이션서비스
4 삼성바이오로직스 12.6 건강관리
5 카카오 8.71 커뮤니케이션서비스
6 LG화학 3.4 소재
select()
함수는 원하는 열을 선택해주는 함수이며, 원하는 열 이름을 입력하면 됩니다. 하나의 열뿐만 아니라 다수의 열을 입력하면 해당 열들이 선택됩니다.
%>%
data_market select(starts_with('시')) %>% head()
# A tibble: 6 × 2
시장구분 시가총액
<chr> <dbl>
1 KOSPI 4.20e14
2 KOSPI 7.68e13
3 KOSPI 6.61e13
4 KOSPI 5.78e13
5 KOSPI 5.54e13
6 KOSPI 5.53e13
%>%
data_market select(ends_with('R')) %>% head()
# A tibble: 6 × 4
PER PBR IDX_NM_KOR SEC_NM_KOR
<dbl> <dbl> <chr> <chr>
1 18.3 1.79 WICS IT IT
2 15.2 1.48 WICS IT IT
3 58.5 8.97 WICS 커뮤니케이션서비스 커뮤니케이션서비스
4 240. 12.6 WICS 건강관리 건강관리
5 337. 8.71 WICS 커뮤니케이션서비스 커뮤니케이션서비스
6 118. 3.4 WICS 소재 소재
%>%
data_market select(contains('가')) %>% head()
# A tibble: 6 × 2
종가 시가총액
<dbl> <dbl>
1 70400 4.20e14
2 105500 7.68e13
3 402500 6.61e13
4 874000 5.78e13
5 124500 5.54e13
6 784000 5.53e13
해당 함수는 다양한 응용 기능도 제공합니다.starts_with()
는 특정 문자로 시작하는 열들을 선택하고, ends_with()
는 특정 문자로 끝나는 열들을 선택하며, contains()
는 특정 문자가 포함되는 열들을 선택합니다.
5.6 mutate()
= data_market %>%
data_market mutate(PBR = as.numeric(PBR),
PER = as.numeric(PER),
ROE = PBR / PER,
ROE = round(ROE, 4),
size = ifelse(시가총액 >=
median(시가총액, na.rm = TRUE),
'big', 'small')
)
%>%
data_market select(종목명, ROE, size) %>% head()
# A tibble: 6 × 3
종목명 ROE size
<chr> <dbl> <chr>
1 삼성전자 0.0977 big
2 SK하이닉스 0.0975 big
3 NAVER 0.153 big
4 삼성바이오로직스 0.0524 big
5 카카오 0.0258 big
6 LG화학 0.0289 big
mutate()
함수는 원하는 형태로 열을 생성하거나 변형하는 함수입니다. 위 예제에 서는 먼저 PBR과 PER 열을 as.numeric()
함수를 통해 숫자형으로 변경한 후 PBR을 PER로 나눈 값을 ROE 열에 생성합니다. 그 후 round()
함수를 통해 ROE 값을 반올림하며, ifelse()
함수를 통해 시가총액의 중앙값보다 큰 기업은 big, 아닐 경우 small임을 size 열에 저장합니다.
이 외에도 mutate()
함수 내에 across()
함수를 사용해 각 상황에 맞게 데이터를 변형할 수 있습니다.
%>%
data_market mutate(across(where(is.numeric), ~.x * 100))
# A tibble: 2,237 × 28
종목코드 종목명 종가 대비 등락률 시장…¹ 업종명 시가…² EPS PER
<chr> <chr> <dbl> <dbl> <dbl> <chr> <chr> <dbl> <dbl> <dbl>
1 005930 삼성전자 7.04e6 -1.1e5 -154 KOSPI 전기전… 4.20e16 384100 1833
2 000660 SK하이닉스 1.06e7 -2 e5 -186 KOSPI 전기전… 7.68e15 695200 1518
3 035420 NAVER 4.03e7 -7.5e5 -183 KOSPI 서비스… 6.61e15 687700 5853
4 207940 삼성바이오… 8.74e7 0 0 KOSPI 의약품 5.78e15 364200 23998
5 035720 카카오 1.25e7 -3.5e5 -273 KOSPI 서비스… 5.54e15 36900 33740
6 051910 LG화학 7.84e7 -4.7e6 -566 KOSPI 화학 5.53e15 666600 11761
7 006400 삼성SDI 7.32e7 1 e5 14 KOSPI 전기전… 5.03e15 859300 8519
8 005380 현대차 2.08e7 -1.5e5 -71 KOSPI 운수장… 4.45e15 545400 3823
9 000270 기아 8.5 e6 -1 e5 -116 KOSPI 운수장… 3.45e15 371000 2291
10 068270 셀트리온 2.1 e7 3.5e5 169 KOSPI 의약품 2.90e15 375600 5591
# … with 2,227 more rows, 18 more variables: BPS <dbl>, PBR <dbl>,
# 주당배당금 <dbl>, `배당수익률(%)` <dbl>, IDX_CD <chr>, IDX_NM_KOR <chr>,
# ALL_MKT_VAL <dbl>, MKT_VAL <dbl>, WGT <dbl>, S_WGT <dbl>, CAL_WGT <dbl>,
# SEC_CD <chr>, SEC_NM_KOR <chr>, SEQ <dbl>, TOP60 <dbl>, APT_SHR_CNT <dbl>,
# ROE <dbl>, size <chr>, and abbreviated variable names ¹시장구분, ²시가총액
across(where(is.numeric)
를 통해 숫자에 해당하는 열만 선택하고, 해당 값들에 100을 곱합니다.
5.7 filter()
%>%
data_market select(종목명, PBR) %>%
filter(PBR < 1)
# A tibble: 612 × 2
종목명 PBR
<chr> <dbl>
1 현대차 0.83
2 POSCO 0.59
3 현대모비스 0.72
4 KB금융 0.54
5 삼성물산 0.73
6 신한지주 0.46
7 LG 0.8
8 한국전력 0.21
9 삼성생명 0.32
10 하나금융지주 0.43
# … with 602 more rows
%>%
data_market select(종목명, PBR, PER, ROE) %>%
filter(PBR < 1 & PER < 20 & ROE > 0.1 ) %>% head()
# A tibble: 6 × 4
종목명 PBR PER ROE
<chr> <dbl> <dbl> <dbl>
1 미래에셋증권 0.75 7.21 0.104
2 한국금융지주 0.96 5.91 0.162
3 DB손해보험 0.64 6.36 0.101
4 메리츠증권 0.76 5.43 0.14
5 KCC 0.58 4.39 0.132
6 키움증권 0.94 3.48 0.270
filter()
함수는 조건을 충족하는 부분의 데이터를 반환하는 함수입니다. 첫 번째 예제와 같이 PBR이 1 미만인 단일 조건을 입력할 수도 있으며, 두 번째 예제와 같이 PBR 1 미만, PER 20 미만, ROE 0.1 초과 등 복수 조건을 입력할 수도 있습니다.
5.8 summarize()
%>%
data_market summarize(PBR_max = max(PBR, na.rm = TRUE),
PBR_min = min(PBR, na.rm = TRUE))
# A tibble: 1 × 2
PBR_max PBR_min
<dbl> <dbl>
1 680 0.15
summarize()
혹은 summarise()
함수는 원하는 요약 통곗값을 계산합니다. PBR_max는 PBR 열에서 최댓값을, PBR_min은 최솟값을 계산해줍니다.
5.9 arrange()
%>%
data_market select(PBR) %>%
arrange(PBR) %>%
head(5)
# A tibble: 5 × 1
PBR
<dbl>
1 0.15
2 0.19
3 0.2
4 0.21
5 0.24
%>%
data_market select(ROE) %>%
arrange(desc(ROE)) %>%
head(5)
# A tibble: 5 × 1
ROE
<dbl>
1 2.56
2 2.48
3 2.17
4 1.02
5 0.823
arrange()
함수는 선택한 열을 기준으로 데이터를 정렬해주며, 오름차순으로 정렬합니다. 내림차순으로 데이터를 정렬하려면 arrange()
내에 desc()
함수를 추가로 입력해주면 됩니다.
5.10 row_number()
%>%
data_market mutate(PBR_rank = row_number(PBR)) %>%
select(종목명, PBR, PBR_rank) %>%
arrange(PBR) %>%
head(5)
# A tibble: 5 × 3
종목명 PBR PBR_rank
<chr> <dbl> <int>
1 지스마트글로벌 0.15 1
2 세원정공 0.19 2
3 경동인베스트 0.2 3
4 한국전력 0.21 4
5 한화생명 0.24 5
%>%
data_market mutate(ROE_rank = row_number(desc(ROE))) %>%
select(종목명, ROE, ROE_rank) %>%
arrange(desc(ROE)) %>%
head(5)
# A tibble: 5 × 3
종목명 ROE ROE_rank
<chr> <dbl> <int>
1 샘씨엔에스 2.56 1
2 이지바이오 2.48 2
3 솔브레인홀딩스 2.17 3
4 한컴라이프케어 1.02 4
5 에스디바이오센서 0.823 5
5.11 ntile()
%>%
data_market mutate(PBR_tile = ntile(PBR, n = 5)) %>%
select(PBR, PBR_tile) %>%
head()
# A tibble: 6 × 2
PBR PBR_tile
<dbl> <int>
1 1.79 3
2 1.48 3
3 8.97 5
4 12.6 5
5 8.71 5
6 3.4 4
ntile()
함수는 분위수를 계산해주며, n 인자를 통해 몇 분위로 나눌지 선택할 수 있습니다. 해당 함수 역시 오름차순으로 분위수를 나눕니다.
5.12 group_by()
%>%
data_market group_by(`SEC_NM_KOR`) %>%
summarize(n())
# A tibble: 11 × 2
SEC_NM_KOR `n()`
<chr> <int>
1 IT 579
2 건강관리 288
3 경기관련소비재 342
4 금융 79
5 산업재 349
6 소재 228
7 에너지 28
8 유틸리티 19
9 커뮤니케이션서비스 116
10 필수소비재 103
11 <NA> 106
group_by()
함수는 선택한 열 중 동일한 데이터를 기준으로 데이터를 묶어줍니다. 위 예제에서는 섹터를 나타내는 SEC_NM_KOR 기준으로 데이터를 묶었으며, n()
함수를 통해 해당 그룹 내 데이터의 개수를 구할 수 있습니다.
%>%
data_market group_by(`SEC_NM_KOR`) %>%
summarize(PBR_median = median(PBR, na.rm = TRUE)) %>%
arrange(PBR_median)
# A tibble: 11 × 2
SEC_NM_KOR PBR_median
<chr> <dbl>
1 유틸리티 0.65
2 금융 0.745
3 필수소비재 1.05
4 산업재 1.21
5 소재 1.24
6 경기관련소비재 1.33
7 에너지 1.46
8 IT 2.07
9 <NA> 2.15
10 커뮤니케이션서비스 2.9
11 건강관리 3.12
위 예제는 섹터를 기준으로 데이터를 묶은 후 summarize()
를 통해 각 섹터에 속하는 종목의 PBR 중앙값을 구한 후 정렬했습니다.
%>%
data_market group_by(`시장구분`, `SEC_NM_KOR`) %>%
summarize(PBR_median = median(PBR, na.rm = TRUE)) %>%
arrange(PBR_median)
`summarise()` has grouped output by '시장구분'. You can override using the
`.groups` argument.
# A tibble: 22 × 3
# Groups: 시장구분 [2]
시장구분 SEC_NM_KOR PBR_median
<chr> <chr> <dbl>
1 KOSPI 금융 0.62
2 KOSPI 유틸리티 0.65
3 KOSPI 필수소비재 0.825
4 KOSPI 에너지 0.87
5 KOSPI 경기관련소비재 1.02
6 KOSPI 산업재 1.02
7 KOSPI 소재 1.07
8 KOSDAQ 유틸리티 1.34
9 KOSDAQ 소재 1.42
10 KOSDAQ 산업재 1.45
# … with 12 more rows
위 예제는 시장과 섹터를 기준으로 데이터를 그룹화한 후 각 그룹별 PBR 중앙값을 구했습니다. 이처럼 그룹은 하나만이 아닌 원하는 만큼 나눌 수 있습니다.
5.13 geom_point()
이번에는 종목정보를 시각화하도록 하겠습니다.
aes()
인자 내부에 x축은 ROE 열을 사용하고, y축은 PBR 열을 사용하도록 정의합니다.geom_point()
함수를 통해 산점도 그래프를 그려줍니다. 원하는 그림이 그려지기는 했으나, ROE와 PBR에 극단치 데이터가 있어 둘 사이에 관계가 잘 보이지 않습니다.
%>% ggplot(aes(x = ROE, y = PBR)) +
data_market geom_point() +
coord_cartesian(xlim = c(0, 0.30), ylim = c(0, 3))
Warning: Removed 845 rows containing missing values (`geom_point()`).
이번에는 극단치 효과를 제거하기 위해 coord_cartesian()
함수 내에 xlim과 ylim, 즉 x축과 y축의 범위를 직접 지정해줍니다. 극단치가 제거되어 데이터를 한눈에 확인할 수 있습니다.
ggplot(data_market, aes(x = ROE, y = PBR,
color = 시장구분,
shape = 시장구분)) +
geom_point() +
geom_smooth(method = 'lm') +
coord_cartesian(xlim = c(0, 0.30), ylim = c(0, 3))
`geom_smooth()` using formula = 'y ~ x'
ggplot()
함수 내부 aes 인자에 color와 shape를 지정해주면, 해당 그룹별로 모양과 색이 나타납니다. 코스피와 코스닥 종목들에 해당하는 데이터의 색과 점 모양을 다르게 표시할 수 있습니다.geom_smooth()
함수를 통해 평활선을 추가할 수도 있으며, 방법으로 lm(linear model)을 지정할 경우 선형회귀선을 그려주게 됩니다. 이 외에도 glm, gam, loess 등의 다양한 회귀선을 그려줄 수 있습니다.
5.14 geom_histogram()
%>% ggplot(aes(x = PBR)) +
data_market geom_histogram(binwidth = 0.1) +
coord_cartesian(xlim = c(0, 10))
Warning: Removed 24 rows containing non-finite values (`stat_bin()`).
geom_histogram()
함수는 히스토그램을 나타내주며, binwidth 인자를 통해 막대의 너비를 선택해줄 수 있습니다. 국내 종목들의 PBR 데이터는 왼쪽에 쏠려 있고 오른쪽으로 꼬리가 긴 분포를 가지고 있습니다.
%>% ggplot(aes(x = PBR)) +
data_market geom_histogram(aes(y = ..density..),
binwidth = 0.1,
color = 'sky blue', fill = 'sky blue') +
coord_cartesian(xlim = c(0, 10)) +
geom_density(color = 'red') +
geom_vline(aes(xintercept = median(PBR, na.rm = TRUE)),
color = 'blue') +
geom_text(aes(label = median(PBR, na.rm = TRUE),
x = median(PBR, na.rm = TRUE), y = 0.05),
col = 'black', size = 6, hjust = -0.5)
Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
ℹ Please use `after_stat(density)` instead.
Warning: Removed 24 rows containing non-finite values (`stat_bin()`).
Warning: Removed 24 rows containing non-finite values (`stat_density()`).
PBR 히스토그램을 좀 더 자세하게 나타내보겠습니다.
geom_histogram()
함수 내에aes(y = ..density..)
를 추가해 밀도함수로 바꿉니다.geom_density()
함수를 추가해 밀도곡선을 그려줍니다.geom_vline()
함수는 세로선을 그려주며, xintercept 즉 x축으로 PBR의 중앙값을 선택합니다.geom_text()
함수는 그림 내에 글자를 표현해주며, label 인자에 원하는 글자를 입력해준 후 글자가 표현될 x축, y축, 색상, 사이즈 등을 선택할 수 있습니다.
5.15 geom_boxplot()
%>% ggplot(aes(x = SEC_NM_KOR, y = PBR)) +
data_market geom_boxplot() +
coord_flip()
Warning: Removed 24 rows containing non-finite values (`stat_boxplot()`).
박스 플롯 역시 데이터의 분포와 이상치를 확인하기 좋은 그림이며, geom_boxplot()
함수를 통해 나타낼 수 있습니다.
- x축 데이터로는 섹터 정보, y축 데이터로는 PBR을 선택합니다.
geom_boxplot()
을 통해 박스 플롯을 그려줍니다.coord_flip()
함수는 x축과 y축을 뒤집어 표현해주며 x축에 PBR, y축에 섹터 정보가 나타나게 됩니다.
결과를 살펴보면 유틸리티나 금융 섹터는 PBR이 잘 모여 있는 반면, IT나 건강관리 섹터 등은 매우 극단적인 PBR을 가지고 있는 종목이 있습니다.
5.16 dplyr과 ggplot을 연결하여 사용하기
%>%
data_market filter(!is.na(SEC_NM_KOR)) %>%
group_by(SEC_NM_KOR) %>%
summarize(ROE_sector = median(ROE, na.rm = TRUE),
PBR_sector = median(PBR, na.rm = TRUE)) %>%
ggplot(aes(x = ROE_sector, y = PBR_sector,
color = SEC_NM_KOR, label = SEC_NM_KOR)) +
geom_point() +
geom_text(color = 'black', size = 3, vjust = 1.3) +
theme(legend.position = 'bottom',
legend.title = element_blank())
앞에서 배운 데이터 분석과 시각화를 동시에 연결해 사용할 수도 있습니다.
- 데이터 분석의 단계로
filter()
를 통해 섹터가 NA가 아닌 종목을 선택합니다. group_by()
를 통해 섹터별 그룹을 묶습니다.summarize()
를 통해 ROE와 PBR의 중앙값을 계산해줍니다.- x축과 y축을 설정한 후 색상과 라벨을 섹터로 지정해주면 각 섹터별로 색상이 다른 산점도가 그려집니다.
geom_text()
함수를 통해 앞에서 라벨로 지정한 섹터 정보들을 출력해줍니다.theme()
함수를 통해 다양한 테마를 지정합니다. legend.position 인자로 범례를 하단에 배치했으며, legend.title 인자로 범례의 제목을 삭제했습니다.
5.17 geom_bar()
%>%
data_market group_by(SEC_NM_KOR) %>%
summarize(n = n()) %>%
ggplot(aes(x = SEC_NM_KOR, y = n)) +
geom_bar(stat = 'identity') +
theme_classic()
geom_bar()
는 막대 그래프를 그려주는 함수입니다.
group_by()
를 통해 섹터별 그룹을 묶어줍니다.summarize()
함수 내부에n()
을 통해 각 그룹별 데이터 개수를 구합니다.ggplot()
함수에서 x축에는 SEC_NM_KOR, y축에는 n을 지정해줍니다.geom_bar()
를 통해 막대 그래프를 그려줍니다. y축에 해당하는 n 데이터를 그대로 사용하기 위해서는 stat 인자를 identity로 지정해주어야 합니다.theme_*()
함수를 통해 배경 테마를 바꿀 수도 있습니다.
한편 위 그래프는 데이터 개수에 따라 순서대로 막대가 정렬되지 않아 보기에 좋은 형태는 아닙니다. 이를 반영해 더욱 보기 좋은 그래프로 나타내보겠습니다.
%>%
data_market filter(!is.na(SEC_NM_KOR)) %>%
group_by(SEC_NM_KOR) %>%
summarize(n = n()) %>%
ggplot(aes(x = reorder(SEC_NM_KOR, n), y = n, label = n)) +
geom_bar(stat = 'identity') +
geom_text(color = 'black', size = 4, hjust = -0.3) +
xlab(NULL) +
ylab(NULL) +
coord_flip() +
scale_y_continuous(expand = c(0, 0, 0.1, 0)) +
theme_classic()
filter()
함수를 통해 NA 종목은 삭제해준 후 섹터별 종목 개수를 구해줍니다.ggplot()
의 x축에reorder()
함수를 적용해 SEC_NM_KOR 변수를 n 순서대로 정렬해줍니다.geom_bar()
를 통해 막대 그래프를 그려준 후geom_text()
를 통해 라벨에 해당하는 종목 개수를 출력합니다.xlab()
과ylab()
에 NULL을 입력해 라벨을 삭제합니다.coord_flip()
함수를 통해 x축과 y축을 뒤집어줍니다.scale_y_continuous()
함수를 통해 그림의 간격을 약간 넓혀줍니다.theme_classic()
으로 테마를 변경해줍니다.
결과를 보면 종목수가 많은 섹터부터 순서대로 정렬되어 보기도 쉬우며, 종목수도 텍스트로 표현되어 한눈에 확인할 수 있습니다.
이처럼 데이터 시각화를 통해 정보의 분포나 특성을 한눈에 확인할 수 있으며, ggplot()
을 이용하면 복잡한 형태의 그림도 매우 간단하고 아름답게 표현할 수 있습니다.