NULL処理#

データフレームの操作では、NULL(欠損値)の存在がデータ分析や計算結果に影響を及ぼすことがあります。適切なNULL処理を行うことで、データの品質を保ち、信頼性の高い結果を得ることが可能です。本章では、NULL値の検出、除去、置換、および活用方法について解説します。

import polars as pl
import numpy as np
from helper.jupyter import row

NULLに関する計算#

Polarsでは、データの欠損を表すために、データとは別にNULL情報を管理するビットマスクを使用しています。Pythonのデータをデータフレームに変換する際、None は自動的にNULLに変換されます。以下の例を用いて、NULLに関する計算の基本ルールを説明します。

  • ❶ 各要素に対する演算では、いずれかの要素が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')) #❶
df2 = df.select( 
    A_sum=pl.col('A').sum(),  #❷
    B_mean=pl.col('B').mean()
) 
row(df, df1, df2)
shape: (4, 2)
AB
i64i64
15
2null
null6
nullnull
shape: (4, 1)
A_plus_B
i64
6
null
null
null
shape: (1, 2)
A_sumB_mean
i64f64
35.5

NULLを処理する関数と演算式#

Polarsでは、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)
AB
i64i64
1null
23
null5
nullnull
57
shape: (1, 2)
AB
list[i64]list[i64]
[1, 2, 5][3, 5, 7]
shape: (5, 2)
AB
i64i64
10
23
05
00
57
shape: (1, 2)
AB
u32u32
22
shape: (1, 2)
AB
boolbool
truetrue
shape: (5, 2)
AB
boolbool
falsetrue
falsefalse
truefalse
truetrue
falsefalse

coalesce は複数の列に対して順番に処理を行い、最初に NULL ではない値を取得します。以下のコードは、列 AB * 2 を順に確認し、NULL ではない最初の値を新しい列 C に収録します。どちらも NULL の場合は、デフォルト値として 100 を設定します。

df.with_columns(
    C=pl.coalesce('A', pl.col('B') * 2, 100)
)
shape: (5, 3)
ABC
i64i64i64
1null1
232
null510
nullnull100
575

interpolate は列中の NULL 値を前後の値で補間して埋めます。method 引数を使用して、以下の方法を選択できます:

  1. 線形補間 (method='linear':デフォルト)
    前後の値を直線で補間します。

  2. 最近接補間 (method='nearest')
    最も近い既存の値を使用して補間します。

以下のコードは、すべての列で線形補間と最近接補間を適用し、それぞれ _interp_nearest のサフィックスを付けた新しい列を作成します。

df.with_columns(
    pl.all().interpolate().name.suffix('_interp'),
    pl.all().interpolate(method='nearest').name.suffix('_nearest')
)
shape: (5, 6)
ABA_interpB_interpA_nearestB_nearest
i64i64f64f64i64i64
1null1.0null1null
232.03.023
null53.05.025
nullnull4.06.057
575.07.057

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: (1, 2)
AB
i64i64
23
shape: (4, 2)
AB
i64i64
10
23
05
30
shape: (1, 2)
AB
u32u32
12

fill_null メソッドと計算式では、strategy 引数を使用してNULL値を埋める方法を指定できます。また、limit 引数を指定することで、NULLを埋める回数を制限することが可能です。

strategy には以下のオプションがあります:

  • "forward": 前の値でNULLを埋めます(前方補完)。

  • "backward": 次の値でNULLを埋めます(後方補完)。

  • "min": 列の最小値でNULLを埋めます。

  • "max": 列の最大値でNULLを埋めます。

  • "mean": 列の平均値でNULLを埋めます。

  • "one": 値をすべて1に置き換えます。

  • "zero": 値をすべて0に置き換えます。

limit は、NULL値を埋める最大回数を指定します。これにより、全てのNULLを埋めずに制限をかけることができます。

df = pl.DataFrame({
    "A": [1.0, None, None, 4],
    "B": [None, 2.0, None, None]
})

# 前方補完でNULLを埋める
df_forward = df.fill_null(strategy="forward")

# 後方補完でNULLを埋める
df_backward = df.fill_null(strategy="backward")

# 平均値でNULLを埋める
df_mean = df.fill_null(strategy="mean")

# 前方補完を使用し、最大1つのNULLのみ埋める
df_limit = df.fill_null(strategy="forward", limit=1)

row(df, df_forward, df_backward, df_mean, df_limit)
shape: (4, 2)
AB
f64f64
1.0null
null2.0
nullnull
4.0null
shape: (4, 2)
AB
f64f64
1.0null
1.02.0
1.02.0
4.02.0
shape: (4, 2)
AB
f64f64
1.02.0
4.02.0
4.0null
4.0null
shape: (4, 2)
AB
f64f64
1.02.0
2.52.0
2.52.0
4.02.0
shape: (4, 2)
AB
f64f64
1.0null
1.02.0
null2.0
4.0null

NULLに関する引数#

Polarsの演算式やDataFrameメソッドにおけるNULLに関する引数は、データ処理時にNULL(欠損値)をどのように扱うかを制御するために使用されます。以下に各引数の意味と具体例を示します。

sort, sort_by, arg_sort, arg_sort_byなどのnulls_last引数を使用して、NULL値をソート時に最後に配置するかどうかを指定します。

df = pl.DataFrame({
  "a": [1, 3, None, 2]
})

df1 = df.with_columns(
    last=pl.col('a').sort(nulls_last=False),
    first=pl.col('a').sort(nulls_last=True)
)
df1
shape: (4, 3)
alastfirst
i64i64i64
1null1
312
null23
23null

