Skip to content

Commit

Permalink
- Updated dependencies.
Browse files Browse the repository at this point in the history
- Upgraded Python to 3.9+.
- Tweaked some validators.
- Updated README.
  • Loading branch information
araichev committed Jul 10, 2024
1 parent c1dc96b commit 0adf4a0
Show file tree
Hide file tree
Showing 13 changed files with 2,781 additions and 2,459 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
repos:
- repo: https://github.com/kynan/nbstripout
rev: 0.6.1
rev: 0.7.1
hooks:
- id: nbstripout
files: ".ipynb"
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.259'
rev: 'v0.5.1'
hooks:
- id: ruff
23 changes: 15 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ Make GTFS
***********
.. image:: https://github.com/mrcagney/gtfs_kit/actions/workflows/test.yml/badge.svg

A Python 3.8+ library to build GTFS feeds from basic route information.
A Python 3.9+ library to build GTFS feeds from basic route information.
Inspired by Conveyal's `geom2gtfs <https://github.com/conveyal/geom2gtfs>`_.
Makes naive timetables, but they are often good enough for preliminary work.

Contributors
============
- Alex Raichev (maintainer), 2014-09


Installation
=============
Create a Python 3.8+ virtual environment and run ``poetry add make_gtfs``.
To use as a library in your own project (called something other than ``make_gtfs``), make a Python 3.9+ virtual environment for your project, then run ``poetry add make_gtfs``.

To develop the ``make_gtfs`` repo, Git clone it, make a Python 3.9+ virtual environment, then run ``poetry install --no-root && pre-commit install``.


Usage
Expand Down Expand Up @@ -129,12 +135,6 @@ Documentation
On Github pages `here <https://mrcagney.github.io/make_gtfs_docs>`_.


Contributors
============
- Alex Raichev (maintainer), 2014-09
- Danielle Gatland (reviewer), 2021-10


Notes
======
- This project's development status is Alpha.
Expand All @@ -146,6 +146,13 @@ Notes
Changes
========

4.0.7, 2024-07-10
-----------------
- Updated dependencies.
- Upgraded Python to 3.9+.
- Tweaked some validators.
- Updated README.

4.0.6, 2023-03-29
-----------------
- Updated dependencies and pre-commit hooks.
Expand Down
2 changes: 1 addition & 1 deletion make_gtfs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
from .hashables import *
from .main import *

__version__ = "4.0.6"
__version__ = "4.0.7"
18 changes: 11 additions & 7 deletions make_gtfs/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""
This module contains the main logic.
"""
from typing import Optional

from __future__ import annotations

from functools import lru_cache
import math

Expand Down Expand Up @@ -161,7 +163,7 @@ def make_stop_points(
offset: float,
side: str,
n: int = 2,
spacing: Optional[float] = None,
spacing: float | None = None,
) -> gpd.GeoDataFrame:
"""
Given a GeoDataFrame of lines with at least the columns
Expand Down Expand Up @@ -252,10 +254,10 @@ def get_dists(L, δ):

def build_stops(
pfeed: pf.ProtoFeed,
shapes: Optional[pd.DataFrame] = None,
shapes: pd.DataFrame | None = None,
offset: float = cs.STOP_OFFSET,
n: int = 2,
spacing: Optional[float] = None,
spacing: float | None = None,
) -> pd.DataFrame:
"""
Given a ProtoFeed, return a DataFrame representing ``stops.txt``.
Expand Down Expand Up @@ -477,7 +479,9 @@ def compute_dists(group):
shapes_g = (
gk.geometrize_shapes_0(shapes)
.to_crs(utm_crs)
.assign(boundary_points=lambda x: x.intersection(speed_zones.boundary))
.assign(
boundary_points=lambda x: x.intersection(speed_zones.boundary, align=True)
)
)

# Assign distances to those boundary points
Expand Down Expand Up @@ -746,7 +750,7 @@ def build_stop_times(
# Convert seconds back to time strings
f[["arrival_time", "departure_time"]] = f[
["arrival_time", "departure_time"]
].applymap(lambda x: gk.timestr_to_seconds(x, inverse=True))
].map(lambda x: gk.timestr_to_seconds(x, inverse=True))

# Free memory
_build_stop_times_for_trip.cache_clear()
Expand All @@ -759,7 +763,7 @@ def build_feed(
buffer: float = cs.BUFFER,
stop_offset: float = cs.STOP_OFFSET,
num_stops_per_shape: int = 2,
stop_spacing: Optional[float] = None,
stop_spacing: float | None = None,
) -> gk.Feed:
"""
Convert the given ProtoFeed to a GTFS Feed with meter distance units.
Expand Down
39 changes: 27 additions & 12 deletions make_gtfs/protofeed.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import Optional

