دیتا ویژوالیزیشن اینتراکتیو روی نقشه به کمک plotly (بخش۱)

سلام من در هفته های گذشته برای پروژه اول کار باید یک دیتایی رو روی نقشه به صورت اینتراکتیو ویژوالایز می کردم و برای این کار فقط با تعداد کمی ابزار روبرو هستید بهترین این ابزار ها plotly, bokeh, folium هستند.

مشکلی که من با انتخاب plotly به اون برخوردم این بود که تابع های plotly فقط برای کشیدن نقشه ی امریکا طراحی شدن. که با استفاده از mapbox access token این مشکل حل میشه .

کد نقشه رو هم می تونید از گیتهاب من بردارید به همراه بقیه ی دیتا که من برای این کار استفاده کردم.




import json
import copy
import numpy as np
import pandas as pd
import plotly
import plotly.offline as py
import plotly.graph_objs as go
import plotly.figure_factory as ff
from fuzzywuzzy import fuzz, process
from matplotlib.colors import Normalize
from matplotlib import cm
from itertools import product
from collections import Counter

MAPBOX_APIKEY = YOUR API KEY

دیتا

من دیتای مورد نیازم رو از سایت سازمان آمار ایران دانلود کردم و یه مقدار هم تمیزش کردم که توی گیتهاب بخش static_visualization می تونید پیداش کنید.

df = pd.read_csv("YOUR DATA PATH", index_col=0)
df = df['Population']/1000000
df.name = 'province'
print(df.head(30))

GeoJSON

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

with open('YOUR GEOJSON PATH') as f:
geojson = json.load(f)
n_provinces = len(geojson['features'])
provinces_names = [geojson['features'][k]['properties']['NAME_1'] for k in range(n_provinces)]
print("there are {} provinces ".format(n_provinces))
print(provinces_names)

فقط دقت کنید که بخش properties هر geojson اسم مخصوص به خودش رو داره که اینجا NAME_1 هست

مرکز هر استان

برای اینکه بتونیم جمعیت هر استان رو نشون بدیم نیاز داریم تا بتونیم مرکز هر استان رو از طریق این تابع بدست و روی نقشه به عنوان یه نقطه نشونش بدیم

def get_centers():
   lon, lat = [], []
   for k in range(n_provinces):
           geometry = geojson['features'][k]['geometry']
           if geometry['type'] == 'Polygon':
                   coords = np.array(geometry['coordinates'][0])
            elif geometry['type'] == 'MultiPolygon':
                   coords = np.array(geometry['coordinates'][0][0])
            lon.append(sum(coords[:,0]) / len(coords[:,0]))
            lat.append(sum(coords[:,1]) / len(coords[:,1]))
    return lon, lat

تطبیق اسامی استان ها

برای اینکه اسامی که توی دیتا هست با اسامی توی geojson یکی باشه ما نیاز داریم که این اسم ها رو با هم مقایسه کنیم و اسم درست رو جایگزین کنیم که این کار رو با کتابخونه fuzzywuzzy انجام می دیم که از بین اسم ها بهترین و نزدیکترین رو انتخاب می کنه

def match_regions(list1, list2):
    matched = [process.extract(list1[i], list2, limit=1, scorer = fuzz.partial_ratio)[0][0] for i in range(0, len(list1))]
    return {key: value for (key, value) in zip(list1, matched)}
    
match_dict = match_regions(df.index, provinces_names)
print(match_dict)

حالا دیتایی که داشتیم رو با استفاده از اسامی که تطبیق دادیم reindex می کنیم

df_tmp = df.copy()
df_tmp.index = df_tmp.index.map(match_dict)
df_tmp = df_tmp[~df_tmp.index.duplicated(keep=False)]
df_reindexed = df_tmp.reindex(index = provinces_names)

در این بخش دیکشنری هایی که plotly نیاز داره تا مرز های استان رو بکشه رو به کمک این تابع تولید می کنیم

def make_sources():
    sources = []
    geojson_copy = copy.deepcopy(geojson['features'])
    for feature in geojson_copy:
        sources.append(dict(type = 'FeatureCollection', features = [feature]))
    return sources

