In [1]:
import geopandas as gpd
import pandas as pd
import math
from tqdm import trange, tqdm
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import silhouette_score
from sklearn.cluster import KMeans
import matplotlib.patheffects as pe
import seaborn as sns
from scipy.cluster import hierarchy
import esda
import libpysal

Proposed framework: Leveraging temporal changes of spatial accessibility measurements for better policy implications

title

1. Data Preparation

1.1. Load inputs

Supply

EV charging stations located in Seoul, South Korea
Source: Korea Environment Corporation EV Monitor. https://www.ev.or.kr/evmonitor.

Demand

Floating population over 24 hours
Source: Seoul Open Data Plaza - Floating Population. https://data.seoul.go.kr/dataVisual/seoul/seoulLivingPopulation.do.

Mobility

Estimated travel time from demand locations to supply location
Source: iNavi Maps API. https://docs.toast.com/en/Application%20Service/Maps/en/Overview/

In [2]:
# Loading DEMAND data
demand = gpd.read_file('./data/demand.shp')
demand = demand.set_index('GRID_ID')
demand['dummy'] = 'dummy'
study_area = demand.dissolve(by='dummy')

# Plotting the distribution of floating population over 24 hours.
fig = plt.figure(figsize=(18, 10))
colorBrewer = ['#eff3ff', '#bdd7e7', '#6baed6', '#2171b5']
plt_cls = [10, 100, 1000, 10000, 100000]
for qTime in range(24):

    ax = fig.add_subplot(4, 6, qTime + 1)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    for idx, cls in enumerate(plt_cls):
        if idx == 0:  
            continue

        temp_hxg = demand.loc[(plt_cls[idx-1] < demand[f'Flo_WD_{qTime}']) & (demand[f'Flo_WD_{qTime}'] <= cls)]
        if temp_hxg.shape[0] > 0:
            temp_hxg.plot(ax=ax, color=colorBrewer[idx-1])

        ax.text(0.95, 0.95, str(qTime), fontsize=20, ha='right', va='top', transform=ax.transAxes)
        
fig.text(0.5, 0.9, 'Distribution of floating population over 24 hours', ha='center', size=24)
Out[2]:
Text(0.5, 0.9, 'Distribution of floating population over 24 hours')
In [3]:
# Loading SUPPLY data
supply = gpd.read_file('./data/supply.shp')
supply = supply.set_index('StationID')

# Plot the changes in the degree of supply over 24 hours
fig = plt.figure(figsize=(18, 10))

for qTime in range(24):

    ax = fig.add_subplot(4, 6, qTime + 1)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    
    study_area.plot(alpha=0.1, ax=ax)
    supply.loc[supply[f'Hour_WD_{qTime}'] == 'Y'].plot(ax=ax, markersize=supply['Weight'], color='black')
    
    ax.text(0.95, 0.95, str(qTime), fontsize=20, ha='right', va='top', transform=ax.transAxes)
    ax.text(0.95, 0.12, 
            'Open Stations: ' + str(supply.loc[supply[f'Hour_WD_{qTime}'] == 'Y'].shape[0]), 
            fontsize=12, ha='right', va='top', transform=ax.transAxes)
    
fig.text(0.5, 0.9, 'Locations and weights of operating EV charging stations over 24 hours', ha='center', size=24)
Out[3]:
Text(0.5, 0.9, 'Locations and weights of operating EV charging stations over 24 hours')
In [4]:
# Loading MOBILITY data
mobility = pd.read_csv('./data/Mobility.csv')
mobility = mobility.loc[mobility['GRID_ID'].isin(demand.index)]

# Plot the changes in the degree of mobility over 24 hours
fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(1, 1, 1)
mobility.loc[:, [f'Tra_WD_{qTime}' for qTime in range(0, 24)]].boxplot(
    showfliers=False, fontsize=14, color=dict(boxes='black', whiskers='black', medians='red', caps='black'))
ax.set_xticklabels([f'{qTime}' for qTime in range(24)])
ax.set_xlabel('Hours', fontsize=20)
ax.set_ylabel('Estimated Travel Time (Minutes)', fontsize=20)
Out[4]:
Text(0, 0.5, 'Estimated Travel Time (Minutes)')

1.2. Preliminary analysis: determining threshold travel time

In [5]:
accessible_hxg = pd.DataFrame(index=[f't_{qTime}' for qTime in range(24)], columns=[f'thres_{i}' for i in range(0, 31, 1)])
accessible_hxg = accessible_hxg.fillna(0.0)

