Home Car traffic and parking density maps from Uber Movement travel times
Post
Cancel

Car traffic and parking density maps from Uber Movement travel times

들어가며

오늘 살펴볼 내용은 Uber Movement Team에서 공개 오픈 중인 (Traffic) Travel Time 데이터셋들을 기반으로 산출한 전 세계 34개 도시의 주차 밀집도 및 운행 활성도(?) 데이터를 들여다 볼 것이다.

여담으로, Uber에선 공익 차원의 도시교통 연구 증진 목적으로 자사 측 기기의 운행 기록들을 가공하여 양질의 데이터셋을 정기적으로 공개 배포하고 있다. 가공 방식에 대해서도 Uber Movement Team에서 공유하고 있으니 궁금하다면 참조하길 바란다. (아래 References 참고)

png Introduction by Uber Movement; Credit: https://movement.uber.com

오늘 중점적으로 살펴볼 데이터는, Aryandoust, A., van Vliet, O., & Patt, A. (2019)에서 Uber 데이터에 그들의 모델을 적용하여 새로 산출한 Parking DensityTraffic Activity 데이터이긴 하지만, 서두에서 그들이 이용한 Uber Travel Time 데이터셋이 어떻게 생겨먹은 것인지 (아주아주) 살짝 들여다 보기로 하자.


References

  1. Uber Movement: Travel Times Calculation Methodology
  2. Uber Movement: Speeds Calculation Methodology


Uber Movement dataset

현재 Uber Movement 홈페이지에서 공개 배포 중인 데이터는 아래 3가지 종류이다.

  1. Travel Time: zone-to-zone average travel time across a city
  2. Speed: street speed across a city
  3. Mobility Heatmap: the volume of activity of mobility devices(Uber share, bikes, Scooters)
    • 모두 도시별로 나눠져 있으며, 런던/맨체스터/마드리드/LA/워싱턴DC/암스테르담/보스턴/브뤼셀(벨기에)/카이로(이집트) 등등 굉장히 많은 글로벌 도시에 대한 데이터가 존재한다.
    • 대체로 분기 단위로 집계하여 배포하는데, 어째서인지 모든 도시의 집계가 2020년 1분기 까지만 공개되어 있다.
    • 본문의 Raw 데이터로 활용되는 Travel Time 데이터셋만 한번 들여다 보기로 하자.


Travel Time for London, UK

  • London의 경우, 2016-1분기 ~ 2020-1분기 까지 (17개의 분기) 데이터가 존재한다. 아마 도시마다 이용 가능한 서비스 범위가 다를 것이다.
  • 각 분기 데이터마다, 집계하여 배포하는 데이터는 7가지 유형으로 나뉜다. (* Travel Time 데이터 기준)
    • (1) by Hour of Day (All Days): {city}-{scale}-{year}-{quarter}-All-HourlyAggregate.csv
    • (2) by Hour of Day (Weekdays Only): {city}-{scale}-{year}-{quarter}-OnlyWeekdays-HourlyAggregate.csv
    • (3) by Hour of Day (Weekends Only): {city}-{scale}-{year}-{quarter}-OnlyWeekends-HourlyAggregate.csv
    • (4) by Month (All Days): {city}-{scale}-{year}-{quarter}-All-MonthlyAggregate.csv
    • (5) by Month (Weekdays Only): {city}-{scale}-{year}-{quarter}-OnlyWeekdays-MonthlyAggregate.csv
    • (6) by Month (Weekends Only): {city}-{scale}-{year}-{quarter}-OnlyWeekends-MonthlyAggregate.csv
    • (7) by Day of Week: {city}-{scale}-{year}-{quarter}-WeeklyAggregate.csv

  • 이들을 다시 크게 보면 3가지 유형인데,
    • Hour of Day: 시간 단위로 응용집계된 데이터; HourlyAggregate.csv
    • Month: 월 단위로 응용집계된 데이터; MonthlyAggregate.csv
    • Day of Week: 요일 단위로 응용집계된 데이터; WeeklyAggregate.csv

그리고, 요일 단위 집계(Day of Week)를 제외하고는, 집계를 할 때 평일만 모아서 집계했는지 / 주말만 모아서 집계했는지 / 평일주말 모두 합쳐서 집계했는지에 따라 각각 ‘OnlyWeekdays’, ‘OnlyWeekends’, ‘All’ 이름이 붙어있다. 데이터 내부의 Data Structure에는 차이가 없으며, 데이터 값이 어떤 집계 기준으로 산출된 것인지 다를 뿐이다. (찍먹 수준도 안되는) 여기 서두에선 (2), (5), (7) 유형의 데이터들만 한번 들여다 본다.

