Pythonのボロノイ図のスタイル調節法

Pythonでボロノイ図を描く関数 voronoi_plot_2d のスタイル調整法を紹介します。以下の内容を扱います。

- 境界線の交点を消す
- 中心点の色や大きさを変える
- 破線を実線にする
- 領域を色で塗りつぶす

参考にしたページ

* [Python, SciPy, Matplotlibでドロネー図・ボロノイ図をプロット](https://note.nkmk.me/python-scipy-matplotlib-delaunay-voronoi/)
* [How do you select custom colours to fill regions of a Voronoi diagram using scipy.spatial's Voronoi package?](https://stackoverflow.com/questions/63347564/how-do-you-select-custom-colours-to-fill-regions-of-a-voronoi-diagram-using-scip)

## 基本コード

scipy の Voronoi と voronoi_plot_2d を使います。前者はボロノイ図の計算用、後者は描画用です。先頭文字は前者が大文字、後者が小文字なので注意して下さい。以下に基本コードを載せます。

```python
# パッケージのインポート
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import Voronoi, voronoi_plot_2d

def voronoi_sample():
  n = 10                           # 点の数を指定
  np.random.seed(42)               # 乱数の種を指定
  X = np.random.randn(n,2)         # ランダムデータを生成
  vor = Voronoi(X)                 # ボロノイオブジェクトを計算
  fig, ax = plt.subplots(figsize=(4,4))  # fig, axを作成
  voronoi_plot_2d(vor, ax=ax)            # ボロノイ図を描画
  fig.tight_layout()                     # 余白などを調整
  fig.show()                             # 表示
  fig.savefig('tmp.png')                 # ファイルに保存
```

実行結果は下図です。既定の設定だと色々と見づらい気がします。

![qiita1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787764/c56446e0-0734-4422-b3f6-84c10ae57caa.png)

## 境界線の交点を消す

まず、境界線の交点（以下、頂点と呼びます）を表すオレンジの点を消してしまいましょう。voronoi_plot_2d 関数の引数に show_vertices=False を入れればOKです。

```python
voronoi_plot_2d(vor, ax=ax, show_vertices=False)
```

![qiita2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787764/f1b0fb3d-53b1-4dad-89b4-e32da1825dad.png)

## 中心点の色や大きさを変える

続いて、各領域の中心点の色や大きさを変えてみます。どうやら voronoi_plot_2d の引数では調節できないみたいです。代わりに、引数に show_points=False を指定して一旦非表示にすることが可能です。その後で、改めて描画すればOKです。以下のような感じです。

ちなみに、どっちも「点」という意味ですが、ここでは points が各領域の中心点、vertices が境界線を枝とするグラフの頂点を指すようです。

```python
voronoi_plot_2d(vor, ax=ax, show_vertices=False, show_points=False)
ax.scatter(*X.T, s=20, c='k')
```

![qiita3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787764/7e8edb84-ef58-4107-9011-27baa1e73454.png)

## 破線を実線にする

よく見ると実線と破線が混ざっていますね。破線は無限遠まで続く半直線、実線は線分です。描画範囲の外まで含めて考えると、実線は他の線とぶつかります。破線も実線にしたいところですが、残念ながら voronoi_plot_2d の引数では指定できません。そこで、十分遠くに4つのダミー点を追加します。

と、言葉で書いてもよく分からないと思いますので、下記コードの実行結果を見て下さい。

```python
### コメントは変更箇所のみ ####
def voronoi_sample():
  n = 10
  np.random.seed(42)
  X = np.random.randn(n,2)
  d = np.abs(X).max()                              # 最大値を取得
  C = 3*d*np.array([[1,1],[1,-1],[-1,1],[-1,-1]])  # ダミー点を作成
  vor = Voronoi(np.vstack([X,C]))                  # ダミー点を加えてボロノイオブジェクトを計算
  fig, ax = plt.subplots(figsize=(4,4))
  voronoi_plot_2d(vor, ax=ax, show_vertices=False, show_points=False)
  ax.scatter(*X.T, s=20, c='k')
  ax.scatter(*C.T, s=20, c='C3')   # 説明用にダミー点を表示
  ax.set_xlim((-5*d,5*d))          # x方向の描画範囲を指定
  ax.set_ylim((-5*d,5*d))          # y方向の描画範囲を指定
  fig.tight_layout()
  fig.show()
  fig.savefig('tmp.png')
```

![qiita4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787764/5fb8db06-a35c-45e5-9d1f-baf15e6ea23b.png)

赤い点がダミー点です。黒い点が元からいた点です。ダミー点を十分遠くに配置すれば、無限遠まで続く破線を所望の描画範囲の外側に持っていくことができます。

実際には描画範囲を狭めて使います。xlim, ylimを手動で設定してももちろん構いませんが、以下のように、先に中心点だけ表示して描画範囲を変数に保存し、ボロノイ図を描いた後で再設定する方法もあります。

```python
### コメントは変更箇所のみ ####
def voronoi_sample():
  n = 10
  np.random.seed(42)
  X = np.random.randn(n,2)
  d = np.abs(X).max()
  C = 3*d*np.array([[1,1],[1,-1],[-1,1],[-1,-1]])
  vor = Voronoi(np.vstack([X,C]))
  fig, ax = plt.subplots(figsize=(4,4))
  ax.scatter(*X.T, s=20, c='k')   # 先に中心点だけを表示
  xlim = ax.get_xlim()            # x方向の描画範囲を保存
  ylim = ax.get_ylim()            # y方向の描画範囲を保存
  voronoi_plot_2d(vor, ax=ax, show_vertices=False, show_points=False)
  ax.set_xlim(xlim)               # x方向の描画範囲を戻す
  ax.set_ylim(ylim)               # y方向の描画範囲を戻す
  fig.tight_layout()
  fig.show()
  fig.savefig('tmp.png')
```

![qiita5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787764/6bb4bf96-b5bc-4660-b46d-7cfbc24cdec5.png)

## 領域を色で塗りつぶす

最後に領域を色で塗りつぶしてみます。ダミー点は必須です。でないと、無限遠まで伸びている領域を塗りつぶせないからです。Voronoiオブジェクトが持つ3つの変数、vertices, regions, point_region を使います。意味を理解しなくて良いという方は、以下のコードを呪文だと思って使って下さい。

```python
for i in range(n):
  V = vor.vertices[vor.regions[vor.point_region[i]]]
  ax.fill(*V.T, alpha=0.2)
```

![qiita6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787764/786c104c-63e6-444d-abcd-51371024bf61.png)

意味を説明すると、vertices は頂点の (x,y) 座標が並んだ配列です。regions は「各領域を囲む頂点の番号を並べたリスト」を並べたリストです。例えば、[1,3,0,5] は、頂点1番、頂点3番、頂点0番、頂点5番からなる四角形です。それぞれの座標は verticesを参照してね、ということみたいです。厄介なのが無限遠まで伸びている領域で、無限遠点は -1 で表されています。ところが、vertices の -1 番はPythonでは一番最後の要素という意味になってしまい、変な挙動になります。上のコードは、-1 を含む領域を読まない形になっています。

もう一つ厄介な話があります。regions は領域の番号順に並んでいないのです。i 番目の領域の頂点リストが欲しかったら何番目を見れば良いかを教えてくれるのが point_region です。point_region[i] は整数を返し、regions リストのその番号の箇所を見れば良いということです。それくらいソートしておいてくれても良いのに、とは思いますが、何らかの理由があるのかもしれません。