
PYCON 2025
๐ ํํ ๋ฆฌ์ผ(folium์ผ๋ก ์ง๋ ํํํ๊ธฐ)
์์ธ์ ๋ฒ์ค์ ๋ฅ์ฅ ๋ฐ์ง๋ ์๊ฐํ
- ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น
pip install geopandas folium pyproj
pip install --upgrade geopandas fiona
- ์ง๋ ๋ฐ์ดํฐ ๋ถ๋ฌ์ ์ขํ๋ฅผ ์ด์ฉํด ๋ง์ปค ํํํ๊ธฐ
import folium
import geopandas as gpd
map = folium.Map(location=[37.4729081, 127.039306], zoom_start=12)
folium.Marker(location=[37.4729081, 127.039306]).add_to(map)
map

- ๋ฒ์ค์ ๋ฅ์ฅ ์์น์ ๋ณด ๋ถ๋ฌ์ ์ง๋์ ๋ฅ๊ทผ ๋ง์ปค๋ก ํ์ํ๊ธฐ
import pandas as pd
import folium
df = pd.read_csv('/content/แแ
ฎแจแแ
ฉแแ
ญแแ
ฉแผแแ
ฎ_แแ
ฅแซแแ
ฎแจ แแ
ฅแแ
ณแแ
ฅแผแ
แ
ฒแแ
กแผ แแ
ฑแแ
ตแแ
ฅแผแแ
ฉ_20241028.csv', encoding='cp949')
df =df[df['๋์๋ช
'].str.contains("์์ธํน๋ณ์")]
map = folium.Map(location=[37.6424341, 127.4890319], zoom_start=12)
for item in df.itertuples():
if item[3]==item[3] and item[4]==item[4]:
folium.CircleMarker(
location = [item[3],item[4]],
radius = 1,
color = 'red',
fill_color = 'red'
).add_to(map)
map

- ๋ฒ์ค์ ๋ฅ์ฅ ์๋ฅผ ๊ตฐ์ง์ผ๋ก ๋ฌถ์ด ์ง๋์ ํ์ํ๊ธฐ
import pandas as pd
import folium
from folium.plugins import MarkerCluster
from folium import Marker
mc = MarkerCluster()
df = pd.read_csv('/content/แแ
ฎแจแแ
ฉแแ
ญแแ
ฉแผแแ
ฎ_แแ
ฅแซแแ
ฎแจ แแ
ฅแแ
ณแแ
ฅแผแ
แ
ฒแแ
กแผ แแ
ฑแแ
ตแแ
ฅแผแแ
ฉ_20241028.csv', encoding='cp949')
df =df[df['๋์๋ช
'].str.contains("์์ธํน๋ณ์")]
map = folium.Map(location=[36.6424341, 127.4890319], zoom_start=12)
mc = MarkerCluster()
for item in df.itertuples():
if item[3]==item[3] and item[4]==item[4]:
mc.add_child(
Marker(location = [item[3], item[4]],
popup=item[2]
)
)
map.add_child(mc)
map


- ์์ธํน๋ณ์ ์์น๊ตฌ ๊ฒฝ๊ณ ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
gdf = gpd.read_file('/content/แแ
ฅแแ
ฎแฏ แแ
กแแ
ตแแ
ฎ แแ
งแผแแ
จ 2017.geojson')
gdf
gdf = gpd.read_file('/content/Hangjeongdong แแ
ฅแแ
ฎแฏแแ
ณแจแแ
งแฏแแ
ต.geojson')
gdf


- ์์น๊ตฌ ๊ฒฝ๊ณ ๋ฐ์ดํฐ์ ๋ฒ์ค์ ๋ฅ์ฅ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ sjoin ์ฐ์ฐํ ํ ์ง๋์ ํํํ๊ธฐ
import pandas as pd
import folium
from folium.plugins import MarkerCluster
from folium import Marker
import json
import geopandas as gpd
from shapely.geometry import Point
map = folium.Map(location=[36.6424341, 127.4890319], zoom_start=12)
mc = MarkerCluster()
df = pd.read_csv('/content/แแ
ฎแจแแ
ฉแแ
ญแแ
ฉแผแแ
ฎ_แแ
ฅแซแแ
ฎแจ แแ
ฅแแ
ณแแ
ฅแผแ
แ
ฒแแ
กแผ แแ
ฑแแ
ตแแ
ฅแผแแ
ฉ_20241028.csv', encoding='cp949')
df =df[df['๋์๋ช
'].str.contains("์์ธํน๋ณ์")]
gdf = gpd.read_file('/content/แแ
ฅแแ
ฎแฏ แแ
กแแ
ตแแ
ฎ แแ
งแผแแ
จ 2017.geojson')
# 3. ์ขํ๋ฅผ Point ๊ฐ์ฒด๋ก ๋ณํ
points = gpd.GeoDataFrame(df, geometry=[Point(xy) for xy in zip(df['๊ฒฝ๋'], df['์๋'])], crs="EPSG:4326")
# 4. ๊ตฌ์ญ ๋งค์นญ (Spatial join)
joined = gpd.sjoin(points, gdf, how="left", predicate='within')
# 5. ๊ตฌ์ญ๋ณ ๊ฐ์ ์ง๊ณ
counts = joined.groupby('SIG_CD').size().reset_index(name='count')
joined
folium.Choropleth(
geo_data='/content/แแ
ฅแแ
ฎแฏ แแ
กแแ
ตแแ
ฎ แแ
งแผแแ
จ 2017.geojson',
data=counts,
columns=['SIG_CD', 'count'],
key_on='feature.properties.SIG_CD',
fill_color='YlOrRd',
fill_opacity=0.7,
line_opacity=0.2,
legend_name='Count'
).add_to(map)
map

์์ธ์ ํด๊ฒ์์์ ์๊ฐํ
- ์์ธ์ ํด๊ฒ์์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ ๋ฐ์ดํฐ ๋ง์ด๋ ํ๊ธฐ
from io import StringIO
import pandas as pd
with open("/content/แแ
ฅแแ
ฎแฏแแ
ต แแ
ฒแแ
ฆแแ
ณแทแแ
ตแจแแ
ฅแท แแ
ตแซแแ
ฅแแ
ก แแ
ฅแผแแ
ฉ.csv", 'r', encoding='euc_kr', errors='ignore') as f:
text = f.read()
brands = ["๋ฒ๊ฑฐํน","๋กฏ๋ฐ๋ฆฌ์","KFC","๋ง์คํฐ์น"]
pattern = "|".join(brands)
store = pd.read_csv(StringIO(text))
filtered_store = store[(store['์์
์ํ๋ช
'] != 'ํ์
') & (store['์ฌ์
์ฅ๋ช
'].str.contains(pattern, case=False ,regex=True))]
#filtered_store = store[(store['์์
์ํ๋ช
'] != 'ํ์
')]
filtered_store = filtered_store.dropna(subset=['์ขํ์ ๋ณด(X)', '์ขํ์ ๋ณด(Y)'])
filtered_store[['์ฌ์
์ฅ๋ช
',"์ขํ์ ๋ณด(X)","์ขํ์ ๋ณด(Y)"]]
gdf = gpd.GeoDataFrame(filtered_store, geometry=gpd.points_from_xy(filtered_store['์ขํ์ ๋ณด(X)'], filtered_store['์ขํ์ ๋ณด(Y)']), crs="EPSG:5174")
gdf = gdf.to_crs(epsg=4326)
gdf[['์ฌ์
์ฅ๋ช
',"์ขํ์ ๋ณด(X)","์ขํ์ ๋ณด(Y)",'geometry']]
gdf = gdf.dropna(subset=['geometry'])
gdf = gdf[~gdf.geometry.is_empty]
gdf

- ์ขํ์ ๋ณด ํ์ํ๊ธฐ
# ๊ฐ ๋ฐ๊พธ๊ธฐ
from pyproj import Transformer
transformer = Transformer.from_crs("EPSG:5174", "EPSG:4326", always_xy=True)
filtered_store['lon'], filtered_store['lat'] = transformer.transform(
filtered_store['์ขํ์ ๋ณด(X)'], filtered_store['์ขํ์ ๋ณด(Y)']
)
gdf = gpd.GeoDataFrame(filtered_store, geometry=gpd.points_from_xy(filtered_store['lon'], filtered_store['lat']), crs="EPSG:4326")
gdf = gdf.to_crs(epsg=4326)
gdf[['์ฌ์
์ฅ๋ช
',"์ขํ์ ๋ณด(X)","์ขํ์ ๋ณด(Y)",'geometry']]
gdf = gdf.dropna(subset=['geometry'])
gdf = gdf[~gdf.geometry.is_empty]
gdf

- ๋งค์ฅ ๊ฐ์ ๋ฐ ์ฃผ์๋ฅผ ์ง๋์ ์๊ฐํํ๊ธฐ(์ฃผ์๋ ํ์ ํํ๋ก)
map = folium.Map(location=[37.56353,127.06344], zoom_start=12)
area = gpd.read_file('/content/Hangjeongdong แแ
ฅแแ
ฎแฏแแ
ณแจแแ
งแฏแแ
ต.geojson')
# 4. ๊ตฌ์ญ ๋งค์นญ (Spatial join)
joined = gpd.sjoin(gdf, area, how="left", predicate='within')
# 5. ๊ตฌ์ญ๋ณ ๊ฐ์ ์ง๊ณ
counts = joined.groupby('adm_cd2').size().reset_index(name='count')
joined
# folium.Choropleth(
# geo_data='/content/แแ
ฅแแ
ฎแฏ แแ
กแแ
ตแแ
ฎ แแ
งแผแแ
จ 2017.geojson',
# data=counts,
# columns=['SIG_CD', 'count'],
# key_on='feature.properties.SIG_CD',
# fill_color='YlOrRd',
# fill_opacity=0.7,
# line_opacity=0.2,
# legend_name='Count',
# popup=counts
# ).add_to(map)
folium.Choropleth(
geo_data='/content/Hangjeongdong แแ
ฅแแ
ฎแฏแแ
ณแจแแ
งแฏแแ
ต.geojson',
data=counts,
columns=['adm_cd2', 'count'],
key_on='feature.properties.adm_cd2',
fill_color='YlOrRd',
fill_opacity=0.7,
line_opacity=0.2,
legend_name='Count'
).add_to(map)
folium.GeoJson(
data='/content/Hangjeongdong แแ
ฅแแ
ฎแฏแแ
ณแจแแ
งแฏแแ
ต.geojson',
name='popup',
tooltip=folium.GeoJsonTooltip(
fields=['adm_nm'], # GeoJSON ์์ฑ ์ค ํ์ํ ํ๋๋ช
localize=True
),
fill_opacity=0,
style_function=lambda feature: {
"color": None,
},
).add_to(map)
map

๊ธฐ์ ์์ธ ์ค๋ช
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋ฆฌ
- pandas: ํ ํํ(ํ ์ด๋ธ) ๋ฐ์ดํฐ ์ ์ฒ๋ฆฌ
- geopandas:
pandas
+ ๊ณต๊ฐ ์ ๋ณด(์ /์ /๋ฉด) ์ง์ (Shapely ๊ธฐ๋ฐ)
- folium: Leaflet.js ๊ธฐ๋ฐ์ ์ธํฐ๋ํฐ๋ธ ์น ์ง๋ ์๊ฐํ (HTML๋ก ์ ์ฅ ๊ฐ๋ฅ)
- pyproj: ์ขํ๊ณ ๋ณํ(ํฌ์/๊ฒฝ์๋ โ ํ๋ฉด, EPSG ๋ณํ)
- fiona: ๋ฒกํฐ ํ์ผ(Shapefile, GeoJSON ๋ฑ) ์ฝ๊ธฐ/์ฐ๊ธฐ ๋ฐฑ์๋
Folium ํต์ฌ ๊ธฐ์ ์ ๋ฆฌ
- Folium์ ์๊ฒฝ๋(WGS84) ๊ธฐ์ค์ผ๋ก ์ง๋๋ฅผ ๊ทธ๋ฆฐ๋ค.
๊ธฐ๋ณธ ์ง๋
import folium m = folium.Map(location=[37.5665, 126.9780], zoom_start=12, tiles="cartodbpositron") m.save("map.html")
๋ง์ปค๋ฅ
# ๋จ์ผ ๋ง์ปค folium.Marker([37.57, 126.98], popup="์ข ๋ก", tooltip="ํด๋ฆญ!").add_to(m) # ์ํ ๋ง์ปค folium.CircleMarker( location=[37.56, 126.97], radius=8, fill=True, fill_opacity=0.7 ).add_to(m)
MarkerCluster
from folium.plugins import MarkerCluster cluster = MarkerCluster().add_to(m) for lat, lon, name in [(37.57,126.98,"A"), (37.56,126.97,"B")]: folium.Marker([lat, lon], popup=name).add_to(cluster)
Chorolpleth (๋ฒ๋ก์ง๋)
# df: pandas DataFrame (์ด: ์ง์ญ์ฝ๋ 'code', ๊ฐ 'value') # geo: ์ง์ญ ๊ฒฝ๊ณ GeoJSON (์์ฑ์ ์ง์ญ์ฝ๋ ํค ์กด์ฌ) folium.Choropleth( geo_data="regions.geojson", data=df, columns=["code", "value"], key_on="feature.properties.code", # GeoJSON ์์ฑ ๊ฒฝ๋ก fill_opacity=0.8, line_opacity=0.4, legend_name="์งํ ๊ฐ" ).add_to(m)
๋ฐ์ดํฐ ํ์ฉ
GeoDataFrame
import pandas as pd import geopandas as gpd from shapely.geometry import Point # CSV -> ์ ํฌ์ธํธ ๋ณํ (๊ฒฝ์๋) df = pd.read_csv("points.csv") # columns: lat, lon, name gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df["lon"], df["lat"]), crs="EPSG:4326") # ํฌ์ ์ขํ๊ณ(๋ฏธํฐ ๋จ์)๋ก ๋ณํ (์: ์์ธ ๊ทผ์ฒ UTM 52N: 32652 ๊ถ์ฅ, ํน์ 3857) gdf_m = gdf.to_crs(3857)
ํ์ผ ์ฝ๊ธฐ/์ฐ๊ธฐ
# ์ง์ญ ๊ฒฝ๊ณ(๋ฉด) ์ฝ๊ธฐ poly = gpd.read_file("regions.geojson") # ๋๋ .shp poly = poly.to_crs(4326) # Folium ๊ทธ๋ฆด ๊ฑฐ๋ฉด 4326 # ์ ์ฅ gdf.to_file("points.geojson", driver="GeoJSON")
sjoin ์ฐ์ฐ
๊ฐ๋
- ๋ ๊ณต๊ฐ ๋ ์ด์ด(์ /์ /๋ฉด) ๋ฅผ ๊ณต๊ฐ ๊ด๊ณ(ํฌํจยท๊ต์ฐจ ๋ฑ)๋ก ์กฐ์ธ
- SQL์ join๊ณผ ๋น์ทํ์ง๋ง, ํค ๋์ ๊ณต๊ฐ๊ด๊ณ(predicate) ์ฌ์ฉ
์ฝ๋
gpd.sjoin(left_gdf, right_gdf, how="left", predicate="intersects")
how
:"left"
(๊ธฐ๋ณธ),"inner"
,"right"
predicate
(์์ฃผ ์ฐ๋ ๊ฒ)"intersects"
: ๊ฒน์น๊ธฐ๋ง ํ๋ฉด ๋งค์นญ(๊ฐ์ฅ ๋ฒ์ฉ)
"within"
: ์ผ์ชฝ ์ง์ค๋ฉํธ๋ฆฌ๊ฐ ์ค๋ฅธ์ชฝ ์์ ์์ด์ผ ํจ
"contains"
: ์ผ์ชฝ์ด ์ค๋ฅธ์ชฝ์ ํฌํจํด์ผ ํจ
"touches"
: ๊ฒฝ๊ณ๋ง ์ ์ด
"overlaps"
: ์ผ๋ถ ๊ฒน์น๋ ์ด๋ ํ์ชฝ์ด ์์ ํฌํจ ์๋
"crosses"
,"covers"
,"covered_by"
๋ฑ๋ ์ฌ์ฉ ๊ฐ๋ฅ(ํ๊ฒฝ/๋ฒ์ ์ ๋ฐ๋ผ ์ง์ predicate๊ฐ ๋ค๋ฅผ ์ ์์ผ๋ Shapely 2.x ๊ธฐ์ค ์ ํญ๋ชฉ๋ค ์ง์)