タグ「Pandas」が付けられているもの

[Pandas] groupby.aggのnested renamingの代替手段

In [1]:
import numpy as np
import pandas as pd

np.random.seed(9)
df = pd.DataFrame({
    '組': np.random.randint(3, size=20),
    '身長': np.random.randint(950, 1050, size=20) / 10,
    '地域': np.random.randint(5, size=20)})
df['組'] = df['組'].map({0: 'もも', 1: 'さくら', 2: 'ばら'})
df['地域'] = df['地域'].map({0: 'A町', 1: 'B町',  2: 'C町',  3: 'D町',  4: 'E町'}) 
df.head()
Out [1]:
身長 地域
0 ばら 101.0 B町
1 もも 100.9 A町
2 ばら 103.8 D町
3 さくら 102.4 C町
4 ばら 100.6 B町

こういうDataFrameがあり、組ごとに、
・身長の最低値と最高値
・A町の子が含まれているか、B町の子が含まれているか
を求めたいとする。
あまりいい例題では無いが、筆者が仕事で実際に必要になった処理と等価な、他にましな例題を思い付かなかった。

筆者は当初、df.groupby('組')['地域'].agg(lambda x: ...)のようにして集計結果を1列ずつ求め、後で結合していたのだが、先週、1回のgroupby.agg()でできる、次のような書き方があることを知った。

In [2]:
df.groupby('組')[['身長', '地域']].agg({
    '身長': {
        '最低身長': np.min,
        '最高身長': np.max
    },
    '地域': {
        'A町の子あり': lambda x: any(x == 'A町'),
        'B町の子あり': lambda x: any(x == 'B町')
    }})
Out [2]:
最低身長 最高身長 A町の子あり B町の子あり
さくら 95.9 104.9 True False
ばら 100.6 103.8 False True
もも 95.1 104.3 True False

同じ列に複数の集約関数を適用し、しかもそれぞれの結果の列に任意の列名を付与できるのである。
これは便利、と思って早速これを使うように書き直して、ローカルPCで動作確認して別PCにコピーして実行すると、

SpecificationError: nested renamer is not supported
というエラーになってしまった。

調べてみると、上のdict-of-dictを渡す書き方(nested renamingというらしい)はPandas v0.20.0でdeprecatedとされ、v1.0で廃止されたらしい。
What's new in 1.0.0より:
Removed support for nested renaming in DataFrame.aggregate(), Series.aggregate(), core.groupby.DataFrameGroupBy.aggregate(), ...
ローカルPCのPandasはv0.25.3だったので、nested renamingが動いた。

それでは代わりの方法は無いのかと思って探すと、"named aggregation"が推奨と書かれているのを見つけた。
What's new in 0.25.0より:

Named aggregation is the recommended replacement for the deprecated "dict-of-dicts" approach to naming the output of column-specific aggregations
他に、aggに列と関数のリストだけのdictを与えて、後で列名をrenameする方法もあるが、通常はaggに渡す関数名が結果の列名になるのに対し、lambda関数を渡すと列名が勝手に付けられるので、面倒なことになる。

Named aggregationを使うと、上のv1.0でエラーになったコードは次のように書ける。

In [3]:
df.groupby('組').agg(
    最低身長=('身長', np.min),
    最高身長=('身長', np.max),
    A町の子あり=('地域', lambda x: any(x == 'A町')),
    B町の子あり=('地域', lambda x: any(x == 'B町')))
Out [3]:
最低身長 最高身長 A町の子あり B町の子あり
さくら 95.9 104.9 True False
ばら 100.6 103.8 False True
もも 95.1 104.3 True False

列名をクォーテーションマークで括ったり括らなかったりするのが統一感に欠けるが、得られる結果が少しわかりやすくなったと思う。それから、前のコードでは[['身長', '地域']]でやっていた、aggに渡す前に列を絞るのが不要になった(絞らないとnested renamingでは列がMultiIndexになってしまう)ので、すっきりしたと感じる。

