県別データの可視化（2024年9月版）

## 概要

日本地図を県別に色分けする**Python**のライブラリ（`japanmap`）を作成したので紹介します。
実行例は、JupyterLab上で確認しています。

## インストール

pipでインストールできます。NumPy、OpenCV、Pillowも一緒にインストールされます。openpyxlは、Excelファイルの読み込みで使います。

```bash:bash
pip install -U japanmap jupyterlab matplotlib pandas openpyxl
```

JupyterLabは、`jupyter lab`で起動します。

## 都道府県コードとは

[都道府県コード](http://nlftp.mlit.go.jp/ksj/gml/codelist/PrefCd.html)（以降、県コードと略す）は、JIS X 0401により定められた、各都道府県ごとの01から47のコードです。
プログラムでは整数で扱います（頭の0を無視します）。

## 県名確認

pref_namesで県コードごとの県名がわかります。

```python
from japanmap import pref_names

pref_names[1]
>>>
'北海道'
```

pref_codeで県名に対する県コードがわかります。

```python
from japanmap import pref_code

pref_code("京都"), pref_code("京都府")
>>>
(26, 26)
```

groupsで八地方区分ごとの県コードがわかります。

```python
from japanmap import groups

groups["関東"]
>>>
[8, 9, 10, 11, 12, 13, 14]
```

## 白地図

pictureで白地図（ラスタデータ）を取得できます。

```python
%config InlineBackend.figure_formats = {"png", "retina"}

import matplotlib.pyplot as plt

from japanmap import picture

plt.rcParams["figure.figsize"] = 6, 6
plt.imshow(picture());
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13955/5c107175-22ca-e737-e928-cdb772fcfbef.png" width="500">

県を色で塗ることもできます。

```python
plt.imshow(picture({"北海道": "blue"}));
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13955/b0ecb9a2-3744-4b0e-ffd8-95eb1ac2fe18.png" width="500">

### PNGファイルへの保存

`savefig`でファイルに保存できます。

```python
plt.imshow(picture({"北海道": "blue"}))
plt.savefig("japan.png")
```

## 隣接情報

`is_faced2sea`で県コードに対して、庁所在地を含むエリアが海に面しているかわかります。

```python
from japanmap import is_faced2sea

for i in [11, 26]:
    print(pref_names[i], is_faced2sea(i))
>>>
埼玉県 False
京都府 True
```

`is_sandwiched2sea`で県コードに対して、庁所在地を含むエリアが海に挟まれているかわかります。（連続でない海辺が2つ以上あるか）

```python
from japanmap import is_sandwiched2sea

for i in [2, 28]:
    print(pref_names[i], is_sandwiched2sea(i))
>>>
青森県 False
兵庫県 True
```

`adjacent`で県コードに対して、庁所在地を含むエリアが隣接する県コードがわかります。

```python
from japanmap import adjacent

for i in [2, 20]:
    adj = " ".join(pref_names[j] for j in adjacent(i))
    print(pref_names[i], ":", adj)
>>>
青森県 : 岩手県 秋田県
長野県 : 群馬県 埼玉県 新潟県 富山県 山梨県 岐阜県 静岡県 愛知県
```

## 境界線のベクトルデータ

`get_data`で各県の点リストと点のindexリストを取得できます。この戻り値を使うと、`pref_points`で県の境界のリストを取得できます。

```python
from japanmap import get_data, pref_points

qpqo = get_data()
pnts = pref_points(qpqo)
pnts[0]  # 北海道の境界座標(経度緯度)リスト
>>>
[[140.47133887410146, 43.08302992960164],
 [140.43751046098984, 43.13755540826223],
 [140.3625317793531, 43.18162745988813],
...
```

`pref_map`で境界線データを可視化できます。

```python
from japanmap import pref_map

svg = pref_map(range(1, 48), qpqo=qpqo, width=2.5)
svg
```

![image](https://qiita-image-store.s3.amazonaws.com/0/13955/d0cf436d-335c-af16-1d65-4247361a66e5.png)

### SVGファイルへの保存

`pref_map`は、SVG形式です。次のようにしてファイルに保存できます。

```python
from pathlib import Path

Path("japan.svg").write_text(svg.data)
```

関東だけをグレースケールにする例です。

```python
pref_map(groups["関東"], cols="gray", qpqo=qpqo, width=2.5)
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/13955/73dfebc8-5223-77e3-5719-012ea3bfa666.png" width="300">


## 県別データ（人口）を使って、県の面積比を変換

人口比で地図上の面積比を変換してみましょう。
人口データは、次の手順で取得できます。

* 総務省統計局のページを開く（https://www.stat.go.jp/）
* 上の「統計データ」の「分野別一覧」を開く（https://www.stat.go.jp/data/guide/1.html）
* 「人口推計」を開く（https://www.stat.go.jp/data/jinsui/index.html）
* 「推計結果」を押し「結果の概要」を開く（https://www.stat.go.jp/data/jinsui/2.html）
* 目次の「II. 各年10月1日現在人口」を押し、一番上の`年`を開く
  * 2024年9月現在は「2023年（令和5年）」（https://www.stat.go.jp/data/jinsui/2023np/index.html）
* 「II　結果の概要」を押し、「第2表　都道府県、男女別人口…」を押してExcelファイルをダウンロードする（例：`05k2023-2.xlsx`）

ダウンロードしたExcelファイルをpandasで開いてみましょう。

```python
import pandas as pd

df = pd.read_excel(
    "05k2023-2.xlsx",
    names=["都道府県", "男女計", "男", "女"],
    index_col=0,
    usecols=[2, 4, 5, 6],
    skiprows=11,
    nrows=47,
)
df[:3]
```

| 都道府県 | 男女計 |   男 |   女 |
| :------- | -----: | ---: | ---: |
| 北海道   |   5092 | 2405 | 2688 |
| 青森県   |   1184 |  559 |  626 |
| 岩手県   |   1163 |  562 |  602 |

人口比の多い順に表示してみましょう。東京都の比の`5.32389`は、県平均の約5.3倍であることを表しています。

```python
df["比"] = df.男女計 / df.男女計.mean()
df.sort_values("比", ascending=False)[:10]
```

| 都道府県 | 男女計 |   男 |   女 |      比 |
| :------- | -----: | ---: | ---: | ------: |
| 東京都   |  14086 | 6914 | 7172 | 5.32389 |
| 神奈川県 |   9229 | 4578 | 4651 | 3.48816 |
| 大阪府   |   8763 | 4191 | 4572 | 3.31203 |
| 愛知県   |   7477 | 3726 | 3751 | 2.82598 |
| 埼玉県   |   7331 | 3640 | 3691 |  2.7708 |
| 千葉県   |   6257 | 3099 | 3158 | 2.36487 |
| 兵庫県   |   5370 | 2551 | 2819 | 2.02963 |
| 福岡県   |   5103 | 2418 | 2685 | 1.92871 |
| 北海道   |   5092 | 2405 | 2688 | 1.92455 |
| 静岡県   |   3555 | 1754 | 1801 | 1.34363 |

可視化してみます。
`trans_area`で県の面積を指定した比率に変換できます。
たとえば、県ごとの変換比率を`[2, 1, 1, 1, ...]`とすると、北海道が元の面積の2倍、他県が元の面積と同じ比率になります。

次では、人口が平均の2倍であれば面積を2倍にするように表示しています。

```python
from japanmap import trans_area

qpqo = get_data(move_hokkaido=True, move_okinawa=True, rough=True)
pref_map(range(1, 48), qpqo=trans_area(df.男女計, qpqo), width=2.5)
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/13955/2b1d6af9-d584-dd4e-9d6c-a257def3c275.png" width="400">

なるべく、ラフにして歪みを減らしたのですが、なかなか厳しいです。

## 白地図上で人口を可視化

次のようにすると、人口の多い県を濃い赤で可視化できます。

```python
cmap = plt.get_cmap("Reds")
norm = plt.Normalize(vmin=0, vmax=df.男女計.max())
fcol = lambda x: "#" + bytes(cmap(norm(x), bytes=True)[:3]).hex()
pic = picture(df.男女計.apply(fcol))
plt.colorbar(plt.imshow(pic, cmap, norm));
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13955/521744b0-24a6-2ca2-a33a-33f1591be85b.png" width="500">

## 4色問題

隣接情報を使って[4色問題](https://ja.wikipedia.org/wiki/%E5%9B%9B%E8%89%B2%E5%AE%9A%E7%90%86)を解いてみましょう。

1つの県を1色とし、隣接する県は異なるように、日本全体を4色で塗りましょう。このように隣り合うもの同士に違う色を割当てる問題を、頂点彩色問題とよびます。
頂点彩色問題とは、グラフ上で隣接する頂点が異なる色になるように、最小の色数で頂点に色を割り当てる問題です。
この応用としては、たとえば、携帯電話の基地局ごとの周波数を決める問題があります。（異なる色→異なる周波数→電波が干渉しないので話せる）

<img src="https://qiita-image-store.s3.amazonaws.com/0/13955/26999634-fcaf-d627-31cc-b74c196b632b.png" width="600">

## 4色問題を解く

どのような平面地図であっても隣接するエリアが異なるようにして、必ず4色以内で塗り分けられることが、数学的に証明されています。しかし、どのように塗り分けたらよいかは、自明ではありません。ここでは、数理最適化を解いて求めることにしましょう。

数理最適化は、コストの最小化などを解くときに使われますが、目的関数がない制約だけの問題でも解くことができます。数理最適化で用いるパッケージPuLPについては、[最適化におけるPython](http://qiita.com/SaitoTsutomu/items/070ca9cb37c6b2b492f0)を見てください。

* 数理モデル`(1)`で決めないといけないことは、`変数`、`目的関数`、`制約条件`の3つ
* 変数：`47都道府県 * 4色 = 188個`の`0`または`1`しか取らない変数を用意する
  * 変数は、パッケージpandasの表で管理する。この変数表`(2)`を使うことで制約条件をわかりやすく書くことができる
* 目的関数：今回は設定しない
* 制約条件：1県ごとに1色とする`(3)`。隣接した県同士は、異なる色にする`(4)`
* ソルバーを実行し解を求める`(5)`
* 変数表に、新しい列`Val`を追加し結果を設定する`(6)`

次のコードを実行するためには、追加でPuLPが必要です（`pip install pulp`）。

```python
import pandas as pd
from pulp import LpBinary, LpProblem, LpVariable, lpSum, value

from japanmap import adjacent, pref_map, pref_names

m = LpProblem()  # 数理モデル(1)
df = pd.DataFrame(
    [(i, pref_names[i], j) for i in range(1, 48) for j in range(4)],
    columns=["Code", "県", "色"],
)
df["Var"] = [  # 変数表(2)
    LpVariable(f"Var{i:03}", cat=LpBinary) for i in df.index
]
for i in range(1, 48):
    m += lpSum(df[df.Code == i].Var) == 1  # 1県1色(3)
    for j in adjacent(i):
        for k in range(4):  # 隣接県を違う色に(4)
            m += lpSum(df.query("Code.isin([@i, @j]) and 色 == @k").Var) <= 1
m.solve()  # 求解(5)
df["Val"] = df.Var.apply(value)  # 結果設定(6)
四色 = ["red", "blue", "green", "yellow"]
cols = df[df.Val > 0].色.apply(四色.__getitem__).reset_index(drop=True)
pref_map(range(1, 48), cols=cols, width=2.5)
```

![image](https://qiita-image-store.s3.amazonaws.com/0/13955/5867775d-a92b-6af0-71d3-767af6e94c6e.png)

以上