GISデータに配列を格納したらあかん！っていう話

## 配列をそのまま取り扱えるデータ形式は少ない

Webでデータのやり取りをする時に、頻出のJSON形式。

GIS界隈、特にWebGIS界隈では、GIS用に拡張し、位置情報を標準で付与できるように拡張した**GeoJSON**が広く利用されていますが、普通のJSONとして取り扱うことも可能であるため非常に利用しやすく、またテキストデータであるため、人間が可読するのに優れ柔軟で良いデータ形式です。

ただまぁ柔軟というか仕様が緩い感じだと色んなことができちゃうわけで…
複数の地物のtypeを共存させることができたり、propertiesに配列とか辞書とか入れれちゃいます。
（GISデータで、Point + Polygonのように複数のtypeを格納できる形式はあまり存在しないです。）

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "int_array": [0, 1, 2, 3],
        "string_dict": { "key": "value" }
      },
      "geometry": {
        "type": "Point",
        "coordinates": [141.43798828125, 43.052833917627936]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "string_array": ["こんにちは", " GIS"]
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [141.44545555114746, 43.0469380870684],
            [141.46386623382568, 43.0469380870684],
            [141.46386623382568, 43.05932494785292],
            [141.44545555114746, 43.05932494785292],
            [141.44545555114746, 43.0469380870684]
          ]
        ]
      }
    }
  ]
}
```

まず、複数のレイヤーが単一ファイルで読み込めちゃうと、QGISなどとソフトウェアで読み込んだときに結局レイヤーを分けられてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/7703c656-28f5-0c07-2aca-fe12b1a624ba.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/587889b8-776c-236f-6c4a-b7707a45933d.png)

片方のtypeだけ読み込んだとしても、フィールドは各地物に付与されているものを全て拾ってきてしまうので、Pointの地物に存在していないはずの`string_array`が`null`として表示されたりしちゃいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/5b65d621-81f5-b2e3-1aa0-7e9fb8f0a10f.png)

このようにPointは`string_array`属性を持っていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/0b6b9a4d-a865-a6f7-0a68-8b5db1637743.png)

Pointのレイヤーを出力しようとすると、**`string_array`が標準でエクスポートされるようになっちゃってますし、なんなら`string_dict`の方は連想配列ではなく、文字列型**になっちゃっていたりと色々ややこしいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/8f0a84d9-ceac-d9f9-70f0-519208fc82dc.png)

実際に吐き出してみるとこんな感じで、やっぱり`string_array`は`null`になっています。
（しかも文字列型って書いてたのに実際の値は連想配列のままなんかい…！っていう。）

```json
{
  "type": "FeatureCollection",
  "name": "sample_point",
  "crs": {
    "type": "name",
    "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" }
  },
  "features": [
    {
      "type": "Feature",
      "properties": {
        "int_array": [0, 1, 2, 3],
        "string_dict": { "key": "value" },
        "string_array": null
      },
      "geometry": {
        "type": "Point",
        "coordinates": [141.43798828125, 43.052833917627936]
      }
    }
  ]
}
```

その他、色々データの管理やバリデーションが大変だったり、テキストデータなので大容量のデータを取り扱おうとするとI/Oがものすごい時間かかります。

## 他のフォーマットに変換してみる

で、GeoJSONだけを取り扱っているならまだいいんですが、大容量のデータだったり（100MBとかでも相当きついです）、他のフォーマットに変換が必要になったりするともう大変です。

**配列をフィールドに格納しておけるフォーマットはほとんどなく、基本的にはカンマ区切り文字列**になってしまいます。

試しに、まずはQGISの標準エクスポート形式のGeoPackageでの出力を試みてみましょう。

先ほどのGeoJSONのPolygonの方は`string_array`という属性が配列で値を持っているので、そちらのレイヤーを利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/1308ad60-3edc-4d3e-789c-b31ce9abe7bb.png)

本来Polygonが持っていないはずの属性値のチェックボックスは外して、さらに「リストを使用する」のチェックは外しておきましょう。
（※後述しますが、この「リストを使用する」のオプションもめちゃくちゃややこしい動作をします）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/68fc5df4-1aaf-11b7-598a-940001eafce8.png)

そうするとちゃんとポリゴンが吐き出せましたね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/2d578a7c-50a8-d817-c8b0-e9bc7eb26247.png)

と、思った方！！！騙されないでくださいね！！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/bdb159d6-1ae7-f09a-cb7a-e570fcdb4994.png)

`fid`はGeoPackageで勝手に作られるフィールドなので置いといて…**肝心の`string_array`はJSON型に変更**されています。
で、それだけならまだ良いんですが、なんと値が消えちゃうんですよね…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/b1e2af43-42ac-fde6-283e-7e9a4bef0cac.png)

じゃあ「リストを使用する」にチェックを入れたらどうなるの！？っていうと…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/ed447dd2-c222-b828-5cf7-38afaa4c7920.png)

カンマ区切り文字列になっちゃいましたー！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/fbe938d2-e993-b3c6-3ce0-a6c47cc448f8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/7d703469-cfc0-9471-d9c3-9a874ae1dd6e.png)

FlatGeobufやshapefileなど主要形式も同様で、配列には対応していないので、リストを使用しなければ値は消えてしまうし、リストを使用すれば文字列になります。

## 余談

じゃあ配列を持ったGeoJSONをGeoJSONに変換するとどうなるの？というと…

「リストを使用する」のチェックを外すと…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/19acd367-3391-4638-50fe-aa7ed2b920e8.png)

リストのまま出てきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/3faf2449-ab10-f993-e31a-d6fe88534a56.png)

```json
{
  "type": "FeatureCollection",
  "name": "from_geojson_not_use_list",
  "crs": {
    "type": "name",
    "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" }
  },
  "features": [
    {
      "type": "Feature",
      "properties": { "string_array": ["こんにちは", " GIS"] },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [141.445455551147461, 43.046938087068398],
            [141.463866233825684, 43.046938087068398],
            [141.463866233825684, 43.059324947852922],
            [141.445455551147461, 43.059324947852922],
            [141.445455551147461, 43.046938087068398]
          ]
        ]
      }
    }
  ]
}
```

「リストを使用する」にチェックを入れると…？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/4d429eac-095a-82f5-68b4-7bcee5ff20bf.png)

文字列に変換されてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/646217d2-fe9e-98c7-3f13-9e49ef4f1c46.png)

しかも、前後の文字列が日本語だからか、全角スペースが入っているっぽい…

```json
{
  "type": "FeatureCollection",
  "name": "from_geojson_use_list",
  "crs": {
    "type": "name",
    "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" }
  },
  "features": [
    {
      "type": "Feature",
      "properties": { "string_array": "こんにちは,  GIS" },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [141.445455551147461, 43.046938087068398],
            [141.463866233825684, 43.046938087068398],
            [141.463866233825684, 43.059324947852922],
            [141.445455551147461, 43.059324947852922],
            [141.445455551147461, 43.046938087068398]
          ]
        ]
      }
    }
  ]
}
```

つまり「リストを使用する」っていうオプションは「**ここに格納されているのはリスト（配列）っぽいけど、リスト取り扱えないフォーマットがほとんどだよね。だからチェックを入れたらリストの中に入っている値を文字列に変換することで、暫定的に変換先のデータにも格納しといてあげるね**」っていうオプションのようですね。

めっちゃややこしい…

## 大容量データだと尚更めんどくさい

軽量データであれば今までやってきたように、QGISなどのGUIツールで属性を確認しながらガチャガチャできますが、数百MBや数GBの大規模なデータセットであればそうはいきません。

冒頭で述べた通り、GeoJSONはテキストデータですので、I/O処理が重たい上、QGISで図形を表示するのに時間がかかり、画面移動のたびに同じだけの時間がかかり、属性の表示にも時間がかかり、なおかつ変換処理には失敗して処理落ちの可能性もあります。

このため、データ処理するためにはまずGeoJSONより軽快に操作できるGeoPackageやFlatGeobufなどに変換するのが一般的です。

が！！！！！前述の通りGUIでは色々と厳しいのでデータ変換に良く使われるogr2ogrを利用しようとすると、ここで問題が発生します。

先ほどから利用しているGeoJSONのPolygonのみ抽出したこんなGeoJSONがあるとして…

```json
{
  "type": "FeatureCollection",
  "name": "sample_polygon",
  "crs": {
    "type": "name",
    "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" }
  },
  "features": [
    {
      "type": "Feature",
      "properties": { "string_array": ["こんにちは", " GIS"] },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [141.445455551147461, 43.046938087068398],
            [141.463866233825684, 43.046938087068398],
            [141.463866233825684, 43.059324947852922],
            [141.445455551147461, 43.059324947852922],
            [141.445455551147461, 43.046938087068398]
          ]
        ]
      }
    }
  ]
}
```

こんなコマンドでGeoJSONからGeoPackageに変換します。

```bash
ogr2ogr -f "GPKG" sample_polygon.gpkg sample_polygon.geojson
```

すると「そんな型はサポートしてねぇっす」とエラーが…

```bash
Warning 1: The output driver does not natively support StringList type for field string_array. Misconversion can happen. -mapFieldType can be used to control field type conversion.
```

実際、出力されたGeoPackageをQGISで開いてみると、文字列で出力されている上、何やら奇妙な文字列に変換されてしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/71e4e131-56c8-0a95-ce42-c3a4b5589946.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/f70026d4-9981-5462-9d59-69531e91b377.png)

このように、データが重たいという理由で変換をかけたいのに、肝心なデータがスキップされてしまうので、手詰まりになってしまいました。

GeoPandasやpyogrioで他のフォーマットに変換しようとするともっと酷くて、**pyogrioではエラーのためGeoDataFrameが生成できませんし、GeoPandasだとGeoDataFrameは生成できますが、配列を持っているカラムはスキップ**されます。

### pyogiro

- ロード

```python
import geojson
import geopandas
import pyogrio

