24  파일 데이터

24.1 유니코드와 UTF-8

사람 간의 의사소통은 다양한 기호 체계를 통해 이루어진다. 영어 알파벳, 한글, 한자 등의 문자가 의사소통에 사용되는 좋은 예이다. 디지털 환경에서 이러한 의사소통을 가능하게 하는 기술적 장치가 바로 문자 집합과 문자 인코딩 및 디코딩이다.

컴퓨터 시스템은 이진수 바이트를 기본 단위로 사용한다. 바이트는 파일 형태로 묶이거나 네트워크를 통해 전송되어 다른 시스템에 도달한다. 이 데이터가 사람에게 의미 있는 정보로 전달되기 위해서는 인코딩(부호화)과 디코딩(복호화) 과정을 거쳐야 한다.

컴퓨터 시스템은 데이터를 바이트(Byte) 형태로 처리한다. 이 바이트 데이터는 이진수, 즉 010101과 같은 형태로 표현되고, 바이트 데이터를 사람이 읽을 수 있는 문자로 변환하는 최초의 표준이 ASCII(아스키)다. 하지만 ASCII는 256개 문자만을 지원하기 때문에, CJK(중국, 일본, 한국)와 같은 동아시아 문화권에서는 그 한계가 명확하다. 이러한 한계를 해결하기 위해 유니코드(Unicode)가 도입되었다. 유니코드는 영문자는 물론이고 지구상의 거의 모든 문자와 기호를 디지털로 표현할 수 있는 방법을 제공한다.

유니코드(Unicode)는 글자와 코드가 1:1로 매핑되어 있는 단순한 코드표에 불과하며 산업 표준으로서 일종의 국가 간 약속이다. 한글이 표현된 유니코드 영역도 위키백과 유니코드 영역에서 찾을 수 있다.

유니코드와 UTF-8

유니코드와 UTF-8
인코딩(Encoding)

문자 인코딩(character encoding), 줄여서 인코딩은 사용자가 입력한 문자나 기호들을 컴퓨터가 이용할 수 있는 신호로 변환하는 것을 말한다. 넓은 의미의 컴퓨터는 이러한 신호를 입력받고 처리하는 기계를 뜻하며, 신호 처리 시스템을 통해 이렇게 처리된 정보를 사용자가 이해할 수 있게 된다.

All text has a character encoding.

24.1.1 인코딩 문제

문자 인코딩은 컴퓨터가 텍스트를 바이트로 변환하거나 바이트를 텍스트로 변환하는 방법이다. 인코딩 과정에서는 다양한 문제가 발생할 수 있고, 그중 세 가지 문제가 많이 알려져 있다. 첫 번째는 ’두부(Tofu)’라 불리는 상황으로, 컴퓨터가 어떤 문자를 표현해야 할지 알지만, 화면에 어떻게 출력해야 할지 모르기 때문에 빈 사각형 상자로 표시된다. 두 번째는 ’문자 깨짐(Mojibake, 文字化け)’이다. 특히 일본어에서 자주 발생하며, 한 인코딩 방식으로 작성된 텍스트가 다른 인코딩 방식으로 해석될 때 문자가 깨지는 현상을 의미한다. 세 번째는 ’물음표(Question Marks)’로, 특정 문자가 다른 문자로 변환될 때 발생한다. 문자 집합과 인코딩이 맞지 않을 때 발생하며, 데이터 손실과 오류도 야기된다.

세 가지 인코딩 문제

세 가지 인코딩 문제

24.1.2 문자 집합

아스키 코드

디지털 글쓰기는 내용과 상관없이 결국 텍스트로 표현되고, 텍스트는 단지 문자이다. 하지만, 컴퓨터가 문자 하나를 어떻게 표현할까?

1960년대 미국식 영문자를 컴퓨터로 표현하는 해결책은 간단했다. 알파벳 26개(대문자, 소문자), 숫자 10개, 구두점 몇 개, 그리고 전신을 보내던 시절에 제어를 위해 사용된 몇 개의 특수 문자(“새줄로 이동”, “본문 시작”, “경고음” 등)가 전부였다. 모두 합쳐도 128개보다 적어서, 아스키(ASCII) 위원회가 문자마다 7비트( \(2^7\) = 128)를 사용하는 인코딩으로 표준화했다. 1

그림 24.1: 제어 문자와 출력 가능한 아스키 문자표 알파벳 예시

그림 24.1는 아스키 문자표에서 제어 문자 10개와 출력 가능한 아스키 문자표 중 영문 대문자 A-I까지 10개를 뽑아 사례로 보여준다. 즉, 문자표는 어떤 문자가 어떤 숫자에 해당하는지를 정의하고 있다.

확장 아스키

아스키(ASCII) 방식으로 숫자 2, 문자 q, 혹은 곡절 악센트 ^를 표현하는 데 충분하다. 하지만, 투르크어족 추바시어 ĕ, 그리스 문자 β, 러시아 키릴 문자 Я는 어떻게 저장하고 표현해야 할까? 7비트를 사용하면 0에서 127까지 숫자를 부여할 수 있지만, 8비트(즉, 1바이트)를 사용하게 되면 255까지 표현할 수 있다. 그렇다면, ASCII 표준을 확장해서 추가되는 128개 숫자에 대해 추가로 문자를 표현할 수 있게 된다.

  • 아스키: 0…127
  • 확장된 아스키: 128…255