NOTE: 가만보면 일별 단위는 없는 것 같은데, 2020년 1분기(3개월)에만 일별(hourly resolution) 데이터가 함께 있다. (* 데이터 꽤 큼. ~ 1.5 GB)

1
2
3
4
5
6
7
8
9
10
import json, os
import sys
import numpy as np, pandas as pd, geopandas as gpd
import matplotlib.pyplot as plt
import dask.dataframe as dd
import zipfile
import shutil
import contextily as cx
from tqdm import tqdm
import imageio.v3 as iio
1
2
3
>>> print(sys.version)

'3.10.10 | packaged by conda-forge | (main, Mar 24 2023, 20:08:06) [GCC 11.3.0]'
1
2
DataPath = os.path.join(os.getcwd(), 'uber_dataset/')
DataContents = [file for file in os.listdir(DataPath)]
1
2
3
>>> print(DataContents)

['london-lsoa-2020-1-OnlyWeekdays-HourlyAggregate.csv', 'london-lsoa-2018-2-OnlyWeekdays-MonthlyAggregate.csv', 'london-lsoa-2018-2-WeeklyAggregate.csv']

기본적으로 Travel Times 데이터들은 모두, 유형에 관계없이, OD(Origin to Destination)에 관한 컬럼정보로 구성되어 있다. 시작노드지역(sourceid)에서 종료노드지역(dstid) 방향으로 운행할 때의 평균 통행시간이 기록되어 있다. 노드 ID는 Uber Movement 팀에서 자체 넘버링한 Sequential ID(0부터 N까지)를 쓰는 듯 하다. ID에 대한 지리정보 데이터는 Uber Movement 홈페이지에서 내려받을 수 있다. (아래 GeoJson Files 내용 참고)


Origin(sourceid)과 Destination(dstid) 컬럼 이후의 또 다른 공통 컬럼들로는 mean_travel_time/standard_deviation_travel_timegeometric_mean_travel_time/geometric_standard_deviation_travel_time이 있다.

mean_travel_time은, 집계된 데이터 분포 상, 일반적인 산술평균(arithmetic mean)을 사용해 얻은 평균 통행시간값이고,
geometric_mean_travel_time은 기하평균(geometric mean) 계산을 통해 얻은 평균 통행시간값이다.

NOTE: 오늘 살펴볼 데이터의 연구팀은 mean_travel_time 산술평균 통행시간을 기준으로 모델을 적용하여 parking density & traffic activity 데이터를 추출했다고 한다.


Hourly / Monthly / Weekly Aggregation마다 컬럼 내용이 다른 한 가지가 있는데, 그 해석은 다음과 같다.

  • HourlyAggregate의 ‘hod’ 컬럼: hour of a day의 약자; [0, 1, 2, …, 23]까지 24개의 integer값이 들어있고, 시간을 의미한다.
  • MonthlyAggregate의 ‘month’ 컬럼: 월간 집계범위를 의미하는 integer 값이 들어있고, 분기에 따라 한 데이터 파일엔 [1,2,3] or [4,5,6] or [7,8,9] or [10,11,12] 값이 들어있다.
  • WeeklyAggregate의 ‘dow’ 컬럼: day of a week의 약자; [1, 2, 3, …, 7]까지 7개의 interger값이 들어있고, 각각 월요일부터 일요일까지에 해당한다.
1
2
3
# HourlyAggregate.csv; (Hour of Day: 시간 단위로 집계된 데이터)
HourlyAgg = pd.read_csv(os.path.join(DataPath, DataContents[0]))
HourlyAgg
sourceiddstidhodmean_travel_timestandard_deviation_travel_timegeometric_mean_travel_timegeometric_standard_deviation_travel_time
049149701861.41537.961807.551.25
148951701241.77406.641176.961.41
2639875122594.00892.392482.401.32
34875370537.26188.19511.691.35
44756570291.68206.60213.752.37
........................
6329297489191131508.00434.211456.691.29
63292984416729822.24270.83793.531.35
632929956952221156.18222.101137.271.19
6329300869753191815.90442.651772.091.24
6329301121703111559.75173.371550.011.12

6329302 rows × 7 columns