In [1]:
import pandas as pd
df = pd.DataFrame({'name': ['a', 'b', 'c', 'd'] * 3,
                               'value': range(12),
                               'flag': [1, 0, 0, 0, 1, 0] * 2})
df
Out [1]:
name value flag
0 a 0 1
1 b 1 0
2 c 2 0
3 d 3 0
4 a 4 1
5 b 5 0
6 c 6 1
7 d 7 0
8 a 8 0
9 b 9 0
10 c 10 1
11 d 11 0

こういうDataFrameがあり、'name'でgroupbyして、'flag'に1つでも1があるグループの、'flag'が1の行がグループ内の'value'の平均、それ以外の行は0という列(この例ではインデックスが0と4の所が'a'の平均、6と10の所が'c'の平均、それ以外は0という列)を追加したいとする。
大体そういう感じのことをtransformでやりたかったが、スマートなやり方がわからず、悩んでいる。

目的の列をグループ毎に返す関数を、transformの代わりにapplyを使って、

In [2]:

def func(groupdf):
    ret = pd.Series(0, index=groupdf.index)
    if any(groupdf['flag']):
        ret[groupdf['flag'] == True] = groupdf['value'].mean()
    return ret
    
df.groupby('name').apply(func)

又は

In [3]:

df.groupby('name').apply(lambda x: x['value'].mean() * x['flag'] * any(x['flag']))

Out [3]:

name    
a     0     4.0
      4     4.0
      8     0.0
b     1     0.0
      5     0.0
      9     0.0
c     2     0.0
      6     6.0
      10    6.0
d     3     0.0
      7     0.0
      11    0.0
Name: flag, dtype: float64

というように作ることができたのだが、[2]のfuncも[3]のlambda関数も、transformに与えると、'flag'という列が無いというエラーになる。
applyなら呼び出される関数に複数列のDataFrameが渡されるので複数列を参照しながら計算ができるが、aggregateやtransformだと呼び出される関数に1列分のSeriesしか渡されないので、複数列を参照しながら計算ができない。

もし、flagが1の行だけがグループの平均という条件を外し、'flag'に1つでも1があるグループはグループ内の'value'の平均、それ以外の行は0という列で良い、つまりグループ内は全て同じ値になるなら、Webでサンプルコードがいくつか見つかり、大きく分けて2つの方法が見つかった。
1つは、列毎にtransformした結果を組み合わせて何とかするという方法である。

In [4]:

grouped = df.groupby('name')
df['ave'] = grouped['value'].transform(np.mean) * grouped['flag'].transform(any)
df
Out [4]:
(省略)

もう1つは、applyを使ってグループ毎に計算した結果をmergeする方法である。

In [5]:

_ = df.groupby('name').apply(lambda x: x['value'].mean() if any(x['flag']) else 0).rename('ave')
df = df.merge(_, how='left', on='name')
df
Out [5]:
name value flag ave
0 a 0 1 4
1 b 1 0 0
2 c 2 0 6
3 d 3 0 0
4 a 4 1 4
5 b 5 0 0
6 c 6 1 6
7 d 7 0 0
8 a 8 0 4
9 b 9 0 0
10 c 10 1 6
11 d 11 0 0

前者は列毎にtransformした後で何とかなる場合しか使えないのに対し、後者は複数の列を参照しながら計算できるので、後者の方が汎用的だと思う。
速度面では、前者は中間データを作成して処理時間がかかりがちなtransformを複数回実行するので不利なように思えたが、筆者のJupyter Notebookの%timeitで計測した限り、DataFrameのサイズを10,000倍とかにしても、処理時間は大差なかった。

元のやりたいことについては、前者(In [4]の例)を応用すると、次のようなのができた。

In [6]:

grouped = df.groupby('name')
df['ave'] = grouped['value'].transform(np.mean) * grouped['flag'].transform(any) * df['flag']
df
Out [6]:
name value flag ave
0 a 0 1 4.0
1 0 0 0 0.0
2 0 0 0 0.0
3 0 0 0 0.0
4 a 4 1 4.0
5 0 0 0 0.0
6 c 6 1 6.0
7 0 0 0 0.0
8 0 0 0 0.0
9 0 0 0 0.0
10 c 10 1 6.0
11 0 0 0 0.0