for qTime in trange(24):

    trvl_time_matrix = pd.DataFrame(index=supply.index, columns=demand.index)
    for grid in demand.index:
        temp_mobility = mobility.loc[mobility['GRID_ID'] == grid][['Station_ID', f'Tra_WD_{qTime}']]
        temp_mobility = temp_mobility.set_index('Station_ID')
        trvl_time_matrix[grid] = temp_mobility

    cum_pop = []
    for threshold_trvl_time in range(0, 31, 1):

        accessible_grid = []
        for grid in demand.index:
            temp_matrix = trvl_time_matrix[grid].loc[trvl_time_matrix[grid] < threshold_trvl_time]
            
            if temp_matrix.shape[0] > 0:
                accessible_hxg.at[f't_{qTime}', f'thres_{threshold_trvl_time}'] += 1
            
        accessible_hxg.at[f't_{qTime}', f'thres_{threshold_trvl_time}'] = accessible_hxg.at[f't_{qTime}', f'thres_{threshold_trvl_time}'] / 703

accessible_hxg
100%|██████████| 24/24 [03:48<00:00,  9.54s/it]
Out[5]:
thres_0 thres_1 thres_2 thres_3 thres_4 thres_5 thres_6 thres_7 thres_8 thres_9 ... thres_21 thres_22 thres_23 thres_24 thres_25 thres_26 thres_27 thres_28 thres_29 thres_30
t_0 0.0 0.091038 0.155050 0.354196 0.603129 0.753912 0.837838 0.866287 0.897582 0.928876 ... 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_1 0.0 0.091038 0.176387 0.423898 0.662873 0.799431 0.856330 0.889047 0.918919 0.945946 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_2 0.0 0.093883 0.179232 0.422475 0.664296 0.799431 0.853485 0.880512 0.916074 0.944523 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_3 0.0 0.093883 0.179232 0.421053 0.664296 0.795164 0.853485 0.880512 0.916074 0.944523 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_4 0.0 0.092461 0.176387 0.416785 0.665718 0.795164 0.854908 0.884780 0.913229 0.945946 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_5 0.0 0.093883 0.177809 0.419630 0.664296 0.798009 0.854908 0.889047 0.916074 0.948791 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_6 0.0 0.093883 0.179232 0.418208 0.664296 0.795164 0.854908 0.887624 0.914651 0.944523 ... 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_7 0.0 0.095306 0.146515 0.257468 0.438122 0.630156 0.735420 0.806543 0.849218 0.881935 ... 0.995733 0.995733 0.995733 0.997155 0.998578 0.998578 1.000000 1.000000 1.000000 1.000000
t_8 0.0 0.095306 0.145092 0.233286 0.406828 0.573257 0.689900 0.766714 0.809388 0.844950 ... 0.990043 0.990043 0.990043 0.991465 0.995733 0.995733 0.995733 0.995733 0.997155 0.998578
t_9 0.0 0.108108 0.156472 0.257468 0.426743 0.597440 0.716927 0.793741 0.829303 0.864865 ... 0.987198 0.987198 0.990043 0.995733 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000
t_10 0.0 0.106686 0.153627 0.257468 0.423898 0.607397 0.735420 0.800853 0.846373 0.873400 ... 0.988620 0.992888 0.994310 0.995733 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000
t_11 0.0 0.108108 0.155050 0.260313 0.426743 0.615932 0.722617 0.802276 0.847795 0.877667 ... 0.991465 0.992888 0.995733 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000
t_12 0.0 0.112376 0.162162 0.263158 0.445235 0.617354 0.739687 0.806543 0.852063 0.884780 ... 0.990043 0.991465 0.998578 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_13 0.0 0.125178 0.169275 0.280228 0.449502 0.640114 0.743954 0.819346 0.866287 0.889047 ... 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_14 0.0 0.116643 0.162162 0.256046 0.406828 0.597440 0.715505 0.793741 0.847795 0.884780 ... 0.995733 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_15 0.0 0.105263 0.150782 0.247511 0.395448 0.578947 0.699858 0.789474 0.849218 0.879090 ... 0.994310 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_16 0.0 0.128023 0.174964 0.268848 0.401138 0.586060 0.722617 0.788051 0.846373 0.883357 ... 0.992888 0.995733 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_17 0.0 0.115220 0.162162 0.247511 0.378378 0.547653 0.684211 0.772404 0.830725 0.876245 ... 0.991465 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_18 0.0 0.113798 0.159317 0.234708 0.366999 0.517781 0.657183 0.748222 0.817923 0.854908 ... 0.984353 0.987198 0.988620 0.988620 0.992888 0.997155 0.998578 1.000000 1.000000 1.000000
t_19 0.0 0.132290 0.176387 0.248933 0.384068 0.540541 0.678521 0.759602 0.809388 0.854908 ... 0.984353 0.987198 0.991465 0.991465 0.992888 0.992888 0.994310 0.994310 0.995733 0.997155
t_20 0.0 0.123755 0.172119 0.271693 0.439545 0.614509 0.743954 0.819346 0.852063 0.883357 ... 0.995733 0.995733 0.995733 0.995733 0.995733 0.995733 0.997155 0.998578 1.000000 1.000000
t_21 0.0 0.100996 0.157895 0.268848 0.477952 0.648649 0.776671 0.836415 0.866287 0.896159 ... 0.995733 0.995733 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_22 0.0 0.100996 0.159317 0.283073 0.509246 0.672831 0.786629 0.842105 0.871977 0.904694 ... 0.997155 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000
t_23 0.0 0.099573 0.162162 0.302987 0.564723 0.719772 0.809388 0.857752 0.893314 0.927454 ... 0.998578 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000

