今天我們來認識Polars常用的基本型別及討論如何處理缺失值(pl.Null
型別)。
本日大綱如下:
pl.Int
與pl.UInt
pl.Float
pl.Bool
pl.Null
NaN
與pl.Null
的注意事項codepanda
import numpy as np
import pandas as pd
import polars as pl
data = {
"int64": [0, 1, 2, 3, 500],
"float64": [6.1, 7.2, 8.3, 9.4, 10.5],
"bool": [True, False, False, True, True],
"null": [5, 10, None, None, 30],
"nan": [11.1, np.nan, float("nan"), 33.3, 22.2],
}
df = pl.DataFrame(data)
shape: (5, 5)
┌───────┬─────────┬───────┬──────┬──────┐
│ int64 ┆ float64 ┆ bool ┆ null ┆ nan │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ bool ┆ i64 ┆ f64 │
╞═══════╪═════════╪═══════╪══════╪══════╡
│ 0 ┆ 6.1 ┆ true ┆ 5 ┆ 11.1 │
│ 1 ┆ 7.2 ┆ false ┆ 10 ┆ NaN │
│ 2 ┆ 8.3 ┆ false ┆ null ┆ NaN │
│ 3 ┆ 9.4 ┆ true ┆ null ┆ 33.3 │
│ 500 ┆ 10.5 ┆ true ┆ 30 ┆ 22.2 │
└───────┴─────────┴───────┴──────┴──────┘
觀察df
可以得知,在沒有指定schema時,Polars預設會將整數指定為pl.Int64
型別,而將浮點數指定為pl.Float64
型別。
pl.Int
與pl.UInt
整數分為帶有正負符號的pl.Int
及不帶正負符號的pl.UInt
。兩者皆有多種位元的型別可以使用,例如預設的pl.Int64
型別,代表使用64位元且帶有正負符號的整數型別。
我們可以使用pl.Expr.cast()轉換型別,例如將「"int64"」列轉換為pl.Int32
及pl.UInt32
型別。
(
df.select(
"int64",
pl.col("int64").cast(pl.Int32).alias("Int32"),
pl.col("int64").cast(pl.UInt32).alias("UInt32"),
)
)
shape: (5, 3)
┌───────┬───────┬────────┐
│ int64 ┆ Int32 ┆ UInt32 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i32 ┆ u32 │
╞═══════╪═══════╪════════╡
│ 0 ┆ 0 ┆ 0 │
│ 1 ┆ 1 ┆ 1 │
│ 2 ┆ 2 ┆ 2 │
│ 3 ┆ 3 ┆ 3 │
│ 500 ┆ 500 ┆ 500 │
└───────┴───────┴────────┘
這邊需注意,型別轉換是有可能會失敗的,例如如果我們將「"int64"」列轉換為pl.Int8
即會報錯,因為其中有一行的數值為500,超出了pl.Int8
的範圍(-128 ~ 127)。
❌
df.select("int64", pl.col("int64").cast(pl.Int8).alias("Int8"))
# InvalidOperationError: conversion from `i64` to `i8` failed
# in column 'int64' for 1 out of 5 values: [500]
除此之外,我們也可以將「"int64"」列轉換為pl.Date
型別:
df.select("int64", pl.col("int64").cast(pl.Date).alias("date"))
shape: (5, 2)
┌───────┬────────────┐
│ int64 ┆ date │
│ --- ┆ --- │
│ i64 ┆ date │
╞═══════╪════════════╡
│ 0 ┆ 1970-01-01 │
│ 1 ┆ 1970-01-02 │
│ 2 ┆ 1970-01-03 │
│ 3 ┆ 1970-01-04 │
│ 500 ┆ 1971-05-16 │
└───────┴────────────┘
這是因為Polars以pl.Int32
型別,儲存自UNIX epoch
(即UTC1970年1月1日0時0分0秒)以來的天數。關於時間相關的型別,我們將於[Day13]說明。
pl.Float
我們可以將「"float64"」列轉換為pl.Float32
型別,但一樣要注意數值不能超過其範圍:
(
df.select(
"float64",
pl.col("float64").cast(pl.Float32).alias("float32"),
)
)
shape: (5, 2)
┌─────────┬─────────┐
│ float64 ┆ float32 │
│ --- ┆ --- │
│ f64 ┆ f32 │
╞═════════╪═════════╡
│ 6.1 ┆ 6.1 │
│ 7.2 ┆ 7.2 │
│ 8.3 ┆ 8.3 │
│ 9.4 ┆ 9.4 │
│ 10.5 ┆ 10.5 │
└─────────┴─────────┘
在Polars有一種特別的NaN
(Not a Number),被歸類於pl.Float
型別中。其可以由使用者生成,例如np.nan
或float("nan")
,又或者於計算中產生:
(
df.select(
"int64",
pl.col("int64").truediv(pl.col("int64")).alias("int64 / int64"),
)
)
shape: (5, 2)
┌───────┬───────────────┐
│ int64 ┆ int64 / int64 │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞═══════╪═══════════════╡
│ 0 ┆ NaN │
│ 1 ┆ 1.0 │
│ 2 ┆ 1.0 │
│ 3 ┆ 1.0 │
│ 500 ┆ 1.0 │
└───────┴───────────────┘
需要注意的是NaN
屬於pl.Float
型別,而不是pl.Null
型別(缺失值型別)。
Polars提供有pl.Expr.is_nan()及pl.Expr.fill_nan()兩個expr,幫助使用者操作。舉例來說,我們可以:
pl.Expr.is_nan()
判斷「"nan"」列各行是否為NaN
。pl.Expr.fill_nan()
將NaN
值取代為10。pl.Expr.fill_nan()
將NaN
值取代為「"nan"」列乘以100。(
df.select(
"nan",
pl.col("nan").is_nan().alias("is_nan"),
pl.col("nan").fill_nan(10).alias("fill_10"),
pl.col("nan")
.fill_nan(pl.col("int64").mul(100))
.alias("fill_expr"),
)
)
shape: (5, 4)
┌──────┬────────┬─────────┬───────────┐
│ nan ┆ is_nan ┆ fill_10 ┆ fill_expr │
│ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ bool ┆ f64 ┆ f64 │
╞══════╪════════╪═════════╪═══════════╡
│ 11.1 ┆ false ┆ 11.1 ┆ 11.1 │
│ NaN ┆ true ┆ 10.0 ┆ 100.0 │
│ NaN ┆ true ┆ 10.0 ┆ 200.0 │
│ 33.3 ┆ false ┆ 33.3 ┆ 33.3 │
│ 22.2 ┆ false ┆ 22.2 ┆ 22.2 │
└──────┴────────┴─────────┴───────────┘
pl.Bool
pl.bool
只有True
與False
兩種元素。以下列舉幾個常用的操作:
True
與False
互換。pl.bool
轉換為整數型別,即False
會變成0,而True
會變成1。(
df.select(
"bool",
pl.col("bool").not_().alias("~bool"),
pl.col("bool").cast(pl.UInt8).alias("bool_to_uint8"),
pl.col("bool").sum().alias("sum"),
)
)
shape: (5, 4)
┌───────┬───────┬───────────────┬─────┐
│ bool ┆ ~bool ┆ bool_to_uint8 ┆ sum │
│ --- ┆ --- ┆ --- ┆ --- │
│ bool ┆ bool ┆ u8 ┆ u32 │
╞═══════╪═══════╪═══════════════╪═════╡
│ true ┆ false ┆ 1 ┆ 3 │
│ false ┆ true ┆ 0 ┆ 3 │
│ false ┆ true ┆ 0 ┆ 3 │
│ true ┆ false ┆ 1 ┆ 3 │
│ true ┆ false ┆ 1 ┆ 3 │
└───────┴───────┴───────────────┴─────┘
pl.Null
pl.Null
一般是由Python的None
而來。與NaN
類似,Polars也提供了pl.Expr.is_null()及pl.Expr.fill_null()兩種expr。以下列舉幾個常見用來填補缺失值的方法:
pl.Expr.fill_null()
填補數字10。pl.Expr.fill_null()
,以「"float64"」列填補缺失值。pl.Expr.fill_null()
,以strategy="forward"
填補缺失值。pl.Expr.fill_null()
,以strategy="backward"
填補缺失值。(
df.select(
pl.col("null", "float64"),
pl.col("null").fill_null(10).alias("fill_10"),
pl.col("null").fill_null(pl.col("float64")).alias("from_float64"),
pl.col("null").fill_null(strategy="forward").alias("forward"),
pl.col("null").fill_null(strategy="backward").alias("backward"),
pl.col("null").interpolate().alias("interpolate"),
)
)
shape: (5, 7)
┌──────┬─────────┬─────────┬──────────────┬─────────┬──────────┬─────────────┐
│ null ┆ float64 ┆ fill_10 ┆ from_float64 ┆ forward ┆ backward ┆ interpolate │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 ┆ i64 ┆ f64 │
╞══════╪═════════╪═════════╪══════════════╪═════════╪══════════╪═════════════╡
│ 5 ┆ 6.1 ┆ 5 ┆ 5.0 ┆ 5 ┆ 5 ┆ 5.0 │
│ 10 ┆ 7.2 ┆ 10 ┆ 10.0 ┆ 10 ┆ 10 ┆ 10.0 │
│ null ┆ 8.3 ┆ 10 ┆ 8.3 ┆ 10 ┆ 30 ┆ 16.666667 │
│ null ┆ 9.4 ┆ 10 ┆ 9.4 ┆ 10 ┆ 30 ┆ 23.333333 │
│ 30 ┆ 10.5 ┆ 30 ┆ 30.0 ┆ 30 ┆ 30 ┆ 30.0 │
└──────┴─────────┴─────────┴──────────────┴─────────┴──────────┴─────────────┘
NaN
與pl.Null
的注意事項在進行聚合運算,如求平均值時,pl.Null
是會被排除在計算之外,而NaN
則會進行傳遞。例如,對「"null"」列求取平均值時,僅會使用「5」、「10」及「30」三個元素,所以會得到45/3=15。而對「"nan"」列求取平均值時,由於其中存有NaN
,所以平均值仍為NaN
。
df.select(pl.col("null", "nan").mean())
shape: (1, 2)
┌──────┬─────┐
│ null ┆ nan │
│ --- ┆ --- │
│ f64 ┆ f64 │
╞══════╪═════╡
│ 15.0 ┆ NaN │
└──────┴─────┘
Polars內部使用一種名叫「"validity bitmap"」的技術,來儲存缺失值的資訊。雖然會耗費一點點儲存空間,可是大幅提升了判斷元素是否為缺失值及計算缺失值數量的效率。也就是說當使用pl.Expr.is_null()
或是pl.Expr.null_count()時,與其說是計算,Polars更像是在針對metadata進行查找並返回結果。
但是Polars並沒有針對NaN
儲存資訊,也就是說在使用pl.Expr.is_nan()
時,Polars會真正進行計算,不如pl.Expr.is_null()
般快速。
codepanda
Pandas經過多年開發後,其型別系統可以用眼花撩亂來形容。大致上可以分為v0時代的numpy型別、v1時代引入的nullable
型別及v2時代引入的pyarrow
型別。
以64bit整數為例,可以有下面三種表達方式:
int64
。int64[pyarrow]
。df_pd = pd.DataFrame({"v0_int64": [0]}, dtype="int64").assign(
v1_Int64=lambda df_: df_.v0_int64.astype({"v0_int64": "Int64"}),
v2_int64pyarrow=lambda df_: df_.v0_int64.astype(
{"v0_int64": "int64[pyarrow]"}
),
)
print(df_pd.dtypes)
v0_int64 int64
v1_Int64 Int64
v2_int64pyarrow int64[pyarrow]
dtype: object
我個人會建議如果是以v2在做開發的朋友,可以試試更有效率的int64[pyarrow]
型別。