Бұл жазба автоматты түрде аударылған. Бастапқы тіл: Орысша
Astanahub оқырмандарына Қайырлы күн.
Мен студентпін, қаладағы көптеген студенттер сияқты, Мен негізінен автобуспен жүремін. Посттың идеясы бір керемет күнде аялдамада тұрып, сүйікті автобусыңызды күтіп, AstraBUS қосымшасында оның орналасқан жеріне мұқият қарау кезінде пайда болды (қосымша астананың автобустарының орналасқан жерін онлайн режимінде көрсетеді). Мен Астанамыздың қоғамдық көлігіне тәуелсіз шағын бағалауды неге жасамасқа деп ойладым. Барлық Графиктер мен карталармен. Айтылды-Жасалды.
Алдымен маған осы бастапқы деректерді жинау керек болды. PacketCapture (трафикті талдау қосымшасы) қолдана отырып, олардың API-де endpoint бар екенін байқады , ол автобус нөмірін біле отырып, барлық автобустардың координаттары мен солтүстікке қатысты бұрылу бұрышы (бағыт бұрышы) бар.
Bus coordinates / 40рис. 1. PacketCapture бар экран
Онда маған керек. Өкінішке орай , бұл қызмет кешегі немесе өткен жылғы деректерді көруге мүмкіндік бермейді, дегенмен мен үшін бұл құрал сарапшылар мен тасымалдаушылардың сұранысына ие болар еді. Ал, realtime сияқты realtime.
Тапсырма дәл деректерді талдау болғандықтан, алдымен деректерді қандай да бір жолмен алу керек. Деректерді жүктеу және сақтау үшін талдау жазамыз:
1. Біз кітапханаларды импорттаймыз және автобус нөмірлерімен тізім жасаймыз
2. Негізгі сценарий өте қарапайым; біз bus_numbers массивімен цикл жасаймыз және бәрін csv файлына сақтаймыз. Мен төменде барлық көздері бар github сілтемесін тіркеймін.
Жоғарыдағы сценарийдің нәтижесі-әр маршрут үшін бір минут аралықпен жазылған 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);
geometry түрі бар pt өрісі-автобус пен аялдаманың кеңістіктік ақпаратын сақтайтын өріс. Кестелер құрылып , жазып қалды, олардың ДҚ-ға. Postgres-те csv - ден деректерді қайта сақтау үшін сценарий жазамыз:
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 репозиторийімді жүктеп, жоғарыдағы әрекеттерді қайталай отырып, аялдамада өтіп бара жатқан автобустармен жағдайды бағалауға тырысуға болады.
Добрый день читателям Astana Hub.
Я студент, как и многие студенты, по городу я передвигаюсь в основном на автобусе. Идея поста возникла, когда одним прекрасным днём стоя на остановке и ожидая свой заветный автобус и судорожно посматривая на его местоположение в приложении AstraBUS (приложение отображает онлайн местоположение автобусов столицы). Я подумал, почему бы не сделать свою независимую мини - оценку общественного транспорта нашей столицы. Со всеми графиками и картами. Сказано - Сделано.
Для начала мне как-то нужно было собрать эти исходные данные. Использовав PacketCapture (приложение для анализа трафика) заметил, что в их API есть endpoint , который зная номер автобуса выдает текущее местоположение всех автобусов с их координатами и их углом поворота относительно севера (дирекционный угол).
рис. 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 отключил. Результат смотреть на рисунке ниже:
рис.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 и повторив действия выше.