24 rows × 31 columns

In [6]:
fig, ax = plt.subplots(figsize=(10,8))
accessible_hxg[[f'thres_{i}' for i in range(26)]].transpose().plot(color='black', linewidth=0.5, ax=ax)

for x in range(5, 25, 5):
    plt.axvline(x=x, linewidth='1', linestyle='dashed', color='grey')
for y in range(0, 11, 1):
    plt.axhline(y=y/10, linewidth='1', linestyle='dashed', color='grey')
plt.axvline(x=15, linewidth='5', linestyle='dashed', color='red')
ax.set_ylim(0, 1.001)
ax.set_xlim(0, 25)
ax.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
ax.set_yticklabels([f'{i}%' for i in range(0, 110, 10)], fontsize=15)
ax.set_xticks([0,5, 10, 15, 20, 25])
ax.set_xticklabels([f'{i}' for i in range(0, 30, 5)], fontsize=15, rotation=0)
ax.set_ylabel(f'Hexagons accessible \n to EV charging stations', fontsize=25)
ax.set_xlabel('Threshold travel time (Minutes)', fontsize=25)
ax.get_legend().remove()
plt.tight_layout()
plt.show()
In [7]:
# Travel time to the nearest EV charging station from each hexagon (represent minimum travel to obtain service)
nearest_trvl_time = pd.DataFrame(index=mobility['GRID_ID'].unique(), columns=[f'WD_{i}' for i in range(24)])

for grid in tqdm(mobility['GRID_ID'].unique()):
    for i in range(24):
        nearest_trvl_time.at[grid, f'WD_{i}'] = mobility.loc[(mobility['GRID_ID'] == grid) & (mobility[f'Tra_WD_{i}'] != 0)][f'Tra_WD_{i}'].min()

# Number of accessible EV charging station per each threshold travel time 
num_of_accessible_supply_5 = pd.DataFrame(index=mobility['GRID_ID'].unique(), columns=[f'WD_{i}' for i in range(24)])
num_of_accessible_supply_10 = pd.DataFrame(index=mobility['GRID_ID'].unique(), columns=[f'WD_{i}' for i in range(24)])
num_of_accessible_supply_15 = pd.DataFrame(index=mobility['GRID_ID'].unique(), columns=[f'WD_{i}' for i in range(24)])
num_of_accessible_supply_20 = pd.DataFrame(index=mobility['GRID_ID'].unique(), columns=[f'WD_{i}' for i in range(24)])
num_of_accessible_supply_25 = pd.DataFrame(index=mobility['GRID_ID'].unique(), columns=[f'WD_{i}' for i in range(24)])

# Average Percentage of accessible EV charging station per each threshold travel time
percent_accessible_supply = pd.DataFrame(index=[f't_{minutes}' for minutes in [5, 10, 15, 20, 25]], columns=[f'WD_{i}' for i in range(24)])

