Help us understand the problem. What is going on with this article?

pandas 1.0とdaskのちょっと細かい調査

背景

2020/01/29にpandas 1.0.0がリリースされました!パチパチ
2020/02/14現在は、1.0.1です。

個人的には、下記の変更点が重要ポイントかなと思ってます。
- pandas独自のNA
- String型の対応強化(Experimental)

んで。

僕は分析時には、下記のライブラリとpandasを一緒に使うことが多いです。

特にdaskのpandas1.0対応状況や、その他の細かな振る舞いについて整理しようかなと思っています。
daskのバージョンは2020/02/14現在2.10.1です。

intakeに関しては、daskが対応してくれれば問題ないっしょ、って思っています。
(daskの処理待ち時間が暇というのもある。)

スクリーンショット 2020-02-14 午前11.20.52.png

気になっていること

  • daskはpandas.NAをちゃんと使えんの?(ver 1.0関連)
  • daskはdtype: stringをちゃんと使えんの?(ver 1.0関連)
  • I/O、特にfastparquetpandas.NAとかdtype: stringとかちゃんと入出力出来んの?(ver 1.0関連)
  • そういや、daskはdtype: categoricalちゃんと使えんの?(その他)

Tom Augspurgerさんという方が、鬼のようにpandas1.0対応してくれてるっぽいし、今のところ期待感大。
スクリーンショット 2020-02-14 午後2.26.08.png

結果

結果だけ知りたい人用。

  • daskは、pandas.NAを含んでいても、四則演算・文字列演算はOK
  • daskは、Int64, string型などのカスタム型でset_indexできない
  • pandas/dask両方とも、pandas.NAを含むboolean列でインデックスフィルタできない
  • daskは、apply(meta='string')としてもobject型になってしまうが、astype('string')で復活させられる。
  • dask内でpandas.Categoricalを使う場合、フィルタ・集計はいけそう
  • dask DataFrameに新しいCategorical列を追加する時はastypeを使う必要がありそう
  • daskは、Int64string型をto_parquetできない(engine=fastparquetの場合)。

調査環境構築

とりあえずクリーンな検証環境を用意します。
OSはmacOS Catalina 10.15.2です。

python versionに関してpandasはminimumしか設定していないのと、daskもpython3.8対応とかしてるっぽいので、3.7.4なら問題ないでしょう。

依存関係に関しては、minimum versions for dependenciesを一通り入れておく。ただし、fastparquetとpyArrowはmac上で共存できない問題が昔あったので、念のためpyArrowは入れないでおく。使わんし。

検証作業はjupyterlab上でやります。

pyenv virtualenv 3.7.4 pandas100
pyenv shell pandas100
pip install -r requirements.txt
requirements.txt
pandas==1.0.1
dask[complete]==2.10.1
fastparquet==0.3.3

jupyterlab==1.2.6

numpy==1.18.1
pytz==2019.3
python-dateutil==2.8.1
numexpr==2.7.1
beautifulsoup4==4.8.2
gcsfs==0.6.0
lxml==4.5.0
matplotlib==3.1.3
numba==0.48.0
openpyxl==3.0.3
pymysql==0.9.3
tables==3.6.1
s3fs==0.4.0
scipy==1.4.1
sqlalchemy==1.3.13
xarray==0.15.0
xlrd==1.2.0
xlsxwriter==1.2.7
xlwt==1.3.0

dask vs pandas1.0

まずは、pd.NAの確認。

  • dtype: int
  • dtype: Int64(...Categorical)
  • dtype: float
  • dtype: string
  • dtype: boolean
  • dtype: datetime64[ns]
  • dtype: timedelta64[ns]
  • dtype: object

それぞれでのpd.NAの振る舞いを確認

