全Jupyterユーザーの願望をサクッと叶える最強ツール「Jupytext」の使い方超まとめ

# はじめに
Jupytextはとても便利なツールですが、日本語の情報があまりネットに落ちてませんでした。なのである程度まとめてみました。会社の LT 会で紹介したところ好評だったのでここでも紹介したいと思います。

https://github.com/mwouts/jupytext

本記事のタイトルに「超まとめ」と入っていますが、ここに書いたこと以外にもできることはたくさんあると思います。需要がありそうだと思えば随時追記していきます。また、詳しい方がおられましたら間違い等のご指摘を頂けますと幸いです。

本記事の流れは以下のようになります。
1. [これはなにか](#これはなにか)
1. [どんな要望に応えられそうか](#どんな要望に応えられそうか)
1. [どうやって使うのか](#どうやって使うのか) (長めです)
1. [その他の使えそうな機能](#その他の使えそうな機能) (長めです)
1. [どんなユースケースが考えられるか](#どんなユースケースが考えられるか) (独断と偏見も含みます)

1., 2. で Jupytext とは何かを簡潔に紹介します。
3., 4. で **だらだらと** 機能面の説明をします。
最後の 5. で(やっと)どんなユースケースがありそうかを紹介します。
<font color="Red">忙しい方は、5. を見た上で 2., 3. の中から必要な情報をつまみ食いするのが良いかもしれません。</font>
「んなら最初から 1., 2., 5., 3., 4. の順番で書けや」というクレームは禁止です。

また、環境は以下の通りです。
```bash
$ python --version
Python 3.10.5

$ jupyter --version
Selected Jupyter core packages...
IPython          : 8.4.0
ipykernel        : 6.15.0
ipywidgets       : 7.7.1
jupyter_client   : 7.3.4
jupyter_core     : 4.10.0
jupyter_server   : 1.18.0
jupyterlab       : 3.4.3
nbclient         : 0.6.6
nbconvert        : 6.5.0
nbformat         : 5.4.0
notebook         : 6.4.12
qtconsole        : not installed
traitlets        : 5.3.0

$ node --version
v15.13.0

$ npm --version 
7.7.6

# こちらは後にインストール方法を記載します。
$ jupytext --version 
1.14.1
```
ちなみに私は Mac を使っていますが、Windows でも大体同じだと思います。

# これはなにか
- ipynb file とその他(py, md, jl など) の file を相互変換することができるツールです。
- 主に ipynb file を git 管理するために py file に変換するのに使用されていたりします。
- 調べてみるとそこそこ奥が深そうなツールです。

# どんな要望に応えられそうか
- Notebook のバージョン管理をしたい (個人であってもチームであっても)。
- Notebook ベースで組んだ分析フローを簡単にスクリプトベースに落とし込みたい。
- ノートブック内の関数を抜き出したい。
- etc…

# どうやって使うのか
同じことを実現するにも色々やり方があります(特定の方法でしか実現できないこともある)。
例えば…
1. Jupyter Notebook の UI で Jupytext の機能を使用する (本記事の対象外)
1. [Jupyter Lab の UI で Jupytext の機能を使用する](#jupyter-lab-の-ui-で-jupytext-の機能を使用する)
1. [CLI で Jupytext の機能を使用する](#cli-で-jupytext-の機能を使用する)
1. [python で Jupytext の機能を使用する](#python-で-jupytext-の機能を使用する)
1. [VS Code で Jupytext の機能を使用する](#vs-code-で-jupytext-の機能を使用する)

今回は 1. 以外を紹介します。(自分が Jupyter Notebook を使っていないので…)

## まずはインストール
※Jupyter Lab 環境が整っていることを前提とします。
```bash
# バージョン指定しない場合
$ pip install jupytext

# バージョン指定する場合
$ pip install jupytext==1.14.1
```
これだけで python module も Jupyter extension も CLI ツールもインストールされます。

## Jupyter Lab の UI で Jupytext の機能を使用する
ここでは以下の 2 点について紹介します。
- [同期された ipynb file と py file の組み合わせを生成](#同期された-ipynb-file-と-py-file-の組み合わせを生成)
- [同期された ipynb file と py file の挙動について](#同期された-ipynb-file-と-py-file-の挙動について)

### 同期された ipynb file と py file の組み合わせを生成
同期された ipynb と py の組み合わせを生成する方法を以下で紹介します。同期された組はどちらを編集してももう片方に変更が自動で適用されます。(超便利)
1. まずはいつも通り Jupyter Lab でノートブックを作成し、適当に分析します。(以下の画像を分析と呼ぶかどうかは今回は無視してください)
![jupytext1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/a0fc154a-8d82-8208-3994-901eb696b217.png)
1. 操作対象の ipynb タブが選択された状態で 「Shift + Command + C」(Windows の場合は Shift + Ctrl + C) を押します。(コマンドパレットを開く)
1. コマンドパレットに「pair」と入力します。
1. Pair Notebook with percent Script をクリックします。(light Script や Hydrogen Script、nomarker Script でも良いですが、percent Script が無難でおすすめです。)
![jupytext2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/e3383b0e-1a2d-baa7-b056-6d9bee2e164a.png)
1. ノートブックを保存すると ipynb file と同じ名前の py file が生成されます。
- これ以降は ipynb file, py file どちらを編集しても、保存するともう片方にも変更が適用されます。
- 同期の解除が UI から行うことができません。バグなのか、仕様なのか、方法が見つけられていないだけなのかは不明です。
- ちなみに、percent Script 形式で書き出された py file は以下のようになっています。
![jupytext3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/8b10e0b1-c2cf-a666-8c1e-60a38e62b871.png)
1 行目から 14 行目まではヘッダー情報?になっており、16 行目からコードが始まります。percent Script は 「`# %%`」で区切られる形式で、cell 毎に記述されています。また、Markdown形式 や raw 形式の cell は 「`# %% [markdown]`」であったり、「`# %% [raw]`」のような区切りになります。light Script[^1] などの他の形式を指定すると書式が変化します。

### 同期された ipynb file と py file の挙動について
これは Jupyter Lab の UI に限った話ではなく、Jupytext 全体で言えることではありますが、ここで挙動と仕様について述べておきたいと思います。
- Jupytext で同期設定をすると、ipynb file にメタデータが追加されます。
- 同期された ipynb file と py file をどちらも開いた状態で片方を編集保存しても、その場ではもう片方に変更は適用されません。要するに <font color="Red">ipynb file を変更して保存した後、py file を開き直して初めて変更が適用されます。逆も然りです。</font>

上記のような挙動から、Jupytext の仕様は以下であると予想しています。
- メタデータを元にファイル間の同期を行う仕様だと思われます。
- メタデータが記述されている ipynb file を変更保存した場合は、ディスク上の py file の内容も書き換える仕様だと思われます。そのため、既に開かれている(メモリ上に展開されている) py file には変更が適用されません。 
- メタデータが記述されている ipynb file を開くと、同期されている py file の中身を確認し差分があれば更新する仕様だと思われます。そのため、既に開かれている ipynb file には変更が適用されません(py file の中を見に行かないので)。

## CLI で Jupytext の機能を使用する
CLI では 以下のことができます。
- [同期された ipynb file と py file の組みを生成](#同期された-ipynb-file-と-py-file-の組み合わせを生成-1)
- [ipynb file と py file の同期を解除](#ipynb-file-と-py-file-の同期を解除)
- [ipynb file から同期の取れていない py file を生成](#ipynb-file-から同期の取れていない-py-file-を生成)
- [py file から 同期の取れていない ipynb file を生成](#py-file-から同期の取れていない-ipynb-file-を生成)
- [etc…](#etc) (md file や jl file も同じ雰囲気でできる。)

### 同期された ipynb file と py file の組み合わせを生成
1. 何らかの ipynb file を用意します。
1. 何らかの terminal ソフトを起動します。
1. 操作対象の ipynb file があるディレクトリに移動し、以下のコマンドを実行します。
```
$ jupytext --set-formats ipynb,py:percent [file_name].ipynb
```
- 上記コマンドの percent を light, Hydrogen, nomarker などに変更することで、自分が使用したい形式の py file を生成できます。

### ipynb file と py file の同期を解除
1. 何らかの terminal ソフトを起動します。
1. 操作対象の ipynb file があるディレクトリに移動し、以下のコマンドを実行します。
```
$ jupytext --set-formats ipynb [file_name].ipynb
```

### ipynb file から同期の取れていない py file を生成
1. 何らかの ipynb file を用意します。
1. 操作対象の ipynb file があるディレクトリに移動し、以下のコマンドを実行します。
```
$ jupytext --to py:percent [file_name].ipynb
```
- 同期がとれていない py file が生成されるので、当然 ipynb, py どちらかを編集して保存しても、もう片方に変更は適用されません。

### py file から同期の取れていない ipynb file を生成
1. 何らかの py file を用意します。
1. 操作対象の ipynb file があるディレクトリに移動し、以下のコマンドを実行します。
```
$ jupytext --to notebook [file_name].py
```
- 同期がとれていない ipynb file が生成されるので、当然 ipynb, py どちらかを編集して保存しても、もう片方に変更は反映されません。

### etc...
上記では ipynb file と py file との同期について記述してきましたが、md file などにも対応しています。どうやって扱うかですが、`py` のところを `md` に変更するだけです。ただし、percent Script 形式などは py file の話なので、`py:percent` と書かれているところは `:percent` も含めて削除する必要がありますのでご注意ください。

また、以下のように書けば、 ipynb, py, md file の 3 つを同期することも可能です。
```
$ jupytext --set-formats ipynb,py:percent,md [file_name].ipynb
```
ただ、注意が必要なのは、「[同期された ipynb file と py file の挙動について](#同期された-ipynb-file-と-py-file-の挙動について)」で述べたように、<font color="Red">ipynb file をいじった時に同期が働く</font>ので、py file のみをいじった場合は、md file に変更が適用されません。py file 変更後、ipynb file を開き、保存した時に初めて md file に変更が適用されます。ただし、ipynb file を変更して保存した場合は、py file, md file 共に同期されます。

## Python で Jupytext の機能を使用する
Python module としての Jupytext では、以下のことができます。
- [ipynb file を読み込んで script 形式で py file に書き出す](#ipynb-file-を読み込んで-script-形式で-py-file-に書き出す)
- [ipynb file を読み込んで script 形式の文字列に変換して変数に渡す](#ipynb-file-を読み込んで-script-形式の文字列に変換して変数に渡す)

### ipynb file を読み込んで script 形式で py file に書き出す
1. ipython でも jupyter でもいいので起動します。
1. 以下を実行します。
```python
In [1]: import jupytext

In [2]: notebook = jupytext.read('./[file_name1].ipynb') #読み込み

In [3]: jupytext.write(notebook, '[file_name2].py', fmt='py:percent') #書き出し
```
- 上記はもちろん py file に書き込んで Script として実行しても ok です。
- fmt はどの 拡張子で、どんな形式で書き出すかを指定できます。上記では percent Script 形式の py file として書き出しています。
- これは ipynb file と py file の同期は取れていません。CLI で言うところの jupytext --to と同じです。

### ipynb file を読み込んで script 形式の文字列に変換して変数に渡す
1. ipython でも jupyter でもいいので起動します。
2. 以下を実行します。
```python
In [1]: import jupytext

In [2]: notebook = jupytext.read('./[file_name].ipynb') #読み込み

In [3]: script = jupytext.writes(notebook, fmt='py:percent') #書き出し

In [4]: type(script)
Out[4]: str

In [5]: print(script)
Out[5]:
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.14.1
#   kernelspec:
#     display_name: Python 3 (ipykernel)
#     language: python
#     name: python3
# ---

# %%
# 記述されている python script

# %%
# percent 形式では、cell が 「# %%」で区切られる

# %%
```
- writes 関数を使用すれば、ipynb file から python script を文字列として抜き出すことができます。
- 上記は percent Script 形式なので、cell が 「`# %%`」で区切られていますが、別の形式を指定すれば異なる書式になります。

## VS Code で Jupytext の機能を使用する
ここでは以下のことを紹介します。
- [注意事項](#注意事項) (少し使い辛いって話です...)
- [VS Code に Jupytext Extension をインストールする](#vs-code-に-jupytext-extension-をインストールする)
- [VS Code の拡張機能で Jupytext をいじる](#vs-code-の拡張機能で-jupytext-をいじる)

### 注意事項
<font color="Red">VS Code で Jupytext を使用するにはいくつか注意事項があります。</font>
- VS Code で拡張機能をインストールする必要があります。
- ipynb から py を生成できません。
- py であれば ipynb の形式で開くことができますが、開いた ipynb は保存できません。
- 同期された ipynb, py の組みを生成できません。
- Jupyter Lab 上で作られた同期済みの ipynb と py を編集する場合、以下の挙動をします。
    - py を編集した場合、問題なく ipynb でも変更が適用されます。
    - ipynb を編集した場合、py に変更は適用されず、Jupyter Lab で ipynb を開こうとするとエラーでひらけなくなります。py を開いて保存すると py の内容(古い内容)が ipynb に適用され再び開けるようになります。

上記を踏まえると、あまり VS Code で Jupytext を使用するのは(現時点では)あまり好ましくないかもしれません。
あり得る運用としては、
1. CLI で同期された ipynb と py を作成する。
2. VS Code 上では直接 ipynb ファイルを開かず、py file を開く。
3. VS Code の拡張機能を用いて、 py file から ipynb file 開く。
4. 分析を行って保存する。

って感じでしょうか。。。

<font color="Red">VS Code で編集したものは ipynb のように見えて、実体は py file (予想) なので、ipynb の出力結果 (グラフなど) は保存されていないので注意が必要です。出力結果も含めて保存したいのならば、Jupyter Lab, Notebook を ブラウザからもう一度実行する必要があります。</font>この辺りの挙動は非常に複雑なので、一度色々試してみてください。

### VS Code に Jupytext Extension をインストールする
1. VS Code の 拡張機能タブをクリックします。
1. 検索窓に「Jupytext」と入力します。
1. 2 種類出てきますが、おそらくどちらも同じです。congwiyu の方が新しそうなのでそちらをインストールします。

### VS Code の拡張機能で Jupytext をいじる
1. Jupyter の UI または CLI で同期済みの ipynb と py を作成します。
1. VS Code で py file を開きます。
1. 以下の画像の右上の赤枠をクリックします。
![jupytext4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/320619b6-4619-6335-de02-01c4636997b5.png)
1. 自由に編集します。
1. 保存します。
- 編集した ipynb の変更内容は保存するたびに py file にも適用されます。<font color="Red">ただし前述したように、実際には ipynb file を編集している訳ではないので注意が必要です。</font>

# その他の使えそうな機能
他にも使えそうな機能があるので紹介します。
- [デフォルトの設定を変更する](#デフォルトの設定を変更する)
- [Jupyter の tag 機能を用いて py, ipynb のどちらかではコメントアウトする](#jupyter-の-tag-機能を用いて-py-ipynb-のどちらかではコメントアウトする)

## デフォルトの設定を変更する
ここでいうデフォルトの設定とは、ipynb file を作成した時の挙動を示す設定のことを指します。つまり、何もしなければいつも通り ipynb のみが作成されます。しかし、全ての ipynb から同期された py を作りたい人もいれば、ある作業ディレクトリのみで ipynb と py を常に同期したい人もいるはず。そんなときに使える機能です。

以下の順序で紹介します。
- [設定ファイルの置き場所](#設定ファイルの置き場所)
- [設定ファイルを書く](#設定ファイルを書く)

### 設定ファイルの置き場所
ipynb file を作るたびに同期設定をするのはとても面倒くさいです。ので、設定ファイルに「どの拡張子でどの形式の同期ファイルをデフォルトで作成するか」を書いておけます。(超便利)

設定ファイルは jupytext.yml を作成して書きます。他にも toml 形式で書けたりしますが、今回は例として yml にしたいと思います。まず、<font color="Red">グローバルに PATH が通ったファイルの置き場所は以下で確認可能</font>です。

```python
In [1]: from jupytext.config import global_jupytext_configuration_directories

In [2]: list(global_jupytext_configuration_directories())
Out[2]: 
['$HOME/.config/jupytext',
 '$HOME/.config',
 '$HOME/jupytext',
 '$HOME',
 '/usr/local/share/jupytext',
 '/usr/local/share/',
 '/usr/share/jupytext',
 '/usr/share/']
```

NeoVim や anyenv と一緒で、`XDG_CONFIG_HOME` に対応しています。個人的には`$HOME/.config/jupytext`または`$HOME/.config`が無難な気がしています。

<font color="Red">特定の作業ディレクトリのみで設定を適用したい場合</font>は、ipynb file を作成していくディレクトリと同じ階層に jupytext.yml を置きます。

まず、カレントディレクトリに設定ファイルが無いかをみてからグローバルを確認していると思われます。要するに<font color="Red">カレントディレクトリの設定が優先</font>されます。

### 設定ファイルを書く
- ipynb file を作成するたびに同期された percent Script 形式の py file を作成したい。
    - 以下のような jupytext.yml を作成します。
    - ファイルの置き場所は下のコードブロックの通りです。
```yml:$HOME/.config/jupytext/jupytext.yml
formats: "ipynb,py:percent"
```
<br/>

- ipynb file が作成されたディレクトリと同じ階層に、py_scripts ディレクトリを作成し、その中に 同期された percent Script 形式の py file を作成したい。
    - 以下のような jupytext.yml を作成しま(以下略)
    - ファイルの置き場所は下のコードブロックの通りで(以下略)
```yml:$HOME/.config/jupytext/jupytext.yml
formats: "ipynb,py_scripts//py:percent"
```
<br/>

- ipynb file が作成されたディレクトリと同じ階層に、py_scripts ディレクトリを作成し、その中に 同期された percent Script 形式の py file を-jupytext の接尾語付きで作成したい。
```yml:$HOME/.config/jupytext/jupytext.yml
formats: "ipynb,py_scripts//-jupytext.py:percent"
```
<br/>

- グローバルで上記 3 つのうちのどれかが設定されているが、ある特定の作業ディレクトリでは同期された py file が作成されないようにしたい。
    - ファイルの置き場所に注意です。カレントディレクトリ(ipynbを作成するディレクトリ)です。
```yml:./jupytext.yml
formats: "ipynb"
```

## Jupyter の tag 機能を用いて py, ipynb のどちらかではコメントアウトする
ipynb では実行したいけど、Script として運用するときにはコメントアウトしたい場合があると思います。逆も然り。
- 前者の例: Jupyter 特有の表記の Cell はコメントアウトしないと Script として実行すると怒られる。
- 後者の例: 外部から引数を与えたいけど、ipynb file では使用しないので実行されないようにしたい。

そんなときに使用できるのが tag 機能です。
以下の順序で紹介します。
- [py file のみでコメントアウトする](#py-file-のみでコメントアウトする)
- [ipynb file では raw 形式だが py file では実行可能な状態にする](#ipynb-file-では-raw-形式だが-py-file-では実行可能な状態にする)

### py file のみでコメントアウトする
1. 操作対象の ipynb file を Jupyter Lab で開きます。
1. py file でコメントアウトしたい Cell を選択します。この例では、上から 2 番目の Code Cell です。
![jupytext5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/2e0777eb-c686-42d4-db80-e50df06d3acd.png)
1. 以下の画像の一番右上にある歯車マークをクリックします。
![jupytext6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/1d82079c-e0b7-2987-a2c1-5b4ff859f868.png)
1. Add Tag をクリックし、「active-ipynb」と入力し、Enter。
1. 以下の画像のようにチェックマークが入ったことを確認し、保存します。
![jupytext7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256229/1c94b2c0-b57b-09b8-c7bf-c54aef91bbf8.png)
1. py file を開くと該当の Cell にあたる行が、`# %% tags=["active-ipynb"]`から始まっていて、その下がコメントアウトされていることがわかると思います。

### ipynb file では raw 形式だが py file では実行可能な状態にする
一部を除いて上記と同じ方法で OK。変更点は、「acitve-ipynb」を「active-py」にするだけです。

ipynb file で 「active-py」タグをつけて保存した後、file を開き直すと、code cell が raw cell に変化しています。一方で、同期されている py file を開くと実行可能な状態になっています。

# どんなユースケースが考えられるか
- ipynb と py の同期のありなしをどう使い分けるのが良さそうか考えてみます。
- python module として jupytext を使うのはどんな時か考えてみます。

## 同期しておくと良さそうな場合
### Git で分析コードを管理したいとき (個人)
.gitignore に思い切って .ipynb を含めてしまい、py file のみを add, commit, push するようにすれば、差分が見やすくなりそうです。ただし、分析結果を github で見たい場合はこの方法は使えないので注意が必要です。

### Git で分析コードを管理したいとき (チーム)
個人の時と同じです。ただ、共有されているのは py file のみなので、CLI を使用して py file から ipynb file を作成し、さらに CLI または Jupyter UI を使用して ipynb と py を同期する必要があります。少し面倒くさいです。もう少しいいやり方があるかもしれませんが、チームで使ったことがないのでわかりません。

あるいは py file, ipynb file どちらも push してしまい、レビューの時は ipynb file を見てもらうことにして、コードの差分を見たい場合のみ py file を参照するのがいいかもしれません。(当然その場合は、.gitignore から ipynb を削除する必要があります。)

## 同期なしでも良さそうな場合
### Notebook を Script 運用に切り替えたい時
特に同期をする必要はないが、Script ベースの運用に切り替えたい場合、CLI を用いて py file を作成すれば良さそうです。その際に、tag 機能を使えばより便利に使えるかもしれませんね。

この運用の場合、papermil でも似たことが実現できそうです。ipynb 形式のまま Script ベース運用に近いことができます。その時々で使い分けると良さそうですね。

## Python Module として Jupytext を使うと良さそうな場合
### ipynb file から Function や Class を抜き出したい時
writes 関数を用いれば、ipynb file を py file に変換した文字列を取得できるので、あとは文字列に対して処理を行えば、Function や Class 部分のみを引っ張り出してこれそうです。いちいちコピペせずに引っ張り出せそうなので便利かもしれないですね。

# まとめ
こんなことしたいな〜と思ってたことが概ね実装されています。
Jupytext は全 Jupyter ユーザーの願望をサクッと叶える最強ツールです。

# 参考
- [公式ドキュメント](https://jupytext.readthedocs.io/en/latest/formats.html)
- [ソースコード](https://github.com/mwouts/jupytext)
- [Automated reports with Jupyter Notebooks (using Jupytext and Papermill)](https://medium.com/capital-fund-management/automated-reports-with-jupyter-notebooks-using-jupytext-and-papermill-619e60c37330)
- [Jupyter notebook を管理しやすくする Jupytext を使ってみた](https://qiita.com/cfiken/items/8455383f32ee19dfbba3)

[^1]:light Script 形式を指定すると、cell の区切りが「空行」になります。しかしこれは問題で、cell 内で空行を作って保存した後、py file を開いて編集保存をすると、ipynb の空行部分で cell が分割されてしまいます。これは、「空行」と「cell 分割」の表現が縮退しているためです。こういったことが生じるので基本的に percent Script を使用することをおすすめします。