رنگ هایی که هر استان قرار با توجه به دیتا بگیره اینجا تولید می کنیم و استان هایی هم که دیتا مورد نظر رو ندارند هم با رنگ طوسی نشون می دیم و همچنین colorscale رو هم که برای colorbar نیاز هست رو در تابع get_colorscale ایجاد می کنیم این رنگ ها باید حالت استاندارد داشته باشند که من اینجا از حالت استاندارد rgba استفاده کردم

def scalarmappable(cmap, cmin, cmax):
    colormap = cm.get_cmap(cmap)
    norm = Normalize(vmin=cmin, vmax=cmax)
    return cm.ScalarMappable(norm=norm, cmap=colormap)
def get_scatter_colors(sm, df):
    grey = 'rgba(128,128,128,1)'
    return ['rgba' + str(sm.to_rgba(m, bytes = True, alpha = 1)) if not np.isnan(m) else grey for m in df]
def get_colorscale(sm, df, cmin, cmax):
    xrange = np.linspace(0, 1, len(df))
    values = np.linspace(cmin, cmax, len(df))
    return [[i, 'rgba' + str(sm.to_rgba(v, bytes = True))] for i,v in zip(xrange, values) ]

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

def get_hover_text(df) :
    text_value = (df).astype(str) + ""
    with_data = '<b>{}</b> <br> {} Millions'
    no_data = '<b>{}</b> <br> no data'
    return [with_data.format(p,v) if v != 'nan%' else no_data.format(p) for p,v in zip(df.index, text_value)]

حالا تمام تابع های مورد نیاز برای کشیدن نقشه مورد نظر در اختیار داریم و با استفاده از تابع ها متغییر های مورد نیازمون رو تولید می کنیم من از حالت blues برای این نقشه استفاده کردم

colormap = 'Blues'
cmin = df_reindexed.min()
cmax = df_reindexed.max()
sources = make_sources()
lons, lats = get_centers)
sm = scalarmappable(colormap, cmin, cmax)
scatter_colors = get_scatter_colors(sm, df_reindexed)
colorscale = get_colorscale(sm, df_reindexed, cmin, cmax)
hover_text = get_hover_text(df_reindexed)
tickformat = ""

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

data = dict(type='scattermapbox',
                lat=lats,
                lon=lons,
                mode='markers',
                text=hover_text,
                marker=dict(size=1,
                            color=scatter_colors,
                            showscale = True,
                            cmin = df_reindexed.min(),
                            cmax = df_reindexed.max(),
                            colorscale = colorscale,
                            colorbar = dict(tickformat = tickformat)
                            ),
                showlegend=False,
                hoverinfo='text' )

در این بخش layerهایی که برای هر استان نیاز هست تا مرز ها و رنگ اونا رو مشخص کنه رو میسازیم

layers=([dict(sourcetype = 'geojson',
                  source =sources[k],
                  below="",
                  type = 'line', 
                  line = dict(width = 1),
                  color = 'black',
                  ) for k in range(n_provinces)
                   ] +
            [dict(sourcetype = 'geojson',
                        source =sources[k],
                        below="water",
                        type = 'fill',
                        color = scatter_colors[k],
                        opacity=1
                        ) for k in range(n_provinces)
             ])

و در انتها layout مورد نیاز که layer ها رو به نقشه پیوند می زنه

layout = go.Layout(title="IRAN 2016 POPULATION",
                  autosize=False,
                  width=700,
                  height=800,
                  hovermode='closest',
                  mapbox=dict(accesstoken=MAPBOX_APIKEY,
                                                layers=layers,
                                                bearing=0,
                                                center=dict(
                                                                                        lat=35.715298,
                                                                                        lon=51.404343),
                                                 pitch=0,
                                                 zoom=4.9,
                                                 style = 'light'))

و نقشه رو می کشیم:)

fig = dict(data=[data], layout=layout)
py.plot(fig)

در اخر این رو هم اضافه کنم که این اولین تحربه من در نوشتن یه همچین متنی هست امیدوارم که مفید بوده باشه من در حال اماده سازی بخش دوم هم هستم که یه اسلایدر هم به این نقشه اضافه میشه و بیشتر اینتراکتیو میشه

ممنون که تا انتهای این متن همراه من بودید می تونید عضو کانال من هم بشید مطالب بیشتری اونجا میزارم

بخش دوم.