불행하게도, 영어 문자를 사용하지 않는 세계 곳곳에서 많은 사람들이 시도했지만, 방식도 다르고, 호환되지 않는 방식으로 작업이 되어, 결과는 엉망진창이 되었다. 예를 들어, 실제 텍스트가 불가리아어로 인코딩되었는데 스페인어 규칙을 사용해서 인코딩한 것으로 프로그램이 간주하고 처리될 경우 결과는 무의미한 횡설수설 값이 출력된다. 이와는 별도로 한중일(CJK) 동아시아 국가들을 비롯한 많은 국가에서는 256개 이상의 기호를 사용한다. 왜냐하면 8비트로는 특히 동아시아 국가 문자를 표현하는 데 부족하기 때문이다.

한글 완성형과 조합형

1980년대부터 컴퓨터를 사용하신 분이라면 완성형과 조합형의 표준화 전쟁을 지켜봤을 것이고, 그 이면에는 한글 워드프로세서에 대한 주도권 쟁탈전이 있었던 것을 기억할 것이다. 결국 완성형과 조합형을 모두 포용하는 것으로 마무리되었지만, 여기서 끝난 게 아니다. 유닉스 계열에서 KSC5601을 표준으로 받아들인 EUC-KR과 90년대와 2000년대를 호령한 마이크로소프트 CP949가 있었다. 결국 대한민국 정부에서 주도한 표준화 전쟁은 유닉스/리눅스, 마이크로소프트 모두를 녹여내는 것으로 마무리되었고, 웹과 모바일 시대는 유니코드로 넘어가서 KSC5601이 유니코드의 원소로 들어가는 것으로 마무리되었다.

이제 신경 쓸 것은 인코딩, 즉 utf-8만 신경 쓰면 된다. 그리고 남은 디지털 레거시 유산을 잘 처리하면 된다.

유닉스/리눅스(EUC-KR), 윈도우(CP949)

EUC-KR, CP949 모두 2바이트로 한글을 표현하는 방식으로 동일점이 있지만, EUC-KR 방식은 KSC5601-87 완성형을 초기에 사용하였으나, KSC5601-92 조합형도 사용할 수 있도록 확장되었다. CP949는 확장 완성형으로도 불리며 EUC-KR에서 표현할 수 없는 한글 글자 8,822자를 추가한 것으로 마이크로소프트 코드페이지(Code Page) 949를 사용하면서 일반화되었다.

유니코드

1990년대에 나타나기 시작한 해결책을 유니코드(Unicode)라고 부른다. 예를 들어, 영어 A 대문자는 1바이트, 한글 가는 3바이트다. 유니코드는 정수값을 서로 다른 수만 개의 문자와 기호를 표현하는 데 정의한다. ’A’는 U+0041, ’가’는 U+AC00과 같이 고유한 코드 포인트를 가진다. 하지만, 파일이나 메모리에 문자열로 정수값을 저장하는 방식을 정의하지는 않는다.

각 문자마다 8비트를 사용하던 방식에서 32비트 정수를 사용하는 방식으로 전환하면 되지만, 영어, 에스토니아어, 브라질 포르투갈어 같은 알파벳 언어권에는 상당한 공간 낭비가 발생한다. 접근 속도가 중요한 경우 메모리에 문자당 32비트를 종종 사용한다. 하지만, 파일에 데이터를 저장하거나 인터넷을 통해 전송하는 경우 대부분의 프로그램과 프로그래머는 이와는 다른 방식을 사용한다.

다른 방식은 (거의) 항상 UTF-8으로 불리는 인코딩으로, 문자마다 가변 바이트를 사용한다. 하위 호환성을 위해, 첫 128개 문자(즉, 구 아스키 문자 집합)는 1바이트에 저장된다. 다음 1920개 문자는 2바이트를 사용해서 저장된다. 다음 61,000개는 3바이트를 사용해서 저장해 나간다.

궁금하다면, 동작 방식이 다음 표에 나타나 있다. “전통적” 문자열은 문자마다 1바이트를 사용한다. 반대로, “유니코드” 문자열은 문자마다 충분한 메모리를 사용해서 어떤 텍스트 유형이든 저장한다. R, 파이썬 3.x에서 모든 문자열은 유니코드다. 엄청난 바이트를 읽어오거나 저장하여 내보내려고 할 때, 인코딩을 지정하는 것은 엄청난 고통이다.

유니코드 문자열은 여는 인용부호 앞에 소문자 U를 붙여 표시한다. 유니코드 문자열을 바이트 문자열로 전환하려면, 인코딩을 명시해야만 한다. 항상 UTF-8을 사용해야 하고, 그 밖의 인코딩을 사용하는 경우 매우, 매우 특별히 좋은 이유가 있어야만 한다. 특별한 인코딩을 사용하는 경우 두 번 생각해 보라.

아스키에서 유니코드로 진화 과정

아스키에서 유니코드로 진화 과정

컴퓨터가 처음 등장할 때 미국 영어권 중심 아스키가 아니고 4바이트로 전 세계 모든 글자를 표현할 수 있는 유니코드가 사용되었다면 한글을 컴퓨터에 표현하기 위한 지금과 같은 번거로움은 없었을 것이다. 돌이켜보면 초기 컴퓨터가 저장 용량 한계로 인해 유니코드가 표준으로 자리를 잡더라도 실용적인 이유로 인해서 한글을 컴퓨터에 표현하기 위한 다른 대안이 제시됐을 것도 분명해 보인다. 초창기 영어권을 중심으로 아스키 표준이 정립되어 현재까지 내려오고, 유니코드와 UTF-8 인코딩이 사실상 표준으로 자리 잡았으며, 그 사이 유닉스/리눅스 EUC-KR, 윈도우즈 CP949가 빈틈을 한동안 메우면서 역할을 담당했다.