1
2
3
# MonthlyAggregate.csv; (Month: 월 단위로 집계된 데이터)
MonthlyAgg = pd.read_csv(os.path.join(DataPath, DataContents[1]))
MonthlyAgg
sourceiddstidmonthmean_travel_timestandard_deviation_travel_timegeometric_mean_travel_timegeometric_standard_deviation_travel_time
024543442365.92607.612293.371.28
114043341599.58426.721549.971.28
2923885645.00226.34613.621.35
315136362727.62793.672616.751.33
447997962717.92717.782630.251.29
........................
12977119691616718.51390.65647.551.55
129771226868362948.771254.452713.561.50
12977139082316991.62354.11947.181.34
129771459641651635.86926.051522.371.39
129771536270942671.00925.872536.751.36

1297716 rows × 7 columns


1
2
3
# WeeklyAggregate.csv; (Day of Week: 요일 단위로 집계된 데이터)
WeeklyAgg = pd.read_csv(os.path.join(DataPath, DataContents[2]))
WeeklyAgg
sourceiddstiddowmean_travel_timestandard_deviation_travel_timegeometric_mean_travel_timegeometric_standard_deviation_travel_time
021072451671.72709.381573.561.38
136374531664.14455.721606.911.30
237463532089.46463.242040.781.24
336572532218.15767.202116.401.34
434493532708.97856.192570.651.39
........................
298354293345842461.00467.362418.481.20
298354395049772260.49609.042185.161.31
298354496930771312.72305.351280.951.24
298354595841771821.70460.591763.671.31
29835466118671660.76570.091591.591.31

2983547 rows × 7 columns



Parking density maps and Traffic activity rhythms

데이터 파일들은 아래 Harvard Dataverse URL에 들어가 다운받을 수 있다.

1
https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/8HAJFE


Unzip all zip files

배포되는 zip 파일들을 macosx 기반에서 압축된 듯 하다. 이 경우 __MACOSX 디렉토리(내부에는 _.DS_Store 파일들)가 Mac Finder(윈도우 OS의 탐색기 같은) 호환 용도로 함께 포함되어 있다. 본래 데이터만 활용해도 되니 압축해제 과정에서 삭제하도록 하자.

NOTE: 나는 shutil.rmtree()로 __MACOSX 디렉토리를 없앨 건데, 이 함수는 os.rmdir() 와 달리 타겟 디렉토리 내부에 데이터가 있든 없든 무조건 지운다. 그만큼 사용할 때 더욱 신중을 기해야 한다. 더욱 안전한 방법을 원한다면, os.remove()로 먼저 내부 파일들을 삭제하여 empty directory를 만들어주고, os.rmdir()로 해당 빈 디렉토리를 삭제하는 방법도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 최초로 데이터셋 다운받을 때만 사용하는 셀임
BasePath = os.path.join(os.getcwd(), 'derived_dataset/dataverse_files')
for file in os.listdir(BasePath):
    if file.endswith('.zip'):
        dirname = file.split('.')[0]
        target_file_path = os.path.join(BasePath, file)
        target_dir_path = os.path.join(BasePath, dirname)
        if not os.path.exists(target_dir_path):
            os.makedirs(target_dir_path)
        with zipfile.ZipFile(target_file_path, 'r') as zip_obj:
            zip_obj.extractall(target_dir_path)
        
        # Remove a directory of __MACOSX
        # NOTE!! Be cautious when using shutil.rmtree().
        # shutil.rmtree() doesn't care if the directory is empty or not.
        macosx_dir = os.path.join(target_dir_path, '__MACOSX')
        if os.path.exists(macosx_dir):
            shutil.rmtree(macosx_dir)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import fnmatch

