Modeling Return Trips and Clinic Opening Hours

A transit access model that routes only the outbound journey answers whether a patient can get to a clinic. The question they actually face is whether they can get there, be seen, and get home — and on a low-frequency network the return leg fails far more often than the outbound one. This guide, part of Public Transit Accessibility Modeling, builds a round-trip feasibility test.

Problem Context & Constraints

Three asymmetries make the return leg the binding constraint.

Service tapers. Frequency on most networks peaks in the morning and thins from mid-afternoon. A route running every twenty minutes at 08:00 may run hourly at 15:00 and stop entirely at 18:30, so an outbound journey that was comfortable becomes a return journey that is impossible.

The visit has a duration. A patient arriving at 09:40 for an appointment is not available to travel again until perhaps 11:00, and the return options are those departing after that, not those departing on arrival.

Opening hours bound both ends. Arriving at 08:20 for a clinic that opens at 09:00 wastes forty minutes; arriving at 12:10 for a clinic closing at 12:00 is not access at all. Both are invisible to a model that only computes travel time.

The result is that round-trip feasibility is a much smaller set than outbound reachability, and the gap between them is concentrated in the low-frequency periphery.

The Return Leg Is the Binding Constraint A timeline from six in the morning to eight in the evening. Outbound services run frequently until mid-afternoon. The clinic is open from nine to four. The visit takes ninety minutes. Return services thin sharply after three and stop at half past five. The feasible arrival window is therefore much narrower than the outbound service suggests: a patient must arrive between nine and half past two to be seen and still get home. The feasible window is narrower than either leg alone outbound service clinic open return service last 17:30 feasible arrival 09:00 – 14:30 06:00 10:00 14:00 18:00 20:00 outbound-only model 612 origins have access round-trip feasible 377 origins have access

Prerequisites

  • Outbound and return travel-time profiles between each origin and clinic, over the full day rather than a morning window
  • Clinic opening hours and, ideally, appointment-slot availability
  • A stated visit duration, which is a clinical input rather than a modelling one
  • python 3.11, pandas 2.2.2, numpy 1.26.4

Service frequency is not symmetric across the day, and the asymmetry is what makes the return leg the constraint:

Departures Per Hour, Outbound and Return Departures per hour on a representative rural route through the service day. Outbound service runs at three per hour in the morning peak, falling to one by mid-afternoon. Return service runs at two per hour in the morning, one through the middle of the day, and stops entirely after half past five. The last return departure, not the outbound frequency, is what bounds the feasible arrival window. The service day is not symmetric 0 2 4 outbound return last return 17:30 07:00 12:00 17:00 departures per hour on a representative rural route

Step-by-Step Solution

# Round-trip feasibility against opening hours and a visit duration.
# Pinned: pandas==2.2.2, numpy==1.26.4
import logging
import numpy as np
import pandas as pd

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("transit.roundtrip")


def feasible_itineraries(outbound: pd.DataFrame, inbound: pd.DataFrame,
                         open_time: pd.Timestamp, close_time: pd.Timestamp,
                         visit_min: int, max_total_min: int = 240) -> pd.DataFrame:
    """Every outbound departure whose round trip actually completes.

    `outbound` has departure and arrival timestamps; `inbound` has departure and
    arrival for the return. The join is on time, not on route: any return service
    departing after the visit ends qualifies."""
    ok = outbound.loc[(outbound["arrival"] >= open_time)
                      & (outbound["arrival"] <= close_time - pd.Timedelta(minutes=visit_min))]
    if ok.empty:
        return pd.DataFrame(columns=["out_departure", "arrival", "ready",
                                     "return_departure", "home", "total_min"])
    rows = []
    for _, o in ok.iterrows():
        ready = o["arrival"] + pd.Timedelta(minutes=visit_min)
        back = inbound.loc[inbound["departure"] >= ready]
        if back.empty:
            continue                       # stranded: no return service after the visit
        r = back.iloc[0]
        total = (r["arrival"] - o["departure"]).total_seconds() / 60.0
        if total <= max_total_min:
            rows.append({"out_departure": o["departure"], "arrival": o["arrival"],
                         "ready": ready, "return_departure": r["departure"],
                         "home": r["arrival"], "total_min": total})
    out = pd.DataFrame(rows)
    log.info("%d of %d outbound options complete a round trip within %d min",
             len(out), len(ok), max_total_min)
    return out


