데이터 타입 (Data Types)

데이터 타입 (Data Types)

Polars는 크게 다음 범주로 나뉘는 다양한 데이터 타입을 지원해요.

  • 숫자(Numeric): 부호 있는 정수, 부호 없는 정수, 부동소수점, 소수(Decimal)
  • 중첩(Nested): 리스트(List), 구조체(Struct), 배열(Array)
  • 시간(Temporal): 날짜(Date), 날짜시간(Datetime), 시간(Time), 시간 간격(Duration)
  • 기타(Miscellaneous): 문자열(String), 바이너리(Binary), 부울(Boolean), 범주형(Categorical), 열거형(Enum), 객체(Object)

모든 타입은 특수값 null로 나타내는 결측값(missing value)을 지원해요. 이는 부동소수점 타입의 특수값 NaN과 혼동하면 안 돼요. NaN에 대해서는 부동소수점 숫자 섹션에서 더 자세히 다룰게요.

지원하는 모든 데이터 타입을 정리한 전체 표는 부록에서 찾을 수 있어요. 각 타입을 언제 써야 하는지에 대한 설명과 문서에서 해당 부분으로 이어지는 링크도 함께 실려 있죠.

시리즈(Series)

Polars가 제공하는 핵심 기본 데이터 구조는 시리즈와 데이터프레임이에요. 시리즈는 1차원 동질(homogeneous) 데이터 구조예요. 여기서 '동질'이라고 하면 시리즈 안의 모든 요소가 같은 데이터 타입을 가진다는 뜻이에요. 아래 코드는 이름이 붙은 시리즈를 만드는 방법을 보여줘요.

import polars as pl

s = pl.Series("ints", [1, 2, 3, 4, 5])
print(s)
shape: (5,)
Series: 'ints' [i64]
[
    1
    2
    3
    4
    5
]

시리즈를 만들 때 Polars는 넣어준 값에서 데이터 타입을 추론해요. 추론 메커니즘을 덮어쓰려면 구체적인 데이터 타입을 지정할 수도 있어요.

s1 = pl.Series("ints", [1, 2, 3, 4, 5])
s2 = pl.Series("uints", [1, 2, 3, 4, 5], dtype=pl.UInt64)
print(s1.dtype, s2.dtype)
Int64 UInt64

데이터프레임(DataFrame)

데이터프레임은 2차원 이질(heterogeneous) 데이터 구조로, 이름이 고유한 시리즈들을 담고 있어요. 데이터를 데이터프레임에 담아 두면 Polars API를 사용해 데이터를 다루는 쿼리를 작성할 수 있어요. 이때 앞으로 이야기할 컨텍스트(context)와 표현식(expression)을 활용하게 됩니다.

아래 코드는 리스트 딕셔너리(dictionary of lists)에서 데이터프레임을 만드는 방법을 보여줘요.

from datetime import date

df = pl.DataFrame(
    {
        "name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
        "birthdate": [
            date(1997, 1, 10),
            date(1985, 2, 15),
            date(1983, 3, 22),
            date(1981, 4, 30),
        ],
        "weight": [57.9, 72.5, 53.6, 83.1],  # (kg)
        "height": [1.56, 1.77, 1.65, 1.75],  # (m)
    }
)

print(df)
shape: (4, 4)
┌────────────────┬────────────┬────────┬────────┐
│ name           ┆ birthdate  ┆ weight ┆ height │
│ ---            ┆ ---        ┆ ---    ┆ ---    │
│ str            ┆ date       ┆ f64    ┆ f64    │
╞════════════════╪════════════╪════════╪════════╡
│ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   │
│ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   │
│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   │
└────────────────┴────────────┴────────┴────────┘

데이터프레임 들여다보기

이 절에서는 데이터프레임을 빠르게 살펴볼 수 있는 유용한 메서드 몇 가지를 소개해요. 아까 만든 데이터프레임을 출발점으로 사용할게요.

head

head 함수는 데이터프레임의 첫 행들을 보여줘요. 기본값으로 첫 5행을 가져오고, 원하는 행 수를 직접 지정할 수도 있어요.

print(df.head(3))
shape: (3, 4)
┌──────────────┬────────────┬────────┬────────┐
│ name         ┆ birthdate  ┆ weight ┆ height │
│ ---          ┆ ---        ┆ ---    ┆ ---    │
│ str          ┆ date       ┆ f64    ┆ f64    │
╞══════════════╪════════════╪════════╪════════╡
│ Alice Archer ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   │
│ Ben Brown    ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
│ Chloe Cooper ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   │
└──────────────┴────────────┴────────┴────────┘

glimpse

glimpse 함수도 데이터프레임의 첫 몇 행 값을 보여주지만, head와는 출력 형식이 달라요. 각 줄의 출력이 하나의 열에 대응해, 넓은 데이터프레임을 살펴볼 때 더 편리해요.

