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)
|
shape: (4, 1)
|
shape: (1, 2)
|
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)
|
shape: (1, 2)
|
shape: (5, 2)
|
shape: (1, 2)
|
shape: (1, 2)
|
shape: (5, 2)
|
coalesce
は複数の列に対して順番に処理を行い、最初に NULL
ではない値を取得します。以下のコードは、列 A
と B * 2
を順に確認し、NULL
ではない最初の値を新しい列 C
に収録します。どちらも 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: (1, 2)
|
shape: (4, 2)
|
shape: (1, 2)
|
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)
|
shape: (4, 2)
|
shape: (4, 2)
|
shape: (4, 2)
|
shape: (4, 2)
|
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
a | last | first |
---|---|---|
i64 | i64 | i64 |
1 | null | 1 |
3 | 1 | 2 |
null | 2 | 3 |
2 | 3 | null |
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),
)
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
の場合、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),
)
a | no_skip | skip |
---|---|---|
str | str | str |
"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)
|
shape: (4, 1)
|
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)
|
shape: (3, 2)
|
shape: (3, 3)
|
shape: (3, 3)
|
how
引数はfull
の場合は、状況は少し複数になります。
join_nulls=True
: 左側のNULLと右側のNULLを一致とみなしため、結果は4行になります。一致する行のキーはnull
と2
で、一致しない行は左側の3
と右側4
です。join_nulls=False
: 左側のNULLと右側のNULLを一致しない判断なので、結果は5行になります。一致する行のキーは2
だけで、一致しない行は左側のNULL
、3
と右側のNULL
、4
です。
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)
|
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,)
|
DataFrame.write_csv
メソッドのnull_value
引数は、CSVにNULL値を書き込む際のプレースホルダ(例えば、NaN
やNone
など)を指定します。
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
の場合、df2
のa
列の値を全部使用します。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
, read_csv_batched
などの関数のnull_values
引数は、CSVファイルを読み込む際に、どの値をNULLとして扱うかを指定します。通常、"null"
や"NaN"
などが指定されます。
pl.read_csv('data/output.csv', null_values=['NULL'])
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)
|
shape: (1, 2)
|
shape: (1, 2)
|
Inf
(Infinity)は、浮動小数点型の特殊な値で、数学的な無限を表します。PolarsにはInfを直接NULLに置き換える専用の演算式がありませんが、replace()
を使用して処理できます。
df.select(
r1=(1 / pl.col('A')).mean(),
r2=(1 / pl.col('A')).replace(np.inf, None).mean()
)
r1 | r2 |
---|---|
f64 | f64 |
inf | 0.611111 |