まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

Jupyter noebookでデバッグをする方法

この記事はなにか

以下のTweetを見て知らなかったので、自分で手を動かした

なお、Tweet元の方がその後LTをした資料は以下。

pdb/ipdb と jupyterのマジックコマンド %debug それぞれの説明をしている。
サンプル用notebookコードもある。基本的にこのcolabを動かしたらすべてわかる感はある。

speakerdeck.com

なお、

僕は以下のような使い分けをしています。

pdb/ipdb: 誰かが書いた クラス とか 関数 の深いところを動かしながら確認したいとき。
%debug: エラー出た。。。なぜ。。。のとき。

状況にも寄りますが大体こんな感じです。
printデバッグも頻繁にやりますが、あきらかにデバッガを使ったほうが楽なシーンもあるので使い分けてください。

とのこと。

本記事は実際に動かした感想および自分用のメモとしてコマンドなどを残す意図です。

ドキュメントはpdbのデバッガコマンド部分がpdb および %debugで使える。

docs.python.org

以下の記事でもよい

debug対象

以下の記事で作った外れ値の書き換えコードを動かして挙動を確認する。

knknkn.hatenablog.com

class FeatureClipper(BaseEstimator, TransformerMixin):
    def __init__(self, cols_to_clip_lower=None, cols_to_clip_upper=None):
        self.cols_to_clip_lower = cols_to_clip_lower
        self.cols_to_clip_upper = cols_to_clip_upper
    
    def fit(self, X, y=None):
        # 各特徴量の0.001, 0.999パーセンタイル点を取得
        self.lower_bounds = {c: X[c].quantile(0.001) for c in self.cols_to_clip_lower}
        self.upper_bounds = {c: X[c].quantile(0.999) for c in self.cols_to_clip_upper}
        return self
    
    def transform(self, X):
        # 直接の書き換えが起きないようにcopy
        _X = X.copy()
        
        # 各特徴量を0.999パーセンタイル点に収める(0.999を超える値は0.999で置き換え)
        if self.cols_to_clip_lower is not None:
            for c in self.cols_to_clip_lower:
                _X[c] = _X[c].clip(lower=self.lower_bounds[c])
                
        # 各特徴量を0.001パーセンタイル点に収める(0.001より小さい値は0.001で置き換え)
        if self.cols_to_clip_upper is not None:
            for c in self.cols_to_clip_upper:
                _X[c] = _X[c].clip(upper=self.upper_bounds[c]) 
        
        return _X

元記事同様、このTransformerを使ってClip処理をfit_tranformを用いておこなう。

まずはClip処理をわかりやすくするために、boston住宅価格データに加工をおこなう。

#ボストン住宅価格データセットの読み込み
from sklearn.datasets import load_boston
boston = load_boston()

#説明変数
X = pd.DataFrame(boston.data, columns=boston.feature_names)

# CRIM列をテキトーに外れ値に置き換える
X.iloc[0,0] = -500
X.iloc[1,0] = 500

%debugでエラーが起きた原因を調べる

次に、以下のようにtranceformerを適用する。

# Transformerの適用
# 各特徴量を0.999, 0.001パーセンタイル点に収める(超える値は0.999,0.001で置き換え)
tranceformer = FeatureClipper()

X_clipped = tranceformer.fit_transform(X)

エラーが出るので%debugを起動。

f:id:chito_ng:20210903082554p:plain

lでエラー箇所の前後5行を見ると、fit 関数の self.lower_bounds = {c: X[c].quantile(0.001) for c in self.cols_to_clip_lower}がエラー。内容としては、 TypeError: 'NoneType' object is not iterable
u で上のフレーム(深さ)までいくと、 fitの適用時のエラー、更にuで上にいくとfit_transformを使ったときのエラーということがわかる。

f:id:chito_ng:20210903074101p:plain

ちなみに、 llで現在の関数またはフレーム全体を表示したり、l 12 のように引数指定で前後5行を見る中心行を指定することができる。
まぁこのあたりは正直エラー文自体で見れるけど今回はフレームや、記載量が少なかったのでもうちょっと複雑なコードだと必要になりそう。

dを2回押してはじめのフレームに戻る。
self.lower_bounds = {c: X[c].quantile(0.001) for c in self.cols_to_clip_lower} がエラーのようなので、いったん変数cを見るためにpコマンドを使う。

ipdb>  p c
*** NameError: name 'c' is not defined

cが定義されてない」と出る*1のでcのもととなるself.cols_to_clip_lowerを見る。

ipdb>  p self.cols_to_clip_lower
None

