Тестим API приложения "AstraBUS" или насколько обычно опаздывают автобусы столицы

Оцениваем среднюю задержку общественного транспорта столицы на основе данных Realtime API собранного с приложения AstraBUS.

Добрый день читателям Astana Hub. 

Я студент, как и многие студенты, по городу я передвигаюсь в основном на автобусе. Идея поста возникла, когда одним прекрасным днём стоя на остановке и ожидая свой заветный автобус и судорожно посматривая на его местоположение в приложении AstraBUS (приложение отображает онлайн местоположение автобусов столицы). Я подумал, почему бы не сделать свою независимую мини - оценку общественного транспорта нашей столицы. Со всеми графиками и картами. Сказано - Сделано.

Для начала мне как-то нужно было собрать эти исходные данные. Использовав PacketCapture (приложение для анализа трафика) заметил, что в их API есть endpoint , который зная номер автобуса выдает текущее местоположение всех автобусов с их координатами и их углом поворота относительно севера (дирекционный угол).

Bus coordinates/40рис. 1. скрин с PacketCapture

То что мне нужно. К сожалению, данный сервис не позволяет просмотреть вчерашние или прошлогодние данные, хотя по мне данный инструмент пользовался бы спросом у аналитиков и транспортников. Ну раз realtime так realtime.

Так как стоит задача именно проанализировать данные, соответственно, сначала нужно эти данные как-то достать. Пишем парсер для скачивания и сохранения данных:

1. Импортируем библиотеки и создаем список с номерами автобусов

import requests
import time
import json
import csv
from datetime import datetime

bus_numbers = [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, 28, 29, 31, 32, 34, 35, 36,
               37, 39, 40, 41, 44, 46, 47, 48, 50, 51, 52, 53, 54, 56, 60,
               61, 64, 70, 71, 72, 73, 80, 81, 120]

2.  Основной скрипт довольно прост ; пробегаемся циклом по массиву bus_numbers и сохраняем всё в файл csv. Ссылку на github со всеми исходниками прикреплю ниже.

def parser(iter, bus_number):
    url = BUS_COORDINATE_API+str(bus_number)
    r = requests.get(url, headers=headers)
    bus_dict = json.loads(r.text)
    bus_list = []

    for key in bus_dict:
        x = {"bus_id": key, 'iter': iter}
        z = {**bus_dict[key], **x}
        bus_list.append(z)

    save_to_csv(bus_list, bus_number)

def save_to_csv(bus_list, bus_number):
    filename = 'bus/'+'bus_' + str(bus_number) + ".csv"
    with open(filename, 'a+') as outfile:
        fieldnames = ['bus_id', 'route', 'time',
                      'latitude', 'longitude', 'angle', 'iter']
        writer = csv.DictWriter(outfile, fieldnames=fieldnames)
        for bus in bus_list:
            writer.writerow(bus)

def main():
    try:
        global iteration
        for bus in bus_numbers:
            parser(iteration, bus)
        iteration += 1
    except:
        print("Some problems with connection", datetime.now())

Результат скрипта выше это -  50 файлов csv записанные с интервалом в одну минуту на каждый маршрут. 7 - колонка - это данные с итерацией , которые показывают время прошедшее с начала работы скрипта(т.е каждую минуту).

7 апреля поставил скрипт работать с 9 утра и вечером к часам к 11 отключил. Результат смотреть на рисунке ниже:

vscodeрис.2. Пример файлов с данными автобусов

Данные есть. Дополнительно скачал данные о существующих остановках с OpenStreetMap.

Каждый маршрут выглядит как множества точек, лежащих на одной мульти-линии с атрибутивными данными как время и уникальный идентификатор каждого автобуса (см. рис. 3).

макетрис. 3. Пример маршрута №1

 

Создаем БД в PostgreSQL

Для того что бы вычислить среднее время задержки какого либо автобуса нужно вычислить его интервал. Т.е взять точки остановок, построить круглый буфер вокруг остановок и считать входящие туда автобусы и вычислить, в каком порядке и в какое время автобусы проезжали остановку.

На этом этапе возникла потребность как-то правильно агрегировать имеющиеся данные и чтобы не изобретать велосипед решено было пересохранить все данные, реляционную базу данных PostgreSQL для того, чтобы в дальнейшем применять на нем пространственные инструменты через PostGIS (такие как построение буфера вокруг точек и подсчет количества входящих туда автобусов рис. 4).

рис. 4 . Буфер вокруг остановок

В целом структура базы данных выглядит довольно просто,  две основные таблицы и вспомогательная - для хранения времени задержки с отношением один ко многим.

 

рис. 5. Схема бд

Снизу пример SQL запроса на создания таблицы bus

CREATE TABLE IF NOT EXISTS bus
                  (id serial PRIMARY KEY
                  bus_id varchar,
                  route_number varchar
                  latest_bus_stop_id integer,
                  iteration integer
                  time_at timestamp,
                  pt geometry);

поле pt с типом geometry  это поле для хранения пространственной информации автобуса и остановки.  Таблицы созданы,  осталось переписать их в БД. Пишем скрипт для пересохранения данных с csv в postgres:

