14
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

pandas 1.2.0 新機能かいつまみ

Posted at

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_labelsTrueとなっているータフレームは今まで通りの挙動だが、これが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_labelsFalseにできず、やはり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ではかつてはNaNnp.nan)が欠損値のようなものとして扱われてきたが、「数ではない値、実数でない値、例外的な数値」NaNではなく、数値以外のデータ型に対しても一貫して使える「データなし、欠損値」NAが必要だ、的な主張があり、バージョン1.0.0から欠損値を表すNApd.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

NaNNAでは、比較演算のときの結果が異なる。

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
14
19
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
14
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?