気象過去データの利用５（東京の天気遷移）

気象庁が、2020年3月末まで、気象過去データを無償で提供しています。
(参考) 気象過去データの利用環境
https://www.data.jma.go.jp/developer/past_data/index.html

基本的な気象データは、「利用目的・対象を問わず、どなたでもご利用頂けます。」とのことなので、気象データを使って何か行っていきます。

気象庁の「過去の気象データ・ダウンロード」ページからもダウンロードできますが、まとめて一括してダウンロードできるため非常に便利です。
利用可能なデータは、以下の掲載されています。
https://www.data.jma.go.jp/developer/past_data/data_list_20200114.pdf
もうすぐ期限ですので、必要な方は早めのダウンロードを

今回は、天気の推移をネットワークグラフにします。
グラフの表示には、NetworkX(2.4)を利用します。

# データダウンロード
「気象過去データの利用２（東京オリンピック期間の最高気温変遷）」と同様に「地上気象観測」－「時・日別値」を利用します。
ファイルの形式については、以下を確認してください。
http://data.wxbc.jp/basic_data/kansoku/surface/format_surface.pdf

surfaceフォルダにファイルをダウンロードします。約2GBの容量があるため時間がかかります。

```python
import os
import urllib.request

# 地上気象観測　時・日別値ファイルダウンロード
url    = 'http://data.wxbc.jp/basic_data/kansoku/surface/hourly_daily_1872-2019_v191121.tar'
folder = 'surface'
path   = 'surface/hourly_daily_1872-2019_v191121.tar'
# フォルダ作成
os.makedirs(folder, exist_ok=True)
if not os.path.exists(path):
    # ダウンロード
    urllib.request.urlretrieve(url, path)
```

ファイルの詳細は、「気象過去データの利用２（東京オリンピック期間の最高気温変遷）」を参照してください。

# 天気データ
天気データは、天気概況として、「昼間」、「夜間」のデータが格納されています。「昼間」のデータを利用します。詳細は、「気象過去データの利用４（東京オリンピック期間の天気の気持ち）」を参照してください。
今回は、天気の推移を確認するため、最初の天気のみ利用します。
天気の変換用リストを用意しておきます。

```python
weather = ['天気なし', '快晴', '晴', '薄曇', '曇', '霧', '霧雨', '雨', '大雨', '暴風雨',
           'みぞれ', '雪', '大雪', '暴風雪', 'ふぶき', '地ふぶき', '予備', '予備', '予備', '予備',
           '大風', '雷', 'あられ', 'ひょう', '大風・雷', '雷・あられ', '雷・ひょう', '雷・霧', '降水なし', '晴れ間あり',
           '予備', '×']
```

# グラフ作成
天気の推移を表すグラフを作成するため、「晴」→「曇」のように方向をもっています。
NetworkXの有向グラフ(DiGraph)を生成します。
後で、matplotlibで表示を行うため、matplotlibもインポートしておきます。

```python
import networkx as nx
import matplotlib.pyplot as plt

# 有向グラフ
Graph = nx.DiGraph()
```

## ノード
グラフのノード（頂点）に、天気を追加します。
「晴」、「曇」などの天気を表します。天気の出現回数をweightに保持します。
has_node関数にて既に同じ天気が登録されているか確認します。新規の場合は、add_node関数で登録を行います。その際に、weightは1を設定します。すでに登録されている場合は、weightを+1します。
以下、ノード登録部分の抜粋

```python
# ノード追加
if Graph.has_node(conditions): # ノードがすでに登録されている場合weight+1
    Graph.nodes[conditions]['weight'] += 1
else: # ノードが登録されていない場合追加
    Graph.add_node(conditions, weight=1)
```

## エッジ
「晴」→「曇」のように天気の遷移をエッジ（辺）として登録します。
エッジ登録用に、ひとつ前の天気を保持しておきます。ここでは、prev変数に格納されています。
エッジもweightに出現回数を設定します。
has_edge関数にて既に同じ天気の遷移が登録されているか確認します。新規の場合は、add_edge関数で登録を行います。その際に、weightは1を設定します。すでに登録されている場合は、weightを+1します。
以下、エッジ登録部分の抜粋

```python
# エッジ追加
if Graph.has_edge(prev, conditions): # エッジがすでに登録されている場合weight+1
    Graph.edges[prev, conditions]['weight'] += 1
else:
    Graph.add_edge(prev, conditions, weight=1)
```

## データの読み込み
天気データの読み込みは、基本的に「気象過去データの利用４（東京オリンピック期間の天気の気持ち）」と同様ですので、そちらを参照してください。
天気のグラフ作成プログラムです。
東京の天気概況が格納されている1989年以降の天気の遷移を設定します。

