Jupyter noebookでデバッグをする方法
この記事はなにか
以下のTweetを見て知らなかったので、自分で手を動かした
まじでマジックコマンドの"%debug"便利なのでjupyter使ってる人で知らない人いたら一度使ってみてほしい。。。
— fkubota🦉 (@fkubota_) 2021年7月27日
「知らんかった!!」っていう人があまりにも多い。。。
わざわざgif作ったよ!
「エラー出た!!」で後出しでデバッグできるんだから使わない手はないよね。 pic.twitter.com/t2yjMqn1vV
なお、Tweet元の方がその後LTをした資料は以下。
pdb/ipdb と jupyterのマジックコマンド %debug それぞれの説明をしている。
サンプル用notebookコードもある。基本的にこのcolabを動かしたらすべてわかる感はある。
なお、
僕は以下のような使い分けをしています。 pdb/ipdb: 誰かが書いた クラス とか 関数 の深いところを動かしながら確認したいとき。 %debug: エラー出た。。。なぜ。。。のとき。 状況にも寄りますが大体こんな感じです。 printデバッグも頻繁にやりますが、あきらかにデバッガを使ったほうが楽なシーンもあるので使い分けてください。
とのこと。
本記事は実際に動かした感想および自分用のメモとしてコマンドなどを残す意図です。
ドキュメントはpdbのデバッガコマンド部分がpdb および %debugで使える。
以下の記事でもよい
debug対象
以下の記事で作った外れ値の書き換えコードを動かして挙動を確認する。
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を起動。

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を使ったときのエラーということがわかる。

ちなみに、 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が機能していることもわかる。

定義した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を見る。

すると、CRIM, ZNそれぞれの0.999,0.001パーセンタイル点の値を確認することができる。
これはつまり、transform時にclip関数の引数として渡される値なのでこれらの値以上/未満はこの値に置換されることを意味する。
また、よくある使い方としては冒頭に記述した参照元スライドにあるサンプルコードのようにforループ内にst()を起き、b (行数)でブレークポイントを設定しつつnやcでループを進めることでループ毎での変数の変化を確認するなどがある。
*1:書き終わってから気づきましたが、よく考えたら辞書内包表記なのでエラー出てなくても外側からみてcは定義されてない。cも調べたい場合は辞書内包表記ではなくて、for文とかで書く