항목 ASCII (1963) EUC-KR (1980s) CP949 (1990s) Unicode (1991)
범위 128개의 문자 2,350개의 한글 문자 등 약 11,172개의 완성형 한글 문자 등 143,859개의 문자 (버전 13.0 기준)
비트 수 7비트 8~16비트 8~16비트 다양한 인코딩 방식 (UTF-8, UTF-16, UTF-32 등)
표준 ANSI, ISO/IEC 646 KS X 2901 마이크로소프트 ISO/IEC 10646
플랫폼 다양한 시스템 UNIX 계열, 일부 Windows Windows 계열 다양한 플랫폼
문자 집합 영문 알파벳, 숫자, 특수 문자 한글, 영문 알파벳, 숫자, 특수 문자 한글, 한자, 영문 알파벳, 숫자, 특수 문자 전 세계 언어, 특수 문자, 이모티콘 등
확장성 확장 불가능 한정적 더 많은 문자 지원 높은 확장성
국제성 영어 중심 한국어 중심 한국어 중심 다국어 지원
유니코드 호환 호환 가능 (U+0000 ~ U+007F) 호환 불가, 변환 필요 유니코드와 상호 변환 가능 자체가 표준

UTF-8

UTF-8(Universal Coded Character Set + Transformation Format – 8-bit의 약자)은 유니코드 중에서 가장 널리 쓰이는 인코딩으로, 유니코드를 위한 가변 길이 문자 인코딩 방식 중 하나로 켄 톰프슨과 롭 파이크가 제작했다.

UTF-8 인코딩의 가장 큰 장점은 아스키(ASCII), 라틴-1(ISO-8859-1)과 호환되어, 문서를 처리하는 경우 아스키, 라틴-1 문서를 변환 없이 그대로 처리할 수 있고 영어를 비롯한 라틴계열 문서로 저장할 때 용량이 매우 작다. 이러한 이유로 많은 오픈소스 소프트웨어와 데이터를 생산하는 미국을 비롯한 유럽 언어권에서 UTF-8이 많이 사용되고 있지만, 한글은 한 글자당 3바이트 용량을 차지한다.

웹 표준 인코딩

스마트폰의 대중화에 따라 더 이상 윈도우 운영체제에서 사용되는 문자체계가 표준이 되지 못하고 여러 문제점을 야기함에 따라 유니코드 + UTF-8 체제가 대세로 자리잡고 있는 것이 확연히 나타나고 있다.

2010년 구글에서 발표한 자료에 의하면 2010년 UTF-8 인코딩이 웹에서 주류로 부상하기 시작한 것이 확인되었다. (unicode2010?) 웹 기반 플롯 디지털 도구를 활용하여 그래프(WebPlotDigitizer)에서 데이터를 추출하여 시각화하면 유사한 결과를 시각적으로 표현할 수 있다. 2010년 이후 웹에서 가장 점유율이 높은 인코딩 방식은 UTF-8으로 W3Tech 웹 기술 조사(Web Technology Surveys)를 통해 확인할 수 있다. 여기서 주목할 점은, 프랑스어, 독일어, 스페인어와 같은 서유럽 언어의 문자와 기호를 표현하는 ISO-8859-1 인코딩, 종종 “Latin-1”으로 불리는 8비트 문자 인코딩이 현저히 줄고 있다는 점이다.

2010 ~ 2012 웹에서 UTF-8 성장세

2010 ~ 2012 웹에서 UTF-8 성장세

24.2 아스키 파일

아스키 파일은 텍스트 파일로, 데이터를 저장하는 가장 기본적인 형태이다. R에서 데이터프레임으로 다양한 데이터를 가져올 때, 아스키 파일은 CSV(Comma-Separated Values) 파일, TSV(Tab-Separated Values) 파일, 고정 길이 파일 등 다양한 형식으로 존재한다. CSV 파일은 쉼표로 구분된 값들로 이루어진 텍스트 파일이며, TSV 파일은 탭으로 구분된 값들로 이루어진 텍스트 파일이다. 고정 길이 파일은 각 필드가 고정된 길이를 가지는 텍스트 파일이다. 또한, R에서는 데이터를 직접 입력하여 데이터프레임을 생성할 수도 있다. 아스키 파일을 데이터프레임으로 가져올 때는 read.csv(), read.table(), read.fwf() 등의 함수를 사용하며, 데이터를 직접 입력할 때는 열 중심 혹은 행 중심에 따라 tibble(), tribble() 함수를 사용한다.

graph TB

subgraph 가져오기["<strong>가져오기</strong>"]

    스프레드쉬트 --> 핸들러
    데이터베이스 --> 핸들러
    아스키 --> 핸들러
    웹데이터 --> 핸들러
    핸들러 --> 데이터프레임

    subgraph 아스키["<strong>아스키 파일</strong>"]
        데이터입력[데이터 입력]
        csv[CSV 파일]
        tsv[TSV 파일]
        고정길이파일[고정 길이 파일]
    end

end

classDef modern fill:#f0f0f0,stroke:#333,stroke-width:2px,color:#333,font-family:MaruBuri,font-size:12px;
classDef emphasize fill:#d0d0d0,stroke:#333,stroke-width:3px,color:#333,font-family:MaruBuri,font-size:15px,font-weight:bold;
classDef subgraphStyle fill:#e0e0e0,stroke:#333,stroke-width:2px,color:#333,font-family:MaruBuri,font-size:20px;

class csv,데이터입력,tsv,고정길이파일,스프레드쉬트,데이터베이스,웹데이터,핸들러 modern
class 데이터프레임 emphasize
class 아스키,가져오기 subgraphStyle
그림 24.2: 다양한 데이터 종류

24.3 데이터 입력 방식

파일 크기가 작은 경우, 즉 눈으로 식별 가능한 크기의 아스키 파일을 .csv, .txt 등의 형식으로 저장한 후 readr 패키지의 read_csv(), read_table(), read_delim() 등의 함수로 불러오는 것이 오히려 적절하지 못한 경우가 있다.