self.cols_to_clip_lower はNoneとしてこのコードが走っていることがわかる。つまり、self.cols_to_clip_lower がNoneなのでcが定義されず、そのためX[c].quantile(0.001)X[c]を定義することができずエラーが発生していたことになる。

では、そもそも何故self.cols_to_clip_lower はNoneだったのかを考えると、以下のようにclass定義時の初期値がcols_to_clip_lower=Noneとして設定されている。

class FeatureClipper(BaseEstimator, TransformerMixin):
    def __init__(self, cols_to_clip_lower=None, cols_to_clip_upper=None):
        self.cols_to_clip_lower = cols_to_clip_lower
        self.cols_to_clip_upper = cols_to_clip_upper

(略)

そのため、tranceformerインスタンスを定義しているときtranceformer = FeatureClipper()に引数で初期値を渡していないのでデフォルトのNoneとして作成されることとなっている。

# Transformerの適用
# 各特徴量を0.999, 0.001パーセンタイル点に収める(超える値は0.999,0.001で置き換え)
tranceformer = FeatureClipper()

X_clipped = tranceformer.fit_transform(X)

そのため、以下のように引数としてClipをおこないたい列をlistで渡してインスタンスを作成する。

# Transformerの適用
# 各特徴量を0.999, 0.001パーセンタイル点に収める(超える値は0.999,0.001で置き換え)
tranceformer = FeatureClipper(cols_to_clip_lower=['CRIM', 'ZN'], cols_to_clip_upper=['CRIM','ZN'])

X_clipped = tranceformer.fit_transform(X)

そうするとエラーが起きない。
また、結果を見るとちゃんとClipが機能していることもわかる。

f:id:chito_ng:20210903083604p:plain

定義したClassの挙動を確認する

次は実際にtranceformer.fit_transform()をおこなったときに、内部でどのような挙動が起きているか確認する。これは、自分でclassなり関数なりを定義したときに思っていた挙動と違う場合に役に立つ。

やり方としては、チェックしたいタイミングの行にst()を置くとそのタイミングで内部の状態がどうなっているか調べることができる。
なお、前節の%debugはデフォルトで入っているが、st()pdbからset_traceをimportする必要があるので事前にfrom pdb import set_trace as stと宣言しておく。

今回はClipされる各特徴量の0.001, 0.999パーセンタイル点がいくらか調べるために、fitの最終行にst()を仕込む。

class FeatureClipper(BaseEstimator, TransformerMixin):
    def __init__(self, cols_to_clip_lower=None, cols_to_clip_upper=None):
        self.cols_to_clip_lower = cols_to_clip_lower
        self.cols_to_clip_upper = cols_to_clip_upper
    
    def fit(self, X, y=None):
        # 各特徴量の0.001, 0.999パーセンタイル点を取得
        self.lower_bounds = {c: X[c].quantile(0.001) for c in self.cols_to_clip_lower}
        self.upper_bounds = {c: X[c].quantile(0.999) for c in self.cols_to_clip_upper}
        st() # <= ****************** この部分でチェックしたい ******************
        return self
    
    def transform(self, X):
        # 直接の書き換えが起きないようにcopy
        _X = X.copy()
        
        # 各特徴量を0.999パーセンタイル点に収める(0.999を超える値は0.999で置き換え)
        if self.cols_to_clip_lower is not None:
            for c in self.cols_to_clip_lower:
                _X[c] = _X[c].clip(lower=self.lower_bounds[c])
                
        # 各特徴量を0.001パーセンタイル点に収める(0.001より小さい値は0.001で置き換え)
        if self.cols_to_clip_upper is not None:
            for c in self.cols_to_clip_upper:
                _X[c] = _X[c].clip(upper=self.upper_bounds[c]) 
        
        return _X

%debug同様に、p 変数でそのタイミングでの変数の状態を出力できるので、0.001パーセンタイル点self.lower_boundsと0.999パーセンタイル点self.upper_boundsを見る。

f:id:chito_ng:20210903082207p:plain

すると、CRIM, ZNそれぞれの0.999,0.001パーセンタイル点の値を確認することができる。
これはつまり、transform時にclip関数の引数として渡される値なのでこれらの値以上/未満はこの値に置換されることを意味する。

また、よくある使い方としては冒頭に記述した参照元スライドにあるサンプルコードのようにforループ内にst()を起き、b (行数)ブレークポイントを設定しつつncでループを進めることでループ毎での変数の変化を確認するなどがある。

*1:書き終わってから気づきましたが、よく考えたら辞書内包表記なのでエラー出てなくても外側からみてcは定義されてない。cも調べたい場合は辞書内包表記ではなくて、for文とかで書く