しかし、transformを2回使うし、式が場当たり的で汎用的でないのが不満である。
後者(In [5]の例)を応用すると、次のようにするしか思い付かない。

In [7]:

_ = df.groupby('name').apply(lambda x: x['value'].mean() if any(x['flag']) else 0).rename('ave')
df = df.merge(_, how='left', on='name')
df[df['flag'] == 0] = 0
df
Out [7]:
(Out [6] と同じ)

これも、後のboolean indexing部分はグループを無視して処理しているので、そのようにできない時は同じようにできないし、これによって処理時間が大幅に伸びるし、transformの出番と思われるのにtransformを使ってないのが不満である。

area age positive
0 京都 17 False
1 大阪 15 True
2 京都 4 False
3 大阪 9 True
4 大阪 10 True
... ... ... ...
45 京都 16 True
46 大阪 3 False
47 大阪 2 False
48 奈良 10 True
49 京都 13 False

これは下記のプログラムの中で生成される架空のデータだが、このようなデータの地域別、年齢層(5歳未満、5歳以上、10歳以上、15歳以上)毎の人数の分布を、陽性と陰性とに分けて比較できるように、ヒートマップを並べようとして、次のようなプログラムを書いた。

In [1]:
import numpy as np
import pandas as pd

# サンプルデータ作成
np.random.seed(0)
df = pd.DataFrame({'area': [['京都', '大阪', '奈良'][i] for i in np.random.randint(3, size=50)],
                   'age': np.random.randint(20, size=50),
                   'positive': np.random.randint(2, size=50).astype(bool)})
df.loc[df['age'] < 5, 'positive'] = False  # 5歳未満の陽性は無しとする

# 年齢のビニング(区分け)
df['range'] = pd.cut(df['age'], bins=[0, 5, 10, 15, 20], labels=['5歳未満', '5歳以上', '10歳以上', '15歳以上'], right=False)

# 陽性、陰性の地域-年齢区分の分布を比較
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_negative = df[df.positive == False].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)

# ヒートマップを並べて描画
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(8, 2))
plt.subplot(121)
sns.heatmap(df_positive, cmap='Reds', cbar=False, annot=True)
plt.title("陽性")
plt.xlabel("")
plt.ylabel("")
plt.subplot(122)
sns.heatmap(df_negative, cmap='Blues', cbar=False, annot=True)
plt.title("陰性")
plt.xlabel("")
plt.ylabel("")
plt.subplots_adjust(wspace=0.4)
plt.show()

コード内のコメントに書いているように、5歳未満で陽性の人がいないものとする。
すると、次のように、陽性の5歳未満の行が抜けて、行の数が合わなくなってしまった。

Out [1]:

求める結果は、次のものである。

これが出るようにコードを修正しようとしたが、スマートな方法がわからなかった。

陽性のヒートマップの元になっているDataFrameを見ると、5歳未満の行が無い。

In [2]:
df_positive
Out [2]:
area 京都 大阪 奈良
range
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

しかし、このDataFrameのインデックスを見ると、データ型がカテゴリ変数で、そのカテゴリーには'5歳未満'が存在する。

In [3]:
df_positive.index
Out [3]:
CategoricalIndex(['5歳以上', '10歳以上', '15歳以上'], categories=['5歳未満', '5歳以上', '10歳以上', '15歳以上'], ordered=True, name='range', dtype='category')

であればこのインデックスを期待するものに書き換えれば良いのではないかと思って、次のようにインデックスのデータをカテゴリ変数のカテゴリーと同じになるように reindexしてみると、確かに望み通りの結果になった。

In [4]:
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_positive = df_positive.reindex(index=pd.CategoricalIndex(data=df_positive.index.categories, categories=df_positive.index.categories, ordered=True), fill_value=0)
df_positive
Out [4]:
area 京都 大阪 奈良
range
5歳未満 0 0 0
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

