pandas.DataFrame.apply() で shapely.geometry をつくる

**対象読者**
Pandas, GeoPandas ユーザ
GIS ユーザ

**実行環境**
Windows 10
Python 3.9.5
geopandas 0.9.0
pandas 1.3.1

## はじめに
[GeoPandas](https://geopandas.org) でジオメトリックな処理をして、[shapely.geometry](https://shapely.readthedocs.io/en/stable/manual.html#geometric-objects) のオブジェクト（Point とか Polygon とか）を geometry カラムに格納する場合を考えます。

xy座標からポイントを作る[^1]とか、既存のオブジェクトのバッファをとる[^2]といった基本的な処理であれば、geopandas の関数や geopandas.GeoSeries のメソッドで楽に実現できます。

[^1]: [geopandas.points_from_xy](https://geopandas.org/en/stable/docs/reference/api/geopandas.points_from_xy.html)

[^2]: [geopandas.GeoSeries.buffer](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.buffer.html)


ただ実際は、特殊な処理をして複雑なオブジェクトを作りたい場合があります。

ここでは、[pandas.DataFrame.apply()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html) を使って、複雑な形状のオブジェクトを作りたいと思います。

## pandas.DataFrame.apply()
[pandas](https://pandas.pydata.org/pandas-docs/stable/index.html) を使っているとお世話になることの多い apply() ですが、ここでおさらいをします。

公式リファレンス
[pandas.DataFrame.apply](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html)

データフレームの列や行に対して関数を適用するメソッドですが、ここではgeopandas.GeoDataFrame の geometry カラムを作りたいので、列の処理を考えます。

DataFrame で列に対する処理をする場合は引数 axis=1 を指定します。

今回の趣旨に沿って apply メソッドの使用例を示します。
（使用法を網羅するものではありません）

処理する pandas.DataFrame はこちら。

```py
import pandas as pd

df = pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=['A', 'B'])

print(df)
'''
   A  B
0  1  2
1  3  4
2  5  6
'''
```


### python組み込み関数を列に適用
C列に、A列の値を2進数にして格納

```py
df['C'] = df['A'].apply(bin)
```

### 自作関数を列に適用
D列に、A列とB列の和を格納

```py
def add_ab(row):
    return row['A'] + row['B']

df['D'] = df.apply(add_ab, axis=1)
```

### 無名関数を列に適用
E列に、A列とB列の差を格納

```py
df['E'] = df.apply(lambda row: row['A'] - row['B'], axis=1)
```

` なお lambda 式の中で row.name とするとインデックスにでアクセスできます。 `

### 複数の引数をとる関数を列に適用
F列に、A列のm(=2)倍とB列のn(=3)倍の和を格納

```py
def add_am_bn(row, m, n):
    return row['A'] * m + row['B'] * n

df['F'] = df.apply(add_am_bn, axis=1, m=2, n=3)
```

`apply の引数として、キーワード引数を渡します。`

### 複数の返り値を複数の列に返す
G列に、A列の2乗を、H列にB列の3乗を返す

```py
def square_a_cube_b(row):
    return row['A']**2, row['B']**3

df[['G', 'H']] = df.apply(square_a_cube_b, axis=1, result_type='expand')
```

`apply の result_type 引数に'expand'を指定します。`

### 結果
```py
print(df)
'''
   A  B      C   D  E   F   G    H
0  1  2    0b1   3 -1   8   1    8
1  3  4   0b11   7 -1  18   9   64
2  5  6  0b101  11 -1  28  25  216
'''
# C列: A列の値を2進数にして格納
# D列: A列とB列の和を格納
# E列: A列とB列の差を格納
# F列: A列のm(=2)倍とB列のn(=3)倍の和を格納
# G列: A列の2乗
# H列: B列の3乗
```

## 十字マーク入りのひし形を散りばめる
本題に入ります。

GeoPandas の GeoDataFrame に geometry カラムを設定する例です。

点(x, y)の座標をもとに、
交点(x+a, y+b)、長さ（x方向 c、y方向 d）の十字マーク（マルチライン）と
中心（x+a, y+b）、対角線（x方向 2c, y方向 2d）のひし形（ポリゴン）
を作ります。

こう書かれてもよく分からないと思いますが、十字マーク入りのひし形になります。

先に結果を載せます。
![view_qgis.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1409923/22db5516-0983-c863-0f1b-933851bf82f2.png)
出力したシェープファイルをQGISで表示しています。

### コード

複数の引数をとり、複数のオブジェクトを返す自作関数 xy2geometries() を引数として、x座標 と y座標 をカラムにもつ GeoDataFrame に apply() します。

```py
import random

import geopandas as gpd
from shapely.geometry import MultiLineString
from shapely.geometry import Polygon

fp_cross = 'cross.shp'
fp_diamond = 'diamond.shp'

# apply する自作関数
def xy2geometries(row, a, b, c, d):
    x = row['x']
    y = row['y']
    # 十字マーク
    coords_cross = [
        ((x+a-c/2, y+b), (x+a+c/2, y+b)),
        ((x+a, y+b-d/2), (x+a, y+b+d/2))
    ]
    cross = MultiLineString(coords_cross)
    # ひし形
    coords_diamond = [
        (x+a-2*c/2, y+b),
        (x+a, y+b-2*d/2),
        (x+a+2*c/2, y+b),
        (x+a, y+b+2*d/2)
    ]
    diamond = Polygon(coords_diamond)

    return cross, diamond


# ランダムな10点のx座標とy座標
gdf = gpd.GeoDataFrame({
    'x': [random.randint(-100, 100) for _ in range(10)],
    'y': [random.randint(-100, 100) for _ in range(10)]
})

# 2列にそれぞれ十字マーク（ライン）とひし形（ポリゴン）を格納
# ここで apply() を使います
gdf[['cross', 'diamond']] = gdf.apply(
    xy2geometries, axis=1, result_type='expand', a=5, b=6, c=7, d=8
)

# geometryカラムを指定
gdf_cross = gdf.set_geometry('cross')
gdf_diamond = gdf.set_geometry('diamond')

# シェープファイル出力
gdf_cross[['x', 'y', 'cross']].to_file(fp_cross)
gdf_diamond[['x', 'y', 'diamond']].to_file(fp_diamond)
```


## 最後に
Pandas の apply() のおさらいと、GeoPandas の GeoDataFrame への応用について書きました。

for文を使うことなく複雑な列処理を高速にできるというのが apply() の魅力だと思います。

今回は十字マークとひし形にしましたが、ほかにも例えば、角度と長さで矢印を書いたりメッシュコードからメッシュを作成したりと応用できそうです。