背景
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の処理待ち時間が暇というのもある。)
気になっていること
- daskは
pandas.NA
をちゃんと使えんの?(ver 1.0関連) - daskは
dtype: string
をちゃんと使えんの?(ver 1.0関連) - I/O、特にfastparquetは
pandas.NA
とかdtype: string
とかちゃんと入出力出来んの?(ver 1.0関連) - そういや、daskは
dtype: categorical
ちゃんと使えんの?(その他)
Tom Augspurgerさんという方が、鬼のようにpandas1.0対応してくれてるっぽいし、今のところ期待感大。
結果
結果だけ知りたい人用。
- 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は、
Int64
、string
型を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
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の使い方は、
- categoriesとorderedを指定してインスタンス化
- 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どやねんと思っている人の役に立てたら嬉しいです。
ではまたそのうち。