しかし、何かやりたいことに対してコードが冗長だし、ordered=Trueまで面倒を見ないといけないなど、手間がかかり過ぎている感じがする。
次のようにインデックスに欠損が無いDataFrameを別に作ってインデックスをコピーする方が、コードがシンプルだし、考慮すべきことが少なくて楽だが、これだとプログラムの動作が冗長である。

In [5]:
df_all = df.pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', aggfunc='size', fill_value=0)
df_positive = df_positive.reindex(index=df_all.index, fill_value=0)
df_positive
Out [5]:
area 京都 大阪 奈良
range
5歳未満 0 0 0
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

pivot_tableしてからreindexするのではなく、pivot_tableでインデックスに欠損を生じないようにできないかと思って調べると、dropnaという引数があり、次のように aggfunc='count', dropna=False とすればできることがわかった。

In [6]:
df_positive = df[df.positive == True].pivot_table(index='range', columns='area', values='positive', aggfunc='count', fill_value=0, dropna=False)
df_positive
Out [6]:
area 京都 大阪 奈良
range
5歳未満 0 0 0
5歳以上 2 2 2
10歳以上 0 4 3
15歳以上 2 2 1

Pandasのpivotとpivot_tableの違いについて

この間、Pandasの勉強をしていると、PandasのDataFrameクラスにはpivotとpivot_tableという2つのメソッドがあることに気付かず、違う方を使って自分で打ち込んだサンプルコードが動かなかったり、helpを読んで混乱することが度々あった。
pivotとpivot_tableが違うことに気付いた時にあまりにも悔しかったので、pivotとpivot_tableの違いに関してメモしておく。

以下、Jupyter Notebookで書いたコードと出力を使って記述する。

In [1]:
df = pd.DataFrame({'date': ['{}:{:02d}:{:02d}'.format(6, 30, s) for s in (2, 2, 2, 3, 3, 3, 4, 4, 4)],
                    'variable': ['A', 'B', 'C'] * 3,
                    'value': list('abcdefghi')})

df
Out [1]:
date variable value
0 6:30:02 A a
1 6:30:02 B b
2 6:30:02 C c
3 6:30:03 A d
4 6:30:03 B e
5 6:30:03 C f
6 6:30:04 A g
7 6:30:04 B h
8 6:30:04 C i

こういう、いわゆるlongフォーマットの時系列データを以下のwideフォーマットに変形することを考える。(筆者は最近実際にやることがあった)

In [2]:
df.pivot(index='date', columns='variable', values='value')
Out [2]:
variable A B C
date
6:30:02 a b c
6:30:03 d e f
6:30:04 g h i

上ではpivotを使ったが、pivot_tableを使っても同じことができる。
但し、pivot_tableはグループ毎(2次元の表にまとめられたセル毎)に集約関数が適用され、defaultの関数はnp.sumで、この例のようにセルの値が文字列だと適用できず、"DataError: No numeric types to aggregate"というエラーになる為、何か文字列を受け取れる関数を指定する必要がある。
(下記のようにaggfunc='first'とすると、DataFrameGroupBy.firstが用いられると思われる)

In [3]:
df.pivot_table(index='date', columns='variable', values='value', aggfunc='first')
Out [3]:
variable A B C
date
6:30:02 a b c
6:30:03 d e f
6:30:04 g h i

このように、pivot_tableはグループ毎に必ず集約関数が使われるのが、pivotとの主な違いのようである。
pivotでは集約ができないので、次のように結果が同じ行と同じ列に相当する行があると、それらが全く同じ値であってもエラーになる。(筆者は実際にそういうデータに遭遇して難儀した)

In [4]:
df = df.append(pd.DataFrame({'date': ['{}:{:02d}:{:02d}'.format(6, 30, s) for s in (6, 6, 6) * 2],
                    'variable': ['A', 'B', 'C'] * 2,
                    'value': list('jkl') * 2}))
