はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pandas番外編(strアクセサなど再発見)

Pandasのアクセサ、特にstrアクセサについては、シンタックスの外面がやや特殊に感じておおりました。

よって、挙動になれるまではどこまでOKなのかわかりにくいので、気持ちのどこかで利用をさけていたのですが、ある時期以降は宣言的にロジックを書けるので、やっぱり結構便利だなぁと思うところがあります。

f:id:azotar:20200501205327p:plain

ということで、strアクセサだけではないのですが、その類の「Pandas触り始めは避けてたけど、(雰囲気で使っているなど)慣れてくるとそれなりに使うアクセサ」などで、最近気になったものを雑多に集めてみました。

※例によって体系的・網羅的ではなくて、気まぐれマイチョイスです。

なお、以下の例は、すべて事前に次のコードを走行した後の例としています。

import pandas as pd
import io #StringIOを使うため
pesedo_csv = """
A,B,C,D,F,G
a,b,c,X1:Y:Z,2,10
d,e,f,X2:Y:Z,9,20
g,a,b,X3:Y:Z,1,30
"""

df = pd.read_csv(io.StringIO(pesedo_csv))

1. Series.strアクセサ

pandas.pydata.org

pandas.pydata.org

strアクセサは思った以上にいろいろできますが、特に良いなと思ったのは次のものです。

str.split(",").str.get(0)

df['D'].str.split(":").str.get(0)

説明しづらいので実際の例で。

In [86]: df                                                                                                                                                      
Out[86]: 
   A  B  C       D  F   G
0  a  b  c  X1:Y:Z  2  10
1  a  b  c  X2:Y:Z  9  20
2  a  b  c  X3:Y:Z  1  30

In [87]: df["D"].str.split(",").str.get(0)                                                                                                                          
Out[87]: 
0    X1:Y:Z
1    X2:Y:Z
2    X3:Y:Z
Name: D, dtype: object

知らずに同じことをしようとすると、最初簡単だと思ったのに思ったより手間となりがちな例。

df['D'].str.split(":").str[0] も可。

ちょっと正確な言い方ではありませんが、ベクトル化された関数みたいなシンタックスでやりたいことがダイレクトにできますね。

以降、タイプ量低減のため、df['D']などの書き方については、「df.D」の表記としているものがあります。

str.split(separator,expand=True)

df.D.str.split(":",expand=True)

In [100]: df                                                                                                                                                     
Out[100]: 
   A  B  C       D  F   G
0  a  b  c  X1:Y:Z  2  10
1  a  b  c  X2:Y:Z  9  20
2  a  b  c  X3:Y:Z  1  30

In [101]: df.D.str.split(":",expand=True)                                                                                                                        
Out[101]: 
    0  1  2
0  X1  Y  Z
1  X2  Y  Z
2  X3  Y  Z

やや基本に戻って str[要素番号の指定]

df.D.str[0:2]

In [88]: df.D.str[0:2]                                                                                                                                           
Out[88]: 
0    X1
1    X2
2    X3
Name: D, dtype: object

str.extract

str.splitより複雑な分割もできます。 (説明は略)

pandas.Series.str.extract — pandas 1.0.3 documentation

str.replace

名前のとおり置換

df.A.str.replace('a','aaa')

正規表現とかも適用できたはず。

In [66]: df.D.str.replace('X','XXX')                                                                                                                             
Out[66]: 
0    XXX1:Y:Z
1    XXX2:Y:Z
2    XXX3:Y:Z
Name: D, dtype: object

コラム: DataFrame.replace

DataFrameのreplaceもあります。該当する場合、全ての要素に適用されます。

df.replace('a','AAA')

デフォルトは完全一致なので注意。注意というかDataFrame全体に適用されることを考えると、自然な考え方だと思いますが。

str.contains

Booleanインデックスと組み合わせて力を発揮。

詳しい説明は割愛するが、DataFrame.queryでは難しい条件もできるかもしれない。

2. dtアクセサ

Datetime propertiesです。

pandas.pydata.org

時系列データをよく使う人ととそうでない人に分かれると思いますが、普段利用しない人は暗記は不要なものの、存在を知らないといざという時に損するかもしれません。

yearとかmonthはもちろん便利ですが、weekとかis_month_end(月終わりの日か)のようなis_XXXや曜日を判定するものはグループ化などその先のややこしい処理のハードルを下げてくれそうです。