tibble() 혹은 tribble() 함수를 사용해서 인라인 데이터를 생성하는 것이 더 효율적일 수 있다. 다음과 같이 쇼핑몰 초창기 고객 주문 데이터를 입력하여 R로 불러와서 분석하는 방법를 살펴보자.

주문일자,주문번호,고객번호,상품명,상품범주,주문금액
"2023-05-19 13:45:32",203451,A20193,"슬림핏 반팔 티셔츠",의류,21800
2023/05/19 14:23:11,203452,B10582,"여성용 스니커즈, 240mm",,68000
"2023.05.19 16:05:49",203453,"C30281","진공 보온병, 500ml",주방용품,"35,600"
2023-05-20 09:18:22, 203454,"D18734",""귀걸이"세트 (실버)",액세서리,112000
2023-05-20 11:36:58,,E42097,남성용 슬림 진 (32인치),의류,54900
2023/05/21 08:02:44,"203,456",F61052,"무선 게이밍 마우스",전자기기,""88,700""
2023.05.21 15:30:05,203457,,,,42300

데이터가 크지 않기 때문에 열 혹은 행 기준으로 데이터프레임으로 불러올 수 있다. 먼저 tibble() 함수를 사용해서 데이터프레임을 생성한다. 실무에서 결측값도 있고 주문금액에 천 단위 , 구분자도 포함되어 있고 날짜 형식도 다양하게 표현되어 있을 수 있다.

library(tibble)

orders <- tribble(
  ~주문일자, ~주문번호, ~고객번호, ~상품명, ~상품범주, ~주문금액,
  "2023-05-19 13:45:32", "203451", "A20193", "슬림핏 반팔 티셔츠", "의류", "21800",
  "2023/05/19 14:23:11", "203452", "B10582", "여성용 스니커즈, 240mm", NA, "68000",
  "2023.05.19 16:05:49", "203453", "C30281", "진공 보온병, 500ml", "주방용품", "35,600",
  "2023-05-20 09:18:22", "203454", "D18734", "귀걸이세트 (실버)", "액세서리", "112000",
  "2023-05-20 11:36:58", NA, "E42097", "남성용 슬림 진 (32인치)", "의류", "54900",
  "2023/05/21 08:02:44", "203,456", "F61052", "무선 게이밍 마우스", "전자기기", "88,700",
  "2023.05.21 15:30:05", "203457", NA, NA, NA, "42300"
)

orders
#> # A tibble: 7 × 6
#>   주문일자            주문번호 고객번호 상품명                 상품범주 주문금액
#>   <chr>               <chr>    <chr>    <chr>                  <chr>    <chr>   
#> 1 2023-05-19 13:45:32 203451   A20193   슬림핏 반팔 티셔츠     의류     21800   
#> 2 2023/05/19 14:23:11 203452   B10582   여성용 스니커즈, 240mm <NA>     68000   
#> 3 2023.05.19 16:05:49 203453   C30281   진공 보온병, 500ml     주방용품 35,600  
#> 4 2023-05-20 09:18:22 203454   D18734   귀걸이세트 (실버)      액세서리 112000  
#> 5 2023-05-20 11:36:58 <NA>     E42097   남성용 슬림 진 (32인…  의류     54900   
#> 6 2023/05/21 08:02:44 203,456  F61052   무선 게이밍 마우스     전자기기 88,700  
#> 7 2023.05.21 15:30:05 203457   <NA>     <NA>                   <NA>     42300

tibble() 함수는 벡터를 기준으로 열을 생성하고 이를 tibble() 함수로 결합하여 데이터프레임을 생성한다.

library(tibble)

orders <- tibble(
 주문일자 = c("2023-05-19 13:45:32", "2023/05/19 14:23:11", "2023.05.19 16:05:49", 
             "2023-05-20 09:18:22", "2023-05-20 11:36:58", "2023/05/21 08:02:44", 
             "2023.05.21 15:30:05"),
 주문번호 = c("203451", "203452", "203453", "203454", NA, "203,456", "203457"),
 고객번호 = c("A20193", "B10582", "C30281", "D18734", "E42097", "F61052", NA),
 상품명 = c("슬림핏 반팔 티셔츠", "여성용 스니커즈, 240mm", "진공 보온병, 500ml", 
           "귀걸이세트 (실버)", "남성용 슬림 진 (32인치)", "무선 게이밍 마우스", NA),
 상품범주 = c("의류", NA, "주방용품", "액세서리", "의류", "전자기기", NA),
 주문금액 = c("21800", "68000", "35,600", "112000", "54900", "88,700", "42300")
)

orders
#> # A tibble: 7 × 6
#>   주문일자            주문번호 고객번호 상품명                 상품범주 주문금액
#>   <chr>               <chr>    <chr>    <chr>                  <chr>    <chr>   
#> 1 2023-05-19 13:45:32 203451   A20193   슬림핏 반팔 티셔츠     의류     21800   
#> 2 2023/05/19 14:23:11 203452   B10582   여성용 스니커즈, 240mm <NA>     68000   
#> 3 2023.05.19 16:05:49 203453   C30281   진공 보온병, 500ml     주방용품 35,600  
#> 4 2023-05-20 09:18:22 203454   D18734   귀걸이세트 (실버)      액세서리 112000  
#> 5 2023-05-20 11:36:58 <NA>     E42097   남성용 슬림 진 (32인…  의류     54900   
#> 6 2023/05/21 08:02:44 203,456  F61052   무선 게이밍 마우스     전자기기 88,700  
#> 7 2023.05.21 15:30:05 203457   <NA>     <NA>                   <NA>     42300