df
Out [4]:
date variable value
0 6:30:02 A a
1 6:30:02 B b
2 6:30:02 C c
3 6:30:03 A d
4 6:30:03 B e
5 6:30:03 C f
6 6:30:04 A g
7 6:30:04 B h
8 6:30:04 C i
0 6:30:06 A j
1 6:30:06 B k
2 6:30:06 C l
3 6:30:06 A j
4 6:30:06 B k
5 6:30:06 C l
In [5]:
df.pivot(index='date', columns='variable', values='value')
Out [5]:
ValueError: Index contains duplicate entries, cannot reshape

pivot_tableであれば、この場合も成功する。

In [6]:
df.pivot_table(values='value', index='date', columns='variable', aggfunc='first')
Out [6]:
variable A B C
date
6:30:02 a b c
6:30:03 d e f
6:30:04 g h i
6:30:06 j k l

Pandasのmelt/pivotとstack/unstackの違い

この間、Pandasを使った教材を流し読みしていると、DataFrameクラスのmeltというメソッドが出てきて、pivotの逆だと説明されていた。それを読んで、meltされたものをpivotして元に戻してみよう思って、すきま時間を集めてのべ1時間くらいがんばったが、できなかった。
ついでに、meltとstackの違いがわからなかった。

今月、1時間くらい連続してがんばったら成功して、meltとstackの違いも理解できたので、ここに控えておく。

以下、コード例はJupyter Notebookで書いたインタラクティブシェル向けの形式のものをそのまま貼り付けており、出力例はJupyter Notebookの出力を加工して作成している。

In [1]:
import pandas as pd
df = pd.DataFrame({'月': ['1月', '2月', '3月'],
                   '京都': [110, 115, 144],
                   '大阪': [263, 283, 309],
                   '奈良': [12, 13, 21],
                  })
df
Out [1]:
京都 大阪 奈良
0 1月 110 263 12
1 2月 115 283 13
2 3月 144 309 21

このようなDataFrameがあるとして、これをmeltして、pivotで元に戻してみる。

In [2]:
df_melted = df.melt(id_vars='月', var_name='地域', value_name='人数')
df_melted
Out [2]:
地域 人数
0 1月 京都 110
1 2月 京都 115
2 3月 京都 144
3 1月 大阪 263
4 2月 大阪 283
5 3月 大阪 309
6 1月 奈良 12
7 2月 奈良 13
8 3月 奈良 21
In [3]:
df_pivoted = df_melted.pivot(index='月', columns='地域', values='人数')
df_pivoted
Out [3]:
地域 京都 大阪 奈良
1月 110 263 12
2月 115 283 13
3月 144 309 21

meltしたものをpivotすると大体戻ったが、「月」がindexになっているのと、meltで加えた「地域」が残っているのが異なるので、修正する。
(ちなみに、reset_indexしただけでは「月」が「地域」に入っておかしなことになる)

In [4]:
df_pivoted = df_pivoted.reset_index()
df_pivoted.columns.name = None
df_pivoted
Out [4]:
京都 大阪 奈良
0 1月 110 263 12
1 2月 115 283 13
2 3月 144 309 21

次に、meltとstackの違いを見てみる。
上のdfをstackすると「月」と地域名が同列に処理されてしまうので、まず「月」をインデックスにしてからstackする。

In [5]:
df2 = df.set_index('月')
df2
Out [5]:
京都 大阪 奈良
1月 110 263 12
2月 115 283 13
3月 144 309 21

この場合はstackすると結果が1列なのでDataFrameでなくSeriesになるので、変数名の先頭をdf_でなくsr_にしている。

In [6]:
sr_stacked = df2.stack()
sr_stacked
Out [6]:
1月 京都 110
大阪 263
奈良 12
2月 京都 115
大阪 283
奈良 13
3月 京都 144
大阪 309
奈良 21

当然ながら、stackしたものをunstackすると元に戻る。

In [7]:
sr_stacked.unstack()
Out [7]:
京都 大阪 奈良
1月 110 263 12
2月 115 283 13
3月 144 309 21

