๐Ÿ

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 ๊ธฐ์ค€ ์œ„ ํ•ญ๋ชฉ๋“ค ์ง€์›)