データサイエンティストをダメにする(かもしれない)簡単なjupyter notebook拡張機能を作ってみましたので紹介

jupyter notebook Advent Calendar 2019 25日目の記事です。

プログラミング言語のカテゴリに入っているので、数日前まで存在すら気づかなかったですが、滑り込みで投稿してみました。特に最終日にふさわしいようなjupyterの真髄を伝えるといった内容ではなく、こんなの作ってみました、という自己満足な記事ですが、もし眺めていただけると幸いです。

今回は、実際のデータ分析や開発業務等で使っている、自作のjupyter notebookの拡張機能で、一応パブリックに置いているものを簡単にご紹介してみたいと思います。

#### 注意点

以下の拡張機能等を用いる場合は、自己責任でお願いいたします。また今後、何点か改良予定がありますので、以下は2019年12月25日現時点のものとなります。

また制作者はjavascriptに関して**ど**素人ですので、コード上好ましくない点や、改良点などありましたら、ご指摘いただけるとありがたいです。

## "データサイエンティストを堕落させる拡張機能"

jupyter notebookは(魔改造などして)使いこなすとデータ分析等の強力なツールとなる反面、使い手がきちんと制御下に置いておかないと、"大変な事"になりがちです。しかしながら、実際の業務経験者は分かると思うのですが、データ（ファイル名やフォーマット含む）が目も当てられなかったり、分析ロジックが固まっていなかったりすると、どうしても試行錯誤をせざるを得ません。

特にipython notebook(ipythonカーネルを用いるjupyter notebook)として書いたコードを、pythonスクリプトへ移植する際など、苦労している方も一定数おられるのではないでしょうか？それを緩和する目的で書いた拡張機能を2つ程、今回紹介してみたいと思います。

### collect import

必要なモジュールを予め全て分かっている事は稀なので、ノートブックの各所にimport文が散らばっている事を経験した、さらには毎回繰り返している方も多いので無いでしょうか？

一方で、pythonスクリプトや、ちゃんと整理されたipython notebookでは、先頭にimport文が記述されています。実際、[PEP 8 のimport文に関する規約](https://www.python.org/dev/peps/pep-0008/#imports)には、

> Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.

とあります。PEPを尊守するかはともかく、あちこちでインポートしたモジュールを一箇所に集めたい事はあるかと思います。

さらに言えば、ロジックの切り分けがまだできていない段階で、一箇所にimport文をまとめて書いてしまうと、ロジックを切り分けて複数スクリプト化する際に、モジュール依存性がよく分からなくなります。なので、個人的には、ノートブック上では、そのモジュールが必要な直前でimportする方が望ましいケースがあったりします。

だったら、ノートブックの方はPEP 8のimport文規約を守らずに書いて、代わりにimport文を1箇所に集める拡張機能を作ってしまおう、という事で作ったのを使用したデモが以下です：

<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211360/e41c772c-ea34-5590-1467-d4bf6af3da40.gif" width=75%>
</div>

---

肝心の拡張は[こちら](https://github.com/nrakwtnb/nbextensions/tree/master/collect_import)に置いてあります。

`main.js`が一応メインスクリプトになりますが、中身の処理は大した事をしておらず、各セル各行の中身を格納した`line`変数に対し、パターンマッチを行う事で、インポートしているモジュールを抽出し、`import_modules`や`import_modules_with_from`に結果を格納しているだけです。

```javascript
res = line.match(/^import\s(.*)/);
if (res) {
    res = res[1];
    modules = res.split(",").map(function(text){return text.trim()});
    Array.prototype.push.apply(import_modules, modules);
}

res = line.match(/^(from\s.*)/);
if (res) {
    res = res[1];
    import_modules_with_from.push(res)
}
```

#### 注意点など

現在公開しているバージョンでは、元のimport文削除は行っていませんが、たくさんのテストケースで動作確認できたら、削除するか否かを選択できるようなメッセージを出す事を考えています。

コメントや文字列内のimport文も集めてきちゃうので、そこは将来的に除外する仕組みを入れようと思います。[^1-1-1]また次のケースは未対応です。

```python
from <package-name> import (
  <module 1>, <module 2>, ..., <module-3>
)
```

[^1-1-1]: やってみると分かるのですが、どの範囲までが単なる文字列か、を判定するロジックの(javascript)実装が思ったよりも面倒です...

ちなみにほぼ同じロジックで、関数やクラスを集めてくる拡張も作ったのですが、ちょっとバグ等がまだ残っていたりして、そちらのお披露目はまた今度とさせて頂きます。

### notebook history (仮)

次にご紹介するのは、過去の実行履歴を順番にセルとして挿入しなおしてくれる拡張機能です。

ノートブック上で無節操に処理を行っていると、上下を行ったり来たりしたり、上書きしたりしてしまい、再現ができなくなる事があります。そんな時に、過去の実行履歴を全て出して、順番に並べてくれるとありがたいのではないでしょうか？それを叶えてくれる機能が[こちら](https://github.com/nrakwtnb/nbextensions/tree/master/notebook_history)にあります。

これを追加して、使ったデモが次のような感じです；

<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211360/c12f5b2a-91af-feff-fe4d-e051c81002a5.gif" width=75%>
</div>

これを使えば、過去を振り返ることが簡単に出来ますね。

なおこちらの実装としては、`Jupyter.notebook.kernel.execute`というAPIを使用する事で、IPythonのグローバル変数から必要な履歴情報を抽出するpythonの関数を実行し、戻ってきた値を整形して、セルとして挿入しているだけです：

```javascript
var pyfile = Jupyter.notebook.base_url + "nbextensions/notebook_history/main.py";
var callback = function(output){
    result = output.content.data["text/plain"];
    result = JSON.parse( result.replace(/\\'/g, "'").replace(/\\\\\"/g, "\\\"").replace(/\\\\n/g, "\\n").slice(1,-1) );
    Object.keys(result).forEach(i => {
        Jupyter.notebook.insert_cell_at_index("code", i-1).set_text(result[i]);
        cell = Jupyter.notebook.get_cell(i-1)
        cell.set_input_prompt(parseInt(i))
    });
};
$.get(pyfile).done(
    function(script){
        script = script + "\nget_all_input_cells(globals())";
        Jupyter.notebook.kernel.execute(script, {iopub: {output: callback}}, {silent:false});
    }
);
```


#### 注意点など

当たり前ですが、あくまで乱数シードが固定され、副作用のない（I/O処理や外部APIを使用していない）処理のみしか再現できません。

また、数百のセルを実行したノートブックで、実行するとかなり処理が重いです。軽量化は今後の課題とさせて頂きます。さらには、現時点で出力までは再現していません。理由(の1つ)としては、javascript側の対応するAPI(そもそもドキュメントが存在しない)が見つからないためです。

---

以上、２つの拡張機能を雑に作ってみたので、この機に簡単な紹介をしてみました。これで安心して(?)、ダメダメなノートブックの使い方ができるようになります。

来年もまた楽しいデータ分析ができる1年でありますように！