def build_stops(pfeed, shapes=None):
    """
    Given a ProtoFeed, return a DataFrame representing ``stops.txt``.
    If ``pfeed.stops`` is not ``None``, then return that.
    Otherwise, require built shapes output by :func:`build_shapes`,
    create one stop at the beginning (the first point) of each shape
    and one at the end (the last point) of each shape,
    and drop stops with duplicate coordinates.
    Note that this will yield one stop for shapes that are loops.
    """
    if pfeed.stops is not None:
        stops = pfeed.stops.copy()
    else:
        if shapes is None:
            raise ValueError('Must input shapes built by build_shapes()')

        geo_shapes = gt.geometrize_shapes(shapes)
        rows = []
        for shape, geom in geo_shapes[['shape_id',
          'geometry']].itertuples(index=False):
            stop_ids = build_stop_ids(shape)
            stop_names = build_stop_names(shape)
            for i in range(2):
                stop_id = stop_ids[i]
                stop_name = stop_names[i]
                stop_lon, stop_lat = geom.interpolate(i,
                  normalized=True).coords[0]
                rows.append([stop_id, stop_name, stop_lon, stop_lat])

        stops = (
            pd.DataFrame(rows, columns=['stop_id', 'stop_name',
              'stop_lon', 'stop_lat'])
            .drop_duplicates(subset=['stop_lon', 'stop_lat'])
        )

    return stops