pandas 1.2.0 新機能かいつまみ
pandas 1.2.0 がリリースされたので、気になった新機能をメモ
興味のない内容や細かい変更修正はスルーしたので、すべての変更はこちら
行名・列名に重複を許さない設定
前提
pandasのIndex
は重複したラベルを持つことができる。つまり、データフレームおよびシリーズの行名や列名は一意でなくても良い。
例えば以下のデータフレームdf_dlabel
は、行名が'a'
の行が2つ、列名が'B'
の列が2つある。
df_dlabel = pd.DataFrame(np.arange(9).reshape(3, 3),
index=list('aab'), columns=list('ABB'))
df_dlabel
A | B | B | |
---|---|---|---|
a | 0 | 1 | 2 |
a | 3 | 4 | 5 |
b | 6 | 7 | 8 |
データこねくりまわし民にとって、これが許されていることは時に不自然に思われる。例えばSQLの主キーは基本的に重複を許さない。
行名が一意でないと不都合になる例
pandasのデータフレームはスライスや.loc
等で列名を指定すると、普通はシリーズが返ってくる。しかし重複した列名だと返ってくるのはデータフレームである。このように、戻り結果が曖昧なのはよくないことである。
df_dlabel['A']
df_dlabel['B']
# df_dlabel['A'] -> pd.Series a 0 a 3 b 6 Name: A, dtype: int32 # df_dlabel['B'] -> pd.DataFrame B B a 1 2 a 4 5 b 7 8
同様に、.loc
等で行名と列名を同時に指定したときや、.at
を用いた場合、通常返ってくるのはスカラー値であるが、名前が重複しているとシリーズやデータフレームが返ってくる。
df_dlabel.at['b', 'A']
df_dlabel.at['b', 'B']
df_dlabel.at['a', 'B']
# df_dlabel.at['b', 'A'] -> int 6 # df_dlabel.at['b', 'B'] -> pd.Series B 7 B 8 Name: b, dtype: int32 # df_dlabel.at['a', 'B'] -> pd.DataFrame B B a 1 2 a 4 5
pandasには行名・列名が一意であることが前提の関数・メソッドが存在しており、それに直面するとエラーが出る(あるいは運が悪ければバグる)。
以下は引数に重複した列名を指定した場合にエラーが出る例。
df_dlabel.merge(df_dlabel, on='A')
df_dlabel.merge(df_dlabel, on='B')
# df_dlabel.merge(df_dlabel, on='A') うまくいく A B_x B_x B_y B_y 0 0 1 2 1 2 1 3 4 5 4 5 2 6 7 8 7 8 # df_dlabel.merge(df_dlabel, on='B') だめ ValueError: The column label 'B' is not unique.
以下は重複した列名を持つデータフレームであれば、引数にそれを指定していなくてもエラーが出る例。
df_dlabel.reindex(list('AC'))
ValueError: cannot reindex from a duplicate axis
以下は重複した列名があると、結果は自動で判断されて、エラーでなく警告が出る例。
df_dlabel.to_dict()
UserWarning: DataFrame columns are not unique, some columns will be omitted. {'A': {'a': 3, 'b': 6}, 'B': {'a': 5, 'b': 8}}
新たなる挙動
データフレームのフラグ(.flags
属性により確認可)にallows_duplicate_labels
フラグが追加された(.flags
自体も今回追加されたものだが)。
df_dlabel = (pd.DataFrame(np.arange(9).reshape(3, 3),
index=list('abc'), columns=list('ABC'))
.set_flags(allows_duplicate_labels=True))
df_dlabel.flags.allows_duplicate_labels
df_ulabel = (pd.DataFrame(np.arange(9).reshape(3, 3),
index=list('abc'), columns=list('ABC'))
.set_flags(allows_duplicate_labels=False))
df_ulabel.flags.allows_duplicate_labels
# df_dlabel True # df_ulabel False
allows_duplicate_labels
がTrue
となっているータフレームは今まで通りの挙動だが、これがFalse
のデータフレームは行名・列名が一意であることが保証され、重複を許さない。重複ラベル禁止データフレームに対して行名・列名が重複するような操作を行おうとすると、DuplicateLabelError
が発生する(ということになっているが、別のエラーが出たり、暗黙的に操作に失敗したりすることもあるようだ)。
df_dlabel.reindex(list('aab'))
df_ulabel.reindex(list('aab'))
# df_dlabel できる A B C a 0 1 2 a 0 1 2 b 3 4 5 # df_ulabel できない DuplicateLabelError: Index has duplicates.
なお、データフレームのフラグは上にあるように.set_flags()
メソッドで変更可能。なお、すでに行名・列名が重複しているデータフレームはallows_duplicate_labels
をFalse
にできず、やはりDuplicateLabelError
となる。
(pd.DataFrame(np.arange(9).reshape(3, 3),
index=list('aab'), columns=list('ABB'))
.set_flags(allows_duplicate_labels=False))
DuplicateLabelError: Index has duplicates.
欠損値許容浮動小数データ型
pandasではかつてはNaN
(np.nan
)が欠損値のようなものとして扱われてきたが、「数ではない値、実数でない値、例外的な数値」NaN
ではなく、数値以外のデータ型に対しても一貫して使える「データなし、欠損値」NA
が必要だ、的な主張があり、バージョン1.0.0から欠損値を表すNA
(pd.NA
)が導入された。
これにしたがって今回、少し先立って存在していた欠損値許容整数データ型(Int64Dtype
等)に追従する形で、欠損値許容浮動小数データ型(Float64Dtype
等)が作られた。
dtype
引数に'Float64'
を指定すると欠損値許容浮動小数データ型になる('Float32'
等も存在)。'Int64'
と同様、一文字目が大文字であることで通常の浮動小数データ型と区別される。
欠損値許容浮動小数データ型ではNaN
のかわりにNA
が用いられる。
s_float = pd.Series([0, 1, np.nan, np.nan], dtype='float64')
s_nfloat = pd.Series([0, 1, np.nan, pd.NA], dtype='Float64')
# float64 0 0.0 1 1.0 2 NaN 3 NaN dtype: float64 # Float64 0 0.0 1 1.0 2 <NA> 3 <NA> dtype: Float64
NaN
とNA
では、比較演算のときの結果が異なる。
s_float < 0
s_nfloat < 0
# float64 0 False 1 False 2 False 3 False dtype: bool # Float64 0 False 1 False 2 <NA> 3 <NA> dtype: boolean
データフレーム連結時などにおけるインデックスの名前の維持
Index
オブジェクトには名前(name
属性)がある。一般には「インデックスの列名」と認識されているものである(特に.set_index()
で特定の列をインデックスに設定した場合はよりそう認識するであろう)。
以下の例では'idx'
という名前を持つインデックスを作成している。
df_n = pd.DataFrame(np.arange(6).reshape(3, 2), columns=list('AB'),
index=pd.Index(list('abc'), name='idx'))
df_n
idx | A | B |
---|---|---|
a | 0 | 1 |
b | 2 | 3 |
c | 4 | 5 |
データフレームを結合する時に、インデックスの名前が同じであっても、インデックスが食い違いのような感じになっているもの(?)が結合されると名前が消えてしまっていたらしい。
ct = pd.concat([df_n.iloc[:2, :1], df_n.iloc[1:, 1:]], axis=1)
ct
A | B | |
---|---|---|
a | 0 | nan |
b | 2 | 3 |
c | nan | 5 |
これが修正され、名前がなるべく保存されるようになった。(明らかにバグ修正なのになぜか新機能として紹介されている)
applymap()
に欠損値を無視するオプション追加
pd.DataFrame.applymap()はデータフレームの全ての要素に、要素毎に特定の関数を適用するメソッド。
df_str = pd.DataFrame([['where', 'why'], ['when', 'who']], dtype='string')
df_str.applymap(len)
0 | 1 | |
---|---|---|
0 | 5 | 3 |
1 | 4 | 3 |
欠損値NA
には関数を適用させずに、そのまま欠損値を伝播させてほしいと思うことがある。
df_str = pd.DataFrame([['where', 'why'], ['when', pd.NA]], dtype='string')
df_str.applymap(len)
TypeError: object of type 'NAType' has no len()
今までは関数自体に「NA
だったらNA
を返す」などの条件分岐を設定するか、欠損値を伝播させるオプションを持つpd.Series.map()
をシリーズ毎に適用させる必要があった。
df_str.apply(lambda s: s.map(len, na_action='ignore'))
0 | 1 | |
---|---|---|
0 | 5 | 3 |
1 | 4 | <NA> |
applymap()
にもna_action
引数が追加され、'ignore'
を渡すことで欠損値は伝播されるようになった。
df_str.applymap(len, na_action='ignore')
0 | 1 | |
---|---|---|
0 | 5 | 3 |
1 | 4 | <NA> |
オブジェクトデータ型Index
の乗算・除算
pandasのインデックスは、シリーズ同様、各要素がすべて数値ならばデータ型が'object'
であっても問題なく加算・減算ができる。
pd.Series([0, 1, 2], dtype='object') + 2
pd.Index([0, 1, 2], dtype='object') + 2
# pd.Series([0, 1, 2], dtype='object') + 2 0 2 1 3 2 4 dtype: object # pd.Index([0, 1, 2], dtype='object') + 2 Int64Index([2, 3, 4], dtype='int64')
しかし、乗算・除算はエラーが出てしまっていた。これはシリーズとは異なる挙動であった。
pd.Series([0, 1, 2], dtype='object') * 2
pd.Index([0, 1, 2], dtype='object') * 2
# pd.Series([0, 1, 2], dtype='object') * 2 0 0 1 2 2 4 dtype: object # pd.Index([0, 1, 2], dtype='object') * 2 TypeError: cannot perform __mul__ with this index type: Index
これが修正された。
pd.Index([0, 1, 2], dtype='object') * 2
Int64Index([0, 2, 4], dtype='int64')
.explode()
メソッドが集合型に対応
pd.DataFrame.explode()
およびpd.Series.explode()
は、リストやタプルを要素として持つ場合に、それらを崩すメソッド。
s_pack = pd.Series(['abc', list('abc'), tuple('abc')])
s_pack
0 abc 1 [a, b, c] 2 (a, b, c) dtype: object
s_pack.explode()
0 abc 1 a 1 b 1 c 2 a 2 b 2 c dtype: object
いままで集合型には対応していなかった(.explode()
を適用してもそのままだった)が、セットも崩されるようになった。
pd.Series(['abc', list('abc'), set('abc')]).explode()
0 abc 1 a 1 b 1 c 2 c 2 b 2 a dtype: object
.lookup()
メソッドは非推奨
pd.DataFrame.lookup()
とはなにかというと、
np.random.seed(0)
df_map = pd.DataFrame(np.random.randn(10, 3), columns=list('ABC'))
np.random.seed(0)
df_table = (pd.DataFrame({'col': list('ABC')}).iloc[np.random.randint(0, 3, 7)]
.assign(idx=np.random.randint(0, 10, 7)).reset_index(drop=True))
df_map
df_table
↓こういう対応表的なテーブルがあって
A | B | C | |
---|---|---|---|
0 | 1.76405 | 0.400157 | 0.978738 |
1 | 2.24089 | 1.86756 | -0.977278 |
2 | 0.950088 | -0.151357 | -0.103219 |
3 | 0.410599 | 0.144044 | 1.45427 |
4 | 0.761038 | 0.121675 | 0.443863 |
5 | 0.333674 | 1.49408 | -0.205158 |
6 | 0.313068 | -0.854096 | -2.55299 |
7 | 0.653619 | 0.864436 | -0.742165 |
8 | 2.26975 | -1.45437 | 0.0457585 |
9 | -0.187184 | 1.53278 | 1.46936 |
↓こういうデータがあるときに、
col | idx | |
---|---|---|
0 | A | 7 |
1 | B | 6 |
2 | A | 8 |
3 | B | 8 |
4 | B | 1 |
5 | C | 6 |
6 | A | 7 |
↓こういうことができるメソッドである。
df_map.lookup(df_table['idx'], df_table['col'])
array([ 0.6536186 , -0.85409574, 2.26975462, -1.45436567, 1.86755799, -2.55298982, 0.6536186 ])
わざわざこんなメソッドいらなくね、ということで廃止される見込みとなった模様。上の例では、.lookup()
を使わない場合、例えば以下のような方法がある。
df_map.unstack()[df_table.set_index(['col', 'idx']).index]
df_map.unstack()[zip(df_table['col'], df_table['idx'])]
df_map.unstack()[df_table.to_records(index=False).tolist()]
df_map.unstack()[df_table.itertuples(index=False)]
# -> pd.Series
df_map.to_numpy()[df_table['idx'], df_map.columns.get_indexer(df_table['col'])]
# -> np.ndarray