print(df.glimpse(return_type="string"))
Rows: 4
Columns: 4
$ name       <str> 'Alice Archer', 'Ben Brown', 'Chloe Cooper', 'Daniel Donovan'
$ birthdate <date> 1997-01-10, 1985-02-15, 1983-03-22, 1981-04-30
$ weight     <f64> 57.9, 72.5, 53.6, 83.1
$ height     <f64> 1.56, 1.77, 1.65, 1.75

참고: glimpse는 Python 사용자만 사용할 수 있어요.

tail

tail 함수는 데이터프레임의 마지막 행들을 보여줘요. 기본값으로 마지막 5행을 가져오고, head처럼 원하는 행 수를 지정할 수도 있어요.

print(df.tail(3))
shape: (3, 4)
┌────────────────┬────────────┬────────┬────────┐
│ name           ┆ birthdate  ┆ weight ┆ height │
│ ---            ┆ ---        ┆ ---    ┆ ---    │
│ str            ┆ date       ┆ f64    ┆ f64    │
╞════════════════╪════════════╪════════╪════════╡
│ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
│ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   │
│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   │
└────────────────┴────────────┴────────┴────────┘

sample

데이터프레임의 첫 행이나 마지막 행이 데이터를 대표하지 못한다고 생각되면, sample을 사용해 데이터프레임에서 무작위로 선택한 임의 개수의 행을 가져올 수 있어요. 반환되는 행이 데이터프레임에 나타나는 순서와 반드시 일치하지는 않는다는 점을 주의하세요.

pl.set_random_seed(42)  # For reproducibility.

print(df.sample(2))
shape: (2, 4)
┌────────────────┬────────────┬────────┬────────┐
│ name           ┆ birthdate  ┆ weight ┆ height │
│ ---            ┆ ---        ┆ ---    ┆ ---    │
│ str            ┆ date       ┆ f64    ┆ f64    │
╞════════════════╪════════════╪════════╪════════╡
│ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   │
│ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   │
└────────────────┴────────────┴────────┴────────┘

describe

describe를 사용하면 데이터프레임의 모든 열에 대한 요약 통계를 계산할 수 있어요.

print(df.describe())
shape: (9, 5)
┌────────────┬────────────────┬─────────────────────┬───────────┬──────────┐
│ statistic  ┆ name           ┆ birthdate           ┆ weight    ┆ height   │
│ ---        ┆ ---            ┆ ---                 ┆ ---       ┆ ---      │
│ str        ┆ str            ┆ str                 ┆ f64       ┆ f64      │
╞════════════╪════════════════╪═════════════════════╪═══════════╪══════════╡
│ count      ┆ 4              ┆ 4                   ┆ 4.0       ┆ 4.0      │
│ null_count ┆ 0              ┆ 0                   ┆ 0.0       ┆ 0.0      │
│ mean       ┆ null           ┆ 1986-09-04 00:00:00 ┆ 66.775    ┆ 1.6825   │
│ std        ┆ null           ┆ null                ┆ 13.560082 ┆ 0.097082 │
│ min        ┆ Alice Archer   ┆ 1981-04-30          ┆ 53.6      ┆ 1.56     │
│ 25%        ┆ null           ┆ 1983-03-22          ┆ 57.9      ┆ 1.65     │
│ 50%        ┆ null           ┆ 1985-02-15          ┆ 72.5      ┆ 1.75     │
│ 75%        ┆ null           ┆ 1985-02-15          ┆ 72.5      ┆ 1.75     │
│ max        ┆ Daniel Donovan ┆ 1997-01-10          ┆ 83.1      ┆ 1.77     │
└────────────┴────────────────┴─────────────────────┴───────────┴──────────┘

스키마(Schema)

(데이터프레임 안이든 다른 곳이든) 데이터에 대해 이야기할 때 우리는 그 데이터의 스키마를 말할 수 있어요. 스키마는 열(또는 시리즈)의 이름을 그 열(또는 시리즈)의 데이터 타입에 매핑한 것이에요.

데이터프레임의 스키마는 schema로 확인할 수 있어요.

print(df.schema)
Schema({'name': String, 'birthdate': Date, 'weight': Float64, 'height': Float64})

시리즈와 마찬가지로, Polars는 데이터프레임을 만들 때 그 스키마를 추론하지만 필요하면 추론 시스템을 덮어쓸 수도 있어요.

Python에서는 딕셔너리로 열 이름을 데이터 타입에 매핑해 명시적 스키마를 지정할 수 있어요. 특정 열의 추론을 덮어쓰고 싶지 않다면 그 열에 값 None을 사용하면 돼요.

df = pl.DataFrame(
    {
        "name": ["Alice", "Ben", "Chloe", "Daniel"],
        "age": [27, 39, 41, 43],
    },
    schema={"name": None, "age": pl.UInt8},
)

print(df)
shape: (4, 2)
┌────────┬─────┐
│ name   ┆ age │
│ ---    ┆ --- │
│ str    ┆ u8  │
╞════════╪═════╡
│ Alice  ┆ 27  │
│ Ben    ┆ 39  │
│ Chloe  ┆ 41  │
│ Daniel ┆ 43  │
└────────┴─────┘

