def build_stop_times(pfeed, routes, shapes, stops, trips, buffer=cs.BUFFER):
    """
    Given a ProtoFeed and its corresponding routes (DataFrame),
    shapes (DataFrame), stops (DataFrame), trips (DataFrame),
    return DataFrame representing ``stop_times.txt``.
    Includes the optional ``shape_dist_traveled`` column.
    Don't make stop times for trips with no nearby stops.
    """
    # Get the table of trips and add frequency and service window details
    routes = (
        routes
        .filter(['route_id', 'route_short_name'])
        .merge(pfeed.frequencies.drop(['shape_id'], axis=1))
    )
    trips = (
        trips
        .assign(service_window_id=lambda x: x.trip_id.map(
          lambda y: y.split(cs.SEP)[2]))
        .merge(routes)
    )

    # Get the geometries of ``shapes`` and not ``pfeed.shapes``
    geometry_by_shape = dict(
        gt.geometrize_shapes(shapes, use_utm=True)
        .filter(['shape_id', 'geometry'])
        .values
    )

    # Save on distance computations by memoizing
    dist_by_stop_by_shape = {shape: {} for shape in geometry_by_shape}

    def compute_stops_dists_times(geo_stops, linestring, shape,
      start_time, end_time):
        """
        Given a GeoDataFrame of stops on one side of a given Shapely
        LineString with given shape ID, compute distances and departure
        times of a trip traversing the LineString from start to end
        at the given start and end times (in seconds past midnight)
        and stopping at the stops encountered along the way.
        Do not assume that the stops are ordered by trip encounter.
        Return three lists of the same length: the stop IDs in order
        that the trip encounters them, the shape distances traveled
        along distances at the stops, and the times the stops are
        encountered, respectively.
        """
        g = geo_stops.copy()
        dists_and_stops = []
        for i, stop in enumerate(g['stop_id'].values):
            if stop in dist_by_stop_by_shape[shape]:
                d = dist_by_stop_by_shape[shape][stop]
            else:
                d = gt.get_segment_length(linestring,
                  g.geometry.iat[i])/1000  # km
                dist_by_stop_by_shape[shape][stop] = d
            dists_and_stops.append((d, stop))
        dists, stops = zip(*sorted(dists_and_stops))
        D = linestring.length/1000
        dists_are_reasonable = all([d < D + 100 for d in dists])
        if not dists_are_reasonable:
            # Assume equal distances between stops :-(
            n = len(stops)
            delta = D/(n - 1)
            dists = [i*delta for i in range(n)]

        # Compute times using distances, start and end stop times,
        # and linear interpolation
        t0, t1 = start_time, end_time
        d0, d1 = dists[0], dists[-1]
        # Interpolate
        times = np.interp(dists, [d0, d1], [t0, t1])
        return stops, dists, times

    # Iterate through trips and set stop times based on stop ID
    # and service window frequency.
    # Remember that every trip has a valid shape ID.
    # Gather stops geographically from ``stops``.
    rows = []
    geo_stops = gt.geometrize_stops(stops, use_utm=True)
    # Look on the side of the traffic side of street for this timezone
    side = cs.traffic_by_timezone[pfeed.meta.agency_timezone.iat[0]]
    for index, row in trips.iterrows():
        shape = row['shape_id']
        geom = geometry_by_shape[shape]
        stops = get_nearby_stops(geo_stops, geom, side, buffer=buffer)
        # Don't make stop times for trips without nearby stops
        if stops.empty:
            continue
        length = geom.length/1000  # km
        speed = row['speed']  # km/h
        duration = int((length/speed)*3600)  # seconds
        frequency = row['frequency']
        if not frequency:
            # No stop times for this trip/frequency combo
            continue
        headway = 3600/frequency  # seconds
        trip = row['trip_id']
        __, route, window, base_timestr, direction, i = (
          trip.split(cs.SEP))
        direction = int(direction)
        base_time = gt.timestr_to_seconds(base_timestr)
        start_time = base_time + headway*int(i)
        end_time = start_time + duration
        stops, dists, times = compute_stops_dists_times(stops, geom, shape,
          start_time, end_time)
        new_rows = [[trip, stop, j, time, time, dist]
          for j, (stop, time, dist) in enumerate(zip(stops, times, dists))]
        rows.extend(new_rows)

    g = pd.DataFrame(rows, columns=['trip_id', 'stop_id', 'stop_sequence',
      'arrival_time', 'departure_time', 'shape_dist_traveled'])

    # Convert seconds back to time strings
    g[['arrival_time', 'departure_time']] =\
      g[['arrival_time', 'departure_time']].applymap(
      lambda x: gt.timestr_to_seconds(x, inverse=True))

    return g
Given a ProtoFeed and its corresponding routes (DataFrame),
    shapes (DataFrame), stops (DataFrame), trips (DataFrame),
    return DataFrame representing ``stop_times.txt``.
    Includes the optional ``shape_dist_traveled`` column.
    Don't make stop times for trips with no nearby stops.