これは何?
この qiita の URL は 2020-01-29 以前は pandas 1.0.0 (rc0) での pd.NA の特徴
として公開したが、 pandas 1.2.0 でのアップデートを反映した記事に 2020-01-30 に更新した。(記事内でのpandasは1.2.1にて検証している)
検証環境 は最後に。
Contents major update
- 2021-01-30: pandas 1.2.1 にて内容を更新。この記事
- 2020-01-13: pandas 1.0.0 (rc4) にて 初回公開。 gist に内容を退避
要約
-
pd.NA
は missing value の意味として登場。 -
pd.NA
が 使えるのは FloatingArray, IntegerArray, BooleanArray, StringArray -
pd.NA
導入により int class にも missing value が表現可能となった(不用意な float への変換がなくなった)。- float は Float64 (Float32) として pd.NA を扱うことができるようになったが。
-
pd.NA
は singleton object で 全ての data types と一貫性をもつ。 -
pd.NA
に対する比較演算子の返り値は全てpd.NA
(Julia のmissing
object, R のNA
) と同じ挙動) - 論理演算子での演算は、いわゆる three-valued logic に従う
-
pd.read_csv()
ではFloat64
,Int64
,string
,boolean
の指定により NA が認識される。個別の指定が面倒なため、convert_dtypes()
というメソッドが便利である。
data type
pandas 内に新たに NAType
という class が導入される。目的は missing value としての値を示す。
>>> import pandas as pd
>>> pd.NA
<NA>
>>> type(pd.NA)
<class 'pandas._libs.missing.NAType'>
pd.Series, pd.DataFrame では型を指定しないと object 型、明示するとその型として扱われる。なお Int64Dtype
という class は pandas 0.24 から導入された Nullable interger (An ExtensionDtype for int64 integer data. Array of integer (optional missing) values) である。 int64
ではなく Int64
と大文字で dtype の指定が必要なことに注意が必要。 技術的には Pandas Extension Arrays
の導入により ExtensionDType の利用が可能となった。
- https://pandas.pydata.org/pandas-docs/stable/reference/arrays.html
- https://dev.pandas.io/pandas-blog/pandas-extension-arrays.html
>>> pd.Series([pd.NA]).dtype
dtype('O') # O means Object
# dtype の指定は文字列エイリアスでも type そのものでもどちらでもいける。以下は文字列での指定。
>>> pd.Series([pd.NA], dtype="Float64").dtype
Float64Dtype()
>>> pd.Series([pd.NA], dtype="Int64").dtype
Int64Dtype()
>>> pd.Series([pd.NA], dtype="boolean").dtype
BooleanDtype
>>> pd.Series([pd.NA], dtype="string").dtype
StringDtype
NAType
の実装はこちら。
また、 Int32
, Int16
, Int8
, Float32
も pd.NA の扱いは可能である。取り扱うデータ範囲が確定している場合はこれらの指定によりメモリを節約できる(はず)。
>>> pd.Series([pd.NA], dtype="Int32")
0 <NA>
dtype: Int32
>>> pd.Series([pd.NA], dtype="Int16")
0 <NA>
dtype: Int16
>>> pd.Series([pd.NA], dtype="Int8")
0 <NA>
dtype: Int8
>>> pd.Series([pd.NA], dtype="Float32")
0 <NA>
dtype: Float32
pd.Series, pd.DataFrame での型
型の確定について、 int64
ではなく、大文字の Int64
で指定が必要。
>>> pd.Series([1, 2]) + pd.Series([pd.NA, pd.NA])
0 <NA>
1 <NA>
dtype: object
>>> pd.Series([1, 2]) + pd.Series([pd.NA, pd.NA], dtype="Int64")
0 <NA>
1 <NA>
dtype: Int64
int64
の指定は error となる。
>>> pd.Series([pd.NA], dtype="int64").dtype
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.9/site-packages/pandas/core/series.py", line 335, in __init__
data = sanitize_array(data, index, dtype, copy, raise_cast_failure=True)
File "/usr/local/lib/python3.9/site-packages/pandas/core/construction.py", line 480, in sanitize_array
subarr = _try_cast(data, dtype, copy, raise_cast_failure)
File "/usr/local/lib/python3.9/site-packages/pandas/core/construction.py", line 587, in _try_cast
maybe_cast_to_integer_array(arr, dtype)
File "/usr/local/lib/python3.9/site-packages/pandas/core/dtypes/cast.py", line 1723, in maybe_cast_to_integer_array
casted = np.array(arr, dtype=dtype, copy=copy)
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NAType'
各dtypeでの指定方法
>>> pd.Series([1, 2, pd.NA], dtype="Float64")
0 1.0
1 2.0
2 <NA>
dtype: Float64
>>> pd.Series([1, 2, pd.NA], dtype="Int64")
0 1
1 2
2 <NA>
dtype: Int64
>>> pd.Series([True, False, pd.NA], dtype="boolean")
0 True
1 False
2 <NA>
dtype: boolean
>>> pd.Series(["a", "b", pd.NA], dtype="string")
0 a
1 b
2 <NA>
dtype: string
なお、 Float64 では np.nan (float('nan')) は pd.NA に変換されて共存できない
>>> pd.Series([1, np.nan, float('nan'), None, pd.NA], dtype="Float64")
0 1.0
1 <NA>
2 <NA>
3 <NA>
4 <NA>
dtype: Float64
これらの pd.NA
として見なされる値を代入しても、 pd.NA
へ強制変換される。
x = pd.Series(range(5), dtype="Float64")
x[1] = np.nan
x[2] = float('nan')
x[3] = None
x[4] = pd.NA
>>> x
0 0.0
1 <NA>
2 <NA>
3 <NA>
4 <NA>
dtype: Float64
と書いたが、実は共存することはできる。ただしこの NaN を代入しても NA に変化されてしまう。 ~この辺りの経緯は issue#32256 で議論されているようが、深くは追えていないので謎である。~ このようにNaNが基本扱えないことは、 1.2.0 から意図した挙動であると大御所からコメントが確認できる。 issue#39039
>>> pd.Series([-1, 0, 1, pd.NA],dtype="Float64") / pd.Series([0, 0, 0, 0],dtype="Float64")
0 -inf
1 NaN
2 inf
3 <NA>
dtype: Float64
>>> x[0] = x[1]
>>> x
0 <NA>
1 NaN
2 inf
3 <NA>
dtype: Float64
boolean の挙動
boolean との演算の結果は Julia の missing
や R の NA
と同じ挙動。
>>> pd.Series([True, False, pd.NA], dtype="boolean") & True
0 True
1 False
2 <NA>
dtype: boolean
# dtypeをしない場合の挙動は、バグか意図か不明
>>> pd.Series([True, False, pd.NA]) & True
0 True
1 False
2 False
dtype: bool
>>> pd.Series([True, False, pd.NA], dtype="boolean") | True
0 True
1 True
2 True
dtype: boolean
>>> pd.NA & True
<NA>
>>> pd.NA & False
False
>>> pd.NA | True
True
>>> pd.NA | False
<NA>
NA
伝播 (propagate)
sum 演算の結果は NA
伝播 (propagate) するが、引数を指定しない pd.Series.sum()
は 0
として取り扱い propagate しない。 propagate 扱いするためには sum(skipna=False)
を指定する必要がある。 Int64型だけでなくobject型でもskipna=Falseの指定は動作する。
>>> sum([1, pd.NA])
<NA>
# pd.Series object
>>> pd.Series([1, pd.NA])
0 1
1 <NA>
dtype: object
>>> pd.Series([1, pd.NA]).sum()
1
>>> pd.Series([1, pd.NA]).sum(skipna=False)
<NA>
# pd.Series Int64
>>> pd.Series([1, pd.NA], dtype='Int64')
0 1
1 <NA>
dtype: Int64
>>> pd.Series([1, pd.NA], dtype='Int64').sum()
1
>>> pd.Series([1, pd.NA], dtype='Int64').sum(skipna=False)
<NA>
cumsum, cumprod の挙動は次の通り。なぜか Int64 時での cumsum() は skipna=True のデフォルト指定が反映されない。まった Float64 も dtype が object へ変更されてしまう。後日 issue を報告しておく。(即日 issue 立てました #39479 )
>>> pd.Series([1, 2, pd.NA, 4, pd.NA], dtype='Int64').cumsum()
0 1
1 3
2 <NA>
3 <NA>
4 <NA>
dtype: object
>>> pd.Series([1, 2, None, 4, float('nan')], dtype='Int64').cumsum(skipna=False)
0 1
1 3
2 <NA>
3 <NA>
4 <NA>
dtype: object
>>> pd.Series([1, 2, pd.NA, 4, pd.NA], dtype='Float64').cumsum()
0 1.0
1 3.0
2 3.0
3 7.0
4 7.0
dtype: object
>>> pd.Series([1, 2, pd.NA, 4, pd.NA], dtype='Int64').cumsum(skipna=False)
0 1
1 3
2 <NA>
3 <NA>
4 <NA>
dtype: object
pow 関数
累乗の扱いは R の NA_integer_
と整合的。 julia の挙動は謎い。
>>> pd.NA ** 0
1
>>> 1 ** pd.NA
1
>>> -1 ** pd.NA
-1
> R.version.string
[1] "R version 3.6.1 (2019-07-05)"
> NA_integer_ ^ 0L
[1] 1
> 1L ^ NA_integer_
[1] 1
> -1L ^ NA_integer_
[1] -1
julia> VERSION
v"1.3.1"
julia> missing ^ 0
missing
julia> 1 ^ missing
missing
julia> -1 ^ missing
missing
read_csv での指定
次の csv ファイルで実験。 (test.csv
)
X_float,X_int,X_bool,X_string
1.0,1,True,"a"
2.0,2,False,"b"
NA,NA,NA,"NA"
dtypeの指定無しでは、 pandas 0.25.3 の挙動と同じ。
>>> df1 = pd.read_csv("test.csv")
>>> df1
X_float X_int X_bool X_string
0 1.0 1.0 True a
1 2.0 2.0 False b
2 NaN NaN NaN NaN
>>> df1.dtypes
X_float float64
X_int float64
X_bool object
X_string object
dtype: object
NAとして認識するには、dtypeを指定する。
dtype に Int64
, string
は指定可能.
# dtype は 文字リテラルでなく次の type class でも可能。
# df2 = pd.read_csv("test.csv", dtype={'X_int': pd.Int64Dtype(), 'X_string': pd.StringDtype()})
>>> df2 = pd.read_csv("test.csv", dtype={'X_float': 'Float64', 'X_int': 'Int64', 'X_bool': 'boolean', 'X_string': 'string'})
>>> df2
X_float X_int X_bool X_string
0 1.0 1 True a
1 2.0 2 False b
2 <NA> <NA> <NA> <NA>
>>> df2.dtypes
X_float Float64
X_int Int64
X_bool boolean
X_string string
dtype: object
と書いたが、dtypeの指定をいちいち書くのは面倒な場合は pd.DataFrame.convert_dtypes()
というメソッドが非常に便利である。今後の定番になるであろう。
>>> df3 = pd.read_csv("test.csv").convert_dtypes()
>>> df3
X_float X_int X_bool X_string
0 1 1 True a
1 2 2 False b
2 <NA> <NA> <NA> <NA>
>>> df3.dtypes
X_float Int64
X_int Int64
X_bool boolean
X_string string
dtype: object
要約
-
pd.NA
は missing value の意味として登場。 -
pd.NA
が 使えるのは FloatingArray, IntegerArray, BooleanArray, StringArray -
pd.NA
導入により int class にも missing value が表現可能となった(不用意な float への変換がなくなった)。- float は Float64 (Float32) として pd.NA を扱うことができるようになった。
-
pd.NA
は singleton object で 全ての data types と一貫性をもつ。 -
pd.NA
に対する比較演算子の返り値は全てpd.NA
(Julia のmissing
object, R のNA
) と同じ挙動) - 論理演算子での演算は、いわゆる three-valued logic に従う
-
pd.read_csv()
ではFloat64
,Int64
,string
,boolean
の指定により NA が認識される。個別の指定が面倒なため、convert_dtypes()
というメソッドが便利である。
最後に
こういうマニアックな話が大好きな方、ぜひ justInCase へ遊びに来て下さい。https://www.wantedly.com/companies/justincase
参考URL
- https://pandas.pydata.org/docs/whatsnew/v1.0.0.html
- https://pandas.pydata.org/docs/whatsnew/v1.2.0.html
- https://dev.pandas.io/docs/user_guide/missing_data.html
- https://dev.pandas.io/docs/user_guide/gotchas.html#nan-integer-na-values-and-na-type-promotions
- https://dev.pandas.io/docs/user_guide/gotchas.html#why-not-make-numpy-like-r
- https://hackmd.io/@jorisvandenbossche/Sk0wMeAmB
- https://github.com/pandas-dev/pandas/blob/493363ef60dd9045888336b5c801b2a3d00e976d/pandas/_libs/missing.pyx#L335-L485
検証環境
docker上で確認した。
FROM python:3.9.1-slim-buster
WORKDIR /home
RUN pip install pandas==1.2.1
CMD ["/bin/bash"]
$ docker build -t pdna .
$ docker run -it --rm -v $(pwd):/home/ pdna
Inside Docker
root@98cfef126cbd:/home# cat /etc/issue
Debian GNU/Linux 10 \n \l
root@98cfef126cbd:/home# uname -a
Linux 98cfef126cbd 4.19.121-linuxkit #1 SMP Tue Dec 1 17:50:32 UTC 2020 x86_64 GNU/Linux
root@98cfef126cbd:/home# python -c "import pandas as pd; pd.show_versions()"
INSTALLED VERSIONS
------------------
commit : 9d598a5e1eee26df95b3910e3f2934890d062caa
python : 3.9.1.final.0
python-bits : 64
OS : Linux
OS-release : 4.19.121-linuxkit
Version : #1 SMP Tue Dec 1 17:50:32 UTC 2020
machine : x86_64
processor :
byteorder : little
LC_ALL : None
LANG : C.UTF-8
LOCALE : en_US.UTF-8
pandas : 1.2.1
numpy : 1.19.5
pytz : 2020.5
dateutil : 2.8.1
pip : 21.0
setuptools : 52.0.0
Cython : None
pytest : None
hypothesis : None
sphinx : None
blosc : None
feather : None
xlsxwriter : None
lxml.etree : None
html5lib : None
pymysql : None
psycopg2 : None
jinja2 : None
IPython : None
pandas_datareader: None
bs4 : None
bottleneck : None
fsspec : None
fastparquet : None
gcsfs : None
matplotlib : None
numexpr : None
odfpy : None
openpyxl : None
pandas_gbq : None
pyarrow : None
pyxlsb : None
s3fs : None
scipy : None
sqlalchemy : None
tables : None
tabulate : None
xarray : None
xlrd : None
xlwt : None
numba : None