일부 열의 추론만 덮어쓰면 된다면 schema_overrides 매개변수가 더 편리할 때가 많아요. 추론을 덮어쓰고 싶지 않은 열은 생략할 수 있으니까요.

df = pl.DataFrame(
    {
        "name": ["Alice", "Ben", "Chloe", "Daniel"],
        "age": [27, 39, 41, 43],
    },
    schema_overrides={"age": pl.UInt8},
)

print(df)
shape: (4, 2)
┌────────┬─────┐
│ name   ┆ age │
│ ---    ┆ --- │
│ str    ┆ u8  │
╞════════╪═════╡
│ Alice  ┆ 27  │
│ Ben    ┆ 39  │
│ Chloe  ┆ 41  │
│ Daniel ┆ 43  │
└────────┴─────┘

데이터 타입 내부 구조

Polars는 데이터 저장 방향으로 Arrow Columnar Format을 사용해요. 이 스펙을 따르기 때문에 Polars는 동일하게 Arrow 스펙을 사용하는 다른 도구와 거의 오버헤드 없이 데이터를 주고받을 수 있어요.

Polars가 얻는 성능의 대부분은 쿼리 엔진, 쿼리 플랜에 수행하는 최적화, 표현식을 실행할 때 사용하는 병렬화에서 나와요.

부동소수점 숫자

Polars는 일반적으로 Float32와 Float64에 대해 IEEE 754 부동소수점 표준을 따르되, 몇 가지 예외가 있어요.

  • 어떤 NaN이든 다른 어떤 NaN과도 동일하게 비교되며, NaN이 아닌 어떤 값보다도 크게 취급돼요.
  • 연산은 0의 부호나 NaN의 부호, 그리고 NaN 값의 페이로드에 대해 특정 동작을 보장하지 않아요. 이는 산술 연산에만 국한되지 않는데, 예를 들어 정렬이나 group by 연산은 효율적인 동일성 검사를 위해 모든 0을 +0으로, 모든 NaN을 페이로드 없는 양의 NaN으로 정규화할 수 있어요.

Polars는 항상 부동소수점 계산에서 합리적으로 정확한 결과를 제공하려고 노력하지만, 별도로 언급하지 않는 한 오차에 대한 보장은 하지 않아요. 일반적으로 100% 정확한 결과를 얻는 것은 실행 불가능할 정도로 비싸서(64비트 부동소수점보다 훨씬 큰 내부 표현이 필요), 어느 정도의 오차는 항상 있을 것으로 기대하는 게 맞아요.

부록: 전체 데이터 타입 표

타입 설명
Boolean 비트로 효율적으로 압축(packed)되는 부울 타입.
Int8, Int16, Int32, Int64, Int128 정밀도가 다른 부호 있는 정수 타입.
UInt8, UInt16, UInt32, UInt64, UInt128 정밀도가 다른 부호 없는 정수 타입.
Float16, Float32, Float64 정밀도가 다른 부호 있는 부동소수점 숫자.
Decimal 선택적 정밀도와 음이 아닌 스케일(scale)을 가진 128비트 소수 타입. 부동소수점의 정밀도를 이 연산에 대해 세밀하게 제어해야 할 때 사용해요. 소수 데이터 타입이 무엇인지에 대한 설명은 Python의 decimal.Decimal 문서를 참고하세요.
String 가변 길이 UTF-8 인코딩 문자열 데이터로, 보통 사람이 읽을 수 있음.
Binary 임의의 가변 길이 원시 바이너리 데이터를 저장.
Date 달력 날짜를 나타냄.
Time 하루 중의 시간을 나타냄.
Datetime 달력 날짜와 하루 중의 시간을 나타냄.
Duration 시간 간격(duration)을 나타냄.
Array 시리즈마다 알려진 고정된 모양(shape)을 가진 배열. numpy 배열과 유사해요. 배열과 리스트가 어떻게 다른지, 둘을 어떻게 다뤄야 하는지 자세히 알아보세요.
List 가변 길이의 동질 1D 컨테이너. 배열과 리스트가 어떻게 다른지, 둘을 어떻게 다뤄야 하는지 자세히 알아보세요.
Object 임의의 Python 객체를 감싸는 타입.
Categorical 범주가 실행 시점에 추론되는 문자열 데이터의 효율적 인코딩. 범주형과 열거형이 어떻게 다른지, 둘을 어떻게 다뤄야 하는지 자세히 알아보세요.
Enum 미리 정해진 문자열 범주 집합의 효율적인 순서 있는 인코딩. 범주형과 열거형이 어떻게 다른지, 둘을 어떻게 다뤄야 하는지 자세히 알아보세요.
Struct 여러 필드를 저장할 수 있는 합성 곱 타입(composite product type). Struct 데이터 타입에 대해서는 전용 문서 섹션에서 자세히 다뤄요.
Null null 값을 나타냄.