Google Colaboratoryでバグに悩まされずにcartopyを使う



Python（pandasやNumPy）でデータサイエンスをしたい場合、描画ライブラリとしては[matplotlib](https://matplotlib.org/)を使うのが現在のデファクトスタンダードで、地理空間データや地図の描画には[cartopy](https://scitools.org.uk/cartopy/docs/latest/)を使うのがよいです。そのcartopyですが、Googleクラウド上の[Jupyter notebook](https://jupyter.org/)である[Google Colaboratory](https://colab.research.google.com/)で使おうとすると、2020-04現在は、公式の方法ではバグで悩まされ、実質的に使えません。回避策とその説明をこちらにまとめてみました。

## 結論

以下のどちらかでインストールしましょう。

パターン1

```
!grep '^deb ' /etc/apt/sources.list | \
  sed 's/^deb /deb-src /g' | \
  tee /etc/apt/sources.list.d/deb-src.list
!apt-get -qq update

!apt-get -qq build-dep python3-cartopy
!pip uninstall -y shapely

!pip install --no-binary cartopy cartopy==0.17.0
```

パターン2

```
!grep '^deb ' /etc/apt/sources.list | \
  sed 's/^deb /deb-src /g' | \
  tee /etc/apt/sources.list.d/deb-src.list
!apt-get update

!apt-get -qq build-dep python3-cartopy
!apt-get -qq remove python-shapely python3-shapely

!pip install --no-binary shapely shapely --force
!pip install --no-binary cartopy cartopy==0.17.0
```



## 公式の方法だとバグで悩む

Google Colaboratory公式のノートブックにも[cartopyのインストール方法は書かれており](https://colab.research.google.com/notebooks/snippets/importing_libraries.ipynb)、下記のように、OSのパッケージングシステムaptを使ってできることになっています。

```
!apt-get -qq install python-cartopy python3-cartopy
import cartopy
```

しかし、こうしてインストールしたcartopyにはバグがあり、次のような簡単なコードで落ちます。

### 問題1：`gridlines()`がエラーになる

`gridlines()`でエラーになるので、グリッドが引けません。

サンプルコード：

```.py
import cartopy.crs as ccrs
import matplotlib.pyplot as plt

ax = plt.axes(projection=ccrs.PlateCarree())
ax.gridlines()
ax.coastlines()
```

エラー：

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-3e1ebed7d096> in <module>()
      3 
      4 ax = plt.axes(projection=ccrs.PlateCarree())
----> 5 ax.gridlines()
      6 ax.coastlines()

4 frames
/usr/local/lib/python3.6/dist-packages/matplotlib/ticker.py in _validate_steps(steps)
   2108         steps = np.asarray(steps)
   2109         if np.any(np.diff(steps) <= 0) or steps[-1] > 10 or steps[0] < 1:
-> 2110             raise ValueError('steps argument must be an increasing sequence '
   2111                              'of numbers between 1 and 10 inclusive')
   2112         if steps[0] != 1:

ValueError: steps argument must be an increasing sequence of numbers between 1 and 10 inclusive
```

### 問題2：`coastlines()`で落ちる

`coastlines()`を用いた海岸線の描画が、次のようなランタイムログとともに、「不明な理由により、セッションがクラッシュしました」となります。

サンプルコード：

```.py
import cartopy.crs as ccrs
import matplotlib.pyplot as plt

ax = plt.axes(projection=ccrs.PlateCarree())
ax.coastlines()
```

エラーログ（ランタイムログ）：

```
Apr 14, 2020, 6:18:57 PM	WARNING	warn("IPython.utils.traitlets has moved to a top-level traitlets package.")
Apr 14, 2020, 6:18:57 PM	WARNING	/usr/local/lib/python3.6/dist-packages/IPython/utils/traitlets.py:5: UserWarning: IPython.utils.traitlets has moved to a top-level traitlets package.
Apr 14, 2020, 6:18:57 PM	WARNING	WARNING:root:kernel e5263554-c566-4983-aea5-418e4cb6441a restarted
Apr 14, 2020, 6:18:57 PM	INFO	KernelRestarter: restarting kernel (1/5), keep random ports
Apr 14, 2020, 6:18:54 PM	WARNING	python3: geos_ts_c.cpp:3991: int GEOSCoordSeq_getSize_r(GEOSContextHandle_t, const geos::geom::CoordinateSequence*, unsigned int*): Assertion `0 != cs' failed.
```

## それぞれの問題の原因

### 問題1

問題1の原因は、インストールされているcartopyがv0.14.2と古いことです。cartopyの[Issue #1374](https://github.com/SciTools/cartopy/issues/1374)などでも報告されていますが、バグ自体は[PR #773](https://github.com/SciTools/cartopy/pull/773)で修正され、2018年2月のv0.16.0以降は直っています。

ちなみに、v0.14.2は2016年4月にリリースされたバージョンで、そのように古いものがインストールされてしまうのは、OSがUbuntu 18.04.3 LTSであるためです。LTSとあるとおりlong-term support（長期サポート）されているバージョンで、最初のリリースの18.04は2018年4月にリリースされています。長期サポート版なので長期間安心して使えるのですが、マイナーなソフトウェアだったりまだ安定していないソフトウェアだったりすると、バグが入ったバージョンがパッケージ化されてずっと使われることになってしまうのは少し難点です（[パッケージ情報](https://packages.ubuntu.com/bionic/python-cartopy)参照）。

```
In [1]: cartopy.__version__

Out[1]: '0.14.2'

In [2]: !cat /etc/os-release

Out[2]:
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.3 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
```

### 問題2

問題2は、cartopyとcartopyが使っているShapelyが、異なるバージョンのGEOSを使っており、非互換が発生しているためです。cartopyの[Issue #871](https://github.com/SciTools/cartopy/issues/871)や[Issue #1490](https://github.com/SciTools/cartopy/issues/1490)として報告されており、[Issue #805](https://github.com/SciTools/cartopy/issues/805)で根本的な対応が議論されていますが、まだ解決していません。

DebianやUbuntuにおいては、ライブラリパッケージのバージョンが上がればそれに依存するパッケージはリビルドされるため、異なるバージョンのライブラリにリンクされることはないはずです。実際、aptでcartopyを入れると、下記のとおり、OS提供のdebパッケージ版Shapely 1.6.4が、それにリンクされたcartopyと一緒に入ります。そのShapelyが使われれば問題は起きません。

冒頭の`apt-get install`で入るdebパッケージたち：

```
python-pkg-resources (39.0.1-2 Ubuntu:18.04/bionic [all])
python-pyshp (1.2.12+ds-1 Ubuntu:18.04/bionic [all])
python-shapely (1.6.4-1 Ubuntu:18.04/bionic [amd64])
python-six (1.11.0-2 Ubuntu:18.04/bionic [all])
python-cartopy (0.14.2+dfsg1-2build3 Ubuntu:18.04/bionic [amd64])
python3-pkg-resources (39.0.1-2 Ubuntu:18.04/bionic [all])
python3-pyshp (1.2.12+ds-1 Ubuntu:18.04/bionic [all])
python3-shapely (1.6.4-1 Ubuntu:18.04/bionic [amd64])
python3-six (1.11.0-2 Ubuntu:18.04/bionic [all])
python3-cartopy (0.14.2+dfsg1-2build3 Ubuntu:18.04/bionic [amd64])
```

しかし、実は下記のとおり、Google Colaboratoryには最初からpipでShapelyの最新版v1.7.0が入ってしまっているようです。

```
In [3]: !pip list | grep Shapely

Out[3]: Shapely                  1.7.0
```

この状態で単純にimportすると、cartopyはaptのもの、Shapelyはpipのものが使われます。これが非互換の原因です。面倒くさいですね。

```
In [4]:
import shapely
shapely.__version__

Out[4]: '1.7.0'
```

## 余談：問題2だけなら実は対応策は簡単

問題2についてはpipとaptの併用が悪さをしているので、こちらだけなら実は簡単です。冒頭の`apt-get install`の前か後に下記を実行してあげれば、Shapelyはaptで入れたもの（deb版）が使われるからです（`pip uninstall`前に`import cartopy`してしまうと、その中で`import shapely`されてしまうので注意）。

```
!pip uninstall -y shapely
```

## 対応策

原因が上記であることから、対応策の方針は下記のようになります。

* cartopy v0.16.0以降（できれば最新版v0.17.0）を入れる
* 同じGEOSにリンクされたShapelyとcartopyを使う

具体的には、こうします。

1. pipで入れたShapelyは削除する
2. ShapelyがリンクされているGEOSを用いて、cartopy最新版をpipでソースからビルドして入れる

### apt-get build-dep

ソースから入れる際には、GEOSなどの依存ライブラリが入っていないといけません。1つ1つ入れるのは面倒なので、今回は`apt-get build-dep`を使います。

`apt-get build-dep`は、指定したdebパッケージのソースパッケージをビルドするのに必要なdebパッケージをインストールしてくれるコマンドです。今回だと、cartopy v0.17.0のビルドに必要な環境を、cartopy v0.14.2のdebパッケージのソースパッケージのビルド情報に基づいて整えることになります。当然ながらv0.14.2とv0.17.0とで依存関係に変化がある場合はこれだとうまくいきませんが、大きな変化がなければ大丈夫でしょう（実際、今回はこれでうまくいきました）。

### aptのsources.listの編集

では早速`apt-get build-dep`を、と行きたいところですが、いきなり走らせてもエラーになります。

```
In [5]: !apt-get build-dep cartopy

Out[5]:
Reading package lists... Done
E: Unable to find a source package for cartopy
```

`apt-get build-dep`で使うソースパッケージ情報は、aptのパッケージ取得元設定である`/etc/apt/sources.list`（および`/etc/apt/sources.list.d/*`）では、デフォルトではコメントアウトされているからです。

```
In [6]: !grep deb /etc/apt/sources.list

Out[6]:
deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
# deb-src http://archive.ubuntu.com/ubuntu/ bionic main restricted
deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
# deb-src http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
deb http://archive.ubuntu.com/ubuntu/ bionic universe
# deb-src http://archive.ubuntu.com/ubuntu/ bionic universe
deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
# deb-src http://archive.ubuntu.com/ubuntu/ bionic-updates universe
deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
# deb-src http://archive.ubuntu.com/ubuntu/ bionic multiverse
deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
# deb-src http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse
# deb-src http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse
# deb http://archive.canonical.com/ubuntu bionic partner
# deb-src http://archive.canonical.com/ubuntu bionic partner
deb http://security.ubuntu.com/ubuntu/ bionic-security main restricted
# deb-src http://security.ubuntu.com/ubuntu/ bionic-security main restricted
deb http://security.ubuntu.com/ubuntu/ bionic-security universe
# deb-src http://security.ubuntu.com/ubuntu/ bionic-security universe
deb http://security.ubuntu.com/ubuntu/ bionic-security multiverse
# deb-src http://security.ubuntu.com/ubuntu/ bionic-security multiverse
deb https://cloud.r-project.org/bin/linux/ubuntu bionic-cran35/
```

原因はそれだけなので、単に`deb`で始まる行を`deb-src`に置換したファイルを用意してやり、`apt-get update`でパッケージ情報を更新してあげるだけで解決できます。

```
In [7]:
!grep '^deb ' /etc/apt/sources.list | \
  sed 's/^deb /deb-src /g' | \
  tee /etc/apt/sources.list.d/deb-src.list
!apt-get -qq update
```

これで前述の`apt-get build-dep`も通るようになります。

### インストール

あとは、`pip install --no-binary`を用いて、バイナリパッケージを使わずビルドするようにすれば冒頭で書いた手順になります。aptのShapelyで問題ない場合とGoogle Colaboratoryに元々入っていた最新版を使いたい場合の両方があると思いますので、2パターン示します。どのShapelyを使いたいかによって変えるとよいでしょう。

パターン1：aptのdebパッケージ版Shapely (v1.6.4) を使う場合

```
!grep '^deb ' /etc/apt/sources.list | \
  sed 's/^deb /deb-src /g' | \
  tee /etc/apt/sources.list.d/deb-src.list
!apt-get -qq update

!apt-get -qq build-dep python3-cartopy
!pip uninstall -y shapely

!pip install --no-binary cartopy cartopy==0.17.0
```

パターン2：最新版のShapely (v1.7.0) をソースから入れて使う場合

```
!grep '^deb ' /etc/apt/sources.list | \
  sed 's/^deb /deb-src /g' | \
  tee /etc/apt/sources.list.d/deb-src.list
!apt-get update

!apt-get -qq build-dep python3-cartopy
!apt-get -qq remove python-shapely python3-shapely

!pip install --no-binary shapely shapely --force
!pip install --no-binary cartopy cartopy==0.17.0
```

## 動作確認

問題1と2のPythonプログラムはどちらも問題なく動きます。下記のように、[公式ギャラリーのwaves](https://scitools.org.uk/cartopy/docs/v0.17/gallery/waves.html)もきちんと動きます。

```.py
import matplotlib.pyplot as plt
import numpy as np

import cartopy.crs as ccrs


def sample_data(shape=(73, 145)):
    """Return ``lons``, ``lats`` and ``data`` of some fake data."""
    nlats, nlons = shape
    lats = np.linspace(-np.pi / 2, np.pi / 2, nlats)
    lons = np.linspace(0, 2 * np.pi, nlons)
    lons, lats = np.meshgrid(lons, lats)
    wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)
    mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)

    lats = np.rad2deg(lats)
    lons = np.rad2deg(lons)
    data = wave + mean

    return lons, lats, data


def main():
    fig = plt.figure(figsize=(10, 5))
    ax = fig.add_subplot(1, 1, 1, projection=ccrs.Mollweide())

    lons, lats, data = sample_data()

    ax.contourf(lons, lats, data,
                transform=ccrs.PlateCarree(),
                cmap='nipy_spectral')
    ax.coastlines()
    ax.set_global()
    plt.show()


main()
```
![waves.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39513/b6e63042-874a-358f-e6a5-e34654182865.png
)

## 追記（2020-04-15）：初期状態に戻す

インストールではOSのファイルシステムの中身（状態）が変わりますので、インストールを試行錯誤する場合、初期状態に戻せるやり方を知っておいたほうがよいでしょう。下記の方法でリセットできます。

1. 「ランタイム」メニューから「ランタイムを出荷時設定にリセット」を選択

「ランタイムを再起動」だと、コードの実行状態は初期状態（何もメモリに載っていない状態）になりますが、ファイルシステムは変化したままです。