for i in trange(24):
    for grid in mobility['GRID_ID'].unique():
        num_of_accessible_supply_5.at[grid, f'WD_{i}'] = mobility.loc[(mobility['GRID_ID'] == grid) &
                                                                       (mobility[f'Tra_WD_{i}'] <= 5)].shape[0]
        num_of_accessible_supply_10.at[grid, f'WD_{i}'] = mobility.loc[(mobility['GRID_ID'] == grid) &
                                                                       (mobility[f'Tra_WD_{i}'] <= 10)].shape[0]
        num_of_accessible_supply_15.at[grid, f'WD_{i}'] = mobility.loc[(mobility['GRID_ID'] == grid) &
                                                                       (mobility[f'Tra_WD_{i}'] <= 15)].shape[0]
        num_of_accessible_supply_20.at[grid, f'WD_{i}'] = mobility.loc[(mobility['GRID_ID'] == grid) &
                                                                       (mobility[f'Tra_WD_{i}'] <= 20)].shape[0]
        num_of_accessible_supply_25.at[grid, f'WD_{i}'] = mobility.loc[(mobility['GRID_ID'] == grid) &
                                                                       (mobility[f'Tra_WD_{i}'] <= 25)].shape[0]
    percent_accessible_supply.at['t_5', f'WD_{i}'] = num_of_accessible_supply_5[f'WD_{i}'].mean() / supply.shape[0]
    percent_accessible_supply.at['t_10', f'WD_{i}'] = num_of_accessible_supply_10[f'WD_{i}'].mean() / supply.shape[0]
    percent_accessible_supply.at['t_15', f'WD_{i}'] = num_of_accessible_supply_15[f'WD_{i}'].mean() / supply.shape[0]
    percent_accessible_supply.at['t_20', f'WD_{i}'] = num_of_accessible_supply_20[f'WD_{i}'].mean() / supply.shape[0]
    percent_accessible_supply.at['t_25', f'WD_{i}'] = num_of_accessible_supply_25[f'WD_{i}'].mean() / supply.shape[0] 
    
percent_accessible_supply
100%|██████████| 703/703 [02:03<00:00,  5.70it/s]
100%|██████████| 24/24 [09:50<00:00, 24.61s/it]
Out[7]:
WD_0 WD_1 WD_2 WD_3 WD_4 WD_5 WD_6 WD_7 WD_8 WD_9 ... WD_14 WD_15 WD_16 WD_17 WD_18 WD_19 WD_20 WD_21 WD_22 WD_23
t_5 0.0460864 0.0619606 0.0623096 0.0612013 0.0616813 0.0610966 0.0611402 0.0179424 0.0125056 0.0129245 ... 0.0135441 0.0131164 0.013387 0.0124357 0.0112751 0.0127586 0.0149578 0.019138 0.0221051 0.0304829
t_10 0.268708 0.347712 0.347765 0.342031 0.346002 0.343288 0.342738 0.107349 0.0657393 0.0653815 ... 0.0675458 0.0635925 0.0598661 0.0545689 0.044568 0.04805 0.0828439 0.115133 0.132412 0.181632
t_15 0.608496 0.709117 0.706281 0.698907 0.704815 0.701394 0.699273 0.270907 0.166796 0.170086 ... 0.174519 0.165391 0.152406 0.137474 0.107366 0.120221 0.219663 0.302228 0.341874 0.45219
t_20 0.863146 0.929766 0.926398 0.923291 0.927253 0.923117 0.921991 0.515276 0.312622 0.309044 ... 0.328723 0.309288 0.284696 0.257032 0.19953 0.215675 0.394828 0.534737 0.589481 0.715976
t_25 0.972126 0.991491 0.991064 0.990706 0.990985 0.99006 0.990051 0.751276 0.495658 0.472489 ... 0.500284 0.4696 0.436726 0.400449 0.313843 0.326567 0.576897 0.749627 0.802163 0.893114

5 rows × 24 columns

In [8]:
fig, ax = plt.subplots(figsize=(12, 8))
line_style = ['--', '--', '-', '--', '--']
line_colors = ['black', 'black', 'red', 'black', 'black']
for idx, minutes in enumerate([5, 10, 15, 20, 25]):
    ax.plot(range(0, 24), percent_accessible_supply.loc[f't_{minutes}'].values, line_style[idx], linewidth=1.5, color=line_colors[idx], label=f'{minutes} Minutes')
    
ax.plot(range(0, 24), percent_accessible_supply.loc[f't_15'].values, '-', linewidth=5, color='red')

for y in range(0, 10, 1):
    plt.axhline(y=y/10, linewidth='1', linestyle='dashed', color='grey')
    
for x in range(1, 23, 1):
    plt.axvline(x=x, linewidth='1', linestyle='dashed', color='grey')    

plt.legend(loc='upper center', fontsize='xx-large')
ax.set_xlim(-0.1, 23.1)
ax.set_xticks(list(range(0, 24)))
ax.set_xticklabels([f'{qTime}' for qTime in range(24)], fontsize=15)
ax.set_xlabel('Hours', fontsize=25)
ax.set_ylim(0, 1.001)
ax.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
ax.set_yticklabels([f'{i}%' for i in range(0, 110, 10)], fontsize=15)
ax.set_ylabel(f'Average EV charging stations \n accessible from hexagons', fontsize=25)
ax.get_legend().remove()
plt.tight_layout()
plt.show()