LoginSignup
16
15

More than 3 years have passed since last update.

pandas 1.2.0+ での pd.NA の特徴

Last updated at Posted at 2020-01-13

これは何?

この 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

検証環境

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
16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15