tribble(), tibble() 함수 모두 데이터 입력을 통해 orders 데이터프레임을 생성하였으나 결측값에 대한 처리와 자료형이 모두 문자형(<chr>)으로 되어 있어 후속 작업을 위해 추가 데이터 정제 작업이 필수적이다.

24.4 자료형

가장 많이 사용되는 콤마 구분자 아스키 파일(.csv) 파일로 불러올 때 함께 고민해야 하는 사항이 바로 자료형이다.

R에서는 다양한 자료형을 지원한다. 가장 기본적인 자료형은 숫자형, 문자형, 범주형, 논리형이다. 숫자형은 정수형과 실수형으로 구분되며, 문자형은 문자열을 저장하는 자료형이다. 논리형은 참과 거짓을 나타내는 자료형이고, 범주형은 내부적으로 정수로 저장되지만 한정된 범주를 갖는 문자형으로 표현된다. 그 외에도 날짜와 시간을 저장하는 자료형, 지도 정보를 담고 있는 자료형, 이미지 정보를 담고 있는 자료형 등 다양한 자료형이 있다.

readr 패키지의 spec() 함수를 사용하면 아스키 파일을 불러읽어 오면서 각 열의 자료형을 확인할 수 있다. spec() 함수에서 출력한 각 열 자료형이 정답은 아니지만 나름 최선의 추정으로 각 열의 자료형을 살펴본 후 최종열별 자료형을 지정하는 데 도움이 되는 것은 사실이다.

spec( read_csv("data/file/nine_penguins.csv") )
#> cols(
#>   species = col_character(),
#>   bill_length_mm = col_double(),
#>   body_mass_g = col_double(),
#>   year = col_double()
#> )

readr 패키지의 col_types 인자를 사용하여 각 열의 자료형을 지정할 수 있다. col_types 인자에는 cols() 함수를 사용하여 각 열의 자료형을 지정한다. cols() 함수에는 col_factor(), col_character(), col_double(), col_integer(), col_logical() 함수를 사용하여 각 열의 자료형을 지정한다. col_factor() 함수는 범주형 자료형을 지정할 때 사용하며, col_character() 함수는 문자형 자료형을 지정할 때 사용한다. col_double() 함수는 실수형 자료형을 지정할 때 사용하며, col_integer() 함수는 정수형 자료형을 지정할 때 사용한다. col_logical() 함수는 논리형 자료형을 지정할 때 사용한다.

spec() 함수가 텍스트로 된 열은 모두 문자형(col_character())으로 인식하였지만, species, sex 열은 범주형 자료형으로 지정하는 것이 더 적절하다. bill_length_mm, flipper_length_mm, body_mass_g, year 열은 실수형, 정수형 자료형으로 지정하는 것이 적절하다고 판단되어 다음과 같이 .csv 파일을 불러오면서 각 열의 자료형도 함께 지정한다.

penguins_tbl <- read_csv("data/file/nine_penguins.csv",
         col_types = cols(
            species = col_factor(level = c("Adelie", "Chinstrap", "Gentoo")),
            island = col_character(),
            bill_length_mm = col_double(),
            flipper_length_mm = col_double(),
            body_mass_g = col_double(),
            sex = col_factor(levels = c("female", "male")),
            year = col_integer()
          )
)

penguins_tbl
#> # A tibble: 9 × 4
#>   species   bill_length_mm body_mass_g  year
#>   <fct>              <dbl>       <dbl> <int>
#> 1 Adelie              37          3000  2007
#> 2 Adelie              40.2        3400  2009
#> 3 Adelie              40.6        3475  2009
#> 4 Gentoo              42          4150  2007
#> 5 Gentoo              55.9        5600  2009
#> 6 Gentoo              46.5        4550  2007
#> 7 Chinstrap           46.4        3450  2007
#> 8 Chinstrap           50.3        3300  2007
#> 9 Chinstrap           46.2        3650  2008

24.5 다양한 파일 형태

24.5.1 파일 저장

펭귄 데이터에서 종별로 3마리를 무작위로 추출해서 nine_penguins 데이터프레임을 만든 후에 다양한 형식의 아스키 파일로 저장한다. 펭귄 9마리 데이터프레임으로 아스키 파일 형식으로 저장된 다양한 형태(탭 구분자, 콤마 구분자, 고정 길이)의 데이터를 불러오는 방법을 살펴본다. 구분자로 탭과 콤마가 가장 많이 사용되지만 경우에 따라서는 “;”, “:”, “|” 등 다양한 구분자를 사용할 수 있다.

library(palmerpenguins)

nine_penguins <- palmerpenguins::penguins |> 
    drop_na() |> 
    slice_sample(n = 3, replace = FALSE, by = species) |> 
    select(-island, -bill_depth_mm, -sex, -flipper_length_mm)

탭 구분자

write_delim() 함수에 delim 인자를 으로 명시하여 탭 구분자 아스키 파일로 저장하는 방법과 write_tsv() 함수를 사용하는 방법이 있다. 탭 구분자 파일로 저장하는 동일한 기능을 수행하지만 함수명에서 차이가 난다.

nine_penguins |> 
    # write_tsv("data/file/ASCII/nine_penguins.tsv") |> 
    write_delim("data/file/nine_penguins.txt", delim = "\t") 
species	bill_length_mm	body_mass_g	year
Adelie	39.6	3550	2008
Adelie	42.5	4500	2007
Adelie	36.5	3150	2007
Gentoo	41.7	4700	2009
Gentoo	45.2	4750	2008
Gentoo	40.9	4650	2007
Chinstrap	42.5	3350	2008
Chinstrap	46.4	3450	2007
Chinstrap	49.2	4400	2007

CSV 구분자

