NULL 処理#
NULL はデータにつきものです。1行に値が 1つ欠けるだけで、集約の結果も結合の結果も、型の判定まで変わってしまいます。Polars は NULL と NaN をはっきり区別し、「NULL に出会ったらどうするか」を決める引数も数多く用意しています。
この章では、まず NULL が計算に参加したときの規則を確認し、次に検出・削除・補完の関数を見て、最後に各メソッドに散らばっている NULL 関連の引数をまとめて確認します。
import polars as pl
import numpy as np
from helper.jupyter import row
NULL に関する計算#
Polars は欠損を NULL として扱い、値とは別に「どの行が NULL か」をビットマスクで管理しています。Python の None は、データフレームに変換するときに NULL になります。基本ルールは 2つです。
❶ 要素ごとの演算では、どちらかが NULL なら結果も NULL になります。
❷
sum()やmean()などの集計では、NULL は自動的に無視されます。
df = pl.DataFrame(dict(
A=[1, 2, None, None],
B=[5, None, 6, None],
))
df1 = df.select(A_plus_B=pl.col("A") + pl.col("B")) # ❶ NULL + 数値 は NULL
df2 = df.select(
A_sum=pl.col("A").sum(), # ❷ NULL は数えない
B_mean=pl.col("B").mean(),
)
row(df, df1, df2)
shape: (4, 2)
|
shape: (4, 1)
|
shape: (1, 2)
|
NULL を処理する関数と式#
NULL を扱うための関数と式をまとめます。
drop_nulls— NULL を含む行を落とします。列ごとに要素数が変わるため、表の形を保ちたいときは ❶ のようにimplode()でリストにまとめます。fill_null— NULL を指定した値で埋めます。null_count— 列ごとの NULL の個数を数えます。has_nulls— 列に NULL が含まれるかを判定します。is_null— 要素ごとに NULL かどうかを返します。is_not_null— 要素ごとに NULL でないかどうかを返します。
df = pl.DataFrame({"A": [1, 2, None, None, 5], "B": [None, 3, 5, None, 7]})
# 各列の NULL を削除してリスト列にする
df1 = df.select(pl.all().drop_nulls().implode()) # ❶
# NULL を 0 で埋める
df2 = df.with_columns(pl.all().fill_null(0))
# 列ごとの NULL 数
df3 = df.select(pl.all().null_count())
# 列に NULL が含まれるか
df4 = df.select(pl.all().has_nulls())
# 要素ごとに NULL かどうか
df5 = df.select(pl.all().is_null())
row(df, df1, df2, df3, df4, df5)
shape: (5, 2)
|
shape: (1, 2)
|
shape: (5, 2)
|
shape: (1, 2)
|
shape: (1, 2)
|
shape: (5, 2)
|
coalesce() は複数の候補を順に見て、最初に NULL でない値を返します。次のコードは列 A、B * 2 の順に確認し、どちらも NULL なら 100を使います。
df.with_columns(
C=pl.coalesce("A", pl.col("B") * 2, 100)
)
| A | B | C |
|---|---|---|
| i64 | i64 | i64 |
| 1 | null | 1 |
| 2 | 3 | 2 |
| null | 5 | 10 |
| null | null | 100 |
| 5 | 7 | 5 |
interpolate() は前後の値から NULL を補間します。method で補間のしかたを選びます。
線形補間(
method='linear'、既定)— 前後の値を直線でつなぎます。最近接補間(
method='nearest')— 最も近い既存の値を使います。
次のコードは、すべての列に両方を適用し、_interp と _nearest の接尾辞を付けます。
df.with_columns(
pl.all().interpolate().name.suffix("_interp"),
pl.all().interpolate(method="nearest").name.suffix("_nearest"),
)
| A | B | A_interp | B_interp | A_nearest | B_nearest |
|---|---|---|---|---|---|
| i64 | i64 | f64 | f64 | i64 | i64 |
| 1 | null | 1.0 | null | 1 | null |
| 2 | 3 | 2.0 | 3.0 | 2 | 3 |
| null | 5 | 3.0 | 5.0 | 2 | 5 |
| null | null | 4.0 | 6.0 | 5 | 7 |
| 5 | 7 | 5.0 | 7.0 | 5 | 7 |
同じ機能は DataFrame のメソッドとしても使えます。
DataFrame.drop_nulls()— NULL を含む行を削除します。DataFrame.fill_null()— 指定した値で NULL を埋めます。DataFrame.null_count()— 列ごとの NULL 数を返します。
row(
df.drop_nulls(),
df.fill_null(0),
df.null_count()
)
shape: (2, 2)
|
shape: (5, 2)
|
shape: (1, 2)
|
fill_null には strategy を指定すると、値ではなく列の統計や並び順から埋められます。limit を使えば、埋める個数を制限できます。
"forward"— 前の値で埋める(前方補完)"backward"— 次の値で埋める(後方補完)"min"/"max"— 列の最小値・最大値で埋める"mean"— 列の平均値で埋める"one"/"zero"— すべて 1 / 0にする
df = pl.DataFrame({
"A": [1.0, None, None, 4],
"B": [None, 2.0, None, None],
})
df_forward = df.fill_null(strategy="forward") # 前方補完
df_backward = df.fill_null(strategy="backward") # 後方補完
df_mean = df.fill_null(strategy="mean") # 平均で埋める
df_limit = df.fill_null(strategy="forward", limit=1) # 前方補完を 1 個まで
row(df, df_forward, df_backward, df_mean, df_limit)
shape: (4, 2)
|
shape: (4, 2)
|
shape: (4, 2)
|
shape: (4, 2)
|
shape: (4, 2)
|
NULL に関する引数#
演算式や DataFrame のメソッドには、NULL をどう扱うかを決める引数があります。ここでは代表的なものを、実際の出力とともに確認します。
sort 系の nulls_last は、NULL を末尾に置くかどうかを指定します。
df = pl.DataFrame({"a": [1, 3, None, 2]})
# nulls_last=True で NULL を末尾に、False で先頭に置く
df1 = df.with_columns(
last=pl.col("a").sort(nulls_last=False),
first=pl.col("a").sort(nulls_last=True),
)
df1
| a | last | first |
|---|---|---|
| i64 | i64 | i64 |
| 1 | null | 1 |
| 3 | 1 | 2 |
| null | 2 | 3 |
| 2 | 3 | null |
concat_str や ewm_* の ignore_nulls を True にすると、NULL を飛ばして計算します。
df = pl.DataFrame({
"a": [1, None, 2, 3],
"b": [None, 10, 20, 30],
})
df.select(
ignore=pl.concat_str(('a', 'b'), separator='-', ignore_nulls=True),
not_ignore=pl.concat_str(('a', 'b'), separator='-', ignore_nulls=False),
)
| ignore | not_ignore |
|---|---|
| str | str |
| "1" | null |
| "10" | null |
| "2-20" | "2-20" |
| "3-30" | "3-30" |
diff() の null_behavior は、最初の行の差分をどう扱うかを決めます。'drop' なら結果から外し、'ignore' なら NULL として残します。
df = pl.DataFrame({
"a": [10, 12, 11, None, 7, 10]
})
df1 = df.select(pl.col("a").diff(null_behavior="drop"))
df2 = df.select(pl.col('a').diff(null_behavior='ignore'))
row(df, df1, df2)
shape: (6, 1)
|
shape: (5, 1)
|
shape: (6, 1)
|
map_elements() の skip_nulls は、NULL をユーザー関数に渡すかどうかを決めます。False なら None として渡し、True なら呼び出し自体を飛ばします。
df = pl.DataFrame({
"a": ["a", "b", None, "c"]
})
def f(x):
if x is None:
return ":O"
else:
return x.upper()
df.with_columns(
no_skip=pl.col("a").map_elements(f, skip_nulls=False, return_dtype=pl.String),
skip=pl.col("a").map_elements(f, skip_nulls=True, return_dtype=pl.String),
)
| a | no_skip | skip |
|---|---|---|
| str | str | str |
| "a" | "A" | "A" |
| "b" | "B" | "B" |
| null | ":O" | null |
| "c" | "C" | "C" |
NumPy 配列からデータフレームを作るとき、nan_to_null=True を指定すると NaN を NULL に変換します。pl.from_numpy() や pl.from_pandas() にも同じ引数があります。pandas では NaN と None が混在しがちです。変換の前に、どちらへ統一するかを決めておきます。統一しないと、同じ列に「欠けている」と「数ではない」の 2つの状態が同居することになります。
data = np.array([1.0, 2.0, float("nan"), 4.0])
df1 = pl.DataFrame({"a":data})
df2 = pl.DataFrame({"a":data}, nan_to_null=True)
row(df1, df2)
shape: (4, 1)
|
shape: (4, 1)
|
DataFrame.equals() の null_equal は、NULL 同士を等しいとみなすかどうかを決めます。既定では NULL は NULL と等しくありません(SQL の意味論です)。ただし「2つの表の中身が同じか」を判定したいときは、NULL と NULL を同じと数えたいのが普通です。
df1 = pl.DataFrame({"a": [None, 2, 3]})
df2 = pl.DataFrame({"a": [None, 2, 3]})
print(f"{df1.equals(df2, null_equal=True) = }") # NULL を等しいとみなす
print(f"{df1.equals(df2, null_equal=False) = }") # NULL は不一致
df1.equals(df2, null_equal=True) = True
df1.equals(df2, null_equal=False) = False
DataFrame.join() の nulls_equal は、NULL を結合キーとして一致とみなすかどうかを決めます。Polars 2.0 で引数名が join_nulls から nulls_equal に変わりました。
df1 = pl.DataFrame({
"key": [None, 2, 3],
"val1": ["A", "B", "C"],
})
df2 = pl.DataFrame({
"key": [None, 2, 4],
"val2": ["x", "y", "z"],
})
df_join_null = df1.join(df2, on="key", how="left", nulls_equal=True)
df_not_join_null = df1.join(df2, on="key", how="left", nulls_equal=False)
row(df1, df2, df_join_null, df_not_join_null)
shape: (3, 2)
|
shape: (3, 2)
|
shape: (3, 3)
|
shape: (3, 3)
|
how="full" にすると、結果が少し複雑になります。
nulls_equal=True— 左右の NULL を一致とみなすので、結果は 4行です。一致するのはキーが NULL と2の行、一致しないのは左の3と右の4です。nulls_equal=False— NULL 同士は一致しないので、結果は 5行です。一致するのは2だけで、左の NULL・3と右の NULL・4がそれぞれ残ります。
df_join_null = df1.join(df2, on="key", how="full", nulls_equal=True)
df_not_join_null = df1.join(df2, on="key", how="full", nulls_equal=False)
row(df1, df2, df_join_null, df_not_join_null)
shape: (3, 2)
|
shape: (3, 2)
|
shape: (4, 4)
|
shape: (5, 4)
|
mean_horizontal や sum_horizontal の ignore_nulls は、行方向の計算で NULL を無視するかどうかを決めます。
df = pl.DataFrame({
"a": [1, None, 3],
"b": [4, 5, None]
})
df1 = df.mean_horizontal(ignore_nulls=True)
df2 = df.mean_horizontal(ignore_nulls=False)
row(df, df1, df2)
shape: (3, 2)
|
shape: (3,)
|
shape: (3,)
|
write_csv() の null_value は、NULL を書き出すときの文字列を指定します。既定では空文字で書かれ、読み戻すと NULL になります。ただし外部システムとデータを交換するときは、相手が明示的な NULL や NaN を要求することがあります。
df = pl.DataFrame({"a": [1, None, 3]})
# NULL を "NULL" という文字列で書き出す
df.write_csv("data/output.csv", null_value="NULL")
with open("data/output.csv") as f:
print(f.read())
a
1
NULL
3
DataFrame.update() の include_nulls は、更新元の NULL で上書きするかどうかを決めます。True なら NULL も含めてすべての値を使い、False なら NULL 以外だけを使います。
df1 = pl.DataFrame({
"a": [None, 2, 3, 4],
"b": [10, 20, 30, 40],
})
df2 = pl.DataFrame({
"a": [1, None, 3, 10]
})
df_inc_nulls = df1.update(df2, include_nulls=True)
df_not_inc_nulls = df1.update(df2, include_nulls=False)
row(df1, df2, df_inc_nulls, df_not_inc_nulls)
shape: (4, 2)
|
shape: (4, 1)
|
shape: (4, 2)
|
shape: (4, 2)
|
read_csv / scan_csv の null_values は、読み込み時にどの文字列を NULL とみなすかを指定します。CSV でよく現れる NA、N/A、null はすべて指定しておきます。指定し忘れると文字列として読み込まれ、そのあとの型変換がすべて崩れます。
pl.read_csv('data/output.csv', null_values=['NULL'])
| a |
|---|
| i64 |
| 1 |
| null |
| 3 |
NaN 処理#
NaN(Not a Number)は浮動小数点型の特殊な値で、0/0 のように計算結果が定義できない場合を表します。
NULL とは別物です。NULL は「値が無い」、NaN は「値は浮動小数点数だが、数ではない」という状態です。is_null() では NaN を検出できませんし、sum() も NaN を飛ばしません。1つの NaN で列全体の合計が NaN になります。
import numpy as np
df = pl.DataFrame(dict(
A=[0.0, 1.0, 2.0, None, 3.0], # NULL を含む
B=[0.0, 1.0, 2.0, np.nan, 3.0], # NaN を含む
))
# そのまま合計すると、NaN は合計を壊し、NULL は無視される
df1 = df.select(pl.all().sum())
# NaN を NULL に変えてから合計する
df2 = df.select(pl.all().fill_nan(None).sum())
row(df, df1, df2)
shape: (5, 2)
|
shape: (1, 2)
|
shape: (1, 2)
|
Inf(無限大)も浮動小数点型の特殊な値です。NULL に直接置き換える専用の式はないので、replace() で変換します。
df.select(
r1=(1 / pl.col("A")).mean(), # Inf が混じると平均が壊れる
r2=(1 / pl.col("A")).replace(np.inf, None).mean(), # Inf を NULL にしてから平均
)
| r1 | r2 |
|---|---|
| f64 | f64 |
| inf | 0.611111 |