import pathlib as pl
from dataclasses import dataclass

Expand Down Expand Up @@ -39,8 +39,8 @@ class ProtoFeed:
service_windows: pd.DataFrame
shapes: gpd.GeoDataFrame
frequencies: pd.DataFrame
stops: Optional[pd.DataFrame] = None
speed_zones: Optional[gpd.GeoDataFrame] = None
stops: pd.DataFrame | None = None
speed_zones: gpd.GeoDataFrame | None = None

@staticmethod
def clean_speed_zones(
Expand All @@ -55,8 +55,10 @@ def clean_speed_zones(
``default_speed_zone_id`` and the speed there will be set to ``default_speed``.
Return the resulting service area of (Multi)Polygons, now partitioned into speed
zones.
The result is a GeoDataFrame with the columns 'speed_zone_id', 'speed',
'geometry'.
"""
if service_area.geom_equals(speed_zones.unary_union).all():
if service_area.geom_equals(speed_zones.union_all()).all():
# Speed zones already partition the study area, so good
result = speed_zones
else:
Expand All @@ -68,12 +70,13 @@ def clean_speed_zones(
# Union chunks
.overlay(service_area, how="union")
.assign(
speed_zone_id=lambda x: x.speed_zone_id.fillna(
route_type=lambda x: x["route_type"].ffill().astype(int),
speed_zone_id=lambda x: x["speed_zone_id"].fillna(
default_speed_zone_id
),
speed=lambda x: x.speed.fillna(default_speed),
speed=lambda x: x["speed"].fillna(default_speed),
)
.filter(["speed_zone_id", "speed", "geometry"])
.filter(["route_type", "speed_zone_id", "speed", "geometry"])
.sort_values("speed_zone_id", ignore_index=True)
)
return result
Expand All @@ -91,7 +94,7 @@ def __post_init__(self):
# <shape ID> -> <trip directions using the shape (0, 1, or 2)>
def my_agg(group):
d = {}
dirs = group.direction.unique()
dirs = group["direction"].unique()
if len(dirs) > 1 or 2 in dirs:
d["direction"] = 2
else:
Expand All @@ -114,7 +117,7 @@ def my_agg(group):
# it infinite speed so it won't override route speeds present in
# ``self.frequencies``.
frames = []
for route_type in self.frequencies.route_type.unique():
for route_type in self.frequencies["route_type"].unique():
g = service_area.assign(
route_type=route_type,
speed_zone_id=f"default{cs.SEP}{route_type}",
Expand All @@ -132,15 +135,25 @@ def my_apply(group):
)

self.speed_zones = (
self.speed_zones.groupby("route_type")
self.speed_zones.groupby("route_type", group_keys=False)
.apply(my_apply)
.reset_index()
.filter(["route_type", "speed_zone_id", "speed", "geometry"])
)

lon, lat = self.shapes.geometry.iat[0].coords[0]
self.utm_crs = gk.get_utm_crs(lat, lon)

def __eq__(self, other) -> bool:
for k in self.__dataclass_fields__:
v1 = getattr(self, k)
v2 = getattr(other, k)
if isinstance(v1, (pd.DataFrame, gpd.GeoDataFrame)):
if not v1.equals(v2):
return False
elif not isinstance(v2, type(v1)) or v1 != v2:
return False
return True

def copy(self) -> ProtoFeed:
"""
Return a copy of this ProtoFeed, that is, a feed with all the
Expand Down Expand Up @@ -293,8 +306,10 @@ def read_protofeed(path: str | pl.Path) -> ProtoFeed:
d["speed_zones"] = None
if (path / "speed_zones.geojson").exists():
g = gpd.read_file(path / "speed_zones.geojson")
if "route_type" in g.columns:
g["route_type"] = g["route_type"].astype(int)
if "speed_zone_id" in g.columns:
g["speed_zone_id"] = g.speed_zone_id.astype(str)
g["speed_zone_id"] = g["speed_zone_id"].astype(str)
d["speed_zones"] = g

pfeed = ProtoFeed(**d)
Expand Down
18 changes: 13 additions & 5 deletions make_gtfs/validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
ProtoFeed validators.
"""

import re
import pytz

Expand Down Expand Up @@ -163,29 +164,33 @@
def check_meta(pfeed: pf.ProtoFeed) -> pd.DataFrame:
"""
Return `pfeed.meta` if it is valid.
Otherwise, raise a Pandera SchemaError.
Otherwise, raise a ValueError or a Pandera SchemaError.
"""
if not isinstance(pfeed.meta, pd.DataFrame):
raise ValueError("Meta must be a DataFrame")

return SCHEMA_META.validate(pfeed.meta)


def check_shapes(pfeed: pf.ProtoFeed) -> pd.DataFrame:
"""
Return `pfeed.shapes` if it is valid.
Otherwise, raise a Pandera SchemaError.
Otherwise, raise a ValueError or a Pandera SchemaError.
"""
result = SCHEMA_SHAPES.validate(pfeed.shapes)

if not isinstance(pfeed.shapes, gpd.GeoDataFrame):
raise ValueError("Shapes must be a GeoDataFrame")

return result
return SCHEMA_SHAPES.validate(pfeed.shapes)


def check_service_windows(pfeed: pf.ProtoFeed) -> pd.DataFrame:
"""
Return `pfeed.service_windows` if it is valid.
Otherwise, raise a Pandera SchemaError.
"""
if not isinstance(pfeed.service_windows, pd.DataFrame):
raise ValueError("Service windows must be a DataFrame")

return SCHEMA_SERVICE_WINDOWS.validate(pfeed.service_windows)


Expand All @@ -194,6 +199,9 @@ def check_frequencies(pfeed: pf.ProtoFeed) -> pd.DataFrame:
Return `pfeed.frequencies` if it is valid.
Otherwise, raise a Pandera SchemaError.
"""
if not isinstance(pfeed.frequencies, pd.DataFrame):
raise ValueError("Frequencies must be a DataFrame")

return SCHEMA_FREQUENCIES.validate(pfeed.frequencies)


Expand Down
6 changes: 3 additions & 3 deletions notebooks/examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"source": [
"# Map some trips\n",
"tids = [feed.trips.trip_id.iat[0], feed.trips.trip_id.iat[-1]]\n",
"feed.map_trips(tids, include_arrows=True, include_stops=True)\n",
"feed.map_trips(tids, show_direction=True, show_stops=True)\n",
" "
]
},
Expand Down Expand Up @@ -120,9 +120,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.8"
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 1
"nbformat_minor": 4
}
6 changes: 3 additions & 3 deletions notebooks/prototype_speed_zones.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"import gtfs_kit as gk\n",
"import shapely\n",
"import shapely.geometry as sg\n",
"import geo_kit as geo\n",
"import folium as fl\n",
"import geo_kit as geo # Only works for MRCagney staff\n",
"\n",
"sys.path.append('../')\n",
"\n",
Expand Down Expand Up @@ -232,9 +232,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.4"
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 1
"nbformat_minor": 4
}
Loading

0 comments on commit 0adf4a0

Please sign in to comment.