📚 Data¶
Data čerpáme ze stránek otevřených dat Pražské integrované dopravy. Konkrétně pracujeme s daty cestovních řádů, která jsou původně ve formátu GTFS (General Transit Feed Specification).
📦 Import knihoven¶
V této části:
- importuji všechny potřebné knihovny,
- vypínám rušivé warningy,
- zafixuji random seed pro konzistentní výsledky
- a ukládám/nastavuji styl prezentace.
import collections
import math
import warnings
import contextily as ctx
import matplotlib as mpl
import matplotlib.cm as cm
import matplotlib.colors as mcolors
import matplotlib.font_manager as fm
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
import pywaffle as waff
from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar
from matplotlib.lines import Line2D
warnings.simplefilter(action="ignore", category=UserWarning)
warnings.simplefilter(action="ignore", category=DeprecationWarning)
np.random.seed(1)
plt.style.use("ggplot")
font = fm.FontProperties(size=9)
red = (226 / 255, 74 / 255, 51 / 255)
redl = (226 / 255, 74 / 255, 51 / 255, 0.6)
redf = (226 / 255, 74 / 255, 51 / 255, 1)
blue = (52 / 255, 138 / 255, 189 / 255)
grey = (100 / 255, 100 / 255, 100 / 255)
📚 Dataset¶
Datasety nahravám do paměti a zobrazuji základní informace potřebné k předzpracování.
conns = pd.read_csv("d.csv")
stops = pd.read_csv("stops.txt")
conns.head()
stop_from | stop_from_name | stop_to | stop_to_name | depart_from | arrive_to | route_type | is_night | monday | tuesday | wednesday | thursday | friday | saturday | sunday | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | U2991Z301 | Hněvice | T58005 | Hněvice seř.n. | 4:53:00 | 4:54:30 | 2 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
1 | T58005 | Hněvice seř.n. | U4610Z301 | Záluží | 4:54:30 | 4:56:00 | 2 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
2 | U4610Z301 | Záluží | U4609Z301 | Dobříň | 4:56:00 | 4:59:00 | 2 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
3 | U4609Z301 | Dobříň | U4608Z301 | Roudnice nad Labem | 4:59:00 | 5:03:00 | 2 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
4 | U4608Z301 | Roudnice nad Labem | U4609Z301 | Dobříň | 4:36:00 | 4:38:00 | 2 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
conns.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1642433 entries, 0 to 1642432 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 stop_from 1642433 non-null object 1 stop_from_name 1642433 non-null object 2 stop_to 1642433 non-null object 3 stop_to_name 1642433 non-null object 4 depart_from 1642433 non-null object 5 arrive_to 1642433 non-null object 6 route_type 1642433 non-null int64 7 is_night 1642433 non-null int64 8 monday 1642433 non-null int64 9 tuesday 1642433 non-null int64 10 wednesday 1642433 non-null int64 11 thursday 1642433 non-null int64 12 friday 1642433 non-null int64 13 saturday 1642433 non-null int64 14 sunday 1642433 non-null int64 dtypes: int64(9), object(6) memory usage: 188.0+ MB
stops.head()
stop_id | stop_name | stop_lat | stop_lon | zone_id | stop_url | location_type | parent_station | wheelchair_boarding | level_id | platform_code | asw_node_id | asw_stop_id | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | U50S1 | Budějovická | 50.044411 | 14.448787 | P | NaN | 1 | NaN | 1 | NaN | NaN | 50.0 | NaN |
1 | U52S1 | Chodov | 50.031672 | 14.490961 | P | NaN | 1 | NaN | 1 | NaN | NaN | 52.0 | NaN |
2 | U75S1 | Kolbenova | 50.110395 | 14.516398 | P | NaN | 1 | NaN | 1 | NaN | NaN | 75.0 | NaN |
3 | U78S1 | Ládví | 50.126591 | 14.469451 | P | NaN | 1 | NaN | 1 | NaN | NaN | 78.0 | NaN |
4 | U100S1 | Vltavská | 50.100298 | 14.438492 | P | NaN | 1 | NaN | 1 | NaN | NaN | 100.0 | NaN |
stops.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 16435 entries, 0 to 16434 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 stop_id 16435 non-null object 1 stop_name 15936 non-null object 2 stop_lat 16435 non-null float64 3 stop_lon 16435 non-null float64 4 zone_id 15408 non-null object 5 stop_url 0 non-null float64 6 location_type 16435 non-null int64 7 parent_station 954 non-null object 8 wheelchair_boarding 16435 non-null int64 9 level_id 954 non-null object 10 platform_code 14750 non-null object 11 asw_node_id 15748 non-null float64 12 asw_stop_id 15354 non-null float64 dtypes: float64(5), int64(2), object(6) memory usage: 1.6+ MB
⚙️ Předzpracování dat¶
Procházím postupně klíčové příznaky a transformuji je do správných formátů. Nepoužívané hodnoty zahazuji.
Výsledkem této části jsou dva čísté datasety conns
(spoje) a stops
(zastávky).
🚈 Spoje - conns
¶
⏳ Časové údaje¶
Upravuji časové údaje do korektního formátu a validních hodnot.
Oba časové údaje reprezentuji třemi sloupcemi: H, M, S
def split_time_column(df, col):
colH, colM, colS = f"{col}_H", f"{col}_M", f"{col}_S"
cols = [colH, colM, colS]
slice = df.loc[:, col].str.split(":", expand=True).astype("int64")
slice[cols] = slice
slice[colH] = slice[colH].mod(24)
return slice[cols], cols
for col in ["depart_from", "arrive_to"]:
if col in conns.columns:
slice, cols = split_time_column(conns, col)
conns.drop(col, axis=1, inplace=True)
conns[cols] = slice
def to_timedelta(df, h_col, m_col, s_col):
seconds = 3600 * df[h_col] + 60 * df[m_col] + df[s_col]
return seconds
departures = to_timedelta(conns, "depart_from_H", "depart_from_M", "depart_from_S")
arrivals = to_timedelta(conns, "arrive_to_H", "arrive_to_M", "arrive_to_S")
fig, ax = plt.subplots(1, 1, figsize=(9, 4), layout="constrained")
ax.set_title("Time of departure")
ax.hist(departures / 3600, bins=24, range=(0, 24), alpha=0.6)
ax.hist(departures / 3600, bins=24, range=(0, 24), histtype="step", color=red, linewidth=1.5)
ax.set_xlabel("Time [h]")
ax.set_ylabel("Count")
ax.ticklabel_format(axis="y", style="sci", scilimits=(0, 0), useMathText=True)
ax.set_xticks(range(0, 25, 3));
📅 Dny v týdnu a typ provozu¶
Zde převádím příslušné hodnoty na kategorický typ.
days = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
for col in ["is_night", *days]:
conns[col] = conns[col].astype("boolean")
days_dist = pd.DataFrame(conns[days].sum(), columns=["count"])
days_dist.style.format(precision=3, thousands=" ", decimal=".").format_index(str.upper, axis=0).format_index(str.upper, axis=1)
COUNT | |
---|---|
MONDAY | 1 159 992 |
TUESDAY | 1 159 986 |
WEDNESDAY | 1 159 986 |
THURSDAY | 1 159 986 |
FRIDAY | 1 160 097 |
SATURDAY | 492 743 |
SUNDAY | 504 771 |
fig, ax = plt.subplots(1, 1, figsize=(9, 4), layout="constrained")
ax.set_title("Number of departures")
ax.barh(days_dist["count"].index, days_dist["count"], alpha=0.85)
ax.set_ylabel("Day")
ax.set_xlabel("Count")
ax.ticklabel_format(axis="x", style="sci", scilimits=(0, 0), useMathText=True)
ax.invert_yaxis()
ax.grid(visible=False, which="major", axis="x")
🪪 ID zastávek¶
Nepoužívané ID zastávek zahazuji. Zastávky identifikuji názvem.
conns.drop(["stop_from", "stop_to"], axis=1, inplace=True, errors="ignore")
🚢 Dopravní prostředky¶
Dopravní prostředky převádím na kategorický datový typ.
route_type_map = {
0: "Tram",
1: "Metro",
2: "Rail",
3: "Bus",
4: "Ferry",
7: "Funicular",
}
conns["route_type"] = (conns["route_type"].map(route_type_map, na_action="ignore").astype("category"))
types = conns["route_type"].value_counts()
fig, axes = plt.subplots(1, 2, figsize=(12, 4), layout="constrained", gridspec_kw={"wspace": 0.1})
ax = axes[0]
ax.set_title("Route types")
ax.set_xlabel("Frequency")
ax.ticklabel_format(axis="x", style="sci", scilimits=(0, 0), useMathText=True)
bars = ax.barh(types.index, types, alpha=0.85)
ax.invert_yaxis()
ax.set_xlim(0, 1.2 * 10**6)
for bar, count in zip(bars, types):
ax.text(bar.get_width() + 10**4, bar.get_y() + bar.get_height() / 2, f"{count}", va="center", fontsize=10)
val = types / 10000
val_freq = val / val.sum()
waff.Waffle.make_waffle(
ax=axes[1], rows=12, values=val,
title={"label": "Route types", "loc": "center"},
labels=[f"{k} ({v*100:.2f}%)" for k, v in val_freq.items()],
legend={"bbox_to_anchor": (1.5, 1), "ncol": 1, "framealpha": 0},
icons=["bus", "train-tram", "train-subway", "train", "ship", "car"],
font_size=16, icon_style="solid", icon_legend=True, starting_location="NW",
vertical=True, cmap_name="Set2",
)
🚏 Zastávky - stops
¶
🧭 Souřadnice¶
U datasetu zastávek používám pouze souřadnice pro vykreslování spojů. Ostatní příznaky zahazuji.
V případě více zastávek se stejným názvem volím jejich průměr. V datasetu jsou stejné zastávky s lehce odlišnými názvy, to zde neřeším - manuálně upravuji později.
stops = stops[["stop_name", "stop_lat", "stop_lon"]]
stops = stops.groupby(by=["stop_name"], dropna=True).mean()
🏭 Závěr předzpracování¶
Po předzpracování mám dva čisté datasety conns
a stops
, které vypadají následovně (časy jsou zde vynechané):
conns.drop(['depart_from_H', 'depart_from_M', 'depart_from_S', 'arrive_to_H', 'arrive_to_M', 'arrive_to_S'], axis=1).head()
stop_from_name | stop_to_name | route_type | is_night | monday | tuesday | wednesday | thursday | friday | saturday | sunday | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | Hněvice | Hněvice seř.n. | Rail | False | True | True | True | True | True | False | False |
1 | Hněvice seř.n. | Záluží | Rail | False | True | True | True | True | True | False | False |
2 | Záluží | Dobříň | Rail | False | True | True | True | True | True | False | False |
3 | Dobříň | Roudnice nad Labem | Rail | False | True | True | True | True | True | False | False |
4 | Roudnice nad Labem | Dobříň | Rail | False | True | True | True | True | True | False | False |
stops.head()
stop_lat | stop_lon | |
---|---|---|
stop_name | ||
AHr Km 11,485 | 50.146550 | 14.731470 |
Albertov | 50.067917 | 14.420798 |
Ametystová | 49.988201 | 14.362216 |
Amforová | 50.041778 | 14.327298 |
Anděl | 50.071132 | 14.403406 |
🕸️ Síťová analýza¶
V této a následující části se věnuji:
- konstrukci sítě zastávek a spojů,
- vyskleslení klíčových a zajímavých podgrafů
- a základní analýze vrcholů.
🛠️ Sestrojení grafu¶
stop_pos = {n: np.array([c["stop_lon"], c["stop_lat"]]) for n, c in stops.iterrows()}
nodes = [(n, s.to_dict()) for n, s in stops.iterrows()]
# G.add_nodes_from(nodes)
def extract_edges(df):
cc = df[["stop_from_name", "stop_to_name"]].copy()
cc[["stop_from_name", "stop_to_name"]] = np.sort(cc[["stop_from_name", "stop_to_name"]], axis=1)
cc["count"] = 1
cc = cc.groupby(["stop_from_name", "stop_to_name"]).sum().reset_index()
cc = [(s["stop_from_name"], s["stop_to_name"], s["count"]) for _, s in cc.iterrows()]
return cc
def build_graph(conns):
edges = extract_edges(conns)
g = nx.Graph()
g.add_weighted_edges_from(edges)
return g
G = build_graph(conns)
G.remove_node("Praha Masarykovo nádr.")
G.remove_node("Pha hl.n. Lc201,202")
🎨 Vykreslení grafu a podrafů¶
Zde vykresluji:
- celkové pokrytí území Prahy (a okolí),
- centrum Prahy,
- Dejvice,
- Karlovo náměstí,
- Hlavní nádraží
- a Malou Stranu.
def subgroup_map_filter(g, m, lat="stop_lat", lon="stop_lon"):
def filter_subgroup(lbc, urc):
filtered = m[(lbc[0] < m[lat]) & (m[lat] < urc[0]) & (lbc[1] < m[lon]) & (m[lon] < urc[1])]
return g.subgraph(filtered.index)
return filter_subgroup
def subgroup_distance_filter(g, m, lat="stop_lat", lon="stop_lon"):
def filter_subgroup(coor, dist):
x, y = coor
dx = m[lat] - x
dy = m[lon] - y
dists = np.sqrt(dx * dx + dy * dy)
filtered = m[dists < dist]
return g.subgraph(filtered.index)
return filter_subgroup
def subgroup_distance_filter_inv(g, m, lat="stop_lat", lon="stop_lon"):
def filter_subgroup(coor, dist):
x, y = coor
dx = m[lat] - x
dy = m[lon] - y
dists = np.sqrt(dx * dx + dy * dy)
filtered = m[dists > dist]
return g.subgraph(filtered.index)
return filter_subgroup
def add_scalebar(ax, s, loc="lower right", label_top=False):
txt = ""
if s > 0.008:
txt = f"{round(s * 10**2)} km"
else:
txt = f"{round(s * 10**5)} m"
scalebar = AnchoredSizeBar(ax.transData, s, txt, loc, label_top=label_top, fontproperties=font, pad=0.4)
ax.add_artist(scalebar)
filter = subgroup_map_filter(G, stops)
filter_d = subgroup_distance_filter(G, stops)
filter_d_inv = subgroup_distance_filter_inv(G, stops)
def add_map(ax, source, scale, reset_extent=True):
ctx.add_basemap(ax=ax, crs="WGS84", source=ctx.providers.CartoDB.Positron, reset_extent=True, attribution=False)
ax.grid(False)
add_scalebar(ax, scale)
def draw_part(ax, title, title_loc, A, B, radius, node_param, edge_param):
ax.set_title(title, loc=title_loc)
ax.set_xlim(A[1], B[1])
ax.set_ylim(A[0], B[0])
M = (np.array(A) + np.array(B)) / 2
g = filter_d(M, radius)
nx.draw_networkx_nodes(g, pos=stop_pos, ax=ax, node_size=node_param["size"], node_color=node_param["color"], alpha=node_param["alpha"])
nx.draw_networkx_edges(g, pos=stop_pos, ax=ax, width=edge_param["size"], edge_color=edge_param["color"], alpha=edge_param["alpha"])
def draw_part_with_label(ax, title, title_loc, A, B, radius, params):
ax.set_title(title, loc=title_loc)
ax.set_xlim(A[1], B[1])
ax.set_ylim(A[0], B[0])
M = (np.array(A) + np.array(B)) / 2
g = filter_d(M, radius)
nx.draw_networkx(g, pos=stop_pos, ax=ax, node_size=params["node_size"], node_color=params["node_color"], width=params["width"],
font_size=params["font_size"], edge_color=params["edge_color"], with_labels=True)
centre = ["Centre", [50.07347057153624, 14.396852618086926], [50.11329080577483, 14.44677177212586], 0.04]
dejvice = ["Dejvice", [50.09122027369268, 14.385020433301096], [50.10756165438437, 14.403450996019187], 0.01]
charles = ["Charles Square", [50.069188151071394, 14.40950684182552], [50.08070937389346, 14.425281091947769], 0.02]
station = ["Main Railway Station", [50.07344788999524, 14.4278631463291], [50.088977084459446, 14.44535284907064], 0.02]
lesser = ["Lesser Town Square", [50.075600319200056, 14.392449133622146], [50.09242635204253, 14.414991205632134], 0.03]
nparam1 = {"size": 30, "color": red, "alpha": 0.8}
eparam1 = {"size": 1.5, "color": red, "alpha": 0.6}
params1 = {"node_size": 60, "node_color": redl, "width": 1.5, "font_size": 10, "edge_color": redl}
fig, axes = plt.subplots(3, 2, figsize=(12, 15), layout="constrained")
fig.suptitle("Coverage maps", fontsize=20)
ax = axes[0][0]
ax.set_title("All stops", loc="left")
nx.draw_networkx_nodes(G, pos=stop_pos, ax=ax, node_size=10, node_color=red, alpha=0.3)
add_map(ax, ctx.providers.CartoDB.Positron, 0.5)
ax = axes[0][1]
loc = centre
draw_part(ax, loc[0], "right", loc[1], loc[2], loc[3], nparam1, eparam1)
add_map(ax, ctx.providers.CartoDB.Positron, 0.01)
ax = axes[1][0]
loc = dejvice
draw_part_with_label(ax, loc[0], "left", loc[1], loc[2], loc[3], params1)
add_map(ax, ctx.providers.CartoDB.Positron, 0.002)
ax = axes[1][1]
loc = charles
draw_part_with_label(ax, loc[0], "right", loc[1], loc[2], loc[3], params1)
add_map(ax, ctx.providers.CartoDB.Positron, 0.002)
ax = axes[2][0]
loc = station
draw_part_with_label(ax, loc[0], "left", loc[1], loc[2], loc[3], params1)
add_map(ax, ctx.providers.CartoDB.Positron, 0.0025)
ax = axes[2][1]
loc = lesser
draw_part_with_label(ax, loc[0], "right", loc[1], loc[2], loc[3], params1)
add_map(ax, ctx.providers.CartoDB.Positron, 0.0025)
🚈 Pokrytí podle dopravních prostředků¶
Vykresluji jak velké území pokrývají vybrané typy linek (autobusy, tramvaje, vlaky, metra).
def filter_dist(route, dist):
g = route_graph(conns, route)
H = nx.Graph()
H2 = nx.Graph()
l1 = []
for e1, e2 in g.edges:
p1 = stops.loc[e1].to_numpy()
p2 = stops.loc[e2].to_numpy()
if np.linalg.norm(p1 - p2) < dist:
l1.append((e1, e2))
H.add_edges_from(l1)
return H
def route_graph(cc, route_type):
H = nx.Graph()
h_edges = extract_edges(cc[cc["route_type"] == route_type])
H.add_weighted_edges_from(h_edges)
return H
fig, ax = plt.subplots(1, 1, figsize=(9, 9), layout="tight")
route = "Bus"
ax.set_title(f"{route} routes")
h1 = filter_dist(route, 0.05)
h2 = filter_dist(route, 0.15)
nx.draw_networkx(h2, pos=stop_pos, ax=ax, with_labels=False, node_size=0, node_color=redl, width=0.5, edge_color=redl, alpha=0.5)
nx.draw_networkx(h1, pos=stop_pos, ax=ax, with_labels=False, node_size=3, node_color=redl, width=1, edge_color=redl)
add_map(ax, ctx.providers.Esri.WorldGrayCanvas, 0.5)
fig, ax = plt.subplots(1, 1, figsize=(9, 9), layout="tight")
route = "Tram"
ax.set_title(f"{route} routes")
nx.draw_networkx(route_graph(conns, route), pos=stop_pos, ax=ax, with_labels=False, node_size=15, node_color=redf, width=1.4, edge_color=redl)
add_map(ax, ctx.providers.Esri.WorldGrayCanvas, 0.05)
fig, ax = plt.subplots(1, 1, figsize=(9, 9), layout="tight")
route = "Rail"
ax.set_title(f"{route} routes")
h1 = filter_dist(route, 0.1)
h2 = filter_dist(route, 1)
nx.draw_networkx(h2, pos=stop_pos, ax=ax, with_labels=False, node_size=10, node_color=redf, width=1, edge_color=redl, alpha=0.5, style="dashed")
nx.draw_networkx(h1, pos=stop_pos, ax=ax, with_labels=False, node_size=10, node_color=redf, width=1.4, edge_color=redl)
add_map(ax, ctx.providers.Esri.WorldGrayCanvas, 0.5)
fig, ax = plt.subplots(1, 1, figsize=(9, 9), layout="tight")
route = "Metro"
ax.set_title(f"{route} routes")
nx.draw_networkx(route_graph(conns, route), pos=stop_pos, ax=ax, with_labels=False, node_size=50, node_color=redf, width=3, edge_color=redf, alpha=0.95)
add_map(ax, ctx.providers.Esri.WorldGrayCanvas, 0.05)
📝 Poznámky¶
- Autobusy a tramvaje mají z vybraných dopravních prostředků nejhustší linky (-- beru v potaz měřítko).
- PID kromě Prahy pokrývá i relativně velkou část republiky.
Graf Rail routes plnou čarou zobrazuje krátké spoje (<10 km), čárkovanou daleké spoje (<110 km) a spoje delší než 110 km nevykresluje.
📊 Analýza vrcholů¶
Analýzu vrcholů provádím pomocí centralit. V následující vizualizaci vykresluji vybrané centrality zastávek v centru Prahy.
Následně provedu hlubší analýzu vrcholů na celé síti PID.
def make_plotter(g, lbc, urc):
# https://stackoverflow.com/a/67230951/13022894
def plot_centrality(ax, centrality, *, label_count=0, **kwargs):
ax.set_title(centrality.__name__)
ax.set_xlim(lbc[1], urc[1])
ax.set_ylim(lbc[0], urc[0])
deg_cent = centrality(g, **kwargs)
degree_centrality = pd.Series(deg_cent).sort_values(ascending=False)
colors = degree_centrality / np.max(degree_centrality) * 5000
cent = np.fromiter(deg_cent.values(), float)
normalize = mcolors.Normalize(vmin=cent.min(), vmax=cent.max())
scalarmappaple = cm.ScalarMappable(norm=normalize, cmap=colormap)
scalarmappaple.set_array(cent)
fig.colorbar(scalarmappaple, ax=ax)
colors = cent / np.max(cent) * 200
nx.draw(g, stop_pos, ax=ax, node_size=colors, node_color=colors, cmap=colormap, edge_color=grey, width=0.6)
nx.draw_networkx_labels(g.subgraph(degree_centrality[:label_count].index), stop_pos, ax=ax, verticalalignment='bottom')
add_map(ax, ctx.providers.Esri.WorldGrayCanvas, 0.01)
return plot_centrality
fig, axes = plt.subplots(2, 2, figsize=(13, 9), layout="constrained")
fig.suptitle("Network centralities - Centre of Prague", fontsize=20)
colormap = cm.YlOrRd
g = filter_d((np.array(centre[1]) + np.array(centre[2])) / 2, 0.1)
plot_centrality = make_plotter(g, centre[1], centre[2])
plot_centrality(axes[0][0], nx.degree_centrality, label_count=1)
plot_centrality(axes[0][1], nx.closeness_centrality)
plot_centrality(axes[1][0], nx.eigenvector_centrality, weight="weight", label_count=2)
plot_centrality(axes[1][1], nx.betweenness_centrality, weight="weight", label_count=6)
🗨️ Interpretace centralit v centru Prahy¶
🌡️ Stupňová centralita¶
- Nejvyšší významnost podle stupňů mají stanice metra, které jsou v dopravní síti slouží jako huby/autority.
📐 Centralita blízkosti¶
- V centru Prahy, kde jsou spoje velmi husté, jsou z hlediska centrality blízkosti významné všechny zastávky.
⚖️ Vážený počet hran¶
- Centralita nám říká, že stanice metra Karlovo náměstí je v této části Prahy velmi důležitým vrcholem a má velký vliv na své sousedy.
- (Kvantifikuje celkovou důležitost vrcholů, důležitost hran vedoucí z nich a důležitost jejich sousedů.)
🖇️ Centralita mezilehlosti¶
- Udává informaci o důležitosti jednotlivých vrcholů v rámci jiných nekratších cest.
- Linka metra 🟥C je pro tento úsek obzvlášt zajímavý v tom, že přes něj proudí většina nejkratšch spojů.
🌡️ Stupňová centralita¶
V této části se věnuji analýze stupňové centrality na celém grafu.
degrees = pd.Series([d for n, d in G.degree()])
degrees = degrees[degrees < 10]
fig, axes = plt.subplots(2, 1, figsize=(9, 4), sharex=True)
fig.suptitle("Network degree histogram (<10)", size=15)
ax = axes[0]
ax.hist(degrees, bins=(degrees.max() - degrees.min()).astype(int))
ax.ticklabel_format(axis="y", style="sci", scilimits=(0, 0), useMathText=True)
ax.set_yticks([1000, 2000, 3000, 4000])
ax.set_ylabel("Count")
ax.tick_params(labelbottom=False)
ax.tick_params(axis="x", which="both", length=0)
medianprops = dict(linewidth=2.5, color=(1, 0.38, 0.27))
flierprops = dict(marker="d", markerfacecolor=(1, 0.38, 0.27), markersize=8, markeredgecolor="none")
ax = axes[1]
ax.boxplot(degrees, vert=False, widths=[0.4], showfliers=True, flierprops=flierprops, medianprops=medianprops)
ax.get_yaxis().set_visible(False)
ax.set_xlabel("Degree")
fig.subplots_adjust(hspace=0.03)
def display_top(ps):
df = pd.DataFrame(ps, columns=["Coeff"])
styler = df.style.format(precision=7, thousands=" ", decimal=".")
display(styler)
degree_centrality = pd.Series(nx.degree_centrality(G)).sort_values(ascending=False)
top10 = degree_centrality[:10]
print("Top 10 nodes with the highest degree centrality")
display_top(top10)
neighbours = set()
for n in top10.index:
for u in nx.all_neighbors(G, n):
neighbours.add(u)
for _ in range(3):
nn = neighbours.copy()
for n in nn:
for u in nx.all_neighbors(G, n):
neighbours.add(u)
cumulative = list(neighbours)
cumulative.extend(list(top10.index))
len(cumulative)
fig, ax = plt.subplots(1, 1, figsize=(12, 7), layout="constrained")
fig.suptitle("10 nodes with the highest degree centrality and their 3-neighbourhood", fontsize=15)
nx.draw_networkx(G.subgraph(cumulative), pos=stop_pos, ax=ax, node_size=10, node_color=blue, with_labels=False, alpha=0.1, edge_color=blue, width=1)
nx.draw_networkx(G.subgraph(top10.index), pos=stop_pos, ax=ax, node_size=50, node_color=redl, font_size=10, width = 0)
add_map(ax, ctx.providers.CartoDB.Positron, 0.5, False)
A = [49.5568518946815, 13.510323360859733]
B = [50.709352011905555, 15.35150400382775]
ax.set_xlim(A[1], B[1])
ax.set_ylim(A[0], B[0]);
Top 10 nodes with the highest degree centrality
Coeff | |
---|---|
Zličín | 0.0022995 |
Lihovar | 0.0021642 |
Černý Most | 0.0020289 |
Mladá Boleslav,aut.st. | 0.0018937 |
Kladno,Pražská křiž. | 0.0017584 |
Chrášťany | 0.0017584 |
Karlovo náměstí | 0.0017584 |
Jesenice | 0.0016232 |
Kobylisy | 0.0016232 |
Turnov,Terminál u žel.st. | 0.0016232 |
📝 Popis¶
Nejvyšší stupňovou centralitu mají zejména velká autobusová nádraží. Spoje těchto vrcholů směřují mimo Prahu, což je pro proxy autobusové terminály typické.
🏠 Sousedství¶
V následujících mapách vykresluji jejich bezprostřední sousedy. Až na pár vyjímek, jsou sousedé těchto vrcholů velmi vzdálení.
fig, axes = plt.subplots(3, 3, figsize=(12, 8), layout="tight")
fig.suptitle("Hubs and their neighborhoods", fontsize=16)
scales = [0.06, 0.1, 0.2, 0.2, 0.03, 0.2, 0.005, 0.2, 0.01]
for node, ax, scale in zip(top10.index[:9], axes.reshape(-1), scales):
neighbours = set([node])
for u in nx.all_neighbors(G, node):
neighbours.add(u)
ax.set_title(node)
nx.draw_networkx(G.subgraph(neighbours), pos=stop_pos, ax=ax, node_size=30, node_color=grey,
with_labels=False, alpha=0.8, edge_color=grey, width=1.6)
nx.draw_networkx(G.subgraph([node]), pos=stop_pos, ax=ax, node_size=100, node_color=redf, alpha=0.95, with_labels=False)
add_map(ax, ctx.providers.CartoDB.Positron, scale, True)
📐 Centralita blízkosti¶
centrality = pd.Series(nx.closeness_centrality(G)).sort_values(ascending=False)
lbc = [50.03774851490303, 14.331014307052019]
urc = [50.13010922185579, 14.500335441536306]
nodes = filter(lbc, urc)
f = centrality[list(nodes)].sort_values(ascending=False)
f = f.drop('Nad Pahorkem')
f = f.drop('Choceradská')
top10 = f[1:11]
print("Top 10 nodes with the highest closeness centrality")
display_top(top10)
neighbours = set()
for n in top10.index:
for u in nx.all_neighbors(G, n):
neighbours.add(u)
for _ in range(10):
nn = neighbours.copy()
for n in nn:
for u in nx.all_neighbors(G, n):
neighbours.add(u)
cumulative = list(neighbours)
cumulative.extend(list(top10.index))
len(cumulative)
fig, ax = plt.subplots(1, 1, figsize=(12, 6), layout="constrained")
fig.suptitle("10 nodes with the highest closeness centrality", fontsize=15)
ax.set_xlim(lbc[1], urc[1])
ax.set_ylim(lbc[0], urc[0])
nx.draw_networkx(G.subgraph(cumulative), pos=stop_pos, ax=ax, node_size=10, node_color=blue, with_labels=False, alpha=0.2, edge_color=blue, width=1)
nx.draw_networkx(G.subgraph(top10.index), pos=stop_pos, ax=ax, node_size=150, node_color=redl, font_size=10, width=0)
add_map(ax, ctx.providers.CartoDB.Positron, 0.01)
Top 10 nodes with the highest closeness centrality
Coeff | |
---|---|
Kačerov | 0.0741755 |
Na Veselí | 0.0740663 |
Lihovar | 0.0734723 |
Smíchovské nádraží | 0.0727514 |
Hlavní nádraží | 0.0721860 |
I. P. Pavlova | 0.0721479 |
Budějovická | 0.0715759 |
Pankrác | 0.0709528 |
Radlická | 0.0704498 |
Benkova | 0.0700958 |
🗨️ Interpretace¶
Označené zastávky jsou důležitými body dopravní sítě PID. Mají nejnižší průměrnou vzdálenost od všech ostatních zastávek.
🖇️ Centralita mezilehlosti¶
centrality_bet = pd.Series(nx.betweenness_centrality(G, weight="weight", k=200)).sort_values(ascending=False)
lbc = [50.03774851490303, 14.331014307052019]
urc = [50.13010922185579, 14.500335441536306]
nodes = filter(lbc, urc)
top10 = centrality_bet[list(nodes)].sort_values(ascending=False)[:10]
print("Top 10 nodes with highest betweeness centrality")
display_top(top10)
neighbours = set()
for n in top10.index:
for u in nx.all_neighbors(G, n):
neighbours.add(u)
for _ in range(10):
nn = neighbours.copy()
for n in nn:
for u in nx.all_neighbors(G, n):
neighbours.add(u)
cumulative = list(neighbours)
cumulative.extend(list(top10.index))
len(cumulative)
fig, ax = plt.subplots(1, 1, figsize=(12, 6), layout="constrained")
fig.suptitle("10 nodes with the highest betweeness centrality", fontsize=15)
ax.set_xlim(lbc[1], urc[1])
ax.set_ylim(lbc[0], urc[0])
nx.draw_networkx(G.subgraph(cumulative), pos=stop_pos, ax=ax, node_size=10, node_color=blue, with_labels=False, alpha=0.2, edge_color=blue, width=1)
nx.draw_networkx(G.subgraph(top10.index), pos=stop_pos, ax=ax, node_size=150, node_color=redl, font_size=10, width=0)
add_map(ax, ctx.providers.CartoDB.Positron, 0.01)
Top 10 nodes with highest betweeness centrality
Coeff | |
---|---|
Na Veselí | 0.0889256 |
I. P. Pavlova | 0.0714338 |
Botanická zahrada Troja | 0.0397029 |
Hlavní nádraží | 0.0388959 |
Pankrác | 0.0383901 |
Střížkov | 0.0383537 |
Poliklinika Budějovická | 0.0358492 |
Zoologická zahrada | 0.0336205 |
Parkoviště Trojský most | 0.0325963 |
Prosek | 0.0313351 |
🗨️ Interpretace¶
Označené zastávky jsou frekventovaně na nejkratších cestách mezi jinými dvěma vrcholy.
❓ Insights¶
Jak se mění frekvence spojů v závislosti na pracovním dni/víkendu a typu dopravy?¶
fig, axes = plt.subplots(6, figsize=(9, 10), sharex=True)
fig.suptitle(f"Activity by Route Types", y=0.92, fontsize=16)
fig.subplots_adjust(hspace=0.08)
fig.text(0.5, 0.04, "Time [hour]", ha="center", size=12)
fig.text(0.04, 0.5, "Count", va="center", rotation="vertical", size=12)
for route_type, ax in zip(types.index, fig.axes):
d = conns[(conns["route_type"] == route_type) & (conns["monday"] | conns["tuesday"] | conns["wednesday"] | conns["thursday"] | conns["friday"])]
series = d["depart_from_H"] * 3600 + d["depart_from_M"] * 60 + d["depart_from_S"]
series = series / 3600
n, _, _ = ax.hist(series, bins=48, alpha=0.8)
ax.set_title(f"{route_type}", y=1.0, pad=-14, loc="left", color=grey)
ax.set_yticks([0, max(n) * 2 / 4, max(n)])
ax.set_ylim(0, max(n) * 1.4)
ax.tick_params(axis="y", which="both", length=0)
ax.grid(which="major", axis="x", color="grey", linestyle="dotted", linewidth=0.4)
d = conns[(conns["route_type"] == route_type) & (conns["saturday"] | conns["saturday"])]
series = d["depart_from_H"] * 3600 + d["depart_from_M"] * 60 + d["depart_from_S"]
series = series / 3600
n, _, _ = ax.hist(series, bins=48, color="k", histtype="step", linewidth=1, alpha=0.8)
handles, labels = plt.gca().get_legend_handles_labels()
patch = mpatches.Patch(color=red, label="Weekday", alpha=0.8)
line = Line2D([0], [0], label="Weekend", color="k", linewidth=1)
handles.extend([patch, line])
axes[0].legend(handles=handles, bbox_to_anchor=(1.04, 1), loc="upper left", prop={'size': 12})
ax.set_xticks(range(0, 25, 4))
ax.set_xlim(-1, 25);
🗨️ Interpretace¶
- Přes víkend je doprava výrazně méně aktivní. Výjimkou jsou přívozy a lanová dráha, které fungují o víkendech stejně nebo podobně. Kromě dopravních špiček jezdí stejně i metra a vlaky.
- O pracovních dnech je víc spojů v ranních a večerních hodinách, kdy lidé jezdí do práce, resp. z ní odchází. Takové přizpůsobění o víkendu nepozorujeme.
- Četnost spojů je obecně v hodinách od 23 do 4 velmi malá. U přívozů a lanové dráhy je nulová.
Mění se důležité zastávky v závislosti na denním/nočním provozu?¶
V této otázce důležitost zastávek zkoumám z hlediska stupňové centrality.
G_weekday = build_graph(conns[conns['monday'] | conns['tuesday'] | conns['wednesday'] | conns['thursday'] | conns['friday']])
filter_d_weekday = subgroup_distance_filter(G_weekday, stops)
G_weekend = build_graph(conns[conns['saturday'] | conns['sunday']])
filter_d_weekend = subgroup_distance_filter(G_weekend, stops)
G_day = build_graph(conns[~conns['is_night']])
filter_d_day = subgroup_distance_filter(G_day, stops)
G_night = build_graph(conns[conns['is_night']])
filter_d_night = subgroup_distance_filter(G_night, stops)
loc = ['', [50.04909793668076, 14.379518399273855], [50.112147437060834, 14.481384833754964], 0.4]
fig, axes = plt.subplots(2, 1, figsize=(10, 12), layout="constrained")
fig.suptitle("Degree centrality - Central Prague", fontsize=20)
g = filter_d_day((np.array(loc[1]) + np.array(loc[2])) / 2, loc[3])
plot_centrality = make_plotter(g, loc[1], loc[2])
plot_centrality(axes[0], nx.degree_centrality, label_count=18)
axes[0].set_title("Daytime")
g = filter_d_night((np.array(loc[1]) + np.array(loc[2])) / 2, loc[3])
plot_centrality = make_plotter(g, loc[1], loc[2])
plot_centrality(axes[1], nx.degree_centrality, label_count=5)
axes[1].set_title("Nighttime");
🗨️ Interpretace¶
Ano, důležitost zastávek se mění v závislosti na denním/nočním provozu. Např. Lihovar, který je z hlediska stupňové centrality během dne nejdůležitější zastávkou, je během nočního provozu nevýznamnný. U stanice Anděl je tomu třeba přesně naopak.
Které mimopražské zastávky jsou nejvytíženěší?¶
Jako v předchozí otázce důležitost opět zkoumám z hlediska stupňové centrality.
mid = [50.089027446744275, 14.420374242931851]
g = filter_d_inv(mid, 0.2)
H = g.copy()
H.remove_node('Kladno,Pražská křiž.')
H.remove_node('Mladá Boleslav,Jičínská')
g = H
deg_cent = nx.degree_centrality(g)
degree_centrality = pd.Series(deg_cent).sort_values(ascending=False)
n = 6
top = degree_centrality[:n]
print(f"Top {n} nodes with the highest degree centrality")
display_top(top)
fig, ax = plt.subplots(1, 1, figsize=(14, 10), layout="constrained")
fig.suptitle(f"{n} nodes with the highest degree centrality (outside of Prague)", fontsize=16)
colors = degree_centrality / np.max(degree_centrality)
cent = np.fromiter(deg_cent.values(), float)
normalize = mcolors.Normalize(vmin=cent.min(), vmax=cent.max())
scalarmappaple = cm.ScalarMappable(norm=normalize, cmap=colormap)
scalarmappaple.set_array(cent)
fig.colorbar(scalarmappaple, ax=ax)
colors = cent / np.max(cent) * 200
nx.draw(g, stop_pos, ax=ax, node_size=colors*0.4, node_color=colors, cmap=colormap, width=0, alpha=0.4)
indices = []
for n, v in top.items():
indices.append(pd.Index(g.nodes).get_loc(n))
nx.draw_networkx_nodes(g.subgraph(top.index), stop_pos, ax=ax, node_size=colors[indices]*0.3, node_color=redf, alpha=0.4)
nx.draw_networkx_labels(g.subgraph(top.index), stop_pos, ax=ax, verticalalignment='bottom', font_size=11)
add_map(ax, ctx.providers.CartoDB.Positron, 0.5)
Top 6 nodes with the highest degree centrality
Coeff | |
---|---|
Mladá Boleslav,aut.st. | 0.0021771 |
Kladno,autobusové nádraží | 0.0019956 |
Mšeno | 0.0019956 |
Turnov,Terminál u žel.st. | 0.0019956 |
Říčany,K Žel.st. | 0.0019956 |
Mutějovice | 0.0018142 |
🗨️ Interpretace¶
Podobně jako u stupňové centrality na celém grafu jsou nejdůležitější zastávky velké autobusové stanice.
Které zastávky jsou v ranních hodinách pracovního dne nevýznamnější? (6:00 - 11:00)¶
conns_morning = (conns['depart_from_H'] >= 6) & (conns['depart_from_H'] <= 11)
conns_weekday = (conns['monday'] | conns['tuesday'] | conns['wednesday'] | conns['thursday'] | conns['friday'])
G_morning_weekday = build_graph(conns[conns_morning & conns_weekday])
filter_d_mwd = subgroup_distance_filter(G_morning_weekday, stops)
g = filter_d_mwd(mid, 0.02)
deg_cent = nx.eigenvector_centrality(g, weight="weight", max_iter=1000)
degree_centrality = pd.Series(deg_cent).sort_values(ascending=False)
n = 15
top = degree_centrality[:n]
print(f"Top {n} nodes with the highest eigenvector centrality")
display_top(top)
fig, ax = plt.subplots(1, 1, figsize=(12, 10), layout="constrained")
fig.suptitle(f"{n} nodes with the highest eigenvector centrality on weekday morning", fontsize=16)
colors = degree_centrality / np.max(degree_centrality)
cent = np.fromiter(deg_cent.values(), float)
# normalize = mcolors.Normalize(vmin=cent.min(), vmax=cent.max())
# scalarmappaple = cm.ScalarMappable(norm=normalize, cmap=colormap)
# scalarmappaple.set_array(cent)
# fig.colorbar(scalarmappaple, ax=ax)
colors = cent / np.max(cent) * 200
nx.draw(g, stop_pos, ax=ax, node_size=colors*0.4, node_color=colors, cmap=colormap, width=0, alpha=0.6)
indices = []
for n, v in top.items():
indices.append(pd.Index(g.nodes).get_loc(n))
nx.draw_networkx_nodes(g.subgraph(top.index), stop_pos, ax=ax, node_size=colors[indices]*0.3, node_color=redf, alpha=0.8)
nx.draw_networkx_labels(g.subgraph(top.index), stop_pos, ax=ax, verticalalignment='bottom', font_size=11)
add_map(ax, ctx.providers.CartoDB.Positron, 0.01, False)
Top 15 nodes with the highest eigenvector centrality
Coeff | |
---|---|
Karlovo náměstí | 0.4855651 |
Národní třída | 0.4425812 |
Štěpánská | 0.3632039 |
Národní divadlo | 0.3594138 |
Novoměstská radnice | 0.2402880 |
Lazarská | 0.2090533 |
Újezd | 0.2071608 |
I. P. Pavlova | 0.2038303 |
Moráň | 0.1490697 |
Vodičkova | 0.1294884 |
Hellichova | 0.1234722 |
Palackého náměstí | 0.1125779 |
Malostranské náměstí | 0.0819371 |
Václavské náměstí | 0.0787817 |
Staroměstská | 0.0783107 |
🗨️ Interpretace¶
Vyznačené zastávky jsou v pracovních dnech mezi 6:00 a 11:00 nejvýznamnější -- v síti PID spojují vlivné vrcholy a zároveň tvoří zásadní část důležitých spojů.
Open Data Attribution¶
Copyright a license volně dostupných geografických dat z vizualizací:
- OpenStreetMap® - licensed under the Open Data Commons Open Database License by the OpenStreetMap Foundation
- ArcGIS Online - licensed under the Esri Master License Agreement