たくさんのnotebookの内容をコマンドラインから再実行して更新したい

はじめに

たとえばグラフを描画するnotebookをたくさん開いている状態。 ちょっとだけimportしたライブラリに変更があったので実行結果が変わりそうになってしまった。 このようなときに、すべてのipynbファイルを開いてrunallした後にsaveというようなことを手動で行いたくない。

はじめはheadless jupyterのようなものを作ろうとしたけれど。nbconvertで大丈夫だったという話。

nbconvert --execute

jupyterにはnbconvertというコマンドもついてくる。 これは、ipynbファイルを別のフォーマット(例えば、pdfだったりhtmlだったりmarkdownだったり)に変換するためのコマンドという認識だったのだけれど。 なんと--executeというオプションがついている。

これを使うことで再実行が可能になる。こういう感じ。

$ jupyter nbconvert --to notebook --execute Untitled.ipynb

すると、 Untitled.nbconvert.ipynb というファイルに再実行した結果が保存される。

--inplace を使えば上書き保存できる

--inplace を使えば上書き保存できる。こういう感じ。

$ jupyter nbconvert --to notebook --execute Untitled.ipynb

これは、Untitled.ipynbが更新される。

timeoutも伸ばしておいた方が良いかもしれない。

defaultでは30秒のtimeoutが設定されている。30秒以上掛かるような処理だった場合に中断されてしまうのは悲しい。そんなわけでtimeoutを伸ばしておくと良い。

$ jupyter nbconvert --ExecutePreprocessor.timeout=600 --to notebook --execute Untitled.ipynb

おまけ: 手で書いたipynbをレンダリングさせるということもできなくはない 

こういう雑なmatplotlibで図を描くコードがあるとする。

import matplotlib.pyplot as plt
plt.style.use("ggplot")

xs = list(range(10))
ys = [x * x for x in xs]
plt.plot(xs, ys, "g")

これを以下のようにipynbに手動で変換する(metadataなど埋めたほうが良い部分はあったりする)

{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {},
      "source": [
        "%matplotlib inline\n",
        "import matplotlib.pyplot as plt\n",
        "plt.style.use('ggplot')\n",
        "\n",
        "xs = list(range(10))\n",
        "ys = [x * x for x in xs]\n",
        "plt.plot(xs, ys, 'g')"
      ]
    }
  ],
  "metadata": {},
  "nbformat": 4,
  "nbformat_minor": 2
}

.ipynbはJSONファイルなので手書き出来ないことはない。例えばgraph.ipynbなどの名前で保存しておく。 その後この保存したファイルに対してnbconvert --execute を実行してグラフも含んだipynbを手に入れる事ができる。 (警告が出ているので本当はもう少し真面目にjson書いた方が良いかもしれない)

$ jupyter nbconvert --ExecutePreprocessor.timeout=600  --execute --to notebook 01graph.ipynb
[NbConvertApp] Converting notebook 01graph.ipynb to notebook
[NbConvertApp] ERROR | Notebook JSON is invalid: 'outputs' is a required property

Failed validating 'required' in code_cell:

On instance['cells'][0]:
{'cell_type': 'code',
 'execution_count': 1,
 'metadata': {},
 'source': 'import matplotlib.pyplot as plt\n'
           "plt.style.use('ggplot')\n"
           '\n'
           'xs = li...'}
[NbConvertApp] Executing notebook with kernel: python
[NbConvertApp] Writing 570 bytes to 01graph.nbconvert.ipynb

たくさん一気に同様の形式で別のファイルにグラフを描きたいときなどには便利かもしれない。

すると以下の様なグラフを生成した結果を含んだipynbが作れる。

graph

(例が二次関数のグラフなのは寂しいのでもうちょっと良い感じの絵を表示するようなものにしたい気持ちもあったりする)

gistはこちら

追記: ipynbの作成にはnbformatを使うと便利

ipynbの作成にはJSONを手書きするよりnbformatを使うと便利。

例えば以下の様な形で使う。

import textwrap
from nbformat.v4 import new_code_cell, new_notebook, writes_json

notebook = new_notebook()
sources = [
    """
    import random
    random.random()
    """,
    """
    import random
    random.random()
    """,
]

for i, source in enumerate(sources, 1):
    notebook["cells"].append(new_code_cell(textwrap.dedent(source), execution_count=i))

print(writes_json(notebook))

これを実行すると以下の様なipynbが生成される。便利。

{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "import random\n",
    "random.random()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "import random\n",
    "random.random()\n"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 2
}

グラフ描く簡単な環境作っときたいかも

グラフ描くの簡単な環境作っときたいかも。要求は以下の2つ。

  • 手軽にグラフが描ける
  • 手軽にグラフを共有できる

前者は例えばjupyter上でだけ表示みたいなのが嫌な感じ。 後者は例えばGUIで表示とかだけなのは嫌な感じ。

まだ結局一番良いと思える方法は見つかっていない(かなしい)。

今のところの方法

今のところとりあえずグラフの描画にはpyplotを使うという気持ちでいる。

とりあえず表示する場合と共有する場合とで分けていて。全部jupyterで済ませれるならそれ。 そうじゃない場合は以下の方法でやっている。

画像を表示するだけ

import matplotlib.pyplot as plt
plt.style.use('ggplot')

plt.plot([1, 2, 3, 4])
plt.ylabel('y')
plt.show()

line.png

画像を共有する場合

画像を出力してその画像を共有というのが無難な感じそう。

import matplotlib
matplotlib.use("AGG")  # NOQA
import matplotlib.pyplot as plt
plt.style.use('ggplot')

plt.plot([1, 2, 3, 4])
plt.ylabel('y')

dpi = float(plt.gcf().get_dpi())
plt.gcf().set_size_inches(400 / dpi, 300 / dpi)

plt.savefig("images/line-400x300.png", dpi=dpi)

dpiの指定に現在のfigureが持っているdpiを使うのと、rcParamsの中のsavefigのdpiを使うのとどちらが良いのかあんまり分かっていない。

$identify images/line-400x300.png
images/line-400x300.png PNG 400x300 400x300+0+0 8-bit sRGB 10.7KB 0.000u 0:00.000

ただ、gistにuploadするときにはちょっと大変な作業を踏まないといけないのであんまり画像で共有というのが機能しないような気もする。

問題点

問題点は以下の様な感じ。

  • グラフの表示と画像として出力とでコードが変わってしまう
  • 画像として出力してもgist上では楽に共有できない

画像として出力した結果を共有するなら画像サーバー的なものを用意してそこにuploadした後にmarkdown上で参照するという感じが良いのかも。 答えは見つかっていない。

jupyterでやれば良い場合

なんだかんだでgistで共有する場合にはjupyterでやるのが一番ラクな気もする。

import matplotlib.pyplot as plt

plt.style.use('ggplot')

plt.plot([1, 2, 3, 4])
plt.ylabel('some numbers')

以下をわすれずに

%matplotlib inline

こういう感じ

問題点

問題点はipynbのレンダリングが遅いこと数秒程度は普通に待たされる。 その後

Sorry, something went wrong. Reload?

とか悲しい感じ(githubやgist上での話)。