CSV(Comma-Separated Values) 파일은 콤마 구분자를 사용하여 데이터를 저장하는 형식으로 모든 운영체제에서 특별한 별도 프로그램 없이 열어볼 수 있다는 장점이 있어 호환성에서 큰 장점이 있지만 파일에 많은 정보가 담기게 되면 파일 크기가 커져서 저장 공간을 많이 차지한다는 단점이 있다. write_csv() 함수를 사용하여 콤마 구분자 아스키 파일로 저장하는 방법과 write_delim() 함수를 사용하는 방법이 있다. 콤마 구분자 파일로 저장하는 동일한 기능을 수행하지만 함수명에서 차이가 난다.

nine_penguins |> 
    write_csv("data/file/nine_penguins.csv")
species,bill_length_mm,body_mass_g,year
Adelie,39.6,3550,2008
Adelie,42.5,4500,2007
Adelie,36.5,3150,2007
Gentoo,41.7,4700,2009
Gentoo,45.2,4750,2008
Gentoo,40.9,4650,2007
Chinstrap,42.5,3350,2008
Chinstrap,46.4,3450,2007
Chinstrap,49.2,4400,2007

고정 길이 파일

고정 길이 아스키 파일(Fixed-width ASCII file, FWF)은 데이터 저장 및 교환을 위해 초기 컴퓨팅 시대에 개발되었다. 당시에는 데이터 저장 공간이 제한적이었기 때문에 고정 길이 파일은 구분자를 사용하지 않고 데이터를 더 촘촘하게 저장할 수 있었고, 하드웨어와 소프트웨어도 고정 길이 레코드 처리에 최적화되어 있었다.

현재까지도 고정 길이 파일은 레거시 시스템과의 호환성, 데이터 무결성 유지, 데이터 밀도 향상, 대용량 데이터 처리 성능 개선 등의 이유로 명맥을 유지하고 있으며, 의료 및 금융 분야에서 고정 길이 파일을 데이터 교환 표준으로 활용하기도 한다.

하지만, 고정 길이 파일은 파일 구조를 이해하기 위해 별도 문서나 스키마 정의가 필요하고, 데이터 추가나 수정 시 레코드 길이 조정이 요구되는 단점이 크며, 구분자로 구분되는 구조화된 데이터 형식과 비교하면 사용 편의성이 크게 떨어진다.

Adelie    Dream     37.6          181            3300     female   2007
Adelie    Biscoe    35.3          187            3800     female   2007
Adelie    Biscoe    37.8          174            3400     female   2007
Gentoo    Biscoe    47.4          212            4725     female   2009
Gentoo    Biscoe    49.1          220            5150     female   2008
Gentoo    Biscoe    47.5          209            4600     female   2008
Chinstrap Dream     40.9          187            3200     female   2008
Chinstrap Dream     47.6          195            3850     female   2008
Chinstrap Dream     46            195            4150     female   2007

24.5.2 불러오기

탭 구분자

read_delim() 함수에 delim 인자를 으로 명시하여 탭 구분자 아스키 파일을 불러오는 방법과 read_tsv() 함수를 사용하는 방법이 있다. 탭 구분자 파일을 불러오는 동일한 기능을 수행하지만 함수명에서 차이가 난다.

nine_penguins <- 
    read_delim("data/file/nine_penguins.txt", delim = "\t") 

CSV 구분자

readr 패키지의 read_csv() 함수를 사용하여 콤마 구분자를 갖는 파일을 불러읽어온다.

nine_penguins <- 
    read_csv("data/file/nine_penguins.csv")  

고정 길이 파일

readr 패키지의 read_fwf() 함수를 사용하여 고정 길이 파일을 불러읽어오는 방식에서 fwf_widths 인자로 각 열의 길이를 지정하고 col_names 인자로 열 이름을 지정한다.

nine_penguins_fwf <-read_fwf("data/file/nine_penguins.fwf",
                             skip = 0,
         col_positions = fwf_widths(c(10, 10, 14, 15, 9, 9, 5),
           col_names = c("species", "island", "bill_length_mm",
                         "flipper_length_mm", "body_mass_g", "sex", "year")))

nine_penguins_fwf
#> # A tibble: 9 × 7
#>   species   island bill_length_mm flipper_length_mm body_mass_g sex     year
#>   <chr>     <chr>           <dbl>             <dbl>       <dbl> <chr>  <dbl>
#> 1 Adelie    Dream            37.6               181        3300 female  2007
#> 2 Adelie    Biscoe           35.3               187        3800 female  2007
#> 3 Adelie    Biscoe           37.8               174        3400 female  2007
#> 4 Gentoo    Biscoe           47.4               212        4725 female  2009
#> 5 Gentoo    Biscoe           49.1               220        5150 female  2008
#> 6 Gentoo    Biscoe           47.5               209        4600 female  2008
#> 7 Chinstrap Dream            40.9               187        3200 female  2008
#> 8 Chinstrap Dream            47.6               195        3850 female  2008
#> 9 Chinstrap Dream            46                 195        4150 female  2007

24.6 공공 데이터

공공데이터포털을 비롯한 많은 정부기관에서 제공하는 데이터는 대부분 EUC-KR로 인코딩되어 있다. 이유는 여러 가지가 있겠지만 가장 큰 이유는 아마도 엑셀에서 .csv 파일을 열었을 때 한글이 깨지는 민원을 처리하기 위함이 아닐까 싶다. 정형 .csv 파일 형태로 데이터를 받게 되면 먼저 인코딩을 확인해야 한다. readr 패키지의 guess_encoding() 함수를 사용하면 파일의 인코딩을 확인할 수 있다.

공공데이터포털 인천광역시_정류장별 이용승객 현황 데이터를 다운로드받아 로컬 파일로 저장한 후 인코딩을 확인한다.

library(readr)