In [81]: timetable = pd.DataFrame({ 
    ...:     'time': pd.to_datetime(['20160525 13:30:00.023', 
    ...:                             '20160526 13:30:00.023', 
    ...:                             '20160527 13:30:00.030', 
    ...:                             '20160528 13:30:00.041', 
    ...:                             '20160529 13:30:00.048', 
    ...:                             '20160531 13:30:00.049', 
    ...:                             '20170525 13:30:00.072', 
    ...:                             '20180625 13:30:00.075'])}, 
    ...:     columns=['time'])               

In [82]: timetable.time.dt.year                                                                                                                                  
Out[82]: 
0    2016
1    2016
2    2016
3    2016
4    2016
5    2016
6    2017
7    2018
Name: time, dtype: int64

In [83]: timetable.time.dt.week                                                                                                                                  
Out[83]: 
0    21
1    21
2    21
3    21
4    21
5    22
6    21
7    26
Name: time, dtype: int64
                                                                                                                    

In [85]: timetable.time.dt.is_month_end                                                                                                                          
Out[85]: 
0    False
1    False
2    False
3    False
4    False
5     True
6    False
7    False
Name: time, dtype: bool

3. Series.idxmax

値が最大となる行番号を戻す。

In [93]: df                                                                                                                                                      
Out[93]: 
   A  B  C       D  F   G
0  a  b  c  X1:Y:Z  2  10
1  a  b  c  X2:Y:Z  9  20
2  a  b  c  X3:Y:Z  1  30

In [94]: df.F.idxmax()                                                                                                                                           
Out[94]: 1


Fの値が9で「最大である行」のインデックス「1」が戻る。

よって、次のような使い方が可能。 i = df.F.idxmax() df.iloc[i]

また、ここでは詳しく述べないが、Series.applyなどで、最大値との比率などを計算する際にも使える。

4. 少し脱線してnumpyから少し

目的外使用かもしれないが、numpyは試験データ(DataFrame初期化)の半自動生成などに便利。

全て覚えておかなくても、その都度調べる価値ありだが、以下は覚えておいてよいかも。

import numpy as np

np.zeros(50)

np.ones(50)

np.empty(50)

5. DataFrameどおしの演算

DataFrame.gt

df1.gt(df2)

df1とdf2の対応する要素の比較

和(?)

In [95]: df + df                                                                                                                                                 
Out[95]: 
    A   B   C             D   F   G
0  aa  bb  cc  X1:Y:ZX1:Y:Z   4  20
1  aa  bb  cc  X2:Y:ZX2:Y:Z  18  40
2  aa  bb  cc  X3:Y:ZX3:Y:Z   2  60

In [96]:                         

文字列でもくっつけちゃう。

スカラー値をかける

df * 2

これはブロードキャストとしては自然かな。

同一かの比較

df + df == df * 2

In [96]: df + df == df * 2                                                                                                                                       
Out[96]: 
      A     B     C     D     F     G
0  True  True  True  True  True  True
1  True  True  True  True  True  True
2  True  True  True  True  True  True

In [97]:                             

↓ dfと同じ構造のTrue or Falseの DataFrameが戻る。 ビジネスロジックの実装時にはあまり使わないかもしれないが、データラングリングの途中でassertionに使うといったことはありそう。 あるいは全体的に「同じ」とか「違う」という場合のやや揺らぎを考慮した比較とかはありかも。

6. DataFrame.combine_first

df1.combine_first(df2)

下記で手厚い説明をされているのでそちらを参照。

qiita.com

値が取れない場合にデフォルト値を設定したいが、fillnaだと大まかすぎるので、初期化用のDataFrame(上記のdf2)を用意しておくというような用にに便利だとおもいました。

7. transform

ひとつの列から複数の演算の結果の値(DataFrame)を派生

df.transform([np.abs, lambda x: x + 1, np.abs])

↑一見意味不明かもですが、transformで何ができるかみてみてください。

In [97]: df                                                                                                                                                      
Out[97]: 
   A  B  C       D  F   G
0  a  b  c  X1:Y:Z  2  10
1  a  b  c  X2:Y:Z  9  20
2  a  b  c  X3:Y:Z  1  30

In [98]: df.transform([np.abs, lambda x: x + 1, np.abs])                                                                                                         
Out[98]: 
         F                          G                  
  absolute <lambda> absolute absolute <lambda> absolute
0        2        3        2       10       11       10
1        9       10        9       20       21       20
2        1        2        1       30       31       30

                                     

transformは、こんな↓感じのこともできる。 df.transform({'F': np.abs, 'G': [lambda x: x + 1, 'sqrt']})