concat_str, ewm_mean, ewm_std, ewm_varなどの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),
)
shape: (4, 2)
ignorenot_ignore
strstr
"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)
a
i64
10
12
11
null
7
10
shape: (5, 1)
a
i64
2
-1
null
null
3
shape: (6, 1)
a
i64
null
2
-1
null
null
3

map_elementsでのskip_nulls引数は、NULL値の要素をスキップするかどうかを指定します。Falseの場合、NULL値を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),
)
shape: (4, 3)
ano_skipskip
strstrstr
"a""A""A"
"b""B""B"
null":O"null
"c""C""C"

DataFrame()でNumPyの配列からデータフレームを作成するとき、nan_to_null引数で、NaN値をNULLに変換するかどうかを指定します。polars.from_pandas()も同様の引数があります。

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)
a
f64
1.0
2.0
NaN
4.0
shape: (4, 1)
a
f64
1.0
2.0
null
4.0

DataFrame.equalsメソッドのnull_equal引数は、NULL値を等しいものと見なすかどうかを指定します。Trueに設定すると、NULLをNULLとして等しいと扱います。

df1 = pl.DataFrame({
  "a": [None, 2, 3]
})

df2 = pl.DataFrame({
  "a": [None, 2, 3]
})

print(f'{df1.equals(df2, null_equal=True) = }')
print(f'{df1.equals(df2, null_equal=False) = }')
df1.equals(df2, null_equal=True) = True
df1.equals(df2, null_equal=False) = False

DataFrame.joinメソッドのjoin_nulls引数は、NULL値を結合条件として扱うかどうかを指定します。Trueに設定すると、NULL値を一致とみなして結合します。

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", join_nulls=True)
df_not_join_null = df1.join(df2, on="key", how="left", join_nulls=False)

row(df1, df2, df_join_null, df_not_join_null)
shape: (3, 2)
keyval1
i64str
null"A"
2"B"
3"C"
shape: (3, 2)
keyval2
i64str
null"x"
2"y"
4"z"
shape: (3, 3)
keyval1val2
i64strstr
null"A""x"
2"B""y"
3"C"null
shape: (3, 3)
keyval1val2
i64strstr
null"A"null
2"B""y"
3"C"null

how引数はfullの場合は、状況は少し複数になります。

  • join_nulls=True: 左側のNULLと右側のNULLを一致とみなしため、結果は4行になります。一致する行のキーはnull2で、一致しない行は左側の3と右側4です。

  • join_nulls=False: 左側のNULLと右側のNULLを一致しない判断なので、結果は5行になります。一致する行のキーは2だけで、一致しない行は左側のNULL3と右側のNULL4です。

df_join_null = df1.join(df2, on="key", how="full", join_nulls=True)
df_not_join_null = df1.join(df2, on="key", how="full", join_nulls=False)

row(df1, df2, df_join_null, df_not_join_null)
shape: (3, 2)
keyval1
i64str
null"A"
2"B"
3"C"
shape: (3, 2)
keyval2
i64str
null"x"
2"y"
4"z"
shape: (4, 4)
keyval1key_rightval2
i64stri64str
null"A"null"x"
2"B"2"y"
nullnull4"z"
3"C"nullnull
shape: (5, 4)
keyval1key_rightval2
i64stri64str
nullnullnull"x"
2"B"2"y"
nullnull4"z"
3"C"nullnull
null"A"nullnull

mean_horizontalsum_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)
ab
i64i64
14
null5
3null
shape: (3,)
mean
f64
2.5
5.0
3.0
shape: (3,)
mean
f64
2.5
null
null

DataFrame.write_csvメソッドのnull_value引数は、CSVにNULL値を書き込む際のプレースホルダ(例えば、NaNNoneなど)を指定します。

df = pl.DataFrame({
  "a": [1, None, 3],
})

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値を更新対象に含めるかどうかを指定します。次の例では、include_null=Trueの場合、df2a列の値を全部使用します。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)
ab
i64i64
null10
220
330
440
shape: (4, 1)
a
i64
1
null
3
10
shape: (4, 2)
ab
i64i64
110
null20
330
1040
shape: (4, 2)
ab
i64i64
110
220
330
1040

read_csv, scan_csv, read_csv_batchedなどの関数のnull_values引数は、CSVファイルを読み込む際に、どの値をNULLとして扱うかを指定します。通常、"null""NaN"などが指定されます。

pl.read_csv('data/output.csv', null_values=['NULL'])
shape: (3, 1)
a
i64
1
null
3

NaN処理#

NaN(Not a Number)は、浮動小数点型特有の値で、計算エラーや未定義の結果を表すために使用されます。Polarsでは、NaNを効率的に処理するための方法が提供されています。以下の例を使用して、NaN処理の基本を説明します。

import numpy as np
import polars as pl

df = pl.DataFrame(dict(
    A=[0.0, 1.0, 2.0, None, 3.0],  # 列AにはNULLも含まれる
    B=[0.0, 1.0, 2.0, np.nan, 3.0]  # 列Bには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)
AB
f64f64
0.00.0
1.01.0
2.02.0
nullNaN
3.03.0
shape: (1, 2)
AB
f64f64
6.0NaN
shape: (1, 2)
AB
f64f64
6.06.0

Inf(Infinity)は、浮動小数点型の特殊な値で、数学的な無限を表します。PolarsにはInfを直接NULLに置き換える専用の演算式がありませんが、replace() を使用して処理できます。

df.select(
    r1=(1 / pl.col('A')).mean(),
    r2=(1 / pl.col('A')).replace(np.inf, None).mean()
)
shape: (1, 2)
r1r2
f64f64
inf0.611111