s=... type(s.loc[3])
pandas.Series([1,2,3,None], dtype='int') TypeError
pandas.Series([1,2,3,pandas.NA], dtype='int') TypeError
pandas.Series([1,2,3,None], dtype='Int64') pandas._libs.missing.NAType
pandas.Series([1,2,3,None], dtype='float') numpy.float64
pandas.Series([1,2,3,pandas.NA], dtype='float') TypeError
pandas.Series([1,2,3,None], dtype='Int64').astype('float') numpy.float64
pandas.Series(['a', 'b', 'c' ,None], dtype='string') pandas._libs.missing.NAType
pandas.Series(['a', 'b', 'c' ,None], dtype='object').astype('string') pandas._libs.missing.NAType
pandas.Series([True, False, True ,None], dtype='boolean') pandas._libs.missing.NAType
pandas.Series([1, 0, 1 ,None], dtype='float').astype('boolean') pandas._libs.missing.NAType
pandas.Series(pandas.to_datetime(['2020-01-01', '2020-01-02', '2020-01-03', None])) pandas._libs.tslibs.nattype.NaTType
pandas.Series(pandas.to_timedelta(['00:00:01', '00:00:02', '00:00:03', None])) pandas._libs.tslibs.nattype.NaTType
pandas.Series([object(), object(), object(), None], dtype='object') NoneType
pandas.Series([object(), object(), object(), pandas.NA], dtype='object') pandas.Series([object(), object(), object(), pandas.NA], dtype='object')

まとめると、

  • dtype intは、pandas.NAにならない(そのままTypeError)
  • dtype Int64,string,booleanはpandas.NAになる。
  • dtype floatは、numpy.NaNになる
  • dtype datetime64, timedelta64は、NATになる
  • dtype objectは、Noneを自動でpandas.NAに変換しない

これをdask.dataframe.from_pandasするとどうなるか調査する。

>>> import pandas
>>> import dask.dataframe

>>> df = pandas.DataFrame({'i': [1,2,3,4], 
...                        'i64': pandas.Series([1,2,3,None], dtype='Int64'), 
...                        's': pandas.Series(['a', 'b', 'c' ,None], dtype='string'), 
...                        'f': pandas.Series([1,2,3,None], dtype='Int64').astype('float')})

>>> ddf = dask.dataframe.from_pandas(df, npartitions=1)
>>> df
    i   i64 s   f
0   1   1   a   1.0
1   2   2   b   2.0
2   3   3   c   3.0
3   4   <NA>    <NA>    NaN

>>> ddf
Dask DataFrame Structure:
    i   i64 s   f
npartitions=1               
0   int64   Int64   string  float64
3   ... ... ... ...

なるほど、Int64は、dask上でもInt64となっている。stringも同様。

>>> # Int64に対する(整数)演算
>>> df.i64 * 2
0       2
1       4
2       6
3    <NA>
Name: i64, dtype: Int64

>>> (ddf.i64 * 2).compute()
0       2
1       4
2       6
3    <NA>
Name: i64, dtype: Int64

Int64->Int64の処理はちゃんと動く。

>>> # Int64に対する(浮動少数点数)演算
>>> df.i64 - df.f
0    0.0
1    0.0
2    0.0
3    NaN
dtype: float64

>>> (ddf.i64 - ddf.f).compute()
0    0.0
1    0.0
2    0.0
3    NaN
dtype: float64

Int64->float64の処理もちゃんと動く。

>>> # pandas.NAを含むInt64列でset_index
>>> df.set_index('i64')
    i   s   f   i64_result  i64-f
i64                 
1   1   a   1.0 2   0.0
2   2   b   2.0 4   0.0
3   3   c   3.0 6   0.0
NaN 4   <NA>    NaN <NA>    NaN

>>> ddf.set_index('i64').compute()
TypeError: data type not understood

>>> # pandas.NAがなかったらどうなるか
>>> ddf['i64_nonnull'] = ddf.i64.fillna(1)
... ddf.set_index('i64_nonnull').compute()
TypeError: data type not understood

えー!daskはInt64列でset_indexできないんかい!
もちろんpandasはできる。

>>> # pandas.NAを含むstring列でset_index
>>> df.set_index('s')
    i   i64 f
s           
a   1   1   1.0
b   2   2   2.0
c   3   3   3.0
NaN 4   <NA>    NaN

>>> ddf.set_index('s').compute()
TypeError: Cannot perform reduction 'max' with string dtype

>>> # pandas.NAがなかったらどうなるか
>>> ddf['s_nonnull'] = ddf.s.fillna('a')
... ddf.set_index('s_nonnull')
TypeError: Cannot perform reduction 'max' with string dtype

stringもできんな。これは(僕の使い方では)まだ使えないなぁ。

# .str関数を試してみる
>>> df.s.str.startswith('a')
0     True
1    False
2    False
3     <NA>
Name: s, dtype: boolean

>>> ddf.s.str.startswith('a').compute()
0     True
1    False
2    False
3     <NA>
Name: s, dtype: boolean

ふむ、これは動くと。