def read_derived_dataset(BasePath:str, d_type:str, city:str, year:int, quarter:int, time_scope:str):
    """
        Dataset tree 에서 원하는 데이터 파일을 조금 더 간편하게 불러 오는 용도로 제작

    Parameters
    ----------
    BasePath : str
        Dataset Tree의 기본 경로
    d_type : str
        parking | driving
        parking: parking density.
        driving: traffic activity.
    city : str
        city name among 34 cities worldwide. 
        Don't worry about capitalization when writing city name.
        ['Amsterdam', 'Bangalore', 'Bogota', 'Boston', 'Brisbane', 'Brussels', 
        'Cairo', 'Cincinnati', 'Hyderabad', 'Johannesburg', 'Leeds', 'London', 
        'Los Angeles', 'Manchester', 'Melbourne', 'Miami', 'Mumbai', 'Nairobi', 
        'New Delhi', 'Orlando', 'Paris', 'Perth', 'Pittsburgh', 'San Francisco', 
        'Santiago', 'Sao Paulo', 'Seattle', 'Stockholm', 'Sydney', 'Taipei', 
        'Tampa Bay', 'Toronto', 'Washington DC', 'West Midlands UK']
    year : int
        year
    quarter : int
        quarter of a year, which must be matching one of (1, 2, 3, 4).
    time_scope : str
        all | weekends | weekdays
        각각 모든 날짜들 / 주말만 / 평일만 고려해 집계한 데이터에 해당.
    
    Returns
    -------
    pd.DataFrame
        if False returned, 아무튼 데이터 로드 실패
    """
    city = city.lower()
    city_dir = []
    for name in city.split(' '):
        if name in ['uk', 'dc']:
            city_dir.append(name.upper())
        else:
            city_dir.append(name[0].upper() + name[1:])
    city_dir = ' '.join(city_dir)
    
    false_msg = "[[ FALSE RETURNED !!! ]]"
    
    target_path = os.path.join(BasePath, city_dir, city_dir, 'data')
    if not os.path.exists(target_path):
        print(f"{false_msg:=^80}")
        print(target_path)
        print("Please, check again the parameters of 'BasePath' and 'city'.")
        print("The 'city' might not be currently supported.")
        print(''.ljust(80, "="), end='\n\n')
        return False

    else:
        type_dict = {'parking':'parkingdensities', 'driving':'trafficactivity'}
        scope_dict = {'all':'All', 'weekends':'OnlyWeekends', 'weekdays':'OnlyWeekdays'}
        for file in os.listdir(target_path):
            if fnmatch.fnmatch(file, f'*{type_dict[d_type]}*{year}-{quarter}-{scope_dict[time_scope]}*.csv'):
                print(f"LOADED FILE:: {file}")
                break
            
        else: # for loop가 break 되지 않는다면, else로 정상적으로 넘어 온다.
            print(f"{false_msg:=^80}")
            print("Please, check again the parameters of 'd_type', 'year', 'quarter', and 'time_scope'.")
            print("The set of parameters might not be currently supported.")
            print()
            print("NOTE!! 'd_type' must be one of ['parking', 'driving'].")
            print("NOTE!! 'time_scope' must be one of ['all', 'weekends', 'weekdays'].")
            print(''.ljust(80, "="), end='\n\n')
            return False
        
        df = pd.read_csv(os.path.join(target_path, file))
        return df


About dataset

본 연구팀은 그들만의 모델을 적용하여 Uber Travel Time 데이터셋을 기반으로 새로운 두 종류의 데이터셋을 추출했다. 첫 번째로는 Traffic Activity 데이터셋이고, 두 번째론 Parking Density Maps 데이터셋이다. 그들의 모델에 대해 아직 정확히 이해하지 못한 단계라 확실치는 않지만, 지금까지 내가 이해한 바로는, 모델이 하는 기본적인 기능은 Uber Travel Time의 운행 기록을 바탕으로 특정 지역 및 시점에서 Driving Vehicles(운행 중인 차량들)과 Parking Vehicles(주차 중인 차량들)의 (Counts)들을 추정해내는 것이다.


Traffic Activity 데이터는 Circadian rhythm of traffic activity 에 대한 데이터인데, 하루 전체 관찰 시점들 중 driving 차량 대수가 가장 많이 관찰된 시점을 기준으로, 나머지 시점에서의 차량 대수를 모두 Normalize 한다. 그렇게 0부터 1사이의 density 값이 기록되고, 이것이 traffic activity가 된다. 따로 공간적인 정보는 없기 때문에, temporal dataset (no spatial dataset)이라 볼 수 있다. (아래 london_traffic 데이터프레임 참고)

Parking Density Maps 데이터는 반면 spatio-temporal dataset 이라 볼 수 있다. 지역적 및 시간대 구분이 되어 있기 때문이다. 이 데이터에서는 현 관찰 시점에서 전체 Parking 차량 대수가 기준이 되는데, 예를 들어, 오후 1시부터 2시 사이에 런던 도시 안에 있는 모든 parking 차량 대수가 1000대로 파악됐다면, 이것이 기준이 되는 것이다. 그리고 런던 도시 내 모든 지역들의 parking 차량수를 이 기준으로 나눠서 density 값을 얻는다. 이것이 parking density maps 데이터이다.

