from __future__ import division
from builtins import str
from builtins import object
__copyright__ = "Copyright 2015 Contributing Entities"
__license__ = """
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
import numpy as np
import pandas as pd
from .Error import NetworkInputError, NotImplementedError, UnexpectedError
from .Logger import FastTripsLogger
from .Util import Util
[docs]class Route(object):
"""
Route class.
One instance represents all of the Routes.
Stores route information in :py:attr:`Route.routes_df` and agency information in
:py:attr:`Route.agencies_df`. Each are instances of :py:class:`pandas.DataFrame`.
Fare information is in :py:attr:`Route.fare_attrs_df`, :py:attr:`Route.fare_rules_df` and
:py:attr:`Route.fare_transfer_rules_df`.
"""
#: File with fasttrips routes information (this extends the
#: `gtfs routes <https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/routes.md>`_ file).
#: See `routes_ft specification <https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/routes_ft.md>`_.
INPUT_ROUTES_FILE = "routes_ft.txt"
#: gtfs Routes column name: Unique identifier
ROUTES_COLUMN_ROUTE_ID = "route_id"
#: gtfs Routes column name: Short name
ROUTES_COLUMN_ROUTE_SHORT_NAME = "route_short_name"
#: gtfs Routes column name: Long name
ROUTES_COLUMN_ROUTE_LONG_NAME = "route_long_name"
#: gtfs Routes column name: Route type
ROUTES_COLUMN_ROUTE_TYPE = "route_type"
#: gtfs Routes column name: Agency ID
ROUTES_COLUMN_AGENCY_ID = "agency_id"
#: fasttrips Routes column name: Mode
ROUTES_COLUMN_MODE = "mode"
#: fasttrips Routes column name: Proof of Payment
ROUTES_COLUMN_PROOF_OF_PAYMENT = "proof_of_payment"
# ========== Added by fasttrips =======================================================
#: fasttrips Routes column name: Mode number
ROUTES_COLUMN_ROUTE_ID_NUM = "route_id_num"
#: fasttrips Routes column name: Mode number
ROUTES_COLUMN_MODE_NUM = "mode_num"
#: fasttrips Routes column name: Mode type
ROUTES_COLUMN_MODE_TYPE = "mode_type"
#: Value for :py:attr:`Route.ROUTES_COLUMN_MODE_TYPE` column: access
MODE_TYPE_ACCESS = "access"
#: Value for :py:attr:`Route.ROUTES_COLUMN_MODE_TYPE` column: egress
MODE_TYPE_EGRESS = "egress"
#: Value for :py:attr:`Route.ROUTES_COLUMN_MODE_TYPE` column: transit
MODE_TYPE_TRANSIT = "transit"
#: Value for :py:attr:`Route.ROUTES_COLUMN_MODE_TYPE` column: transfer
MODE_TYPE_TRANSFER = "transfer"
#: Access mode numbers start from here
MODE_NUM_START_ACCESS = 101
#: Egress mode numbers start from here
MODE_NUM_START_EGRESS = 201
#: Route mode numbers start from here
MODE_NUM_START_ROUTE = 301
#: File with fasttrips fare attributes information (this *subsitutes rather than extends* the
#: `gtfs fare_attributes <https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_attributes_ft.md>`_ file).
#: See `fare_attributes_ft specification <https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_attributes_ft.md>`_.
INPUT_FARE_ATTRIBUTES_FILE = "fare_attributes_ft.txt"
# fasttrips Fare attributes column name: Fare Period
FARE_ATTR_COLUMN_FARE_PERIOD = "fare_period"
# fasttrips Fare attributes column name: Price
FARE_ATTR_COLUMN_PRICE = "price"
# fasttrips Fare attributes column name: Currency Type
FARE_ATTR_COLUMN_CURRENCY_TYPE = "currency_type"
# fasttrips Fare attributes column name: Payment Method
FARE_ATTR_COLUMN_PAYMENT_METHOD = "payment_method"
# fasttrips Fare attributes column name: Transfers (number permitted on this fare)
FARE_ATTR_COLUMN_TRANSFERS = "transfers"
# fasttrips Fare attributes column name: Transfer duration (Integer length of time in seconds before transfer expires. Omit or leave empty if they do not.)
FARE_ATTR_COLUMN_TRANSFER_DURATION = "transfer_duration"
#: File with fasttrips fare periods information
#: See `fare_rules_ft specification <https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_rules_ft.md>`_.
INPUT_FARE_PERIODS_FILE = "fare_periods_ft.txt"
#: fasttrips Fare rules column name: Fare ID
FARE_RULES_COLUMN_FARE_ID = "fare_id"
#: GTFS fare rules column name: Route ID
FARE_RULES_COLUMN_ROUTE_ID = ROUTES_COLUMN_ROUTE_ID
#: GTFS fare rules column name: Origin Zone ID
FARE_RULES_COLUMN_ORIGIN_ID = "origin_id"
#: GTFS fare rules column name: Destination Zone ID
FARE_RULES_COLUMN_DESTINATION_ID = "destination_id"
#: GTFS fare rules column name: Contains ID
FARE_RULES_COLUMN_CONTAINS_ID = "contains_id"
#: fasttrips Fare rules column name: Fare class
FARE_RULES_COLUMN_FARE_PERIOD = FARE_ATTR_COLUMN_FARE_PERIOD
#: fasttrips Fare rules column name: Start time for the fare. A DateTime
FARE_RULES_COLUMN_START_TIME = "start_time"
#: fasttrips Fare rules column name: End time for the fare rule. A DateTime.
FARE_RULES_COLUMN_END_TIME = "end_time"
# ========== Added by fasttrips =======================================================
#: fasttrips Fare rules column name: Fare ID num
FARE_RULES_COLUMN_FARE_ID_NUM = "fare_id_num"
#: fasttrips Fare rules column name: Route ID num
FARE_RULES_COLUMN_ROUTE_ID_NUM = ROUTES_COLUMN_ROUTE_ID_NUM
#: fasttrips fare rules column name: Origin Zone ID number
FARE_RULES_COLUMN_ORIGIN_ID_NUM = "origin_id_num"
#: fasttrips fare rules column name: Destination ID number
FARE_RULES_COLUMN_DESTINATION_ID_NUM = "destination_id_num"
#: File with fasttrips fare transfer rules information.
#: See `fare_transfer_rules specification <https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_transfer_rules_ft.md>`_.
INPUT_FARE_TRANSFER_RULES_FILE = "fare_transfer_rules_ft.txt"
#: fasttrips Fare transfer rules column name: From Fare Class
FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD = "from_fare_period"
#: fasttrips Fare transfer rules column name: To Fare Class
FARE_TRANSFER_RULES_COLUMN_TO_FARE_PERIOD = "to_fare_period"
#: fasttrips Fare transfer rules column name: Transfer type?
FARE_TRANSFER_RULES_COLUMN_TYPE = "transfer_fare_type"
#: fasttrips Fare transfer rules column name: Transfer amount (discount or fare)
FARE_TRANSFER_RULES_COLUMN_AMOUNT = "transfer_fare"
#: Value for :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_TYPE`: transfer discount
TRANSFER_TYPE_TRANSFER_DISCOUNT = "transfer_discount"
#: Value for :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_TYPE`: free transfer
TRANSFER_TYPE_TRANSFER_FREE = "transfer_free"
#: Value for :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_TYPE`: transfer fare cost
TRANSFER_TYPE_TRANSFER_COST = "transfer_cost"
#: Valid options for :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_TYPE`
TRANSFER_TYPE_OPTIONS = [TRANSFER_TYPE_TRANSFER_DISCOUNT,
TRANSFER_TYPE_TRANSFER_FREE,
TRANSFER_TYPE_TRANSFER_COST]
#: File with route ID, route ID number correspondence (and fare id num)
OUTPUT_ROUTE_ID_NUM_FILE = "ft_intermediate_route_id.txt"
#: File with fare id num, fare id, fare class, price, xfers
OUTPUT_FARE_ID_FILE = "ft_intermediate_fare.txt"
#: File with fare transfer rules
OUTPUT_FARE_TRANSFER_FILE = "ft_intermediate_fare_transfers.txt"
#: File with mode, mode number correspondence
OUTPUT_MODE_NUM_FILE = "ft_intermediate_supply_mode_id.txt"
[docs] def __init__(self, input_archive, output_dir, gtfs, today, stops):
"""
Constructor. Reads the gtfs data from the transitfeed schedule, and the additional
fast-trips routes data from the input file in *input_archive*.
"""
self.output_dir = output_dir
self.routes_df = gtfs.routes
FastTripsLogger.info("Read %7d %15s from %25d %25s" %
(len(self.routes_df), 'date valid route', len(gtfs.routes), 'total routes'))
# Read the fast-trips supplemental routes data file
routes_ft_df = gtfs.get(Route.INPUT_ROUTES_FILE)
# verify required columns are present
routes_ft_cols = list(routes_ft_df.columns.values)
assert(Route.ROUTES_COLUMN_ROUTE_ID in routes_ft_cols)
assert(Route.ROUTES_COLUMN_MODE in routes_ft_cols)
# verify no routes_ids are duplicated
if routes_ft_df.duplicated(subset=[Route.ROUTES_COLUMN_ROUTE_ID]).sum()>0:
error_msg = "Found %d duplicate %s in %s" % (routes_ft_df.duplicated(subset=[Route.ROUTES_COLUMN_ROUTE_ID]).sum(),
Route.ROUTES_COLUMN_ROUTE_ID, Route.INPUT_ROUTES_FILE)
FastTripsLogger.fatal(error_msg)
FastTripsLogger.fatal("\nDuplicates:\n%s" % \
str(routes_ft_df.loc[routes_ft_df.duplicated(subset=[Route.ROUTES_COLUMN_ROUTE_ID])]))
raise NetworkInputError(Route.INPUT_ROUTES_FILE, error_msg)
# Join to the routes dataframe
self.routes_df = pd.merge(left=self.routes_df, right=routes_ft_df,
how='left',
on=Route.ROUTES_COLUMN_ROUTE_ID)
# Get the mode list
self.modes_df = self.routes_df[[Route.ROUTES_COLUMN_MODE]].drop_duplicates().reset_index(drop=True)
self.modes_df[Route.ROUTES_COLUMN_MODE_NUM] = self.modes_df.index + Route.MODE_NUM_START_ROUTE
self.modes_df[Route.ROUTES_COLUMN_MODE_TYPE] = Route.MODE_TYPE_TRANSIT
# Join to mode numbering
self.routes_df = Util.add_new_id(self.routes_df, Route.ROUTES_COLUMN_MODE, Route.ROUTES_COLUMN_MODE_NUM,
self.modes_df, Route.ROUTES_COLUMN_MODE, Route.ROUTES_COLUMN_MODE_NUM)
# Route IDs are strings. Create a unique numeric route ID.
self.route_id_df = Util.add_numeric_column(self.routes_df[[Route.ROUTES_COLUMN_ROUTE_ID]],
id_colname=Route.ROUTES_COLUMN_ROUTE_ID,
numeric_newcolname=Route.ROUTES_COLUMN_ROUTE_ID_NUM)
FastTripsLogger.debug("Route ID to number correspondence\n" + str(self.route_id_df.head()))
FastTripsLogger.debug(str(self.route_id_df.dtypes))
self.routes_df = self.add_numeric_route_id(self.routes_df,
id_colname=Route.ROUTES_COLUMN_ROUTE_ID,
numeric_newcolname=Route.ROUTES_COLUMN_ROUTE_ID_NUM)
FastTripsLogger.debug("=========== ROUTES ===========\n" + str(self.routes_df.head()))
FastTripsLogger.debug("\n"+str(self.routes_df.dtypes))
FastTripsLogger.info("Read %7d %15s from %25s, %25s" %
(len(self.routes_df), "routes", "routes.txt", Route.INPUT_ROUTES_FILE))
self.agencies_df = gtfs.agency
FastTripsLogger.debug("=========== AGENCIES ===========\n" + str(self.agencies_df.head()))
FastTripsLogger.debug("\n"+str(self.agencies_df.dtypes))
FastTripsLogger.info("Read %7d %15s from %25s" %
(len(self.agencies_df), "agencies", "agency.txt"))
self.fare_attrs_df = gtfs.fare_attributes
FastTripsLogger.debug("=========== FARE ATTRIBUTES ===========\n" + str(self.fare_attrs_df.head()))
FastTripsLogger.debug("\n"+str(self.fare_attrs_df.dtypes))
FastTripsLogger.info("Read %7d %15s from %25s" %
(len(self.fare_attrs_df), "fare attributes", "fare_attributes.txt"))
# subsitute fasttrips fare attributes
self.fare_attrs_df = gtfs.get(Route.INPUT_FARE_ATTRIBUTES_FILE)
if not self.fare_attrs_df.empty:
# verify required columns are present
fare_attrs_cols = list(self.fare_attrs_df.columns.values)
assert(Route.FARE_ATTR_COLUMN_FARE_PERIOD in fare_attrs_cols)
assert(Route.FARE_ATTR_COLUMN_PRICE in fare_attrs_cols)
assert(Route.FARE_ATTR_COLUMN_CURRENCY_TYPE in fare_attrs_cols)
assert(Route.FARE_ATTR_COLUMN_PAYMENT_METHOD in fare_attrs_cols)
assert(Route.FARE_ATTR_COLUMN_TRANSFERS in fare_attrs_cols)
if Route.FARE_ATTR_COLUMN_TRANSFER_DURATION not in fare_attrs_cols:
self.fare_attrs_df[Route.FARE_ATTR_COLUMN_TRANSFER_DURATION] = np.nan
FastTripsLogger.debug("===> REPLACED BY FARE ATTRIBUTES FT\n" + str(self.fare_attrs_df.head()))
FastTripsLogger.debug("\n"+str(self.fare_attrs_df.dtypes))
FastTripsLogger.info("Read %7d %15s from %25s" %
(len(self.fare_attrs_df), "fare attributes", Route.INPUT_FARE_ATTRIBUTES_FILE))
#: fares are by fare_period rather than by fare_id
self.fare_by_class = True
else:
self.fare_by_class = False
# Fare rules (map routes to fare_id)
self.fare_rules_df = gtfs.fare_rules
if len(self.fare_rules_df) > 0:
self.fare_ids_df = Util.add_numeric_column(self.fare_rules_df[[Route.FARE_RULES_COLUMN_FARE_ID]],
id_colname=Route.FARE_RULES_COLUMN_FARE_ID,
numeric_newcolname=Route.FARE_RULES_COLUMN_FARE_ID_NUM)
self.fare_rules_df = pd.merge(left =self.fare_rules_df,
right =self.fare_ids_df,
how ="left")
else:
self.fare_ids_df = pd.DataFrame()
# optionally reverse those with origin/destinations if configured
from .Assignment import Assignment
if Assignment.FARE_ZONE_SYMMETRY:
FastTripsLogger.debug("applying FARE_ZONE_SYMMETRY to %d fare rules" % len(self.fare_rules_df))
# select only those with an origin and destination
reverse_fare_rules = self.fare_rules_df.loc[ pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID])&
pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID]) ].copy()
# FastTripsLogger.debug("reverse_fare_rules 1 head()=\n%s" % str(reverse_fare_rules.head()))
# reverse them
reverse_fare_rules.rename(columns={Route.FARE_RULES_COLUMN_ORIGIN_ID : Route.FARE_RULES_COLUMN_DESTINATION_ID,
Route.FARE_RULES_COLUMN_DESTINATION_ID : Route.FARE_RULES_COLUMN_ORIGIN_ID},
inplace=True)
# FastTripsLogger.debug("reverse_fare_rules 2 head()=\n%s" % str(reverse_fare_rules.head()))
# join them to eliminate dupes
reverse_fare_rules = pd.merge(left =reverse_fare_rules,
right =self.fare_rules_df,
how ="left",
on =[Route.FARE_RULES_COLUMN_FARE_ID,
Route.FARE_RULES_COLUMN_FARE_ID_NUM,
Route.FARE_RULES_COLUMN_ROUTE_ID,
Route.FARE_RULES_COLUMN_ORIGIN_ID,
Route.FARE_RULES_COLUMN_DESTINATION_ID,
Route.FARE_RULES_COLUMN_CONTAINS_ID],
indicator=True)
# dupes exist in both -- drop those
reverse_fare_rules = reverse_fare_rules.loc[ reverse_fare_rules["_merge"]=="left_only"]
reverse_fare_rules.drop(["_merge"], axis=1, inplace=True)
# add them to fare rules
self.fare_rules_df = pd.concat([self.fare_rules_df, reverse_fare_rules])
FastTripsLogger.debug("fare rules with symmetry %d head()=\n%s" % (len(self.fare_rules_df), str(self.fare_rules_df.head())))
# sort by fare ID num so zone-to-zone and their reverse are together
if len(self.fare_rules_df) > 0:
self.fare_rules_df.sort_values(by=[Route.FARE_RULES_COLUMN_FARE_ID_NUM], inplace=True)
fare_rules_ft_df = gtfs.get(Route.INPUT_FARE_PERIODS_FILE)
if not fare_rules_ft_df.empty:
# verify required columns are present
fare_rules_ft_cols = list(fare_rules_ft_df.columns.values)
assert(Route.FARE_RULES_COLUMN_FARE_ID in fare_rules_ft_cols)
assert(Route.FARE_RULES_COLUMN_FARE_PERIOD in fare_rules_ft_cols)
assert(Route.FARE_RULES_COLUMN_START_TIME in fare_rules_ft_cols)
assert(Route.FARE_RULES_COLUMN_END_TIME in fare_rules_ft_cols)
# Split fare classes so they don't overlap
fare_rules_ft_df = self.remove_fare_period_overlap(fare_rules_ft_df)
# join to fare rules dataframe
self.fare_rules_df = pd.merge(left=self.fare_rules_df, right=fare_rules_ft_df,
how='left',
on=Route.FARE_RULES_COLUMN_FARE_ID)
# add route id numbering if applicable
if Route.FARE_RULES_COLUMN_ROUTE_ID in list(self.fare_rules_df.columns.values):
self.fare_rules_df = self.add_numeric_route_id(self.fare_rules_df,
Route.FARE_RULES_COLUMN_ROUTE_ID,
Route.FARE_RULES_COLUMN_ROUTE_ID_NUM)
# add origin zone numbering if applicable
if (Route.FARE_RULES_COLUMN_ORIGIN_ID in list(self.fare_rules_df.columns.values)) and \
(pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID]).sum() > 0):
self.fare_rules_df = stops.add_numeric_stop_zone_id(self.fare_rules_df,
Route.FARE_RULES_COLUMN_ORIGIN_ID,
Route.FARE_RULES_COLUMN_ORIGIN_ID_NUM)
# add destination zone numbering if applicable
if (Route.FARE_RULES_COLUMN_DESTINATION_ID in list(self.fare_rules_df.columns.values)) and \
(pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID]).sum() > 0):
self.fare_rules_df = stops.add_numeric_stop_zone_id(self.fare_rules_df,
Route.FARE_RULES_COLUMN_DESTINATION_ID,
Route.FARE_RULES_COLUMN_DESTINATION_ID_NUM)
# They should both be present
# This is unlikely
if Route.FARE_RULES_COLUMN_ORIGIN_ID not in list(self.fare_rules_df.columns.values):
error_str = "Fast-trips only supports both origin_id and destination_id or neither in fare rules"
FastTripsLogger.fatal(error_str)
raise NotImplementedError(error_str)
# check for each row, either both are present or neither -- use xor, or ^
xor_id = self.fare_rules_df.loc[ pd.isnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID])^
pd.isnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID]) ]
if len(xor_id) > 0:
error_str = "Fast-trips supports fare rules with both origin id and destination id specified, or neither ONLY.\n%s" % str(xor_id)
FastTripsLogger.fatal(error_str)
raise NotImplementedError(error_str)
# We don't support contains_id
if Route.FARE_RULES_COLUMN_CONTAINS_ID in list(self.fare_rules_df.columns.values):
non_null_contains_id = self.fare_rules_df.loc[pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_CONTAINS_ID])]
if len(non_null_contains_id) > 0:
error_str = "Fast-trips does not support contains_id in fare rules:\n%s" % str(non_null_contains_id)
FastTripsLogger.fatal(error_str)
raise NotImplementedError(error_str)
# We don't support rows with only one of origin_id or destination_id specified
elif len(self.fare_rules_df) > 0:
# we have fare rules but no fare periods -- make the fare periods the same
self.fare_rules_df[Route.FARE_RULES_COLUMN_FARE_PERIOD] = self.fare_rules_df[Route.FARE_RULES_COLUMN_FARE_ID]
self.fare_rules_df[Route.FARE_RULES_COLUMN_START_TIME] = Util.read_time("00:00:00")
self.fare_rules_df[Route.FARE_RULES_COLUMN_END_TIME ] = Util.read_time("24:00:00")
# join to fare_attributes on fare_period if we have it, or fare_id if we don't
if len(self.fare_rules_df) > 0:
"""
Fare ID/class (fare period)/attribute mapping.
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
| *Column name* | Column Description |
+=======================+======================================================================================================================================+
|``fare_id`` |GTFS fare_id (See `fare_rules`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``fare_id_num`` |Numbered fare_id |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``route_id`` |(optional) Route(s) associated with this fare ID. (See `fare_rules`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``origin_id`` |(optional) Origin fare zone ID(s) for fare ID. (See `fare_rules`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``origin_id_num`` |(optional) Origin fare zone number for fare ID. |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``destination_id`` |(optional) Destination fare zone ID(s) for fare ID. (See `fare_rules`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``destination_id_num`` |(optional) Destination fare zone number for fare ID. |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``contains_id`` |(optional) Contains fare zone ID(s) for fare ID. (See `fare_rules`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``fare_period`` |GTFS-plus fare_period (See `fare_periods_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``start_time`` |Fare class start time (See `fare_rules_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``end_time`` |Fare class end time (See `fare_rules_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``currency_type`` |Currency of fare class or id (See `fare_attributes`_ or `fare_attributes_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``price`` |Price of fare class or id (See `fare_attributes`_ or `fare_attributes_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``payment_method`` |When the fare must be paid (See `fare_attributes`_ or `fare_attributes_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``transfers`` |Number of transfers permiited on this fare (See `fare_attributes`_ or `fare_attributes_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
|``transfer_duration`` |(optional) Integer length of time in seconds before transfer expires (See `fare_attributes`_ or `fare_attributes_ft`_) |
+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------+
.. _fare_rules: https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_rules.md
.. _fare_rules_ft: https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_rules_ft.md
.. _fare_attributes: https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_attributes.md
.. _fare_attributes_ft: https://github.com/osplanning-data-standards/GTFS-PLUS/blob/master/files/fare_attributes_ft.md
"""
self.fare_rules_df = pd.merge(left =self.fare_rules_df,
right=self.fare_attrs_df,
how ='left',
on = Route.FARE_RULES_COLUMN_FARE_PERIOD if self.fare_by_class else Route.FARE_RULES_COLUMN_FARE_ID)
FastTripsLogger.debug("=========== FARE RULES ===========\n" + str(self.fare_rules_df.head(10).to_string(formatters=\
{Route.FARE_RULES_COLUMN_START_TIME:Util.datetime64_formatter,
Route.FARE_RULES_COLUMN_END_TIME :Util.datetime64_formatter})))
FastTripsLogger.debug("\n"+str(self.fare_rules_df.dtypes))
FastTripsLogger.info("Read %7d %15s from %25s, %25s" %
(len(self.fare_rules_df), "fare rules", "fare_rules.txt", self.INPUT_FARE_PERIODS_FILE))
self.fare_transfer_rules_df = gtfs.get(Route.INPUT_FARE_TRANSFER_RULES_FILE)
if not self.fare_transfer_rules_df.empty:
# verify required columns are present
fare_transfer_rules_cols = list(self.fare_transfer_rules_df.columns.values)
assert(Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD in fare_transfer_rules_cols)
assert(Route.FARE_TRANSFER_RULES_COLUMN_TO_FARE_PERIOD in fare_transfer_rules_cols)
assert(Route.FARE_TRANSFER_RULES_COLUMN_TYPE in fare_transfer_rules_cols)
assert(Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT in fare_transfer_rules_cols)
# verify valid values for transfer type
invalid_type = self.fare_transfer_rules_df.loc[ self.fare_transfer_rules_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE].isin(Route.TRANSFER_TYPE_OPTIONS)==False ]
if len(invalid_type) > 0:
error_msg = "Invalid value for %s:\n%s" % (Route.FARE_TRANSFER_RULES_COLUMN_TYPE, str(invalid_type))
FastTripsLogger.fatal(error_msg)
raise NetworkInputError(Route.INPUT_FARE_TRANSFER_RULES_FILE, error_msg)
# verify the amount is positive
negative_amount = self.fare_transfer_rules_df.loc[ self.fare_transfer_rules_df[Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT] < 0]
if len(negative_amount) > 0:
error_msg = "Negative transfer amounts are invalid:\n%s" % str(negative_amount)
FastTripsLogger.fatal(error_msg)
raise NetworkInputError(Route.INPUT_FARE_TRANSFER_RULES_FILE, error_msg)
FastTripsLogger.debug("=========== FARE TRANSFER RULES ===========\n" + str(self.fare_transfer_rules_df.head()))
FastTripsLogger.debug("\n"+str(self.fare_transfer_rules_df.dtypes))
FastTripsLogger.info("Read %7d %15s from %25s" %
(len(self.fare_transfer_rules_df), "fare xfer rules", Route.INPUT_FARE_TRANSFER_RULES_FILE))
else:
self.fare_transfer_rules_df = pd.DataFrame()
self.write_routes_for_extension()
[docs] def add_numeric_route_id(self, input_df, id_colname, numeric_newcolname):
"""
Passing a :py:class:`pandas.DataFrame` with a route ID column called *id_colname*,
adds the numeric route id as a column named *numeric_newcolname* and returns it.
"""
return Util.add_new_id(input_df, id_colname, numeric_newcolname,
mapping_df=self.route_id_df,
mapping_id_colname=Route.ROUTES_COLUMN_ROUTE_ID,
mapping_newid_colname=Route.ROUTES_COLUMN_ROUTE_ID_NUM)
[docs] def add_access_egress_modes(self, access_modes_df, egress_modes_df):
"""
Adds access and egress modes to the mode list
Writes out mapping to disk
"""
access_modes_df[Route.ROUTES_COLUMN_MODE_TYPE] = Route.MODE_TYPE_ACCESS
egress_modes_df[Route.ROUTES_COLUMN_MODE_TYPE] = Route.MODE_TYPE_EGRESS
implicit_modes_df = pd.DataFrame({Route.ROUTES_COLUMN_MODE_TYPE: [Route.MODE_TYPE_TRANSFER],
Route.ROUTES_COLUMN_MODE: [Route.MODE_TYPE_TRANSFER],
Route.ROUTES_COLUMN_MODE_NUM: [ 1]})
self.modes_df = pd.concat([implicit_modes_df,
self.modes_df,
access_modes_df,
egress_modes_df], axis=0)
self.modes_df.reset_index(inplace=True)
# write intermediate files
self.modes_df.to_csv(os.path.join(self.output_dir, Route.OUTPUT_MODE_NUM_FILE),
columns=[Route.ROUTES_COLUMN_MODE_NUM, Route.ROUTES_COLUMN_MODE],
sep=" ", index=False)
FastTripsLogger.debug("Wrote %s" % os.path.join(self.output_dir, Route.OUTPUT_MODE_NUM_FILE))
[docs] def add_numeric_mode_id(self, input_df, id_colname, numeric_newcolname, warn=False):
"""
Passing a :py:class:`pandas.DataFrame` with a mode ID column called *id_colname*,
adds the numeric mode id as a column named *numeric_newcolname* and returns it.
"""
return Util.add_new_id(input_df, id_colname, numeric_newcolname,
mapping_df=self.modes_df[[Route.ROUTES_COLUMN_MODE_NUM, Route.ROUTES_COLUMN_MODE]],
mapping_id_colname=Route.ROUTES_COLUMN_MODE,
mapping_newid_colname=Route.ROUTES_COLUMN_MODE_NUM,
warn=warn)
[docs] def remove_fare_period_overlap(self, fare_rules_ft_df):
"""
Split fare classes so they don't overlap
"""
fare_rules_ft_df["fare_period_id"] = fare_rules_ft_df.index+1
# FastTripsLogger.debug("remove_fare_period_overlap: initial\n%s" % fare_rules_ft_df)
max_fare_period_id = fare_rules_ft_df["fare_period_id"].max()
loop_iters = 0
while True:
# join with itself to see if any are contained
df = pd.merge(left =fare_rules_ft_df,
right=fare_rules_ft_df,
on =Route.FARE_RULES_COLUMN_FARE_ID,
how ="outer")
# if there's one fare period per fare id, nothing to do
if len(df)==len(fare_rules_ft_df):
FastTripsLogger.debug("One fare period per fare id, no need to split")
return fare_rules_ft_df
# remove dupes
df = df.loc[ df["fare_period_id_x"] != df["fare_period_id_y"] ]
FastTripsLogger.debug("remove_fare_period_overlap:\n%s" % df)
# this is an overlap -- error
# ____y_______ x starts after y starts
# ______x______ x starts before y ends
# x ends after y ends
intersecting_fare_periods = df.loc[ (df["start_time_x"]>df["start_time_y"])& \
(df["start_time_x"]<df["end_time_y"])& \
(df["end_time_x" ]>df["end_time_y"]) ]
if len(intersecting_fare_periods) > 0:
error_msg = "Partially overlapping fare periods are ambiguous. \n%s" % str(intersecting_fare_periods)
FastTripsLogger.error(error_msg)
raise NetworkInputError(Route.INPUT_FARE_PERIODS_FILE, error_msg)
# is x a subset of y?
# ___x___ x starts after y starts
# ______y_______ x ends before y ends
subset_fare_periods = df.loc[ (df["start_time_x"]>=df["start_time_y"])& \
(df["end_time_x" ]<=df["end_time_y"]) ]
# if no subsets, done -- return
if len(subset_fare_periods) == 0:
FastTripsLogger.debug("remove_fare_period_overlap returning\n%s" % fare_rules_ft_df)
return fare_rules_ft_df
# do one at a time -- split first into three rows
FastTripsLogger.debug("splitting\n%s" % str(subset_fare_periods))
row_dict = subset_fare_periods.head(1).to_dict(orient="records")[0]
FastTripsLogger.debug(row_dict)
y_1 = {'fare_id' :row_dict['fare_id'],
'fare_period' :row_dict['fare_period_y'],
'start_time' :row_dict['start_time_y'],
'end_time' :row_dict['start_time_x'],
'fare_period_id' :row_dict['fare_period_id_y']}
x = {'fare_id' :row_dict['fare_id'],
'fare_period' :row_dict['fare_period_x'],
'start_time' :row_dict['start_time_x'],
'end_time' :row_dict['end_time_x'],
'fare_period_id' :row_dict['fare_period_id_x']}
y_2 = {'fare_id' :row_dict['fare_id'],
'fare_period' :row_dict['fare_period_y'],
'start_time' :row_dict['end_time_x'],
'end_time' :row_dict['end_time_y'],
'fare_period_id' :max_fare_period_id+1} # new
max_fare_period_id += 1
new_df = pd.DataFrame([y_1,x,y_2])
FastTripsLogger.debug("\n%s" % str(new_df))
# put it together with the unaffected fare_periodes we already had
prev_df = fare_rules_ft_df.loc[ (fare_rules_ft_df["fare_period_id"]!=row_dict["fare_period_id_x"])&
(fare_rules_ft_df["fare_period_id"]!=row_dict["fare_period_id_y"]) ]
fare_rules_ft_df = prev_df.append(new_df)
# sort by fare_id, start_time
fare_rules_ft_df.sort_values([Route.FARE_RULES_COLUMN_FARE_ID,
Route.FARE_RULES_COLUMN_START_TIME], inplace=True)
# reorder columns
fare_rules_ft_df = fare_rules_ft_df[[Route.FARE_RULES_COLUMN_FARE_ID,
Route.FARE_RULES_COLUMN_FARE_PERIOD,
"fare_period_id",
Route.FARE_RULES_COLUMN_START_TIME,
Route.FARE_RULES_COLUMN_END_TIME]]
FastTripsLogger.debug("\n%s" % str(fare_rules_ft_df))
loop_iters += 1
# don't loop forever -- there's a problem
if loop_iters > 5:
error_str = "Route.remove_fare_period_overlap looping too much! Something is wrong."
FastTripsLogger.critical(error_str)
raise UnexpectedError(error_str)
# this shouldn't happen
FastTripsLogger.warn("This shouldn't happen")
[docs] def write_routes_for_extension(self):
"""
Write to an intermediate formatted file for the C++ extension.
Since there are strings involved, it's easier than passing it to the extension.
"""
from .Assignment import Assignment
# write intermediate file -- route id num, route id
self.route_id_df[[Route.ROUTES_COLUMN_ROUTE_ID_NUM, Route.ROUTES_COLUMN_ROUTE_ID]].to_csv(
os.path.join(self.output_dir, Route.OUTPUT_ROUTE_ID_NUM_FILE), sep=" ", index=False)
FastTripsLogger.debug("Wrote %s" % os.path.join(self.output_dir, Route.OUTPUT_ROUTE_ID_NUM_FILE))
# write fare file
if len(self.fare_rules_df) > 0:
# copy for writing
fare_rules_df = self.fare_rules_df.copy()
# replace with float versions
fare_rules_df[Route.FARE_RULES_COLUMN_START_TIME] = (fare_rules_df[Route.FARE_RULES_COLUMN_START_TIME] - Assignment.NETWORK_BUILD_DATE_START_TIME)/np.timedelta64(1,'m')
fare_rules_df[Route.FARE_RULES_COLUMN_END_TIME ] = (fare_rules_df[Route.FARE_RULES_COLUMN_END_TIME ] - Assignment.NETWORK_BUILD_DATE_START_TIME)/np.timedelta64(1,'m')
# fillna with -1
for num_col in [Route.FARE_RULES_COLUMN_ROUTE_ID_NUM, Route.FARE_RULES_COLUMN_ORIGIN_ID_NUM, Route.FARE_RULES_COLUMN_DESTINATION_ID_NUM, Route.FARE_ATTR_COLUMN_TRANSFERS]:
if num_col in list(fare_rules_df.columns.values):
fare_rules_df.loc[ pd.isnull(fare_rules_df[num_col]), num_col] = -1
fare_rules_df[num_col] = fare_rules_df[num_col].astype(int)
else:
fare_rules_df[num_col] = -1
# temp column: duraton; sort by this so the smallest duration is found first
fare_rules_df["duration"] = fare_rules_df[Route.FARE_RULES_COLUMN_END_TIME ] - fare_rules_df[Route.FARE_RULES_COLUMN_START_TIME]
fare_rules_df.sort_values(by=[Route.FARE_RULES_COLUMN_FARE_ID_NUM,"duration"], ascending=True, inplace=True)
# transfer_duration fillna with -1
fare_rules_df.fillna({Route.FARE_ATTR_COLUMN_TRANSFER_DURATION:-1}, inplace=True)
# File with fare id num, fare id, fare class, price, xfers
fare_rules_df.to_csv(os.path.join(self.output_dir, Route.OUTPUT_FARE_ID_FILE),
columns=[Route.FARE_RULES_COLUMN_FARE_ID_NUM,
Route.FARE_RULES_COLUMN_FARE_ID,
Route.FARE_ATTR_COLUMN_FARE_PERIOD,
Route.FARE_RULES_COLUMN_ROUTE_ID_NUM,
Route.FARE_RULES_COLUMN_ORIGIN_ID_NUM,
Route.FARE_RULES_COLUMN_DESTINATION_ID_NUM,
Route.FARE_RULES_COLUMN_START_TIME,
Route.FARE_RULES_COLUMN_END_TIME,
Route.FARE_ATTR_COLUMN_PRICE,
Route.FARE_ATTR_COLUMN_TRANSFERS,
Route.FARE_ATTR_COLUMN_TRANSFER_DURATION],
sep=" ", index=False)
FastTripsLogger.debug("Wrote %s" % os.path.join(self.output_dir, Route.OUTPUT_FARE_ID_FILE))
if len(self.fare_transfer_rules_df) > 0:
# File with fare transfer rules
self.fare_transfer_rules_df.to_csv(os.path.join(self.output_dir, Route.OUTPUT_FARE_TRANSFER_FILE),
sep=" ", index=False)
FastTripsLogger.debug("Wrote %s" % os.path.join(self.output_dir, Route.OUTPUT_FARE_TRANSFER_FILE))
else:
FastTripsLogger.debug("No fare rules so no file %s" % os.path.join(self.output_dir, Route.OUTPUT_FARE_ID_FILE))
[docs] def add_fares(self, trip_links_df):
"""
Adds (or replaces) fare columns to the given :py:class:`pandas.DataFrame`.
New columns are
* :py:attr:`Assignment.SIM_COL_PAX_FARE`
* :py:attr:`Assignment.SIM_COL_PAX_FARE_PERIOD`
* :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD`
* :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_TYPE`
* :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT`
* :py:attr:`Assignment.SIM_COL_PAX_FREE_TRANSFER`
"""
FastTripsLogger.info(" Adding fares to pathset")
from .Assignment import Assignment
if Assignment.SIM_COL_PAX_FARE in list(trip_links_df.columns.values):
trip_links_df.drop([Assignment.SIM_COL_PAX_FARE,
Assignment.SIM_COL_PAX_FARE_PERIOD,
Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD,
Route.FARE_TRANSFER_RULES_COLUMN_TYPE,
Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT,
Assignment.SIM_COL_PAX_FREE_TRANSFER], axis=1, inplace=True)
# no fares configured
if len(self.fare_rules_df) == 0:
trip_links_df[Assignment.SIM_COL_PAX_FARE ] = 0
trip_links_df[Assignment.SIM_COL_PAX_FARE_PERIOD ] = None
trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD] = None
trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE ] = None
trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT ] = None
trip_links_df[Assignment.SIM_COL_PAX_FREE_TRANSFER ] = None
return trip_links_df
orig_columns = list(trip_links_df.columns.values)
fare_columns = [Assignment.SIM_COL_PAX_FARE,
Assignment.SIM_COL_PAX_FARE_PERIOD]
transfer_columns = [Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD,
Route.FARE_TRANSFER_RULES_COLUMN_TYPE,
Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT,
Assignment.SIM_COL_PAX_FREE_TRANSFER]
# give them a unique index and store it for later
trip_links_df.reset_index(drop=True, inplace=True)
trip_links_df["trip_links_df index"] = trip_links_df.index
num_trip_links = len(trip_links_df)
FastTripsLogger.debug("add_fares initial trips (%d):\n%s" % (num_trip_links, str(trip_links_df.head(20))))
FastTripsLogger.debug("add_fares initial fare_rules (%d):\n%s" % (len(self.fare_rules_df), str(self.fare_rules_df.head(20))))
# initialize
trip_links_unmatched = trip_links_df
trip_links_matched = pd.DataFrame()
del trip_links_df
from .Passenger import Passenger
# level 0: match on all three
fare_rules0 = self.fare_rules_df.loc[pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ROUTE_ID ])&
pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID ])&
pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID])]
if len(fare_rules0) > 0:
trip_links_match0 = pd.merge(left =trip_links_unmatched,
right =fare_rules0,
how ="inner",
left_on =[Route.FARE_RULES_COLUMN_ROUTE_ID,"A_zone_id","B_zone_id"],
right_on =[Route.FARE_RULES_COLUMN_ROUTE_ID,Route.FARE_RULES_COLUMN_ORIGIN_ID,Route.FARE_RULES_COLUMN_DESTINATION_ID],
suffixes =["","_fare_rules"])
# delete rows where the board time is not within the fare period
trip_links_match0 = trip_links_match0.loc[ pd.isnull(trip_links_match0[Route.FARE_ATTR_COLUMN_PRICE])|
((trip_links_match0[Assignment.SIM_COL_PAX_BOARD_TIME] >= trip_links_match0[Route.FARE_RULES_COLUMN_START_TIME])&
(trip_links_match0[Assignment.SIM_COL_PAX_BOARD_TIME] < trip_links_match0[Route.FARE_RULES_COLUMN_END_TIME])) ]
FastTripsLogger.debug("add_fares level 0 (%d):\n%s" % (len(trip_links_match0), str(trip_links_match0.head(20))))
if len(trip_links_match0) > 0:
# update matched and unmatched == they should be disjoint with union = whole
trip_links_unmatched = pd.merge(left =trip_links_unmatched,
right=trip_links_match0[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM]],
how ="left",
on =[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM],
indicator=True)
trip_links_unmatched = trip_links_unmatched.loc[ trip_links_unmatched["_merge"] == "left_only" ]
trip_links_unmatched.drop(["_merge"], axis=1, inplace=True)
trip_links_matched = pd.concat([trip_links_matched, trip_links_match0], axis=0, copy=False)
FastTripsLogger.debug("matched: %d unmatched: %d total: %d" % (len(trip_links_matched), len(trip_links_unmatched), len(trip_links_matched)+len(trip_links_unmatched)))
del trip_links_match0
# TODO - Addding stop gap solution - if there are duplicates, drop them
# but there's probably a better way to handle this, like flagging in input
# See https://app.asana.com/0/15582794263969/319659099709517
# trip_links_matched["dupe"] = trip_links_matched.duplicated(subset="trip_links_df index")
# FastTripsLogger.debug("dupes: \n%s" % trip_links_matched.loc[trip_links_matched["dupe"]==True].sort_values(by="trip_links_df index"))
trip_links_matched.drop_duplicates(subset="trip_links_df index", keep="first", inplace=True)
# level 1: match on route only
fare_rules1 = self.fare_rules_df.loc[pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ROUTE_ID ])&
pd.isnull (self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID ])&
pd.isnull (self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID])]
if len(fare_rules1) > 0:
trip_links_match1 = pd.merge(left =trip_links_unmatched,
right =fare_rules1,
how ="inner",
on =Route.FARE_RULES_COLUMN_ROUTE_ID,
suffixes =["","_fare_rules"])
# delete rows where the board time is not within the fare period
trip_links_match1 = trip_links_match1.loc[ pd.isnull(trip_links_match1[Route.FARE_ATTR_COLUMN_PRICE])|
((trip_links_match1[Assignment.SIM_COL_PAX_BOARD_TIME] >= trip_links_match1[Route.FARE_RULES_COLUMN_START_TIME])&
(trip_links_match1[Assignment.SIM_COL_PAX_BOARD_TIME] < trip_links_match1[Route.FARE_RULES_COLUMN_END_TIME])) ]
FastTripsLogger.debug("add_fares level 1 (%d):\n%s" % (len(trip_links_match1), str(trip_links_match1.head())))
if len(trip_links_match1) > 0:
# update matched and unmatched == they should be disjoint with union = whole
trip_links_unmatched = pd.merge(left =trip_links_unmatched,
right=trip_links_match1[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM]],
how ="left",
on =[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM],
indicator=True)
trip_links_unmatched = trip_links_unmatched.loc[ trip_links_unmatched["_merge"] == "left_only" ]
trip_links_unmatched.drop(["_merge"], axis=1, inplace=True)
trip_links_matched = pd.concat([trip_links_matched, trip_links_match1], axis=0, copy=False)
FastTripsLogger.debug("matched: %d unmatched: %d total: %d" % (len(trip_links_matched), len(trip_links_unmatched), len(trip_links_matched)+len(trip_links_unmatched)))
del trip_links_match1
# level 2: match on origin and destination zones only
fare_rules2 = self.fare_rules_df.loc[pd.isnull (self.fare_rules_df[Route.FARE_RULES_COLUMN_ROUTE_ID ])&
pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID ])&
pd.notnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID])]
if len(fare_rules2) > 0:
trip_links_match2 = pd.merge(left =trip_links_unmatched,
right =fare_rules2,
how ="inner",
left_on =["A_zone_id","B_zone_id"],
right_on =[Route.FARE_RULES_COLUMN_ORIGIN_ID,Route.FARE_RULES_COLUMN_DESTINATION_ID],
suffixes =["","_fare_rules"])
# delete rows where the board time is not within the fare period
trip_links_match2 = trip_links_match2.loc[ pd.isnull(trip_links_match2[Route.FARE_ATTR_COLUMN_PRICE])|
((trip_links_match2[Assignment.SIM_COL_PAX_BOARD_TIME] >= trip_links_match2[Route.FARE_RULES_COLUMN_START_TIME])&
(trip_links_match2[Assignment.SIM_COL_PAX_BOARD_TIME] < trip_links_match2[Route.FARE_RULES_COLUMN_END_TIME])) ]
FastTripsLogger.debug("add_fares level 2 (%d):\n%s" % (len(trip_links_match2), str(trip_links_match2.head())))
if len(trip_links_match2) > 0:
# update matched and unmatched == they should be disjoint with union = whole
trip_links_unmatched = pd.merge(left =trip_links_unmatched,
right=trip_links_match2[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM]],
how ="left",
on =[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM],
indicator=True)
trip_links_unmatched = trip_links_unmatched.loc[ trip_links_unmatched["_merge"] == "left_only" ]
trip_links_unmatched.drop(["_merge"], axis=1, inplace=True)
trip_links_matched = pd.concat([trip_links_matched, trip_links_match2], axis=0, copy=False)
FastTripsLogger.debug("matched: %d unmatched: %d total: %d" % (len(trip_links_matched), len(trip_links_unmatched), len(trip_links_matched)+len(trip_links_unmatched)))
del trip_links_match2
# level 3: no route, origin or destination specified
fare_rules3 = self.fare_rules_df.loc[pd.isnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ROUTE_ID ])&
pd.isnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_ORIGIN_ID ])&
pd.isnull(self.fare_rules_df[Route.FARE_RULES_COLUMN_DESTINATION_ID])].copy()
if len(fare_rules3) > 0:
# need a column to merge on
merge_column = "fare level 3 merge col"
fare_rules3[merge_column] = 1
trip_links_unmatched[merge_column] = 1
FastTripsLogger.debug("fare_rules3 (%d):\n%s" % (len(fare_rules3), str(fare_rules3.head())))
trip_links_match3 = pd.merge(left =trip_links_unmatched,
right =fare_rules3,
how ="inner",
on =merge_column,
suffixes =["","_fare_rules"])
trip_links_match3.drop([merge_column], axis=1, inplace=True)
trip_links_unmatched.drop([merge_column], axis=1, inplace=True)
# delete rows where the board time is not within the fare period
trip_links_match3 = trip_links_match3.loc[ pd.isnull(trip_links_match3[Route.FARE_ATTR_COLUMN_PRICE])|
((trip_links_match3[Assignment.SIM_COL_PAX_BOARD_TIME] >= trip_links_match3[Route.FARE_RULES_COLUMN_START_TIME])&
(trip_links_match3[Assignment.SIM_COL_PAX_BOARD_TIME] < trip_links_match3[Route.FARE_RULES_COLUMN_END_TIME])) ]
FastTripsLogger.debug("add_fares level 3 (%d):\n%s" % (len(trip_links_match3), str(trip_links_match3.head())))
if len(trip_links_match3) > 0:
# update matched and unmatched == they should be disjoint with union = whole
trip_links_unmatched = pd.merge(left =trip_links_unmatched,
right=trip_links_match3[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM]],
how ="left",
on =[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM],
indicator=True)
trip_links_unmatched = trip_links_unmatched.loc[ trip_links_unmatched["_merge"] == "left_only" ]
trip_links_unmatched.drop(["_merge"], axis=1, inplace=True)
trip_links_matched = pd.concat([trip_links_matched, trip_links_match3], axis=0, copy=False)
FastTripsLogger.debug("matched: %d unmatched: %d total: %d" % (len(trip_links_matched), len(trip_links_unmatched), len(trip_links_matched)+len(trip_links_unmatched)))
del trip_links_match3
# put them together
trip_links_df = pd.concat([trip_links_matched, trip_links_unmatched], axis=0, copy=False)
trip_links_df.sort_values(by=[Passenger.TRIP_LIST_COLUMN_TRIP_LIST_ID_NUM,
Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM],
inplace=True)
del trip_links_matched
del trip_links_unmatched
# rename price to fare
trip_links_df.rename(columns={Route.FARE_ATTR_COLUMN_PRICE:Assignment.SIM_COL_PAX_FARE}, inplace=True)
# join fails mean 0
trip_links_df.fillna(value={Assignment.SIM_COL_PAX_FARE:0.0}, inplace=True)
# reorder columns
trip_links_df = trip_links_df[orig_columns + fare_columns + [Route.FARE_ATTR_COLUMN_TRANSFERS, Route.FARE_ATTR_COLUMN_TRANSFER_DURATION]]
FastTripsLogger.debug("trip_links_df (%d):\n%s" % (len(trip_links_df), str(trip_links_df.head())))
# make sure we didn't lose or add any
assert len(trip_links_df) == num_trip_links
# apply fare transfers
trip_links_df = self.apply_fare_transfer_rules(trip_links_df)
trip_links_df = self.apply_free_transfers(trip_links_df)
# drop other columns
trip_links_df = trip_links_df[orig_columns + fare_columns + transfer_columns]
return trip_links_df
[docs] def apply_fare_transfer_rules(self, trip_links_df):
"""
Applies fare transfers by attaching previous fare period.
Adds (or replaces) columns :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD`, :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_TYPE`
and :py:attr:`Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT` and adjusts the values in
:py:attr:`Assignment.SIM_COL_PAX_FARE`.
"""
from .Passenger import Passenger
from .Assignment import Assignment
if Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD in list(trip_links_df.columns.values):
trip_links_df.drop([Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD,
Route.FARE_TRANSFER_RULES_COLUMN_TYPE,
Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT], axis=1, inplace=True)
# no transfer rules => nothing to do
if len(self.fare_transfer_rules_df) == 0:
trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD] = None
trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE] = None
trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT] = None
return trip_links_df
# FastTripsLogger.debug("apply_fare_transfers (%d):\n%s" % (len(trip_links_df), str(trip_links_df.head(20))))
# previous trip link
trip_links_df["%s prev" % Passenger.PF_COL_LINK_NUM] = trip_links_df[Passenger.PF_COL_LINK_NUM] - 2
trip_links_df = pd.merge(left =trip_links_df,
right =trip_links_df[[Passenger.PERSONS_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM,
Assignment.SIM_COL_PAX_FARE_PERIOD]],
left_on =[Passenger.PERSONS_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
"%s prev" % Passenger.PF_COL_LINK_NUM],
right_on=[Passenger.PERSONS_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM],
suffixes=["","_prev"],
how ="left")
# extra columns are linknum prev, linknum_prev, fare_prev, fare_period_prev,
# join with transfers table
trip_links_df = pd.merge(left =trip_links_df,
right =self.fare_transfer_rules_df,
left_on =["fare_period_prev","fare_period"],
right_on=[Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD,
Route.FARE_TRANSFER_RULES_COLUMN_TO_FARE_PERIOD],
how ="left")
# FastTripsLogger.debug("apply_fare_transfers (%d):\n%s" % (len(trip_links_df), str(trip_links_df.head(20))))
# keep Route.FARE_TRANSFER_RULES_COLUMN_FROM_FARE_PERIOD, Route.FARE_TRANSFER_RULES_COLUMN_TYPE, Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT
# so lose the rest
trip_links_df.drop(["%s prev" % Passenger.PF_COL_LINK_NUM,
"%s_prev" % Passenger.PF_COL_LINK_NUM,
"%s_prev" % Assignment.SIM_COL_PAX_FARE_PERIOD,
Route.FARE_TRANSFER_RULES_COLUMN_TO_FARE_PERIOD], axis=1, inplace=True)
# FastTripsLogger.debug("apply_fare_transfers (%d):\n%s" % (len(trip_links_df), str(trip_links_df.head(20))))
# apply transfer discount
trip_links_df.loc[ pd.notnull(trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE])&
(trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE]==Route.TRANSFER_TYPE_TRANSFER_DISCOUNT),
Assignment.SIM_COL_PAX_FARE ] = trip_links_df[Assignment.SIM_COL_PAX_FARE] - trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT]
# apply transfer free
trip_links_df.loc[ pd.notnull(trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE])&
(trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE]==Route.TRANSFER_TYPE_TRANSFER_FREE),
Assignment.SIM_COL_PAX_FARE ] = 0.0
# apply transfer fare
trip_links_df.loc[ pd.notnull(trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE])&
(trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_TYPE]==Route.TRANSFER_TYPE_TRANSFER_COST),
Assignment.SIM_COL_PAX_FARE ] = trip_links_df[Route.FARE_TRANSFER_RULES_COLUMN_AMOUNT]
# make sure it's not negative
trip_links_df.loc[ trip_links_df[Assignment.SIM_COL_PAX_FARE] < 0, Assignment.SIM_COL_PAX_FARE] = 0.0
FastTripsLogger.debug("apply_fare_transfers (%d):\n%s" % (len(trip_links_df), str(trip_links_df.head(20))))
return trip_links_df
[docs] def apply_free_transfers(self, trip_links_df):
"""
Apply the free transfers allowed in to trip_links_df fare_attributes_ft.txt (configured by columns transfers, transfer_duration).
Sets columns Assignment.SIM_COL_PAX_FREE_TRANSFER to None, 0.0 or 1.0
"""
# free transfers within a fare id
from .Assignment import Assignment
from .Passenger import Passenger
from .PathSet import PathSet
# create a fare_index that counts up for a unique person-trip id, pathnum, and fare_period
trip_links_df["fare_index"] = trip_links_df.groupby([Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Assignment.SIM_COL_PAX_FARE_PERIOD]).cumcount()
trip_links_df.loc[ trip_links_df[Passenger.PF_COL_LINK_MODE]!=PathSet.STATE_MODE_TRIP, "fare_index"] = -1
# transfer_time in seconds (to compare with transfer_duration) get the first fare board
first_fare_board = trip_links_df[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM,
Assignment.SIM_COL_PAX_FARE_PERIOD,
"fare_index",
Assignment.SIM_COL_PAX_BOARD_TIME]].loc[trip_links_df["fare_index"]==0]
FastTripsLogger.debug("apply_free_transfers: first_fare_board=\n%s" % str(first_fare_board.head(10)))
trip_links_df = pd.merge(left =trip_links_df,
right =first_fare_board,
on =[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Assignment.SIM_COL_PAX_FARE_PERIOD],
how ="left",
suffixes=["","_ffb"])
# calculate time from first board (for this fare period id) in seconds
trip_links_df["transfer_time_sec"] = (trip_links_df[Assignment.SIM_COL_PAX_BOARD_TIME]-trip_links_df["%s_ffb" % Assignment.SIM_COL_PAX_BOARD_TIME])/np.timedelta64(1,'s')
# FastTripsLogger.debug("apply_free_transfers: trip_links_df=\n%s" % str(trip_links_df.loc[ trip_links_df["transfer_time_sec"] >0 ]))
trip_links_df[Assignment.SIM_COL_PAX_FREE_TRANSFER] = 0.0
# free transfer if transfers > 0 and 0 < fare_index <= transfers
trip_links_df.loc[ (trip_links_df[Route.FARE_ATTR_COLUMN_TRANSFERS] > 0)& # transfers > 0
(trip_links_df["fare_index"]>0)& # is a transfer
(trip_links_df["fare_index"]<=trip_links_df[Route.FARE_ATTR_COLUMN_TRANSFERS]), # is within the number of free transfers allowed
Assignment.SIM_COL_PAX_FREE_TRANSFER] = 1.0
# only applicable to transit links
trip_links_df.loc[ trip_links_df[Passenger.PF_COL_LINK_MODE]!=PathSet.STATE_MODE_TRIP,
Assignment.SIM_COL_PAX_FREE_TRANSFER ] = None
# only applicable if transfer is within transfer_duration -- revoke if transfer_time_sec > transfer duration
trip_links_df.loc[ (trip_links_df[Assignment.SIM_COL_PAX_FREE_TRANSFER]==1.0) &
(trip_links_df["transfer_time_sec"] > trip_links_df[Route.FARE_ATTR_COLUMN_TRANSFER_DURATION]),
Assignment.SIM_COL_PAX_FREE_TRANSFER] = 0.0
# make the transfer free
trip_links_df.loc[ trip_links_df[Assignment.SIM_COL_PAX_FREE_TRANSFER]==1.0, Assignment.SIM_COL_PAX_FARE] = 0.0
# debug: show transfers within fare period
FastTripsLogger.debug("apply_free_transfers: fare_index>0\n%s" % str(trip_links_df[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM,
Passenger.PF_COL_LINK_MODE,
Assignment.SIM_COL_PAX_BOARD_TIME,
Assignment.SIM_COL_PAX_FARE_PERIOD,
Route.FARE_ATTR_COLUMN_TRANSFERS,
Route.FARE_ATTR_COLUMN_TRANSFER_DURATION,"transfer_time_sec",
"fare_index",Assignment.SIM_COL_PAX_FREE_TRANSFER]].loc[trip_links_df["fare_index"] > 0].head(10)))
# debug: show transfers within fare period
FastTripsLogger.debug("apply_free_transfers: free_transfer=1.0\n%s" % str(trip_links_df[[Passenger.TRIP_LIST_COLUMN_PERSON_ID,
Passenger.TRIP_LIST_COLUMN_PERSON_TRIP_ID,
Passenger.PF_COL_PATH_NUM,
Passenger.PF_COL_LINK_NUM,
Passenger.PF_COL_LINK_MODE,
Assignment.SIM_COL_PAX_BOARD_TIME,
Assignment.SIM_COL_PAX_FARE_PERIOD,
Route.FARE_ATTR_COLUMN_TRANSFERS,
Route.FARE_ATTR_COLUMN_TRANSFER_DURATION,"transfer_time_sec",
"fare_index",Assignment.SIM_COL_PAX_FREE_TRANSFER]].loc[trip_links_df[Assignment.SIM_COL_PAX_FREE_TRANSFER] > 0].head(10)))
# drop new columns
trip_links_df.drop(["fare_index", "fare_index_ffb", "transfer_time_sec",
"%s_ffb" % Passenger.PF_COL_LINK_NUM,
"%s_ffb" % Assignment.SIM_COL_PAX_BOARD_TIME], axis=1, inplace=True)
return trip_links_df