>>> # pandas.NAを含むboolean列でフィルタ
>>> df[df.s.str.startswith('a')]
ValueError: cannot mask with array containing NA / NaN values

>>> # pandas.NAがダメなのかな?
>>> df['s_nonnull'] = df.s.fillna('a')
... df[df.s_nonnull.str.startswith('a')]
    i   i64 s   f   i64_nonnull s_nonnull
0   1   1   a   1.0 1   a
3   4   <NA>    <NA>    NaN 1   a

>>> ddf[ddf.s.str.startswith('a')].compute()
ValueError: cannot mask with array containing NA / NaN values

>>> ddf['s_nonnull'] = ddf.s.fillna('a')
... ddf[ddf.s_nonnull.str.startswith('a')].compute()
    i   i64 s   f   i64_nonnull s_nonnull
0   1   1   a   1.0 1   a
3   4   <NA>    <NA>    NaN 1   a
>>> ddf[ddf.s.str.startswith('a')].compute()

え!!!pandas.NA含むとフィルタできないの?
これはダメでしょ!

>>> # applyでmeta='Int64'指定してみる
>>> ddf['i10'] = ddf.i.apply(lambda v: v * 10, meta='Int64')
>>> ddf
Dask DataFrame Structure:
    i   i64 s   f   i64_nonnull s_nonnull   i10
npartitions=1                           
0   int64   Int64   string  float64 Int64   string  int64
3   ... ... ... ... ... ... ...

>>> # applyでmeta='string'指定してみる
>>> ddf['s_double'] = ddf.s.apply(lambda v: v+v, meta='string')
Dask DataFrame Structure:
    i   i64 s   f   i64_nonnull s_nonnull   i10 s_double
npartitions=1                               
0   int64   Int64   string  float64 Int64   string  int64   object
3   ... ... ... ... ... ... ... ...

>>> # astype('string')してみる
>>> ddf['s_double'] = ddf['s_double'].astype('string')
>>> ddf
Dask DataFrame Structure:
    i   i64 s   f   i64_nonnull s_nonnull   i10 s_double
npartitions=1                               
0   int64   Int64   string  float64 Int64   string  int64   string
3   ... ... ... ... ... ... ... ...

meta=で指定する場合、反映されないのか。。。
astypeで復活させられるけど、面倒だな。。。

結果

  • 演算はOK
  • daskでは、Indexとしては使えない(pandas.NA対応している型がそもそも使えないから)
  • pandas/dask両方で、filterできない!
  • .apply(meta='string')などしても無視される。astypeしないとだめ。

dask vs pandas.Categorical

pandasでCategoricalを調査するために、今回はCategoricalDtypeを使用した方法を使用します。
基本的なCategoricalDtypeの使い方は、

  1. categoriesとorderedを指定してインスタンス化
  2. dtype=CategoricalDtypeインスタンスとして、pandas.Seriesをインスタンス化

だと思っています。以下サンプルコード

>>> # まずは、CategoricalDtypeを作る
>>> int_category = pandas.CategoricalDtype(categories=[1,2,3,4,5], 
...                                        ordered=True)
>>> int_category
CategoricalDtype(categories=[1, 2, 3, 4, 5], ordered=True)

>>> int_category.categories
Int64Index([1, 2, 3, 4, 5], dtype='int64')

>>> # こんな感じでpandas.Seriesを作る
>>> int_series = pandas.Series([1,2,3], dtype=int_category)
>>> int_series
0    1
1    2
2    3
dtype: category
Categories (5, int64): [1 < 2 < 3 < 4 < 5]

>>> # 生成時なら、カテゴリに無い値をNaNに変換してくれる
>>> int_series = pandas.Series([1,2,3,6], dtype=int_category)
>>> int_series
0      1
1      2
2      3
3    NaN
dtype: category
Categories (5, int64): [1 < 2 < 3 < 4 < 5]

>>> # 生成後は怒られる
>>> int_series.loc[3] = 10
ValueError: Cannot setitem on a Categorical with a new category, set the categories first

次に、dask上でCategoricalを使ってみる。

>>> import pandas
>>> import dask.dataframe
>>> # pandas.DataFrameを生成
>>> df = pandas.DataFrame({'a': pandas.Series([1, 2, 3, 1, 2, 3], dtype=int_category), 
...                        'b': pandas.Series([1, 2, 3, 1, 2, 3], dtype='int64')})
>>> df
    a   b