def insert_row_to_db(row):
    insert_query = """ INSERT INTO bus (bus_id,route_number,time_at, iteration, pt)
                              VALUES (%s, %s, %s, %s, ST_GeomFromText(%s, 4326))"""
    date_time_obj = datetime.strptime(row[2],'%Y-%m-%d %H:%M:%S')
    point = "POINT({} {})".format(row[4],row[3])
    item_tuple = (row[0],row[1],date_time_obj,row[6],point)
    cursor.execute(insert_query, item_tuple)
    conn.commit()

#csv contain 7 columns
def insert_bus_to_db(path):
    with open(path) as file:
        file = csv.reader(file,delimiter = ',')
        for row in file:
            insert_row_to_db(row)

def save_bus_to_db():
    for bus in bus_numbers:
        path = bus_data_path + str(bus)+".csv"
        insert_bus_to_db(path)

def insert_bus_stop_row_to_db(row):
    insert_query = """ INSERT INTO bus_stop (pt)
                              VALUES (ST_GeomFromText(%s, 4326))"""
    point = "POINT({} {})".format(row[1],row[2])
    item_tuple = (point,)
    cursor.execute(insert_query, item_tuple)
    conn.commit()

def insert_bus_stop_to_db():
    with open("./bus/bus_stops/bus_stops.csv") as file:
        file = csv.reader(file,delimiter = ',')
        for row in file:
            insert_bus_stop_row_to_db(row)

В целом получилось примерно 320 тысяч записей это только данные на 7 апреля. А сама БД со всеми таблицами весит 66 МБ.

рис. 6 . Админ панель Postgresql

Пишем SQL запросы

Переходим к расчету и написанию PostgreSQL/Postgis запросов.

C Postgis нам нужны только две функции это ST_Buffer и ST_Contains (для построения буфера и проверки вхождения элементов внутрь буфера).

Записывать задержки будем в таблицу bus_stop_delay_history . Алгоритм на псевдокоде таков:

Для каждой итерации :    

      проверить входит ли автобус в буферную зону остановки, если входит:

              запиши в bus_stop_delay_history  время вхождения и в поле delay отними текущее  значение времени  от предыдущего

       иначе:       

                          пропусти

Пример SQL запроса на создания буфера и проверки вхождения автобуса в буфер на первую итерацию:

 select bus.*,bus_stop.*  from bus, bus_stop 
            where ST_contains(ST_Buffer(bus_stop.pt,0.0008,'quad_segs=8'),bus.pt) and 
            (bus.iteration = 1) and 
            (bus.latest_bus_stop_id is null or 
            bus.latest_bus_stop_id != bus_stop.id);

Занятный факт: В столице всего 539 уникальных автобусов (по крайней со включенным GPS). Простой SQL запрос показывает все уникальные автобусы курсирующие в 9:30 утра

рис. 7. Все автобусы в столице в 9:30

Визуализация и выводы

Визуализировать результаты буду через библиотеку Matplotlib. Код примера ниже.

from matplotlib.dates import DateFormatter
import matplotlib.dates as dates
import numpy as np


fig, ax = plt.subplots()
ax.plot_date(x, y,"b-", label="№2")
ax.xaxis.set_major_formatter(dates.DateFormatter('%H:%M'))
y_mean = [np.mean(y)]*len(x)
ax.plot_date(x, y_mean,"b--", label="№2 среднее")
plt.title('Хронология времени задержки автобусов на остановке "Парк Жабаева"')
plt.ylabel('Время задержки в минутах')
plt.xlabel('Время (часов)')
plt.legend()
plt.show()

В целом можно сделать отдельный аудит - проверку на каждую остановку и каждый маршрут

рис. 8

Есть одна большая ошибка в моей оценке. Заключается она в том, что происходит двойная запись автобусов. Т.е когда автобус из точки А проезжает в точку С, посередине стоящая остановка Б записывает время, когда автобус подъехал и забрал пассажиров - с этим всё ОК. Но когда другой автобус, проезжающий на противоположной линии маршрута из точки С едит в точку А и подъезжает к остановке Б, происходит еще одна запись. Данную погрешность можно исправить, если знать текущий дирекционный угол автобуса и учитывать ее при записи задержки, но так как целью данного поста не является точная оценка,  данный момент я пропустил и просто использовал среднее значение.

 

рис. 9.  У №10 очень небольшой интервал подачи автобуса

В среднем автобусы в столице  движутся с интервалом 22 минуты. В центре города чаще, но на периферии есть автобусы  с задержкой 40 минут.

Я думаю, что пост получился информативный и полезный. В планах дополнительно сделать интерактивную карту, которая покажет проблемные маршруты на дорогах и выявить возможные причины задержки.

Спасибо за прочтение. Вы так же можете попробовать оценить ситуацию с автобусами проезжающие на вашей остановке скачав мой репозитории на GitHub и повторив действия выше.

Комментарии (2)

Авторизуйтесь чтобы оставить комментарий


Eskendr Nurdaulet · 13-04-2021 в 09:23

Очень полезная Информация

-8
Ответить

adil kairolla · 12-04-2021 в 17:03

Круто!

0
Ответить