1
2
3
4
5
# 1. Circadian rhythm of traffic activity (temporal dataset)
# 2018년 4분기 내 평일 기준, 런던의 traffic activity는 평균적으로 오후 5시에 가장 많은 차량 대수를 보인다는 것을 확인할 수 있다.
BasePath = os.path.join(os.getcwd(), 'derived_dataset/dataverse_files')
london_traffic = read_derived_dataset(BasePath, d_type='driving', city='london', year=2018, quarter=4, time_scope='weekdays')
london_traffic
1
LOADED FILE:: results_trafficactivity_london-lsoa-2018-4-OnlyWeekdays-HourlyAggregate.csv
t = 1ht = 2ht = 3ht = 4ht = 5ht = 6ht = 7ht = 8ht = 9ht = 10h...t = 15ht = 16ht = 17ht = 18ht = 19ht = 20ht = 21ht = 22ht = 23ht = 24h
00.1606370.1027010.0537470.00.195090.5727940.8744990.9428320.7922830.7301...0.9219950.9890381.00.8850430.6920440.5120770.434050.4320480.3862910.223329

1 rows × 24 columns


Regional ID and GeoJson Files

Parking density maps의 지역ID는 Uber Movement Travel Time 데이터에서 등장하는 지역ID를 그대로 따른다. 그리고 Parking density 데이터 컬럼 순서(0부터~N까지)가 각각의 지역ID에 대응한다. 즉, 0번째 컬럼의 데이터 정보는 지역ID=0 에 해당하는 정보라는 의미다.


고유 지역ID를 지닌 도시별 GeoJson 파일은 Uber Movement 홈페이지에서 역시 다운받을 수 있다.

Uber Movement 홈페이지 $ \rightarrow $ [Products/Travel Times] $ \rightarrow $ ‘Download data’ $ \rightarrow $ [GEO BOUNDARIES] $ \rightarrow $ ‘약관동의 및 .JSON 클릭 후 다운로드’

내려받은 GeoJson파일 내 MOVEMENT_ID 를 참조해서 사용하면 된다.

1
2
3
# 2. Parking Density Maps (spatio-temporal dataset)
london_parking = read_derived_dataset(BasePath, d_type='parking', city='london', year=2018, quarter=4, time_scope='weekdays')
london_parking
1
LOADED FILE:: results_parkingdensities_london-lsoa-2018-4-OnlyWeekdays-HourlyAggregate.csv
t = 1ht = 2ht = 3ht = 4ht = 5ht = 6ht = 7ht = 8ht = 9ht = 10h...t = 15ht = 16ht = 17ht = 18ht = 19ht = 20ht = 21ht = 22ht = 23ht = 24h
00.0006040.0006710.0006950.0006270.0006500.0005630.0004170.0003640.0003500.000284...0.0005360.0005860.0006550.0006280.0006560.0006620.0005540.0005520.0006190.000649
10.0013600.0012680.0011800.0011840.0016840.0024040.0029780.0023040.0020170.001952...0.0022840.0022110.0021280.0018600.0016030.0016200.0016390.0015810.0015230.001458
20.0005490.0004180.0002970.0002730.0004910.0015040.0021210.0015680.0011480.001172...0.0009470.0009600.0009460.0008940.0007450.0005350.0004800.0003750.0004680.000482
30.0013960.0012820.0012090.0011790.0014380.0021730.0026090.0021440.0018820.001870...0.0020030.0017960.0016120.0015750.0014050.0013100.0012440.0012620.0013370.001320
40.0014920.0014070.0013470.0013940.0014740.0015720.0017050.0015770.0016600.001907...0.0014600.0013130.0012360.0013890.0016700.0017910.0016990.0015770.0015230.001459
..................................................................
9780.0010610.0010230.0009140.0008790.0011500.0017250.0022330.0028890.0031180.003042...0.0020730.0019160.0016210.0017190.0017090.0016700.0015210.0014030.0012570.001154
9790.0010650.0008640.0008100.0009060.0011130.0012600.0019070.0027170.0031180.003018...0.0018360.0014390.0014150.0017030.0018420.0015560.0014320.0013200.0012080.001108
9800.0009370.0007860.0007670.0009420.0012960.0015250.0019840.0025340.0029150.003009...0.0016260.0013260.0012670.0013670.0015680.0014370.0012380.0010330.0009770.000878
9810.0010030.0008520.0008100.0008830.0010430.0011870.0016900.0025730.0028390.002771...0.0016690.0014980.0014180.0015850.0017460.0015210.0013970.0012110.0011820.001064
9820.0008630.0008910.0009160.0008610.0009160.0007810.0009130.0010310.0011220.000862...0.0005940.0006240.0005840.0005500.0004860.0004530.0005730.0007240.0008210.000890

983 rows × 24 columns


1
2
3
4
5
6
7
# 3. GeoJson from Uber Movement
with open('cities_json/london_lsoa.json', 'r') as file:
    geojson = json.load(file)
    geojson = json.dumps(geojson)
    geojson_df = gpd.read_file(geojson)

