駅データ.jpのCSVファイルから東京の地下鉄路線図を作る

# はじめに

ちょっと古い記事ですが、「[駅とか路線のマスターデータの入手方法 - Qiita](https://qiita.com/kou/items/e0d3fab106e4c52a7f26)」を読んで、路線のCSVデータがあることを知りました。この記事で紹介されている[『駅データ.jp』](http://www.ekidata.jp/)にあるCSVファイルを使って東京の地下鉄路線図を作成してみます。使用するJavaのバージョンは14です。グラフの表示には[yEd - Graph Editor](https://www.yworks.com/products/yed)を使います。
すべてのソースコードは[ここ](https://gist.github.com/saka1029/7d08fef8744a508b29e1c8206510f21e)にあります。

# ダウンロード

[マイページ | 駅データ 無料ダウンロード 『駅データ.jp』](https://ekidata.jp/dl/?p=1)から路線データ、駅データ、接続駅データをダウンロードします。

![マイページ _ 駅データ 無料ダウンロード 『駅データ.jp』 - Google Chrome 2020_07_15 8_04_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78427/f6f9df38-4eab-b38d-2a61-c6d60f0af387.png)

ダウンロードするためには無料のアカウントを作成しておく必要があります。

# CSVファイルの読み込み

CSVファイルを読み込んで`List<List<String>>`に変換する共通のプログラムを作成します。

```java
static final Charset CHARSET = StandardCharsets.UTF_8;
static final Path DIRECTORY = Paths.get("data", "eki");
static final Path LINE_CSV = DIRECTORY.resolve("line20200619free.csv");
static final Path STATION_CSV = DIRECTORY.resolve("station20200619free.csv");
static final Path JOIN_CSV = DIRECTORY.resolve("join20200619.csv");
static final Path GML = DIRECTORY.resolve("metro.gml");

static List<List<String>> readCSV(Path file) throws IOException {
    return Files.readAllLines(file, CHARSET).stream()
        .map(line -> List.of(line.split(",")))
        .collect(toList());
}
```
各項目は引用符などで囲まれていないので簡単に読み込むことができます。

# 路線データの読み込み

最初に路線データを読み込みます。東京の地下鉄路線図を作成するので、路線名が「東京メトロ」または「都営」で始まるものだけを抽出します。

```java
// 東京の地下鉄の路線
List<List<String>> lines = readCSV(LINE_CSV).stream()
    .filter(line -> line.get(2).startsWith("東京メトロ") || line.get(2).startsWith("都営"))
    .collect(toList());
```

# 駅データの読み込み

次に駅データを読み込みますが、先に読み込んだ路線データの路線コードに該当するものだけを抽出するために、路線コードのリストを作成します。（ListではなくSetにすべきでした）

```java
// 東京の地下鉄の路線コードのリスト
List<String> lineCodes = lines.stream()
    .map(line -> line.get(0))
    .collect(toList());
```

このリストにある路線コードを持つ駅だけを抽出します。

```java
// 東京の地下鉄の駅
List<List<String>> stations = readCSV(STATION_CSV).stream()
    .filter(station -> lineCodes.contains(station.get(5)))
    .collect(toList());
```

# 接続駅データの読み込み

最後に接続駅データを読み込みます。ここでも東京の地下鉄の路線コードに関連するものだけを抽出します。

```java
// 東京の地下鉄の駅の接続
List<List<String>> joins = readCSV(JOIN_CSV).stream()
    .filter(line -> lineCodes.contains(line.get(0)))
    .collect(toList());
```

# グラフの作成

読み込んだデータからグラフを作成します。グラフは[yEd - Graph Editor](https://www.yworks.com/products/yed)を使って読み込むので、GML (Graphic Modeling Language)という形式のテキストデータを作成します。

```java
// グラフの作成
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(GML))) {
    w.println("graph [");
    for (List<String> s : stations) {
        w.println("  node [");
        w.println("    id " + s.get(0));     // 駅コード
        w.println("    label \"" + s.get(2) + "\"");  // 駅名
        w.println("  ]");
    }
    for (List<String> j : joins) {
        w.println("  edge [");
        w.println("    source " + j.get(1));    // 接続駅コード1
        w.println("    target " + j.get(2));    // 接続駅コード2
        w.println("  ]");
    }
    w.println("]");
}
```

駅をnode、接続駅をedgeとして作成します。

# 最初のグラフ

yEdで読み込んで以下の整形をします。

* Tools -> Fit Node to Label<br>
各ノードは固定の大きさの正方形として表示されますが、ノードの中に表示されるラベルテキストの大きさに合わせてノードの幅を可変にします。
* Layout -> Organic<br>
各ノードは位置情報をもっていないので、すべてのノードが画面中央に表示されます。Layoutメニューはこれを見やすくレイアウトしてくれます。ここではOrganicレイアウトを使います。

そうするとこんなグラフになりました。13ある路線がバラバラに作成されています。左上は環状線を含むので大江戸線、その隣はY字型なので方南町支線を含む丸の内線であることがわかります。

![metro-最初のプログラム.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78427/4c373782-8044-fda6-47c5-1296917058a9.png)

しかし、これでは路線図とは言えません。
こうなった原因は同じ駅が路線ごとに別々に登録されているためです。

# 同じ駅名で集約

駅コードを同一の駅名を持つもので集約します。結果はリストのマップで、キーが駅名、値がその駅名を持つ駅コードのリストです。例えば`新宿=[2800218, 9930128, 9930401]`といった感じです。

```java
// 駅名でグループ化した駅コード (ex. 新宿=[2800218, 9930128, 9930401])
Map<String, List<String>> stationNameMap = stations.stream()
    .collect(groupingBy(e -> e.get(2),
        mapping(e -> e.get(0), toList())));
```

新宿を表すコードをひとつにまとめるため、駅コードを代表の駅コードに変換するためのマップを作成します。駅コードリストの先頭に登場する駅コードを代表駅コードとします。新宿の場合、`2800218=2800218, 9930128=2800218, 9930401=2800218`となります。

```java
// 駅コードから代表駅コードへのマップ (ex. 2800218=2800218, 9930128=2800218, 9930401=2800218)
Map<String, String> stationCodeMap = stationNameMap.values().stream()
    .flatMap(codes -> codes.stream().map(code -> Map.entry(code, codes.get(0))))
    .collect(toMap(Entry::getKey, Entry::getValue));
```

# 同じ駅名を集約したグラフの作成

同じ駅名をひとつにまとめたグラフを作成します。

```java

// グラフの作成
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(GML))) {
    w.println("graph [");
    for (Entry<String, List<String>> e : stationNameMap.entrySet()) {   // 駅名で集約したデータを使用
        w.println("  node [");
        w.println("    id " + e.getValue().get(0));     // 代表駅コード
        w.println("    label \"" + e.getKey() + "\"");  // 駅名
        w.println("  ]");
    }
    for (List<String> j : joins) {
        w.println("  edge [");
        w.println("    source " + stationCodeMap.get(j.get(1)));  // 代表駅コードに変換
        w.println("    target " + stationCodeMap.get(j.get(2)));  // 代表駅コードに変換
        w.println("  ]");
    }
    w.println("]");
}
```

yEdで読み込ませて、整形するとこうなりました。

![metro-市ケ谷問題あり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78427/f9155e05-93ef-e067-22ac-366807941328.png)

# 市ケ谷と市ヶ谷の統合

路線図らしくなりましたが市ケ谷が２つあります。

![metro-市ケ谷問題あり-拡大.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78427/1014601e-a7d6-0045-57a6-ffbcbb2f9b41.png)

よく見ると「ケ」と「ヶ」の違いで、２つの市ケ谷駅ができてしまっています。
Wikipediaで[市ケ谷駅](https://ja.wikipedia.org/wiki/%E5%B8%82%E3%82%B1%E8%B0%B7%E9%A7%85)を調べてみると、こんなことが書いてあります。

> JR東日本と東京メトロの駅は「市ケ谷」、都営地下鉄の駅は「市ヶ谷」と表記する。

グラフでは

* 曙橋→市ヶ谷→九段下 (都営新宿線)
* 飯田橋→市ケ谷→四ツ谷 (東京メトロ南北線)

となっているので、駅データ.jpはこの違いを正確に表現していることがわかります。
この違いを吸収するためには、先に記述した駅データの読み込みを以下のように変更します。

```java
// 東京の地下鉄の駅
List<List<String>> stations = readCSV(STATION_CSV).stream()
    .filter(station -> lineCodes.contains(station.get(5)))
    .map(station -> station.stream()
        .map(item -> item.replace('ヶ', 'ケ')).collect(toList())) // 市ヶ谷と市ケ谷を統一
    .collect(toList());
```

ここでは東京メトロ流に統一することにしました。

# 完成したグラフ

グラフの最終形はこのようになりました。

![metro.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78427/203cc66b-59b0-c1f9-a883-c8ab33cffaf5.png)

# まとめ

「大手町」が中央付近にあってそれらしいのですが、よく見ると左端に「西船橋」、右端に「荻窪」、上端に「西馬込」、下端に「西高島平」があります。東西と南北がそれぞれ逆転している感じです。グラフには位相幾何学的な情報しか与えていないので仕方がないのですが、駅データには駅の緯度・経度の情報も含まれているので、地理的に正確に近い路線図も作ることができるでしょう。GMLのnodeにはcolor属性もあるので、路線ごとに色分けすることもできます。yEdには様々なレイアウトがあるので、いろいろ試してみると面白いです。
今回作成したプログラムのほとんどがStream APIを使用しています。ほとんどが「ワンライナー」で簡潔に記述できるので非常に便利だと感じました。