# pyogrioではogr的に読み取れない型があったときにエラーが出る上、スキップできずpyogrioでは読み込めない
pyogrio_data = pyogrio.read_dataframe("./sample_polygon.geojson")
```

- エラー

```python
Skipping field string_array: unsupported OGR type: 5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 pyogrio_data = pyogrio.read_dataframe("./sample_polygon.geojson")

...

File pyogrio/_io.pyx:764, in pyogrio._io.ogr_read()

File pyogrio/_io.pyx:557, in pyogrio._io.get_features()

File pyogrio/_io.pyx:443, in pyogrio._io.process_fields()

TypeError: an integer is required
```

### GeoPandas

```python
dgf_data = geopandas.read_file(
    "./sample_polygon.geojson",
    driver="GeoJSON"
)

dgf_data.dtypes
# geometry    geometry
# dtype: object
```

GeoPandasのGeoDataFrameを作成するだけでも一苦労します。
作成するには一度**geojsonライブラリでgeojsonインスタンスを作成したのち、GeoPandasのfrom_featuresメソッドを利用して読み込む**必要があります。

```python
with open("./sample_polygon.geojson") as f:
    geojson_data = geojson.load(f)

features_gdf = geopandas.GeoDataFrame.from_features(geojson_data)

features_gdf.dtypes
# geometry        geometry
# string_array      object
# dtype: object