geojson_df
msoa_codemsoa_namela_codela_namegeoeastgeonorthpopeastpopnortharea_km2MOVEMENT_IDDISPLAY_NAMEgeometry
0E02000508Hillingdon 01500ASHillingdon5061631835365059781838112.7466000Hillingdon, 00AS (0)MULTIPOLYGON (((-0.47794 51.55485, -0.47665 51...
1E02000716Newham 00300BBNewham5419781860095418701855681.5651701Newham, 00BB (1)MULTIPOLYGON (((0.05255 51.56171, 0.05310 51.5...
2E02000747Newham 03400BBNewham5395781813175398911814382.0824102Newham, 00BB (2)MULTIPOLYGON (((0.01001 51.52181, 0.01003 51.5...
3E02000748Newham 03500BBNewham5425001811525424391813391.3317503Newham, 00BB (3)MULTIPOLYGON (((0.05392 51.51611, 0.05174 51.5...
4E02000749Newham 03600BBNewham5410471811035408471812941.4190204Newham, 00BB (4)MULTIPOLYGON (((0.03241 51.51704, 0.03179 51.5...
.......................................
978E02000974Westminster 01500BKWestminster5270281812545271721811790.689337978Westminster, 00BK (978)MULTIPOLYGON (((-0.17019 51.51994, -0.16019 51...
979E02000975Westminster 01600BKWestminster5263961811295263751810420.484638979Westminster, 00BK (979)MULTIPOLYGON (((-0.17867 51.52008, -0.17898 51...
980E02000980Westminster 02100BKWestminster5299211786565297581786980.539208980Westminster, 00BK (980)MULTIPOLYGON (((-0.12279 51.49453, -0.12305 51...
981E02000981Westminster 02200BKWestminster5291231784885291401784010.363777981Westminster, 00BK (981)MULTIPOLYGON (((-0.14126 51.49455, -0.14080 51...
982E02000983Westminster 02400BKWestminster5294641780545294881781110.559636982Westminster, 00BK (982)MULTIPOLYGON (((-0.12710 51.48764, -0.12957 51...

983 rows × 12 columns


Parking Density Maps

본 글에서 traffic activity 데이터는 해당 데이터를 읽어본 것만으로 만족하고 넘어가도록 한다. (데이터 컬럼 한줄 있는거 보고 흥미/의욕 저하..) 대신 (spatio-temporal) Parking Density Maps 데이터를 중점적으로 뜯어보고 시각화해보고자 한다.


내가 할 일은 이러하다.

  1. 우선, 특정 시점에서 density snapshot 그림을 그려본다.
  2. density map의 24시간 하루 변화를 연속적으로 시각화해보자.

지역(구역)별 Density의 대수적 차이의 표현은 값의 크기에 따라 scatter size를 달리하여 표현하도록 하자. 아래 Rescaling equation을 density 컬럼에 적용하여 scatter size들을 추출한다.


Rescaling data range

Parking density 값에 따라 Scatter size를 다르게 부여할 것이다. Parking density 값 자체가 작기 때문에 표현이 어렵기 때문이다. 주어진 데이터 분포를 기반으로, 아래 식에 따라 rescaling을 진행했다. \(\Large x' = (s_{max} - s_{min})\times\left(\frac{x-x_{min}}{x_{max}-x_{min}}\right)^{1.5} + s_{min}\)

$ s_{min} $과 $ s_{max} $는 각각 내가 원하는 scattter size의 최소 및 최대 크기를 나타낸다. 따라서 임의로 조정 가능한 hyper-parameter들이다. 그리고 $ x_{min} $과 $ x_{max} $ 는 데이터 분포 상의 최소 및 최대값이다. 분모 term은 단순 min-max normalization으로서 [0, 1] range를 갖게 되고, 나머지 term들에 의해 최종적으로 $ x $값은 [$ s_{min} $, $ s_{max} $] range로 normalize 된다.

NOTE: Min-max Normalization term 에 1.5 power를 준 이유는 작은 값의 density point 시각화는 억제하고, 어느 정도 큰 값 위주로만 강조하기 위함이다. power 가 커질 수록, density 가 큰 애들만 시각화에 강조된다. (아래 셀 참고)

1
2
3
4
5
6
7
8
9
# Choosing the suitable power for visualization
x_test = np.arange(0, 1, 0.01)
fig, axs = plt.subplots(nrows=1, ncols=3, facecolor='w', figsize=(15, 3))
for ax, a in zip(axs.flatten(), [1.5, 3, 6]):
    ax.scatter(x_test, x_test**a, marker='*', facecolors='w', edgecolor='grey')
    ax.set_title(f"Power of {a}", fontsize=10)
    ax.set_ylabel("rescaled x")
    ax.set_xlabel("original x")
plt.show()

png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def rescaled_values(xs:pd.Series, x_min, x_max, s_min, s_max, power=1.5):
    """
        float 자릿수를 가지는 Density값을 가시적인 scatter size 로 rescaling 하기 위함.

    Parameters
    ----------
    xs : pd.Series
        Density series
    x_min : float
        Minimum Density correspodning to s_min
    x_max : float
        Maximum Density corresponding to s_max 
    s_min : int
        Minimum scatter size corresponding to minimum density
    s_max : int
        Maximum scatter size correspoding to maximum density
    power : float, optional
        parameter to control how making the small values to be much smaller, by default 1.5.

    Returns
    -------
    pd.Series
        Scatter size series as rescaled densities
    """
    x_min, x_max = min(xs), max(xs)
    cal_func = lambda x: ((s_max - s_min) * ((x - x_min) / (x_max - x_min)) ** power) + s_min
    new_xs = pd.Series([cal_func(x) for x in xs])
    return new_xs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
x_min = london_parking.min().min()
x_max = london_parking.max().max()

ms = rescaled_values(london_parking.iloc[:, 8], x_min, x_max, 0.5, 400)

fig, ax = plt.subplots(facecolor='w', figsize=(10, 10))
geojson_df.plot(ax=ax, facecolor='none', edgecolor='black', linewidth=.4)
geojson_df.representative_point().plot(ax=ax, marker='o', color='blue', markersize=ms, alpha=.4)
cx.add_basemap(ax, crs=geojson_df.crs.to_string(), source=cx.providers.Stamen.Watercolor)
cx.add_basemap(ax, crs=geojson_df.crs.to_string(), source=cx.providers.Stamen.TonerLabels)
ax.axis('off')
ax.set_aspect('auto')
ax.set_title("LONDON, t = 9h", fontsize=20)
plt.show()

png

Circadian Parking Density Maps

London, Paris, LA, Melbourne, Mumbai, Sydney 에 대해 모두 하루 중 Parking Density Maps을 추출하여 Graphics Interchange Format(GIF)로 만들어 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Check if it's possible to load all datasets for each city.
BasePath = os.path.join(os.getcwd(), 'derived_dataset/dataverse_files')

cities = ['london', 'paris', 'los angeles', 'melbourne', 'mumbai', 'sydney']
parking_cities = []
geojson_cities = []
for city in cities:
    df = read_derived_dataset(BasePath, d_type='parking', city=city, year=2018, quarter=4, time_scope='weekdays')
    parking_cities.append(df)

    city = city.lower().replace(' ', '_')
    for fn in os.listdir('cities_json/'):
        if fnmatch.fnmatch(fn, f'{city}*.json'):
            with open(f'cities_json/{fn}', 'r') as file:
                gj = json.load(file)
                gj = json.dumps(gj)
                geojson_cities.append(gpd.read_file(gj))
            
            print(f"LOADED JSON:: {fn}")
            break
    else:
        print(f"Failed to find .json matching to {city}.")
    
    print()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LOADED FILE:: results_parkingdensities_london-lsoa-2018-4-OnlyWeekdays-HourlyAggregate.csv
LOADED JSON:: london_lsoa.json

LOADED FILE:: results_parkingdensities_paris-iris-2018-4-OnlyWeekdays-HourlyAggregate.csv
LOADED JSON:: paris_iris.json

LOADED FILE:: results_parkingdensities_los_angeles-censustracts-2018-4-OnlyWeekdays-HourlyAggregate.csv
LOADED JSON:: los_angeles_censustracts.json

LOADED FILE:: results_parkingdensities_melbourne-tz-2018-4-OnlyWeekdays-HourlyAggregate.csv
LOADED JSON:: melbourne_tz.json

LOADED FILE:: results_parkingdensities_mumbai-hexclusters-2018-4-OnlyWeekdays-HourlyAggregate.csv
LOADED JSON:: mumbai_hexclusters.json

LOADED FILE:: results_parkingdensities_sydney-tz-2018-4-OnlyWeekdays-HourlyAggregate.csv
LOADED JSON:: sydney_tz.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
SavePath = os.path.join(os.getcwd(), 'img_output/')
for ii, city in enumerate(cities):
    city_dir_name = city.lower().replace(' ', '_')
    city_dir_path = os.path.join(SavePath, city_dir_name)
    if not os.path.exists(city_dir_path):
        os.mkdir(city_dir_path)
    
    target_city = parking_cities[ii]
    x_min = target_city.min().min()
    x_max = target_city.max().max()

    for jj, col in tqdm(enumerate(target_city.columns)):
        snap_parking = target_city.loc[:, col]
        markersizes = rescaled_values(snap_parking, x_min, x_max, 0.5, 400)

        fig, ax = plt.subplots(facecolor='w', figsize=(10, 10))
        geojson_cities[ii].representative_point().plot(ax=ax, marker='o', color='blue', markersize=markersizes, alpha=.5)
        cx.add_basemap(ax, crs=geojson_cities[2].crs.to_string(), source=cx.providers.OpenStreetMap.Mapnik, alpha=.7)

        ax.axis('off')
        ax.set_aspect('auto') # turn-off of forcing to ignore my figsize
        ax.set_title(f'Weekdays in {city_dir_name.upper()}, {col}', fontsize=20)

        save_name = f'{jj}_{jj+1}h_ParkingDensity_{city_dir_name.upper()}_2018_4q_OnlyWeekdays.png'
        plt.savefig(os.path.join(city_dir_path, save_name), pad_inches=.15, bbox_inches='tight')
        plt.close()
        plt.clf()

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

24it [00:39,  1.65s/it]
24it [01:07,  2.82s/it]
24it [00:23,  1.01it/s]
24it [00:30,  1.27s/it]
24it [00:24,  1.03s/it]
24it [00:26,  1.10s/it]
1
2
3
4
5
6
7
8
9
10
11
12
for city in cities:
    city = city.lower().replace(' ', '_')
    img_path = os.path.join(SavePath, city)
    img_set = []
    for img in sorted(os.listdir(img_path), key=lambda x: int(x.split('_')[0]), reverse=False):
        img_set.append(iio.imread(os.path.join(img_path, img)))
    
    gif_path = os.path.join(os.getcwd(), 'gif_output')
    if not os.path.exists(gif_path):
        os.mkdir(gif_path)
    gif_fname = f"{city.upper()}_ParkingDensity_2018-4q_OnlyWeekdays.gif"
    iio.imwrite(os.path.join(gif_path, gif_fname), img_set, duration=2.7, loop=0) # loop = 0 means infinite loop.
Drawing Drawing
Drawing Drawing
Drawing Drawing


Take-Home Messages and Conclusion

  • Uber Movement에서 공개 배포 중인 Travel Time 데이터 정보를 바탕으로, Parking Density & Traffic Activity 값을 추산한 데이터를 살펴 보았다.
    • Uber Movement 팀은 Travel Time 말고도, 현재 (도로 단위별) Speed 데이터와 New Mobility Heatmap 데이터도 공개하고 있다. (!! 2023년 11월 기준, 데이터 비공개됨ㅜ)
  • 본 연구팀에서 raw data로 활용한 Uber Travel Time 데이터는 어떤 형식과 내용을 지녔는지만 살짝 확인해봤다.
  • 본 연구팀 모델의 주요 아웃풋이라 할 수 있는 Parking Density 데이터를 중점적으로 살펴 보았고, 해외 6개 주요 도시에 대해 Circadian rhythms of Parking Density 를 시각화해봤다.

안타깝게도, 본 연구팀에서 추출한 도시별 데이터에서 우리 나라 ‘서울’같은 국내 도시는 찾을 수 없었다. 아무래도 Uber의 Travel Time 데이터를 raw data로 활용하는 이유와 국내에선 Uber 이용의 점유율이 높지 않아서 일 것이다. 사실 공개된 Uber Travel Time 데이터에도 국내 도시는 없다.

중요한 건, 연구팀에서 개발한 Parking Density & Traffic Activity를 Travel Time 정보에 기반해 추출해주는 모델은 굉장히 활용도가 높아 보인다. 어느 시간대/어느 지역에 주차 밀집도 및 차량 운행 수준이 높다는 정보는, 도시의 (교통, 상업 등 관련) 인프라 개발 단계에서 좋은 참고 자료가 될 것이기 때문이다. (물론 차량 내비게이션 데이터로는 Parking인지 Driving인지를 추론해내긴 쉽겠지만… 가용 데이터가 없다는 전제하에) 그래서 추후 여유가 된다면, 본 연구팀의 모델과 그 방법론을 더욱 자세히 답습하여, ‘서울’이나 국내 도시에도 적용해보고 싶다. 고맙게도 연구팀은 모델에 관한 소스 코드(julia 언어 사용) 역시 모두 공개하고 있다. 관심 있다면, 여기 CODE OCEAN LAB에 들어가 열람해보자.

fin

This post is licensed under CC BY 4.0 by the author.