```python
# 天気概況取得
import tarfile

# 地点設定=東京
p_no = '662'

prev = None
# tarファイルに含まれるファイルを取得
with tarfile.open(path, 'r') as tf:
    for tarinfo in tf:
        if tarinfo.isfile():
            # 1989年以降のみ対象
            if tarinfo.name[13:17] >= '1989':
                # tar.gzファイルに含まれるファイルを取得
                with tarfile.open(fileobj=tf.extractfile(tarinfo), mode='r') as tf2:
                    for tarinfo2 in tf2:
                        if tarinfo2.isfile():
                            # 地点が一致するファイルのみ読み込み
                            if tarinfo2.name[-3:] == p_no:
                                print(tarinfo2.name)
                                # ファイルをopen
                                with tf2.extractfile(tarinfo2) as tf3:
                                    lines = tf3.readlines()
                                    for line in lines:
                                        # データが含まれないファイルは無視
                                        if line[0:3] == b'   ':
                                            continue
                                        # 年
                                        year = line[14:18].decode()
                                        # 月日
                                        date = line[18:22].decode().replace(' ', '0')
                                        # 天気概況を取得
                                        conditions = ''
                                        p = 1500
                                        # 天気が観測されていない
                                        if int(line[p+4:p+5]) == 2:
                                            prev = None
                                            continue
                                        # 天気
                                        w = int(line[p+2:p+4])
                                        w_rmk = int(line[p+4:p+5])
                                        conditions = weather[w]
                                                                                
                                        # ノード追加
                                        if Graph.has_node(conditions): # ノードがすでに登録されている場合weight+1
                                            Graph.nodes[conditions]['weight'] += 1
                                        else: # ノードが登録されていない場合追加
                                            Graph.add_node(conditions, weight=1)
                                        # エッジ追加
                                        if prev is not None:
                                            if Graph.has_edge(prev, conditions): # エッジがすでに登録されている場合weight+1
                                                Graph.edges[prev, conditions]['weight'] += 1
                                            else:
                                                Graph.add_edge(prev, conditions, weight=1)
                                        prev = conditions
```

## ノードの確認
nodes関数でノードを表示できます。data=Trueでweightの値も確認できます。

```python
# ノード確認
Graph.nodes(data=True)
```

```
NodeDataView({'曇': {'weight': 4016}, '晴': {'weight': 4225}, '快晴': {'weight': 1118}, '雨': {'weight': 1184}, '大雨': {'weight': 150}, 'みぞれ': {'weight': 15}, '薄曇': {'weight': 472}, '霧': {'weight': 1}, '雪': {'weight': 33}, '大雪': {'weight': 4}, '暴風雨': {'weight': 3}, '霧雨': {'weight': 8}})
```

ノード数は、number_of_nodesで確認できます。

```python
Graph.number_of_nodes()
```

```
12
```

## エッジの確認
edges関数でエッジを表示できます。data=Trueでweightの値も確認できます。

```python
# エッジ確認
Graph.edges(data=True)
```

```
OutEdgeDataView([('曇', '晴', {'weight': 1205}), ('曇', '雨', {'weight': 597}), ('曇', '曇', {'weight': 1759}), ('曇', '快晴', {'weight': 214}), ('曇', '大雨', {'weight': 76}), ('曇', '霧', {'weight': 1}), ('曇', 'みぞれ', {'weight': 11}), ('曇', '薄曇', {'weight': 136}), ('曇', '雪', {'weight': 10}), ('曇', '暴風雨', {'weight': 1}), ('曇', '大雪', {'weight': 1}), ('曇', '霧雨', {'weight': 4}), ('晴', '快晴', {'weight': 528}), ('晴', '曇', {'weight': 1274}), ('晴', '雨', {'weight': 270}), ('晴', '晴', {'weight': 1916}), ('晴', '大雨', {'weight': 25}), ('晴', '薄曇', {'weight': 193}), ('晴', '雪', {'weight': 11}), ('晴', 'みぞれ', {'weight': 2}), ('晴', '大雪', {'weight': 3}), ('晴', '霧雨', {'weight': 3}), ('快晴', '晴', {'weight': 545}), ('快晴', '曇', {'weight': 227}), ('快晴', '快晴', {'weight': 270}), ('快晴', '雨', {'weight': 14}), ('快晴', '薄曇', {'weight': 61}), ('快晴', '大雨', {'weight': 1}), ('雨', '曇', {'weight': 497}), ('雨', '晴', {'weight': 335}), ('雨', '雨', {'weight': 231}), ('雨', 'みぞれ', {'weight': 1}), ('雨', '大雨', {'weight': 32}), ('雨', '快晴', {'weight': 54}), ('雨', '雪', {'weight': 6}), ('雨', '暴風雨', {'weight': 2}), ('雨', '薄曇', {'weight': 26}), ('大雨', '晴', {'weight': 51}), ('大雨', '雨', {'weight': 23}), ('大雨', '曇', {'weight': 52}), ('大雨', '快晴', {'weight': 14}), ('大雨', '大雨', {'weight': 6}), ('大雨', '薄曇', {'weight': 4}), ('みぞれ', '晴', {'weight': 7}), ('みぞれ', '曇', {'weight': 4}), ('みぞれ', '雨', {'weight': 2}), ('みぞれ', '薄曇', {'weight': 1}), ('みぞれ', 'みぞれ', {'weight': 1}), ('薄曇', '晴', {'weight': 142}), ('薄曇', '曇', {'weight': 190}), ('薄曇', '雨', {'weight': 45}), ('薄曇', '快晴', {'weight': 31}), ('薄曇', '薄曇', {'weight': 50}), ('薄曇', '雪', {'weight': 4}), ('薄曇', '大雨', {'weight': 9}), ('薄曇', '霧雨', {'weight': 1}), ('霧', '快晴', {'weight': 1}), ('雪', '晴', {'weight': 18}), ('雪', '雪', {'weight': 2}), ('雪', '快晴', {'weight': 5}), ('雪', '曇', {'weight': 6}), ('雪', '大雨', {'weight': 1}), ('雪', '薄曇', {'weight': 1}), ('大雪', '晴', {'weight': 2}), ('大雪', '曇', {'weight': 1}), ('大雪', '快晴', {'weight': 1}), ('暴風雨', '晴', {'weight': 2}), ('暴風雨', '曇', {'weight': 1}), ('霧雨', '晴', {'weight': 2}), ('霧雨', '曇', {'weight': 4}), ('霧雨', '雨', {'weight': 2})])
```