file_path <- "data/file/인천광역시_정류장별 이용승객 현황_20220630.csv"
guess_encoding(file_path)
#> # A tibble: 4 × 2
#>   encoding confidence
#>   <chr>         <dbl>
#> 1 EUC-KR         1   
#> 2 GB18030        0.81
#> 3 Big5           0.48
#> 4 EUC-JP         0.3

따라서, 이를 바로 read_csv() 함수로 읽을 경우 오류가 발생된다. 왜냐하면 read_csv() 함수는 인코딩을 UTF-8을 기본으로 가정하고 있기 때문이다.

read_csv(file_path)
#> Error in nchar(x, "width"): invalid multibyte string, element 1

따라서, read_csv() 함수를 사용할 때는 locale 인수를 사용하여 인코딩을 지정해주어야 한다. “EUC-KR”로 인코딩을 지정하면 파일을 오류 없이 읽을 수 있다.

incheon_bus <- spec(read_csv(file_path, locale = locale(encoding = "EUC-KR")))
incheon_bus |> names() |> dput()
#> c("cols", "default", "delim")

데이터 가져오기는 데이터 분석의 첫 단계로, 외부 데이터를 R로 불러오는 과정으로 첫 단추가 이후 이어질 분석 단계에서 중요한 역할을 한다.

먼저, 파일 형식에 따라 적절한 함수를 선택해야 한다. 텍스트 파일은 read.csv, read.table 등의 함수를 사용하고, 엑셀 파일은 readxl 패키지의 read_excel 함수를 사용한다. 특히, 인코딩도 이 단계에서 반드시 확인해야 한다.

데이터 전처리 단계에서는 구분자와 헤더 유무를 확인하고, 자료형과 열 이름을 결정해야 한다. 결측값 처리를 위해 na = 옵션을 사용할 수 있고, 필요에 따라 특정 행/열을 선택하는 등의 추가 옵션을 설정할 수 있다.

전처리 과정을 거쳐 최종적으로 데이터프레임을 생성하게 된다. 다소 번거롭더라도 데이터를 가져오는 단계에서 전처리 과정을 충실히 수행하게 되면 이후 dplyr, tidyr 패키지 등을 활용해 다양한 데이터 조작 및 시각화를 수월하게 할 수 있다.

file_path <- "data/file/인천광역시_정류장별 이용승객 현황_20220630.csv"

incheon_bus <- read_csv(file_path, locale = locale(encoding = "EUC-KR"),
                        skip = 1,
                        na = c("---", ""),
                        col_names = c("정류소명", "정류소_id", "승차건수_총합계", 
                                     "하차건수_총합계","승차건수_카드", "하차건수_카드",
                                     "승차건수_현금", "일평균_승하차건수"),
                       col_types = cols(
                         정류소명 = col_character(),
                         정류소_id = col_double(),
                         승차건수_총합계 = col_double(),
                         하차건수_총합계 = col_double(),
                         승차건수_카드 = col_double(),
                         하차건수_카드 = col_double(),
                         승차건수_현금 = col_double(),
                         일평균_승하차건수 = col_double()
                       ))  
incheon_bus
#> # A tibble: 6,386 × 8
#>    정류소명             정류소_id 승차건수_총합계 하차건수_총합계 승차건수_카드
#>    <chr>                    <dbl>           <dbl>           <dbl>         <dbl>
#>  1 (구)국제여객터미널       35051              95            1923            21
#>  2 (구)국제여객터미널          NA            2512              43          2465
#>  3 (주)경동세라믹스         89146             341              26           335
#>  4 (주)경인양행앞           42096             945             923           938
#>  5 (주)경인양행앞           42097            1322            3536          1294
#>  6 (주)대한특수금속         39050            1243              89          1238
#>  7 (주)두남                 39135             147              29           147
#>  8 (주)세모입구(린나이…     37585            1410            1517          1404
#>  9 (주)세모입구(린나이…     40893             307             556           304
#> 10 (주)스킨이데아           89388             147             148           147
#> # ℹ 6,376 more rows
#> # ℹ 3 more variables: 하차건수_카드 <dbl>, 승차건수_현금 <dbl>,
#> #   일평균_승하차건수 <dbl>

지금까지 작업한 전반적인 작업 흐름은 그림 24.3에 대략적으로 나와 있다. 공공데이터포털에서 다운로드받은 인천광역시_정류장별 이용승객 현황_20220630.csv는 EUC-KR로 인코딩되어 있고 헤더를 갖고 있으며 쉼표로 구분되어 있다. 결측치는 없으나 임의로 ---으로 정류장 한 곳을 달리 표현하여 na = c("---", "")로 결측값 처리를 하였다.

graph LR
    subgraph "<strong>파일 형식 결정</strong>"
    A[파일 형식 결정] --> |"read.csv, read.table 등"| B[인코딩 확인]
    A --> |"readxl::read_excel 등"| B
    end

    subgraph "<strong>데이터 전처리</strong>"
    C[구분자 확인] --> |"쉼표, 탭 등"| B
    D[헤더 유무] --> |"header = TRUE/FALSE"| B
    B --> E[자료형 및<br> 열 이름 결정]
    F[결측값 처리] --> |"na 옵션"| E
    G[추가 옵션] --> |"특정 행/열 선택 등"| E
    end

E --> H[데이터프레임<br>생성]

style A fill:#f0f0f0,stroke:#333,stroke-width:2px
style B fill:#f0f0f0,stroke:#333,stroke-width:2px
style C fill:#e0e0e0,stroke:#333,stroke-width:2px
style D fill:#e0e0e0,stroke:#333,stroke-width:2px
style E fill:#d0d0d0,stroke:#333,stroke-width:2px
style F fill:#d0d0d0,stroke:#333,stroke-width:2px
style G fill:#c0c0c0,stroke:#333,stroke-width:2px
style H fill:#c0c0c0,stroke:#333,stroke-width:2px
그림 24.3: 인천광역시 정류장별 이용승객 현황 데이터 데이터프레임 가져오는 과정