features_gdf.head()
# geometry	string_array
# 0	POLYGON ((141.44546 43.04694, 141.46387 43.046...	[こんにちは, GIS]

type(features_gdf["string_array"])
# pandas.core.series.Series
```

が、結局GeoPackageなどに吐き出すためにはプログラムを書いて文字列に変換しておく必要がありますのでご注意ください。

### GeoParquet

ちなみに、GeoParquetならそのまま吐き出せます！！！

```python
features_gdf.to_parquet(
    "./sample_polygon.parquet",
    index=True,
    compression="brotli",
)
```

が、最新版のogr2ogrで対応したばかりのフォーマットですし、QGISではまだ出力できないフォーマットですので、あまりおすすめできる方法ではないでしょう…

GeoParquetについてはこちら。
[GeoPandasをやるならFlatGeobufより10倍早いGeoParquetを使おう！](https://qiita.com/nokonoko_1203/items/a01168096c2d4c2d6914)

## 結論

このため実質、配列に対応している汎用フォーマットはGeoJSONのみとなりますし、GeoJSONはファイルサイズが大きくなると他のフォーマットより処理速度に大きく遅れを取ります。

配列で属性を持とうとする以上、**他の型よりも要素数が多くなる → ファイルサイズが大きくなる**ことは目に見えているため、身動きが取れなくなってしまう可能性が高いです。

ということで**GISデータに配列を格納するのはやめましょう！！！！**
（やむを得ない場合でも、　危険性を理解して使ってね。）