def origin_feasibility(profiles: dict, open_time, close_time, visit_min: int,
                       max_total_min: int = 240) -> pd.DataFrame:
    """One row per origin: does any feasible round trip exist, and how long is it."""
    rows = []
    for origin, (out_df, in_df) in profiles.items():
        f = feasible_itineraries(out_df, in_df, open_time, close_time,
                                 visit_min, max_total_min)
        rows.append({
            "origin": origin,
            "feasible": len(f) > 0,
            "n_options": len(f),
            "best_total_min": float(f["total_min"].min()) if len(f) else np.nan,
            # A single feasible itinerary is not resilience: one missed connection
            # and the trip fails, which is why the option count is reported.
            "resilient": len(f) >= 3,
        })
    df = pd.DataFrame(rows).set_index("origin")
    log.info("%d of %d origins have a feasible round trip; %d have three or more options",
             int(df["feasible"].sum()), len(df), int(df["resilient"].sum()))
    return df

Reporting n_options alongside feasibility is what distinguishes a usable journey from a technically possible one. An origin with exactly one feasible itinerary in the whole day has no tolerance for a delayed appointment, a missed connection or a late-running clinic, and treating it as served is the sort of finding that does not survive contact with a patient.

Validation & Edge Cases

1. Test the stranded cases explicitly. Origins where an outbound journey exists but no return does are the most important output of this analysis, and they are exactly what an outbound-only model reports as served:

INFO 14 of 31 outbound options complete a round trip within 240 min
INFO 377 of 900 origins have a feasible round trip; 241 have three or more options
WARNING 63 origins are reachable outbound but have no return service after the visit

2. Use real opening hours, not nominal ones. Many clinics publish hours that differ from their appointment availability, and a service that takes its last appointment at 15:30 while closing at 16:00 has a different effective window.

3. Vary the visit duration. A thirty-minute visit and a three-hour infusion produce very different feasible sets from the same network. Model the actual service, and where several services share a site, model each.

4. Do not cap the total trip time at a convenient number. Four hours is a long day for a routine appointment and the cap materially changes the map, so it should come from a stated standard or be reported as a sensitivity.

5. Check the last return departure specifically. The single most common cause of infeasibility is the last service of the day, and knowing which route and time it is turns an analytic finding into an actionable one — extending one route by ninety minutes may restore access for dozens of origins.

6. Model the appointment time, not just the arrival. Where the service books slots, a patient cannot simply arrive at the most convenient moment: they must arrive before a specific time and leave after it. Restricting the outbound set to services arriving in the half hour before each bookable slot, and the return set to services after the slot plus the visit duration, gives a much tighter and more realistic feasible set than treating the clinic as continuously available.

7. Report feasibility per slot, not only per day. A clinic whose only feasible slots are at 09:30 on Tuesdays is accessible in a technical sense and not in a practical one. The slot-level view identifies which appointment offers a given community can actually take, which is directly useful to a scheduling team.

8. Include the possibility of a one-way solution. Some patients can reach a clinic by transit and return by another means — a volunteer driver scheme, a non-emergency transport benefit. Where such a scheme exists, model it explicitly as an alternative return leg rather than leaving those origins in the stranded set.

9. Treat a same-day round trip as the default question. Overnight stays and multi-day journeys are outside what a routine appointment can ask of a patient, so a feasibility test that permits them is answering a different question than the one a commissioner has.

Reporting resilience rather than bare feasibility changes the picture substantially, and the difference is the population with no margin:

Feasible Origins by Number of Options Origins classified by how many feasible round-trip itineraries exist in the day. Of 900 origins, 523 have none at all, 136 have exactly one, 105 have two, and 136 have three or more. Treating the 377 with at least one as served conceals that 241 of them have fewer than three, so a single missed connection or a late-running clinic removes their access entirely. 377 origins are “served”; 136 have real margin No feasible trip 523 Exactly one option 136 Two options 105 Three or more 136 The middle two rows are access with no tolerance for anything going wrong

Compliance Notes

  • Publish round-trip feasibility rather than outbound reachability for any access claim about people who do not drive. The two differ by enough to change a commissioning decision.
  • Record the visit duration, opening hours and total-trip cap with the result; all three are inputs, and none is a property of the transit network.
  • Report the stranded set separately, since it is the group for whom the intervention is a timetable change rather than a new facility.
  • State the option count distribution, not only the feasible share, because resilience and feasibility are different claims.