24.7 다수 파일

다수 파일을 불러오는 경우를 상정하기 위해서 먼저 앞서 준비한 nine_penguins 데이터프레임을 재사용한다. split() 함수로 species 열을 기준으로 nine_penguins를 분할하여 분할된 데이터를 리스트 형태로 penguins_split에 저장한다. here() 함수로 data/file/ 폴더의 경로를 data_folder 변수에 저장한다. walk2() 함수로 penguins_split 리스트의 각 요소와 해당 요소의 이름을 순회하면서 write_csv() 함수를 사용해 각 분할된 데이터프레임을 CSV 파일로 저장한다. 코드 실행 결과, 프로젝트 디렉토리 내의 data/file/ 폴더에 penguin_Adelie.csv, penguin_Gentoo.csv, penguin_Chinstrap.csv 파일이 생성되며, 각 파일에는 해당 펭귄 종 3마리 관측점 데이터가 저장된다.

# species로 데이터 분할
penguins_split <- split(nine_penguins, nine_penguins$species)

# 분할된 데이터를 CSV 파일로 저장
data_folder <- here::here("data", "file")
walk2(penguins_split, str_glue("penguin_{names(penguins_split)}"), ~ write_csv(.x, here::here(data_folder, str_c(.y, ".csv"))))

list.files() 함수로 data/file 폴더에 penguin으로 시작하는 .csv 파일 3개를 확인할 수 있다.

list.files(data_folder, pattern = "^penguin")
#> [1] "penguin_Adelie.csv"    "penguin_Chinstrap.csv" "penguin_Gentoo.csv"

이제 데이터가 준비되었으니 penguin_Adelie.csv, penguin_Gentoo.csv, penguin_Chinstrap.csv 파일을 읽어와서 하나의 데이터프레임으로 만들어보자. 동일한 자료 구조를 갖는 아스키 파일은 시도, 시군구 데이터처럼 공간적으로 관리를 위해 구분되거나 일, 월, 분기, 년 처럼 시점을 달리하는 경우 관리 목적으로 구분되어 흔히 접하게 되는 데이터다.

data_folder 변수에 CSV 파일들이 저장된 폴더 경로를 지정한다. list.files() 함수로 data_folder 내의 모든 CSV 파일 경로를 csv_files 변수에 저장한다. map_df() 함수로 csv_files의 각 파일 경로에 대해 read_csv() 함수를 적용하여 CSV 파일을 읽어와 읽어온 데이터프레임으로 결합한다. 코드 실행 결과, data/file/ 폴더에 있는 penguin_Adelie.csv, penguin_Gentoo.csv, penguin_Chinstrap.csv 파일을 읽어와서 하나의 데이터프레임으로 결합한 penguins_tbl이 생성되며, 총 9마리 펭귄 3종의 데이터가 포함되어 있다.

# CSV 파일 경로 지정
data_folder <- "data/file/"
csv_files <- list.files(data_folder, pattern = "^penguin", full.names = TRUE)

# CSV 파일들을 읽어와 데이터프레임 결합
penguins_tbl <- purrr::map_df(csv_files, read_csv)

penguins_tbl
#> # A tibble: 9 × 4
#>   species   bill_length_mm body_mass_g  year
#>   <chr>              <dbl>       <dbl> <dbl>
#> 1 Adelie              39.6        3550  2008
#> 2 Adelie              42.5        4500  2007
#> 3 Adelie              36.5        3150  2007
#> 4 Chinstrap           42.5        3350  2008
#> 5 Chinstrap           46.4        3450  2007
#> 6 Chinstrap           49.2        4400  2007
#> 7 Gentoo              41.7        4700  2009
#> 8 Gentoo              45.2        4750  2008
#> 9 Gentoo              40.9        4650  2007

24.8 요약

데이터 가져오기는 데이터 분석의 첫 단계로, 외부 데이터를 R로 불러오는 과정이다. 데이터의 형식과 구조를 파악하고 적절한 전처리를 수행하는 것이 매우 중요하다.

먼저, 유니코드와 UTF-8에 대한 이해를 바탕으로 다양한 문자 집합과 인코딩 방식에 대해 알아보았다. 특히, 한글 데이터를 다룰 때는 EUC-KRUTF-8 인코딩의 차이를 이해하고 적절한 함수와 옵션을 사용해서 불러와야 오류없이 후속 작업을 수행할 수 있다.

다음으로, 아스키 파일과 공공 데이터를 R로 불러오는 방법을 실습해 보았다. 파일 형식에 따라 적절한 함수를 선택하고, 데이터 전처리 과정을 거쳐 최종적으로 데이터프레임을 생성하는 방법을 익혔고 다수의 파일을 효율적으로 처리하는 방법도 함께 살펴보았다.

데이터 가져오기는 모든 데이터 분석의 시작점일 뿐, 이후에도 데이터 정제, 변환, 시각화 등 다양한 과정을 거치게 됩니다. 하지만 그 어떤 과정보다 데이터 가져오기가 가장 중요한 만큼, 이 부분에 시간과 노력을 투자할 것을 권장한다.


  1. 미국정보교환표준부호(American Standard Code for Information Interchange, ASCII)는 영문 알파벳을 사용하는 대표적인 문자 인코딩으로 컴퓨터와 통신 장비를 비롯한 문자를 사용하는 많은 장치에서 사용되며, 대부분의 문자 인코딩이 아스키에 기초하고 있다.↩︎