0   1   1
1   2   2
2   3   3
3   1   1
4   2   2
5   3   3

>>> # dask.dataframe.DataFrameに変換
>>> ddf = dask.dataframe.from_pandas(df, npartitions=1)
>>> ddf
Dask DataFrame Structure:
    a   b
npartitions=1       
0   category[known] int64
5   ... ...

とりあえず、categoricalのままdask化することができた。

# pandasでは新しいカテゴリ値の追加は御法度
>>> df.loc[2, 'a'] = 30
ValueError: Cannot setitem on a Categorical with a new category, set the categories first

# daskでは、Categoricalとか関係なく、そもそもアサインできない
>>> ddf.loc['a', 3] = 10
TypeError: '_LocIndexer' object does not support item assignment

# pandasでは、カテゴリ値の演算も御法度
>>> df.a * 2
TypeError: unsupported operand type(s) for *: 'Categorical' and 'int'

# daskでも、カテゴリ値の演算は御法度
>>> ddf.a * 2
TypeError: unsupported operand type(s) for *: 'Categorical' and 'int'

# daskのapplyで、metaとして指定してみる
>>> ddf['c'] = ddf.a.apply(lambda v: v, meta=int_category)
Dont know how to create metadata from category

# daskのapplyで、meta='category'にしたら頑張ってくれるか?
>>> ddf['c'] = ddf.a.apply(lambda v: v, meta='category')
>>> ddf.dtypes
a    category
b       int64
c      object
dtype: object

>>> # 中身との整合性が取れているかチェックしてみる
>>> ddf.compute().dtypes
a    category
b       int64
c    category
dtype: object

>>> # astypeしてみる
>>> ddf['c'] = ddf.c.astype(int_category)
>>> ddf
Dask DataFrame Structure:
    a   b   c
npartitions=1           
0   category[known] int64   category[known]
5   ... ... ...

なるほど。カテゴリの制約部分は維持されるが、.apply(meta=)とかすると、daskのdtype管理がバグる。
astypeで復活させることは可能だが、面倒だな。。。
フィルタだけなら使えそうってところですかね。

# 集計処理してみる
>>> ddf.groupby('a').b.mean().compute()
a
1    1.0
2    2.0
3    3.0
4    NaN
5    NaN
Name: b, dtype: float64

# Index扱いされることで、型は壊れてしまわないか?
Dask DataFrame Structure:
a   b
npartitions=1       
category[known] float64
... ...
Dask Name: reset_index, 34 tasks

ふむ、集計には対応している感じか。

結果

  • pandas.Categoricalをdaskで使うとき、フィルタと集計は問題なさそう
  • daskのDataFrameに新しくCategoricalな列を追加したい時はastypeを使うべし

to_parquet vs pandas1.0

>>> # まずはpandas.DataFrameを生成
>>> df = pandas.DataFrame(
    {
        'i64': pandas.Series([1, 2, 3,None], dtype='Int64'),
        'i64_nonnull': pandas.Series([1, 2, 3, 4], dtype='Int64'),
        's': pandas.Series(['a', 'b', 'c',None], dtype='string'),
        's_nonnull': pandas.Series(['a', 'b', 'c', 'd'], dtype='string'),
    }
)
>>> df
    i64 i64_nonnull s   s_nonnull
0   1   1   a   a
1   2   2   b   b
2   3   3   c   c
3   <NA>    4   <NA>    d

>>> # dask.dataframe.DataFrameに変換
>>> ddf = dask.dataframe.from_pandas(df, npartitions=1)
>>> ddf
Dask DataFrame Structure:
    i64 i64_nonnull s   s_nonnull
npartitions=1               
0   Int64   Int64   string  string
3   ... ... ... ...

とりあえず、to_parquetしてみる。

>>> ddf.to_parquet('test1', engine='fastparquet')
ValueError: Dont know how to convert data type: Int64

まじか。。。予想はしてたが。。。
Int64はダメでもstringはいけるかも。。。

>>> ddf.to_parquet('test2', engine='fastparquet')
ValueError: Dont know how to convert data type: string

ダメでした。

結果

  • Int64, stringはto_parquet不可。

おわりに

いかがだったでしょうか?
多分、最後まで読みきってこのコメントを読んでいる人は誰もいないと思います。
投稿を分けるべきでしたかね。

pandas 1.0どやねんと思っている人の役に立てたら嬉しいです。

ではまたそのうち。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした