رسم داده ها بر روی نقشه ها با پایتون

مدتی هست که برای یک پروژه تفریحی data science سر و کارم با رسم داده ها بر روی نقشه ها و ابزارهای مختلف این کار افتاده است. اخیرا با یک کتابخانه پایتون به نام folium  آشنا شدم که به ما امکان رسم نقشه ها را با استفاده از کتابخانه leaflet.js جاوااسکریپت با API خود می دهد. برای نشان دادن قابلیت های این کتابخانه از یک دیتاست کاملا ایرانی استفاده می کنم. حدود 1.5 سال پیش شرکت اوبار  که در در زمینه ارسال سفارشات و حمل بار آنلاین ( فکر کنم عمدتا بین شهری) در ایران فعالیت می کند یک مسابقه یادگیری ماشین در پلتفرم kaggle برگزار کرد که در آن  به صورت خلاصه هدف آن این بود که با استفاده از یک مجموعه از اطلاعات و متغیرهای مکانی (spatial) مثل استان مبدا و  مقصد و غیرمکانی مثل وزن بار و نوع وسیله قیمت بارهای حمل شده در سطح ایران به درستی پیشبینی شود  تا بر اساس این پیش بینی یک سیاست گذاری ثابت برای قیمت گذاری بارها پیاده سازی شود.

همانطور که گفتم در این مسابقه و دیتاست مربوط به آن انواع اطلاعات مکانی مثل عرض و طول و جغرافیایی مبدا و مقصد بار مشخص شده اند و همین این دیتاست را برای مثال این پست ایده آل می کند. (دیتاست مربوطه را می توانید از آدرس مسابقه در سایت کگل دانلود کنید)

ubaar= pd.read_csv(&quotdata/train.csv&quot)

ubaar.head()


با این وجود دیتاست اوبار در حدود50 هزار مشاهده مجزا دارد و اگر بخواهیم همه این مشاهدات را بر روی نقشه رسم کنیم به خاطر تعاملی بودن نقشه در بارگزاری آن دچار مشکل خواهیم شد. برای همین برای سادگی تنها یک بخشی از داده ها (1000 مشاهده) را برای ادامه کار انتخاب می کنیم.

sample_ubaar = ubaar.head(1000)

شروع به کار

مثل همیشه ابتدا نیاز به  نصب کتابخانه مدنظرمان که در این جا folium  است داریم:

pip install folium

بعد از نصب کتابخانه نوبت به اضافه کردن کتابخانه folium و یک نقشه از ایران می سازیم. برای ساخت یک نقشه نیاز به عرض جغرافیایی و طول جغرافیایی از مرکز ایران داریم. ( اگر با مقدار ورودی پارامتر zoom_start کمی بازی کنید متوجه کاربرد آن هم می شوید)

import folium

iran_map = folium.Map(location = [32.4279,53.6880],zoom_start = 5)

iran_map

حالا هدف ما این است که نقاط مبدا و مقصد  حمل بار را بر روی  نقشه ای که ایجاد کرده ایم رسم کنیم. برای این کار از کلاس ()folium.CircleMarker  استفاده می کنیم که برای ما بر روی نقشه نقطه رسم می کند. برای رسم هر نقطه لازم است که عرض و جغرافیایی آن نقطه را به عنوان ورودی به این کلاس بدهیمعلاوه بر این می توانیم پارامترهای دیگری مثل رنگ، اندازه و ... هر نقطه را مشخص  کنیم. به طور مثال، برای نقاط مبدا از رنگ قرمز و برای نقاط مقصد از رنگ آبی استفاده میکنیم.

در اینجا بر روی دیتاست نمونه ای که ایجاد کرده ایم حلقه for می زنیم و  عرض و طول جغرافیایی نقاط مبدا را به صورت مجزا به کلاس می دهیم و در انتها این  نقطه را با متد به نقشه قبلی که ایجاد کرده بودیم اضافه می کنیم تا این نقاط بر روی این نقشه رسم شوند.

for lat, lng in zip(sample_ubaar.sourceLatitude,sample_ubaar.sourceLongitude):

folium.CircleMarker(

[lat, lng],

radius=2,

color='red',

fill=True,

fill_color='darkred',

fill_opacity=0.5 ).add_to(iran_map)

iran_map

به صورت مشابه همین فرآیند را برای نقاط مقصد هم اجرا می کنیم.

for lat, lng in zip(sample_ubaar.destinationLatitude,sample_ubaar.destinationLongitude):

folium.CircleMarker(

[lat, lng],

radius=2,

color='blue',

fill=True,

fill_color='blue',

fill_opacity=0.3 ).add_to(iran_map)

iran_map

در نمودارهای قبلی می توانستیم تجمع اصلی مبدا و مقصد نقاط حمل بار را مشاهده کنیم ولی اطلاعاتی در خصوص قیمت حمل بار که اهمیت بالایی دارد مشخص نبود. حالا می توانیم یک کار جالب تر هم انجام بدهیم: به جای این که اندازه هر کدام از نقاط را ثابت و برابر با هم در نظر بگیریم اندازه نقاط را بر اساس هزینه نسبی حمل بار تغییر بدهیم. برای این کار ابتدا هزینه حمل بار را نرمال سازی می کنیم و سپس هزینه حمل هر نقطه را به عنوان ورودی به پارامتر radius کلاس می دهیم و به نقشه ایران اضافه می کنیم.

دقت کنید که اینجا ابتدا لازم است که دوباره نقشه را از ابتدا ایجاد کنیم چون در غیر این صورت نقشه قبلی با تمام نقاطش مورد استفاده قرار می گیرد.

iran_map = folium.Map(location = [32.4279,53.6880],zoom_start = 5)

برای نقاط مبدا به این صورت عمل می کنیم:

for row in range(sample_ubaar.shape[0]):

folium.CircleMarker(

location=[sample_ubaar.iloc[row,:]['sourceLatitude'], sample_ubaar.iloc[row,:]['sourceLongitude']],

radius=sample_ubaar.iloc[row,:]['price_normalized'],

color='red',

fill=True,

fill_color='darkred',

fill_opacity = 0.7

).add_to(iran_map)

iran_map

برای  نقاط مقصد هم  به صورت مشابه همین فرآیند را انجام می دهیم:

for row in range(sample_ubaar.shape[0]):

folium.CircleMarker(

location=[sample_ubaar.iloc[row,:]['destinationLatitude'], sample_ubaar.iloc[row,:]['destinationLongitude']],

radius=sample_ubaar.iloc[row,:]['price_normalized'],

color='blue',

fill=True,

fill_color='blue',

fill_opacity = 0.1

).add_to(iran_map)

iran_map

رسم تک تک نقطه ها بر روی نقشه با این که به ما کمک می کند تا  نقاط خاص  با حجم بیشتر حمل و نقل و قیمت بیشتر در کشور را شناسایی کنیم اما این به ما در خصوص مهم ترین خوشه های مکانی با حجم بالای حمل و نقل یک دید کلی و جامع نمی دهد.

حالا می توانیم یک گام فراتر برویم و برای این که این دید کلی را هم داشته باشیم می توانیم از یک قابلیت فوق العاده دیگر کتابخانه folium استفاده کنیم و نقشه های گرمایی را به جای نقاط بر روی نقشه های جغرافیایی نشان دهیم. برای رسم نقشه های گرمایی در folium از کلاس ()HeatMap  استفاده می کنیم و علاوه بر عرض و طول جغرافیایی وزن هر نقطه که در این جا قیمت است را به عنوان ورودی به آن می دهیم. یک مزیت دیگر استفاده از نقشه های گرمایی در اینجا این است که می توانیم تعداد مشاهدات بیشتری را بر روی نقشه رسم کنیم. برای همین در این مثال به جای 1000 مشاهده 20000 مشاهده را رسم می کنم.

iran_map = folium.Map(location = [32.4279,53.6880],zoom_start = 5)

from folium.plugins import HeatMap

iran_map = folium.Map(location = [32.4279,53.6880],zoom_start = 5)

iran_map

ubaar['price_normalized'] = (ubaar['price'] - ubaar['price'].min())/ ubaar['price'].max()



HeatMap(

    data=list(zip(ubaar.head(20000)['sourceLatitude'],

                  ubaar.head(20000)['sourceLongitude'],

                  ubaar.head(20000)['price_normalized'])

             

             )   ,

    radius   = 12

).add_to(iran_map)

iran_map