エッジ数は、number_of_edgesで確認できます。

```python
Graph.number_of_edges()
```

```
71
```

# グラフ表示
グラフを表示します。
ノードの大きさは、天気の出現回数により設定します。

```python
# ノード表示サイズを天気の出現回数に設定
node_size = [w['weight'] for n, w in Graph.nodes(data=True)]
```

ノード間の線の太さを天気の遷移数に応じて設定します。

```python
# エッジの線の太さを出現回数に設定(0.01倍する)
edge_width = [w['weight']*0.01 for u, v, w in Graph.edges(data=True)]
```

draw_networkxでグラフを表示します。
日本語のフォントを設定します。Windowsのため、Windowsのフォントを指定しています。利用している環境に合わせてフォントを設定してください。
node_size、widthにノードの大きさ、線の太さを設定します。

```python
# グラフ表示
plt.figure(figsize=(10, 10))
nx.draw_networkx(Graph, with_labels=True, font_family='MS Gothic', 
                 node_size=node_size, node_color='blue', 
                 width=edge_width, edge_color='navy')
plt.show()
```

以下のグラフが表示されました。
![networkx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212365/539512f0-c50e-0f61-0bfd-2bbaefe68a3e.png)

中心に寄り過ぎました。

## spring_layout
posパラメータにレイアウトを指定することができます。draw_networkxでは、既定値spring_layoutで表示されます。
spring_layoutにノード間の距離を設定してみます。

```python
# グラフ表示(spring_layout)
plt.figure(figsize=(10, 10))
pos = nx.spring_layout(Graph, k=10)
nx.draw_networkx(Graph, pos, with_labels=True, font_family='MS Gothic', 
                 node_size=node_size, node_color='blue', 
                 width=edge_width, edge_color='navy')
plt.show()
```

![networkx_spring_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212365/bb38c764-ef40-1bad-f290-8d43770bdb03.png)

わかりやすくなりました。
この図を見て何が言えるか？
・「晴」が一番多く、「曇」が次いで多い。
・「快晴」からの遷移は、「晴」が多い。「雨」からの遷移は、「曇」が多い。
当たり前と言えば、当たり前ですね。

## shell_layout
ノードを円形に配置します。

```python
# グラフ表示(shell_layout)
plt.figure(figsize=(10, 10))
pos = nx.shell_layout(Graph)
nx.draw_networkx(Graph, pos, with_labels=True, font_family='MS Gothic', 
                 node_size=node_size, node_color='blue', 
                 width=edge_width, edge_color='navy')
plt.show()
```

![networkx_shell_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212365/bb75b788-4548-8cbf-6f29-8968a9257364.png)

## spiral_layout
ノードを螺旋に配置します。

```python
# グラフ表示(spiral_layout)
plt.figure(figsize=(10, 10))
pos = nx.spiral_layout(Graph)
nx.draw_networkx(Graph, pos, with_labels=True, font_family='MS Gothic', 
                 node_size=node_size, node_color='blue', 
                 width=edge_width, edge_color='navy')
plt.show()
```

![networkx_spiral_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212365/a9304382-1d3d-0f2e-9532-af1bc012f3b1.png)

## kamada_kawai_layout
Kamada-Kawaiのアルゴリズムによる表示

```python
# グラフ表示(kamada_kawai_layout)
plt.figure(figsize=(10, 10))
pos = nx.kamada_kawai_layout(Graph)
nx.draw_networkx(Graph, pos, with_labels=True, font_family='MS Gothic', 
                 node_size=node_size, node_color='blue', 
                 width=edge_width, edge_color='navy')
plt.show()
```

![networkx_kamada_kawai_layout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212365/219665af-9ca8-209f-9e7f-9d043c854a1c.png)

いろいろなレイアウトで表示しました。
matplotlibによる表示の制約上、遷移の向きごとの太さや「晴」→「晴」のような自己ループの表示ができていません。
Graphvizを利用すればもっと表現力のあるグラフが描けたかもしれませんが今回は試しておりません。

データの公開期間が2020年3月末までのため、必要な方は、早めのダウンロードをお勧めします。