For a better version of the sf vignettes see https://r-spatial.github.io/sf/articles/
この vignette では、球面幾何学が何を意味するのか、そして、パッケージ sf
が幾何学的な尺度、述語、変換のために s2geometry ライブラリ (https://s2geometry.io) をどのように使用するかを説明します。 この vignette のコードは、 s2
がインストールされている場合にのみ実行されます。
install.packages("s2")
これで、利用可能です。sf
がロードされた後、s2
が使用されているかどうかを確認することができます。
library(sf)
## Linking to GEOS 3.10.3, GDAL 3.5.1, PROJ 7.2.1; sf_use_s2() is TRUE
sf_use_s2()
## [1] TRUE
以下のように s2
パッケージをアタッチします。
library(s2)
sf
関数名は、ほとんど st_
で始まるのと同じで、s2
パッケージの関数は、ほぼ s2_
で始まります。sf
関数は、楕円座標を扱うときに自動的に、おおむね s2
関数を使用します。そうでない場合、例えば st_voronoi()
では、次のような警告が表示されます。
Warning message:
In st_voronoi.sfc(st_geometry(x), st_sfc(envelope), dTolerance, :
st_voronoi does not correctly triangulate longitude/latitude data
空間座標には、平面上の点を指す「投影座標」(デカルト座標)と、球面(または楕円体)上の点を指す角度(緯度・経度)を指す「非投影座標」(地理座標)があります。平坦な空間は \(R^2\)、球体は \(S^2\) とも呼ばています。
パッケージ sf
は、点 (ノード) と直線 (エッジ) で結ばれたジオメトリを構築するための点、線、および多角形の標準である、simple features を実装しています。シンプルフィーチャの標準は、地理座標を扱うのに適しているかについてはあまり言及していないが、その上に構築された位相的関係系 (DE9-IM) は、2次元平面空間である \(R^2\) を参照している。
しかし、日常的に地理座標を使ってサービスを提供したり、データを交換したりすることが多くなってきています。\(R^2\) を仮定したソフトウェアを使うことで、平面空間はある種の問題には有効です。バージョン 0.9-x までの sf
には球形/楕円形の計算をするための関数(面積、長さ、距離の計算、分割のための lwgeom
パッケージから)がありましたが、そのような座標で \(R^2\) の平面計算をしていることを
although coordinates are longitude/latitude, st_intersects assumes that they are planar
(メッセージの訳:座標は経度/緯度ですが st_intersects はそれらを平面と見なします)
こういったメッセージで喜んでユーザーに警告していましたし、ユーザーが問題の可能性に対処する責任をほのめかすものでもありました。しかし、このようにすると、例えば、 LINESTRING(-179 0,179 0)
が
POINT(0 0)
を通過するのか、POINT(180 0)
を通過するのか、さらには、
こういった点で、あいまいな部分が残ります。
sf
バージョン 1.0 以降では、地理的な座標系で空間オブジェクトを提供すると、 sf
は球面幾何学のための新しいパッケージ s2
(Dunnington, Pebesma, Rubak 2020) を使用します。これによって、
s2
パッケージは Google によって書かれた C++ s2geometry ライブラリのラッパーで、Google の多くの製品 (例: Google Maps, Google Earth Engine, Bigquery GIS) で使用されており、他のいくつかのプログラミング言語にも翻訳されています。
投影座標系では sf
は以前と同様に \(R^2\) で動作します。
\(R^2\) 上の幾何学や DE9-IM と比較して、s2
パッケージは根本的に新しい概念をいくつかもたらしています。
球面 (\(S^2\)) 上では、どんな多角形も2つの領域を定義します。外側の環を追うとき、内側を定義する必要があり、その定義は囲む辺の左側です。これは、多角形を反転させれば(辺の順序を反転させれば)地球儀の他の部分を得ることができ、空の多角形(空集合)に加えて完全な多角形(地球全体)を得ることができることも意味します。
シンプルフィーチャは、リング方向にしたがっています。外側のリングは反時計回り、内側(穴)のリングは時計回りですが、外側のリングと内側のリングの違いはその位置(外側、その後に0個以上の内側)で定義されるので、ある意味でこれは時代遅れです。sf::read_sf
には check_ring_dir
という引数があり、リングの方向をチェックして修正することができます。間違ったリング方向でも、多くのものは動作します。
\(S^2\) の場合、リングの方向は重要です。そのため、st_as_s2
には oriented = FALSE
という引数があり、すべての外側のリングが地球の半分より小さい面積を占めると仮定して、リングの方向をチェックし、修正することができます。
= read_sf(system.file("gpkg/nc.gpkg", package="sf")) # リングの方向が間違っている
nc s2_area(st_as_s2(nc, oriented = FALSE)[1:3]) # リングの方向を直し、面積が正確
## [1] 1137107793 610916077 1423145355
s2_area(st_as_s2(nc, oriented = TRUE)[1:3]) # 方向が間違っている: Earth's surface minus area
## [1] 5.100649e+14 5.100655e+14 5.100646e+14
= read_sf(system.file("gpkg/nc.gpkg", package="sf"), check_ring_dir = TRUE)
nc s2_area(st_as_s2(nc, oriented = TRUE)[1:3]) # 修正は不要
## [1] 1137107793 610916077 1423145355
ここでは、地球全体を表すフルポリゴンとの差分として海を計算した例を紹介します。
= as_s2_geography(TRUE)
g
g## <s2_geography[1]>
## [1] <POLYGON ((0 -90, 0 -90))>
さらに、各国を正射(オルソ画像)投影したものです。
= s2_data_countries()
co = s2_difference(g, s2_union_agg(co)) # oceans
oc = s2_buffer_cells(as_s2_geography("POINT(-30 52)"), 9800000) # visible half
b = s2_intersection(b, oc) # visible ocean
i plot(st_transform(st_as_sfc(i), "+proj=ortho +lat_0=52 +lon_0=-30"), col = 'blue')
(フルポリゴン g
の WKT 表示は、シンプルフィーチャ基準に従っていないため有効な WKT ではなく、入力として使えないことに注意してください。フルポリゴンの内部表現を反映しているようで、WKTの拡張が必要なようです。)
これで、海によって覆われた地球表面の割合を計算することができます。
s2_area(oc) / s2_area(g)
## [1] 0.711301
s2geometry
のポリゴンは、次のようなものがあります。
原理的に、DE9-IM モデルは内部、境界、外部を扱い、交差述語はこれに敏感です(contains と covers の違いは全て境界に関するものです)。しかし、DE9-IM は、ポリゴンがポリゴン coverage(重なりはないが、境界は共有)を形成している場合、ポリゴンに点を一意に割り当てることができない。このため、ポリゴン単位で点数をカウントする場合、ある点がポリゴンの境界を共有している場合、その点を見逃すか( contains )、二重にカウントする( covers 、 intersects )ため、偏りが生じることがあり、後処理が必要になることがあります。SEMI-OPEN の非重複ポリゴンを使用すると、すべての点が交差点内の最大で1つのポリゴンに割り当てられることが保証されます。これは、例えば、グリッド(ラスタ)カバレッジにおいて、各グリッドセルが(通常)左上隅とその上下左右を含む場合に、どのように処理されるかに対応します。
= as_s2_geography("POINT(0 0)")
a = as_s2_geography("POLYGON((0 0,1 0,1 1,0 1,0 0))")
b s2_intersects(a, b, s2_options(model = "open"))
## [1] FALSE
s2_intersects(a, b, s2_options(model = "closed"))
## [1] TRUE
s2_intersects(a, b, s2_options(model = "semi-open")) # a toss
## [1] FALSE
s2_intersects(a, b) # default: semi-open
## [1] FALSE
sf
が st_bbox()
で行っているような,座標範囲に対する最小値と最大値の計算は,球面座標ではあまり意味がありません。なぜなら、球面空間では、 area covered が必ずしも座標範囲に含まれるとは限らないからです。2つの例を示します。
S2には、外接キャップと外接矩形という2つの選択肢があります。
= s2_data_countries("Fiji")
fiji = s2_data_countries("Antarctica")
aa s2_bounds_cap(fiji)
## lng lat angle
## 1 178.7459 -17.15444 1.801369
s2_bounds_rect(c(fiji,aa))
## lng_lo lat_lo lng_hi lat_hi
## 1 177.285 -18.28799 -179.7933 -16.02088
## 2 -180.000 -90.00000 180.0000 -63.27066
キャップは、外接キャップ(円)を中点(lat, lng)として、この点を中心とした角度を報告する。外接矩形は、 lat
と lng
座標の _lo
と _hi
の境界を報告します。 フィジーの場合、 lng_lo
が lng_hi
よりも大きいということは、その領域が反経線を覆っている(交差している)ことを示していることに注意してください。
以前 sf
が使用していた 2 次元 \(R^2\) ライブラリは GEOS で、sf
は GEOS または s2
を使用するよう設定することができる。まず、s2
がデフォルトで使用されているかどうかを確認します。
sf_use_s2()
## [1] TRUE
以下で、s2
をオフにする(GEOSを使う)ことができます。
sf_use_s2(FALSE)
## Spherical geometry (s2) switched off
以下で、オン(s2
を使用)。
sf_use_s2(TRUE)
## Spherical geometry (s2) switched on
library(sf)
library(units)
## udunits database from /Users/baba/Library/R/x86_64/4.2/library/units/share/udunits/udunits2.xml
= read_sf(system.file("gpkg/nc.gpkg", package="sf"))
nc sf_use_s2(TRUE)
= st_area(nc)
a1 sf_use_s2(FALSE)
## Spherical geometry (s2) switched off
= st_area(nc)
a2 plot(a1, a2)
abline(0, 1)
summary((a1 - a2)/a1)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -2.638e-04 -1.650e-04 -7.133e-05 -6.448e-05 1.598e-05 2.817e-04
= st_cast(nc, "MULTILINESTRING")
nc_ls sf_use_s2(TRUE)
## Spherical geometry (s2) switched on
= st_length(nc_ls)
l1 sf_use_s2(FALSE)
## Spherical geometry (s2) switched off
= st_length(nc_ls)
l2 plot(l1 , l2)
abline(0, 1)
summary((l1-l2)/l1)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -0.0012301 -0.0004396 -0.0001258 -0.0001123 0.0001742 0.0009660
sf_use_s2(TRUE)
## Spherical geometry (s2) switched on
= st_distance(nc, nc[1:10,])
d1 sf_use_s2(FALSE)
## Spherical geometry (s2) switched off
= st_distance(nc, nc[1:10,])
d2 plot(as.vector(d1), as.vector(d2))
abline(0, 1)
summary(as.vector(d1)-as.vector(d2))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -1424.5 -613.2 -236.5 -319.5 0.0 460.1
s2
では、パターン付きの st_relate
を除き、すべての単項述語と二項述語が利用可能です。また、 s2
述語を使う場合、 model
に応じて、 model
が closed
(デフォルト) のときのみ、近傍との交点が報告されます。
sf_use_s2(TRUE)
## Spherical geometry (s2) switched on
st_intersects(nc[1:3,], nc[1:3,]) # 自己交差 + 隣接
## Sparse geometry binary predicate list of length 3, where the predicate
## was `intersects'
## 1: 1, 2
## 2: 1, 2, 3
## 3: 2, 3
sf_use_s2(TRUE)
st_intersects(nc[1:3,], nc[1:3,], model = "semi-open") # 自己交差のみ
## Sparse geometry binary predicate list of length 3, where the predicate
## was `intersects'
## 1: 1
## 2: 2
## 3: 3
s2
の同等品として st_intersection
、st_union
、st_difference
、st_sym_difference
が利用可能です。N-ary intersection と difference は (まだ) 存在しません。カスケード接続されたユニオンは存在しますが、機能によるユニオンは s2
では機能しません。
地理座標を持つフィーチャのバッファは、英国を表す非投影オブジェクトを例にとると、以下のように算出できます。
= s2_data_countries("United Kingdom")
uk class(uk)
## [1] "s2_geography" "s2_xptr"
= st_as_sfc(uk)
uk_sfc = s2_buffer_cells(uk, distance = 20000)
uk_buffer = s2_buffer_cells(uk, distance = 20000, max_cells = 10000)
uk_buffer2 = s2_buffer_cells(uk, distance = 20000, max_cells = 100)
uk_buffer3 class(uk_buffer)
## [1] "s2_geography" "s2_xptr"
plot(uk_sfc)
plot(st_as_sfc(uk_buffer))
plot(st_as_sfc(uk_buffer2))
plot(st_as_sfc(uk_buffer3))
= st_as_sf(uk) uk_sf
上のプロットは、s2 バッファ演算の結果の空間精度を max_cells
引数で調整できることを示しています(デフォルトでは1000に設定されています)。適切な値を決めるには、詳細すぎると計算資源が増える (左下の uk_buffer2
で表される) ことと、単純化しすぎる (右下) こととのバランスを取る必要があります。s2で作成されたバッファは、常に s2 のセル境界に沿っており、決して滑らかではないことに注意してください。したがって、 max_cells
に大きな数値を選択すると、一見滑らかに見えますが、拡大すると非常に複雑なバッファになります。
同様の結果を得るためには、まず結果を変換してから sf
の st_buffer()
関数を使用することができます。簡単なベンチマークでは、 s2
ジオメトリエンジンの計算効率を、変換してからバッファを作成する場合と比較して示しています。
# the sf way
system.time({
= st_transform(uk_sfc, 27700)
uk_projected = st_buffer(uk_projected, dist = 20000)
uk_buffer_sf
})## ユーザ システム 経過
## 0.121 0.016 0.377
# sf way with few than the 30 segments in the buffer
system.time({
= st_transform(uk_sfc, 27700)
uk_projected = st_buffer(uk_projected, dist = 20000, nQuadSegs = 4)
uk_buffer_sf2
})## ユーザ システム 経過
## 0.037 0.001 0.038
# s2 with default cell size
system.time({
= s2_buffer_cells(uk, distance = 20000)
uk_buffer
})## ユーザ システム 経過
## 0.036 0.001 0.070
# s2 with 10000 cells
system.time({
= s2_buffer_cells(uk, distance = 20000, max_cells = 10000)
uk_buffer2
})## ユーザ システム 経過
## 0.313 0.005 0.325
# s2 with 100 cells
system.time({
= s2_buffer_cells(uk, distance = 20000, max_cells = 100)
uk_buffer2
})## ユーザ システム 経過
## 0.003 0.000 0.004
これまでのベンチマークの結果は、地理的解像度と計算機資源の間にトレードオフの関係があることを強調しています。Google Maps などの地理サービスに携わるウェブ開発者がよく理解していることです。この場合、デフォルトの transform -> buffer ワークフローよりもわずかに高速に実行される1000セルのデフォルト設定は、英国を表す入力ジオメトリの低解像度を考えると、おそらく適切なものでしょう。
st_buffer
か st_is_within_distance
か?sf
issue tracker で議論されているように、ワークフローを決定し、地理的解像度のレベルを適切に選択することは、反復プロセスになり得ます。\(R^2\) データに対する GEOS による st_buffer
は、滑らかで (ほぼ) 正確です。\(S^2\) データを使用した st_buffer
は、より粗く、複雑で、非滑らかであり、チューニングが必要な場合があります。st_buffer
が使われる一般的なパターンは以下の通りです。
x
(点,線,ポリゴン)を囲むバッファを計算する.y
のすべての出現を見つけ、それらを集約する(例えば、ポイントを数える、または降水量や人口密度などのラスタ変数を平均化する)。このような場合、地理座標を扱うのであれば、バッファを計算するのではなく、 st_is_within_distance
を直接使用して、 x
の各フィーチャに対して、 x
から一定の距離 d
以内にある y
のすべてのフィーチャを選択することが有効な場合があります。この関数の \(S^2\) 版は空間インデックスを使用するため、大きなデータセットでも高速に処理できます。