meltでは元々column名だった列(「地域」列)がindexにならなかったが、stackではindexになっている。つまり、meltはcolumn名を新たに加えた通常の列に展開することによって表を変形し、stackはcolumn名をindexの新たな階層に展開することによって表を変形する。このことがmeltとstackの主な違いのようだ。
次のようにして、stackしたもののindexを通常の列に戻すと、meltした結果(Out [2])と同じになる。

In [8]:
df_stacked = sr_stacked.sort_index(level=1).reset_index()
df_stacked.columns = ['月', '地域', '人数']
df_stacked
Out [8]:
地域 人数
0 1月 京都 110
1 2月 京都 115
2 3月 京都 144
3 1月 大阪 263
4 2月 大阪 283
5 3月 大阪 309
6 1月 奈良 12
7 2月 奈良 13
8 3月 奈良 21

pandasでmergeせずにgroupbyで間接参照したい

この前、pandasを使っていて、次のような感じの、関連する2つのテーブル、access_logとchoice_logがある時に、結合したテーブルを作らずにchoice毎のtimestampの最小値を求めたかったのだが、どう書けば良いのかわからなかった。

import pandas as pd
import numpy as np
access_log = pd.DataFrame({'session': [100, 101, 102, 103, 104, 105, 106, 107, 108, 109],
                           'timestamp': [314, 159, 265, 358, 979, 323, 846, 264, 338, 327]})
choice_log = pd.DataFrame({'session': [100, 100, 101, 102, 102, 103, 104, 104, 105, 106, 106, 107, 108, 108, 109],
                           'choice':  ['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']})
>>> access_log
   session  timestamp
0      100        314
1      101        159
2      102        265
3      103        358
4      104        979
5      105        323
6      106        846
7      107        264
8      108        338
9      109        327
>>> choice_log
   choice  session
0       A      100
1       B      100
2       C      101
3       D      102
4       E      102
5       A      103
6       B      104
7       C      104
8       D      105
9       E      106
10      A      106
11      B      107
12      C      108
13      D      108
14      E      109
>>> 

結合テーブルを作るなら、次のように書ける。

merged = choice_log.merge(access_log, on='session', how='left')
result = merged.groupby('choice')['timestamp'].min()
>>> result
choice
A    314
B    264
C    159
D    265
E    265
Name: timestamp, dtype: int64
>>> 

実際にあったテーブルは巨大で、他にも列がたくさんあり、単純に結合テーブルを作るとRAMが足りなくなってメモリスワップが多発したので、結合テーブルを作らずにこれと同じことがしたかったのだが、その書き方がわからなかった。
結局access_log.set_index('session').to_dict()のようにして一時的なdictを作って、スワップを多発させながら処理してしまった。
それが心残りだったので、改めてpandasのドキュメントを拾い読みしながら方法を探してみた。

  1. 単純に、別のテーブルを参照する関数をSeries.mapに渡す
    def session_to_timestamp(session_series):
        return session_series.map(lambda x: access_log[access_log.session==x]['timestamp'].iat[0])
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    

    Seriesの先頭の要素を取り出す方法には、.iat[0]の他に.iloc[0]や.values[0]などがあり、筆者が試した所values[0]の方が速かったりしたが、pandasのドキュメントに書かれているのはilocとiatなので、ここでは添字が整数なら高速なiatを用いた。

  2. リスト内包表現(list comprehension)で別のテーブルを参照する
    def session_to_timestamp(session_series):
        return [access_log[access_log.session==x]['timestamp'].iat[0] for x in session_series]
    
    result = choice_log.groupby('choice').agg(lambda x: min(session_to_timestamp(x)))
    

    リストにはminメソッドが無いので、session_to_timestamp(x).min()とはできない。

  3. isinを使ったBoolean Indexingで別の表のサブセットを得る
    def session_to_timestamp(session_series):
        return access_log[access_log.session.isin(session_series.values)]['timestamp']
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    
  4. 別の表からSeriesを作ってSeries.mapに渡す
    def session_to_timestamp(session_series):
        return session_series.map(access_log.set_index('session').timestamp)
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    

    一見シンプルで美しそうだが、set_index()はコピーを返すので、timestampの一時的なdictを作るのと変わらない。しかも、グループ数だけ新たなテーブルを作るので、無駄である。

これらの処理時間を色々測ってみたが、2つのテーブルのサイズやグループの数によって変わり、どう比較すれば良いかわからなかったので、省略する。
大まかな傾向としては、1.と2.の処理時間はchoice_logのサイズに依存し、3.と4.の処理時間はsession_logのサイズに依存するようだった。4.はset_indexした中間テーブルを事前に作っておくと高速化するが、それでも、大抵の場合3.が一番速かった。
いずれの方法も最速になる場合があるようなので、場合毎に色々試してみるしかなさそうである。

肝心のメモリ使用量は、適当な測り方がわからなかった。
そもそも、スワップしながらの処理時間が問題だったので、単純にメモリ使用量では測れないと思う。

他にも、以下のような方向で書き方を考えてみたが、うまくできなかった。

  • groupbyでaggregateでなくtransformしてmin()
    transformする時に別のテーブルを参照することを考えたが、transformするとgroup解除されてしまうので、使えなかった。transformする時にmin()するのなら、min()した値を増殖させるだけ無駄なので、確実にaggregateの方が効率が良い。
  • pandas.DataFrame.lookupを使う
    引数としてindexしか受けられないので、使えなかった。
  • pandas.DataFrame.joinを使う
    pandas.DataFrame.mergeを使うのと変わらなかった。

そもそも、lambda式を使わずに書く方法は無いのだろうか。
teratailとかStack Overflowとかで聞いた方が早いか。

pandas.read_csv+numpy.bincountでエラー

ちょっと前に、はやりのAIのプログラミングでよく使われるPythonとNumPyとPandasを使い始めたのだが、PandasでCSVファイルを読み込んで、各ラベルの出現回数を得る為にNumPyのbincountメソッドを使うと、次のようなエラーが出て、困った。

TypeError: Cannot cast array data from dtype('int64') to dtype('int32') according to the rule 'safe'
その時使った環境は、Windows 7(32bit版)+Python 3.5.2(Anaconda 4.1.1 (32-bit))である。Mac OS Xでは出なかった。

これは、32bitのPythonを使っていると起こることらしい。例えば、32bit Pythonで次のプログラムを実行すると、同じエラーが出る。

import numpy as np
import pandas as pd
import io

data = np.random.randint(10, size=30)
buf = io.StringIO("\n".join(str(x) for x in data))
df = pd.read_csv(buf, header=None)
x = df[0].values
print(x)
print(np.bincount(x))	#Error on 32bit Python
これは、Pandasが32bit Pythonでもint64を使うのが原因のようである。

このエラーを回避するには、bincountに渡すデータの型をint32にすれば良い。

print(np.bincount(x.astype('int32')))	#also OK on 32bit Python
出力例
[1 0 6 7 1 7 7 6 9 5 6 2 8 0 7 6 7 8 9 7 8 9 8 0 2 1 2 6 0 3]
[4 3 3 1 0 1 5 6 4 3]

または、np.unique(return_counts=True)を使う方法があり、こちらの方が、大抵の場合はnp.bincountを使うよりも好ましいとされるようである。

print(np.unique(x, return_counts=True))
出力例
(array([0, 1, 2, 3, 5, 6, 7, 8, 9]), array([4, 3, 3, 1, 1, 5, 6, 4, 3]))
確かに、np.unique(return_counts=True)の方が、データに負の値があっても使えるし、大きな値が混ざってても配列が巨大にならないので、安全そうである。

なお、今動作しているPythonが32bitか64bitかを判定する方法は、いくつかあるようである。 例1

import platform
platform.architecture()
出力(上がMacOSX、下がWin32)
('64bit', '')
('32bit', 'WindowsPE')
例2
import sys
"%x" % sys.maxsize
出力(上がMacOSX、下がWin32)
'7fffffffffffffff'
'7fffffff'
例3(platform.architecture()の実装にも使われている方法)
import struct
struct.calcsize("P") * 8
出力(上がMacOSX、下がWin32)
64
32