Repository: mie-lab/trackintel

Issue:
generate_locations is broken


There is a huge problem in the `generate_locations` method at the moment. The problem comes from [line 122](https://github.com/mie-lab/trackintel/blame/master/trackintel/preprocessing/staypoints.py#L122) because `ret_stps_non_noise_labels` and `ret_stps` have disjunct indices (see line 105 and 106](https://github.com/mie-lab/trackintel/blame/master/trackintel/preprocessing/staypoints.py#L106)). Therefore the sum in line 122 basically appends `ret_stps["location_id"]` to `ret_stps_non_noise_labels`

Here is code to reproduce the problem:
```python

from shapely.geometry import Point
import pandas as pd
import geopandas as gpd
import datetime
import trackintel as ti


def example_staypoints():
    """Staypoints to load into the database."""
    p1 = Point(8.5067847, 47.4)
    p2 = Point(8.5067847, 47.5)
    p3 = Point(8.5067847, 47.6)

    t1 = pd.Timestamp("1971-01-01 00:00:00", tz="utc")
    t2 = pd.Timestamp("1971-01-01 05:00:00", tz="utc")
    t3 = pd.Timestamp("1971-01-02 07:00:00", tz="utc")
    one_hour = datetime.timedelta(hours=1)

    list_dict = [
        {"user_id": 0, "started_at": t1, "finished_at": t2, "geom": p1},
        {"user_id": 0, "started_at": t2, "finished_at": t3, "geom": p2},
        {"user_id": 1, "started_at": t3, "finished_at": t3 + one_hour, "geom": p3},
    ]
    stps = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:4326")
    stps.index.name = "id"
    assert stps.as_staypoints
    return stps

sp = example_staypoints()
sp2, locs = sp.as_staypoints.generate_locations(method='dbscan', epsilon=30, num_samples=2,
                                                   distance_metric='haversine', agg_level='user')
pd.set_option('display.max_rows', 10)
print(sp)
print(sp2)
```

`sp`
id        user_id ...     geom                     
0         0  ...  POINT (8.50678 47.40000)
1         0  ...  POINT (8.50678 47.50000)
2         1  ...  POINT (8.50678 47.60000)

`sp2`
    
id user_id                started_at  ...                      geom location_id
0       0.0 1971-01-01 00:00:00+00:00  ...  POINT (8.50678 47.40000)        <NA>
1       0.0 1971-01-01 05:00:00+00:00  ...  POINT (8.50678 47.50000)        <NA>
2       1.0 1971-01-02 07:00:00+00:00  ...  POINT (8.50678 47.60000)        <NA>
0       NaN                       NaT  ...                      None        <NA>
1       NaN                       NaT  ...                      None        <NA>
2       NaN                       NaT  ...                      None        <NA>
[6 rows x 5 columns]




Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
> Merging [#282](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (a23de19) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/8f53d0cc6495e53df06554a76adbd66a57dd8e2d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (8f53d0c) will **decrease** coverage by `2.52%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/282/graphs/tree.svg?width=650&height=150&src=pr&token=ITDZEEE1MO&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)

```diff
@@            Coverage Diff             @@
##           master     #282      +/-   ##
==========================================
- Coverage   95.41%   92.88%   -2.53%     
==========================================
  Files          39       39              
  Lines        1700     1700              
  Branches      273      273              
==========================================
- Hits         1622     1579      -43     
- Misses         41       82      +41     
- Partials       37       39       +2     
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/preprocessing/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3N0YXlwb2ludHMucHk=) | `93.50% <100.00%> (ø)` | |
| [trackintel/io/postgis.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9wb3N0Z2lzLnB5) | `49.33% <0.00%> (-45.34%)` | :arrow_down: |
| [trackintel/io/from\_geopandas.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9mcm9tX2dlb3BhbmRhcy5weQ==) | `90.00% <0.00%> (-8.00%)` | :arrow_down: |
| [trackintel/model/trips.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwcy5weQ==) | `96.42% <0.00%> (-3.58%)` | :arrow_down: |
| [trackintel/model/locations.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9sb2NhdGlvbnMucHk=) | `96.87% <0.00%> (-3.13%)` | :arrow_down: |
| [trackintel/model/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9wb3NpdGlvbmZpeGVzLnB5) | `97.82% <0.00%> (-2.18%)` | :arrow_down: |
| [trackintel/model/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9zdGF5cG9pbnRzLnB5) | `98.03% <0.00%> (-1.97%)` | :arrow_down: |
| [trackintel/model/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/282/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwbGVncy5weQ==) | `98.27% <0.00%> (-1.73%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Last update [8f53d0c...a23de19](https://codecov.io/gh/mie-lab/trackintel/pull/282?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).


Patch:
diff --git a/trackintel/analysis/location_identification.py b/trackintel/analysis/location_identification.py
index 7c13b14..399c212 100644
--- a/trackintel/analysis/location_identification.py
+++ b/trackintel/analysis/location_identification.py
@@ -161,7 +161,8 @@ def pre_filter_locations(
     loc_period = loc["period"] >= thresh_loc_period
     loc_filter_agg = loc_sp & loc_time & loc_period
     loc_filter_agg.rename("loc_filter", inplace=True)  # rename for merging
-    loc_filter = pd.merge(spts[groupby_loc], loc_filter_agg.reset_index(), on=groupby_loc, how="left")["loc_filter"]
+    loc_filter = pd.merge(spts[groupby_loc], loc_filter_agg, how="left", left_on=groupby_loc, right_index=True)
+    loc_filter = loc_filter["loc_filter"]
 
     total_filter = user_filter & loc_filter
 
diff --git a/trackintel/model/triplegs.py b/trackintel/model/triplegs.py
index dbfb855..25e1fac 100644
--- a/trackintel/model/triplegs.py
+++ b/trackintel/model/triplegs.py
@@ -1,5 +1,6 @@
 import pandas as pd
 import trackintel as ti
+
 from trackintel.analysis.labelling import predict_transport_mode
 from trackintel.analysis.modal_split import calculate_modal_split
 from trackintel.analysis.tracking_quality import temporal_tracking_quality
@@ -127,15 +128,15 @@ class TriplegsAccessor(object):
         See :func:`trackintel.preprocessing.triplegs.generate_trips`.
         """
         # if spts in kwargs: 'spts' can not be in args as it would be the first argument
-        if "stps_input" in kwargs:
-            return ti.preprocessing.triplegs.generate_trips(tpls_input=self._obj, **kwargs)
+        if "spts" in kwargs:
+            return ti.preprocessing.triplegs.generate_trips(tpls=self._obj, **kwargs)
         # if 'spts' no in kwargs it has to be the first argument in 'args'
         else:
             assert len(args) <= 1, (
                 "All arguments except 'stps_input' have to be given as keyword arguments. You gave"
                 f" {args[1:]} as positional arguments."
             )
-            return ti.preprocessing.triplegs.generate_trips(stps_input=args[0], tpls_input=self._obj, **kwargs)
+            return ti.preprocessing.triplegs.generate_trips(spts=args[0], tpls=self._obj, **kwargs)
 
     @copy_docstring(predict_transport_mode)
     def predict_transport_mode(self, *args, **kwargs):
diff --git a/trackintel/preprocessing/positionfixes.py b/trackintel/preprocessing/positionfixes.py
index ef872d8..d604382 100644
--- a/trackintel/preprocessing/positionfixes.py
+++ b/trackintel/preprocessing/positionfixes.py
@@ -189,7 +189,7 @@ def generate_staypoints(
 
 def generate_triplegs(
     pfs_input,
-    stps_input,
+    stps_input=None,
     method="between_staypoints",
     gap_threshold=15,
 ):
@@ -238,13 +238,16 @@ def generate_triplegs(
     --------
     >>> pfs.as_positionfixes.generate_triplegs('between_staypoints', gap_threshold=15)
     """
-    # copy the original pfs for adding 'staypoint_id' column
+    # copy the original pfs for adding 'tripleg_id' column
     pfs = pfs_input.copy()
 
     # if the positionfixes already have a column "tripleg_id", we drop it
     if "tripleg_id" in pfs:
         pfs.drop(columns="tripleg_id", inplace=True)
 
+    # we need to ensure pfs is properly ordered
+    pfs.sort_values(by=["user_id", "tracked_at"], inplace=True)
+
     if method == "between_staypoints":
 
         # get case:
@@ -295,8 +298,6 @@ def generate_triplegs(
         pfs["tripleg_id"] = pd.NA
         pfs.loc[~pd.isna(pfs["staypoint_id"]), "tripleg_id"] = -1
 
-        # we need to ensure pfs is properly ordered
-        pfs.sort_values(by=["user_id", "tracked_at"], inplace=True)
         # get all conditions that trigger a new tripleg.
         # condition 1: a positionfix belongs to a new tripleg if the user changes. For this we need to sort pfs.
         # The first positionfix of the new user is the start of a new tripleg (if it is no staypoint)
@@ -321,7 +322,6 @@ def generate_triplegs(
         cond_all = cond_new_user | cond_gap | cond_stp
         # make sure not to create triplegs within staypoints:
         cond_all = cond_all & pd.isna(pfs["staypoint_id"])
-        cond_all.sort_index(inplace=True)
 
         # get the start position of tpls
         tpls_starts = np.where(cond_all)[0]
@@ -362,7 +362,8 @@ def generate_triplegs(
         # a valid linestring needs 2 points
         cond_to_remove = np.take(tpls_starts, np.where(tpls_lengths < 2)[0])
         cond_all.iloc[cond_to_remove] = False
-        pfs.loc[pfs.index.isin(cond_to_remove), "tripleg_id"] = -1
+        # Note: cond_to_remove is the array index of pfs.index and not pfs.index itself
+        pfs.loc[pfs.index[cond_to_remove], "tripleg_id"] = -1
 
         # assign an incrementing id to all positionfixes that start a tripleg
         # create triplegs
diff --git a/trackintel/preprocessing/staypoints.py b/trackintel/preprocessing/staypoints.py
index b366e6b..1a520b7 100644
--- a/trackintel/preprocessing/staypoints.py
+++ b/trackintel/preprocessing/staypoints.py
@@ -119,7 +119,7 @@ def generate_locations(
             # fill first nan with 0 and create the cumulative sum
             loc_id_offset = loc_id_offset.fillna(0).cumsum()
 
-            ret_stps_non_noise_labels["location_id"] = ret_stps["location_id"] + loc_id_offset
+            ret_stps_non_noise_labels["location_id"] = ret_stps_non_noise_labels["location_id"] + loc_id_offset
             ret_stps = gpd.GeoDataFrame(pd.concat([ret_stps_non_noise_labels, ret_stps_noise_labels]), geometry=geo_col)
             ret_stps.sort_values(["user_id", "started_at"], inplace=True)
 
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index 50de0eb..ef18e06 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -1,8 +1,7 @@
-import datetime
+import warnings
 
 import numpy as np
 import pandas as pd
-from tqdm import tqdm
 
 
 def smoothen_triplegs(triplegs, tolerance=1.0, preserve_topology=True):
@@ -37,19 +36,17 @@ def smoothen_triplegs(triplegs, tolerance=1.0, preserve_topology=True):
     return ret_tpls
 
 
-def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=False):
+def generate_trips(spts, tpls, gap_threshold=15):
     """Generate trips based on staypoints and triplegs.
 
     Parameters
-    ---------- 
-    stps_input : GeoDataFrame (as trackintel staypoints)
-        The staypoints have to follow the standard definition for staypoints DataFrames.
-                
-    tpls_input : GeoDataFrame (as trackintel triplegs)
-        The triplegs have to follow the standard definition for triplegs DataFrames.
-                
+    ----------
+    spts : GeoDataFrame (as trackintel staypoints)
+
+    tpls : GeoDataFrame (as trackintel triplegs)
+
     gap_threshold : float, default 15 (minutes)
-        Maximum allowed temporal gap size in minutes. If tracking data is missing for more than 
+        Maximum allowed temporal gap size in minutes. If tracking data is missing for more than
         `gap_threshold` minutes, then a new trip begins after the gap.
 
     print_progress : bool, default False
@@ -58,13 +55,13 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=Fals
     Returns
     -------
     staypoints: GeoDataFrame (as trackintel staypoints)
-        the original staypoints with new columns ``[`trip_id`, `prev_trip_id`, `next_trip_id`]``.
-        
+        The original staypoints with new columns ``[`trip_id`, `prev_trip_id`, `next_trip_id`]``.
+
     triplegs: GeoDataFrame (as trackintel triplegs)
         The original triplegs with a new column ``[`trip_id`]``.
-    
+
     trips: GeoDataFrame (as trackintel trips)
-        The generated trips. 
+        The generated trips.
 
     Notes
     -----
@@ -73,10 +70,9 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=Fals
     The function returns altered versions of the input staypoints and triplegs. Staypoints receive the fields
     [`trip_id` `prev_trip_id` and `next_trip_id`], triplegs receive the field [`trip_id`].
     The following assumptions are implemented
-    
-        - All movement before the first and after the last activity is omitted
-        - If we do not record a person for more than `gap_threshold` minutes, \
-            we assume that the person performed an activity in the recording gap and split the trip at the gap.
+
+        - If we do not record a person for more than `gap_threshold` minutes,
+          we assume that the person performed an activity in the recording gap and split the trip at the gap.
         - Trips that start/end in a recording gap can have an unknown origin/destination
         - There are no trips without a (recored) tripleg
 
@@ -89,283 +85,192 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=Fals
     >>> staypoints, triplegs, trips = triplegs.as_triplegs.generate_trips(staypoints)
 
     """
-    assert (
-        "activity" in stps_input.columns
-    ), "staypoints need the column 'activities' \
-                                         to be able to generate trips"
 
-    # we copy the input because we need to add a temporary column
-    tpls = tpls_input.copy()
-    stps = stps_input.copy()
+    assert "activity" in spts.columns, "staypoints need the column 'activities' to be able to generate trips"
 
-    # if the triplegs already have a column "trip_id", we drop it
+    # Copy the input because we add a temporary columns
+    tpls = tpls.copy()
+    spts = spts.copy()
+    gap_threshold = pd.to_timedelta(gap_threshold, unit="min")
+
+    # If the triplegs already have a column "trip_id", we drop it
     if "trip_id" in tpls:
         tpls.drop(columns="trip_id", inplace=True)
+        warnings.warn("Deleted existing column 'trip_id' from tpls.")
 
-    # if the staypoints already have any of the columns  "trip_id", "prev_trip_id", "next_trip_id", we drop them
+    # if the staypoints already have any of the columns "trip_id", "prev_trip_id", "next_trip_id", we drop them
     for col in ["trip_id", "prev_trip_id", "next_trip_id"]:
-        if col in stps:
-            stps.drop(columns=col, inplace=True)
+        if col in spts:
+            spts.drop(columns=col, inplace=True)
+            warnings.warn(f"Deleted column '{col}' from spts.")
 
     tpls["type"] = "tripleg"
-    stps["type"] = "staypoint"
+    spts["type"] = "staypoint"
 
     # create table with relevant information from triplegs and staypoints.
-    stps_tpls = stps[["started_at", "finished_at", "user_id", "type", "activity"]].append(
-        tpls[["started_at", "finished_at", "user_id", "type"]]
+    spts_tpls = pd.concat(
+        [
+            spts[["started_at", "finished_at", "user_id", "type", "activity"]],
+            tpls[["started_at", "finished_at", "user_id", "type"]],
+        ]
     )
+    # transform nan to bool
+    spts_tpls["activity"].fillna(False, inplace=True)
 
     # create ID field from index
-    stps_tpls["id"] = stps_tpls.index
+    spts_tpls["spts_tpls_id"] = spts_tpls.index
 
-    # transform nan to bool
-    stps_tpls["activity"] = stps_tpls["activity"] == True
-
-    stps_tpls.sort_values(by=["user_id", "started_at"], inplace=True)
-    stps_tpls["started_at_next"] = stps_tpls["started_at"].shift(-1)
-    stps_tpls["activity_next"] = stps_tpls["activity"].shift(-1)
-
-    if print_progress:
-        tqdm.pandas(desc="User trip generation")
-        trips = (
-            stps_tpls.groupby(["user_id"], group_keys=False, as_index=False)
-            .progress_apply(_generate_trips_user, gap_threshold=gap_threshold)
-            .reset_index(drop=True)
-        )
-    else:
-        trips = (
-            stps_tpls.groupby(["user_id"], group_keys=False, as_index=False)
-            .apply(_generate_trips_user, gap_threshold=gap_threshold)
-            .reset_index(drop=True)
-        )
-
-    # index management
-    trips["id"] = np.arange(len(trips))
-    trips.set_index("id", inplace=True)
+    spts_tpls.sort_values(by=["user_id", "started_at"], inplace=True)
+
+    # conditions for new trip
+    # start new trip if the user changes
+    condition_new_user = spts_tpls["user_id"] != spts_tpls["user_id"].shift(1)
+
+    # start new trip if there is a new activity (last activity in group)
+    _, _, condition_new_activity = _get_activity_masks(spts_tpls)
+
+    # gap conditions
+    # start new trip after a gap, difference of started next with finish of current.
+    gap = (spts_tpls["started_at"].shift(-1) - spts_tpls["finished_at"]) > gap_threshold
+    condition_time_gap = gap.shift(1, fill_value=False)  # trip starts on next entry
+
+    new_trip = condition_new_user | condition_new_activity | condition_time_gap
+
+    # assign an incrementing id to all triplegs that start a trip
+    # temporary as empty trips are not filtered out yet.
+    spts_tpls.loc[new_trip, "temp_trip_id"] = np.arange(new_trip.sum())
+    spts_tpls["temp_trip_id"].fillna(method="ffill", inplace=True)
+
+    # exclude activities to aggregate trips together.
+    # activity can be thought of as the same aggregation level as trips.
+    spts_tpls_no_act = spts_tpls[~spts_tpls["activity"]]
+    spts_tpls_only_act = spts_tpls[spts_tpls["activity"]]
+
+    trips_grouper = spts_tpls_no_act.groupby("temp_trip_id")
+    trips = trips_grouper.agg(
+        {"user_id": "mean", "started_at": min, "finished_at": max, "type": list, "spts_tpls_id": list}
+    )
+
+    def _seperate_ids(row):
+        """Split aggregated spts_tpls_ids into staypoint ids and tripleg ids columns."""
+        row_type = np.array(row["type"])
+        row_id = np.array(row["spts_tpls_id"])
+        t = row_type == "tripleg"
+        tpls_ids = row_id[t]
+        spts_ids = row_id[~t]
+        # for dropping trips that don't have triplegs
+        tpls_ids = tpls_ids if len(tpls_ids) > 0 else None
+        return [spts_ids, tpls_ids]
+
+    trips[["spts", "tpls"]] = trips.apply(_seperate_ids, axis=1, result_type="expand")
+
+    # drop all trips that don't contain any triplegs
+    trips.dropna(subset=["tpls"], inplace=True)
+
+    # recount trips ignoring empty trips and save trip_id as for id assignment.
+    trips.reset_index(inplace=True, drop=True)
+    trips["trip_id"] = trips.index
+
+    # add gaps as activities, to simplify id assignment.
+    gaps = pd.DataFrame(spts_tpls.loc[gap, "user_id"])
+    gaps["started_at"] = spts_tpls.loc[gap, "finished_at"] + gap_threshold / 2
+    gaps[["type", "activity"]] = ["gap", True]  # nicer for debugging
+
+    # same for user changes
+    user_change = pd.DataFrame(spts_tpls.loc[condition_new_user, "user_id"])
+    user_change["started_at"] = spts_tpls.loc[condition_new_user, "started_at"] - gap_threshold / 2
+    user_change[["type", "activity"]] = ["user_change", True]  # nicer for debugging
+
+    # merge trips with (filler) activities
+    trips.drop(columns=["type", "spts_tpls_id"], inplace=True)  # make space so no overlap with activity "spts_tpls_id"
+    # Inserting `gaps` and `user_change` into the dataframe creates buffers that catch shifted
+    # "staypoint_id" and "trip_id" from corrupting staypoints/trips.
+    trips_with_act = pd.concat((trips, spts_tpls_only_act, gaps, user_change), axis=0, ignore_index=True)
+    trips_with_act.sort_values(["user_id", "started_at"], inplace=True)
+
+    # ID assignment #
+    # add origin/destination ids by shifting
+    trips_with_act["origin_staypoint_id"] = trips_with_act["spts_tpls_id"].shift(1)
+    trips_with_act["destination_staypoint_id"] = trips_with_act["spts_tpls_id"].shift(-1)
+
+    # add prev_trip_id and next_trip_id for activity staypoints
+    trips_with_act["prev_trip_id"] = trips_with_act["trip_id"].shift(1)
+    trips_with_act["next_trip_id"] = trips_with_act["trip_id"].shift(-1)
+    activity_staypoints = trips_with_act[trips_with_act["type"] == "staypoint"].copy()
+    activity_staypoints.index = activity_staypoints["spts_tpls_id"]
+    spts = spts.join(activity_staypoints[["prev_trip_id", "next_trip_id"]], how="left")
+
+    # transform column to binary
+    trips_with_act["activity"].fillna(False, inplace=True)
+    # delete activities
+    trips = trips_with_act[~trips_with_act["activity"]].copy()
+
+    trips.drop(
+        [
+            "type",
+            "spts_tpls_id",
+            "activity",
+            "temp_trip_id",
+            "prev_trip_id",
+            "next_trip_id",
+        ],
+        inplace=True,
+        axis=1,
+    )
 
+    # now handle the data that is aggregated in the trips
     # assign trip_id to tpls
-    trip2tpl_map = trips[["tpls"]].to_dict()["tpls"]
-    ls = []
-    for key, values in trip2tpl_map.items():
-        for value in values:
-            ls.append([value, key])
-    temp = pd.DataFrame(ls, columns=[tpls.index.name, "trip_id"]).set_index(tpls.index.name)
-    tpls = tpls.join(temp, how="left")
-
-    # assign trip_id to stps, for non-activity stps
-    trip2spt_map = trips[["stps"]].to_dict()["stps"]
-    ls = []
-    for key, values in trip2spt_map.items():
-        for value in values:
-            ls.append([value, key])
-    temp = pd.DataFrame(ls, columns=[stps.index.name, "trip_id"]).set_index(stps.index.name)
-    stps = stps.join(temp, how="left")
-
-    # assign prev_trip_id to stps
-    temp = trips[["destination_staypoint_id"]].copy()
-    temp.rename(columns={"destination_staypoint_id": stps.index.name}, inplace=True)
-    temp.index.name = "prev_trip_id"
-    temp = temp.reset_index().set_index(stps.index.name)
-    stps = stps.join(temp, how="left")
-
-    # assign next_trip_id to stps
-    temp = trips[["origin_staypoint_id"]].copy()
-    temp.rename(columns={"origin_staypoint_id": stps.index.name}, inplace=True)
-    temp.index.name = "next_trip_id"
-    temp = temp.reset_index().set_index(stps.index.name)
-    stps = stps.join(temp, how="left")
+    temp = trips.explode("tpls")
+    temp.index = temp["tpls"]
+    temp = temp[temp["tpls"].notna()]
+    tpls = tpls.join(temp["trip_id"], how="left")
+
+    # assign trip_id to spts, for non-activity spts
+    temp = trips.explode("spts")
+    temp.index = temp["spts"]
+    temp = temp[temp["spts"].notna()]
+    spts = spts.join(temp["trip_id"], how="left")
 
     # final cleaning
     tpls.drop(columns=["type"], inplace=True)
-    stps.drop(columns=["type"], inplace=True)
-    trips.drop(columns=["tpls", "stps"], inplace=True)
+    spts.drop(columns=["type"], inplace=True)
+    trips.drop(columns=["tpls", "spts", "trip_id"], inplace=True)
 
-    ## dtype consistency
+    # dtype consistency
     # trips id (generated by this function) should be int64
     trips.index = trips.index.astype("int64")
-    # trip id of stps and tpls can only be in Int64 (missing values)
-    stps["trip_id"] = stps["trip_id"].astype("Int64")
-    stps["prev_trip_id"] = stps["prev_trip_id"].astype("Int64")
-    stps["next_trip_id"] = stps["next_trip_id"].astype("Int64")
+    trips.index.name = "id"  # TODO: some legacy issue for tests
+    # trip id of spts and tpls can only be in Int64 (missing values)
+    spts["trip_id"] = spts["trip_id"].astype("Int64")
+    spts["prev_trip_id"] = spts["prev_trip_id"].astype("Int64")
+    spts["next_trip_id"] = spts["next_trip_id"].astype("Int64")
     tpls["trip_id"] = tpls["trip_id"].astype("Int64")
 
     # user_id of trips should be the same as tpls
     trips["user_id"] = trips["user_id"].astype(tpls["user_id"].dtype)
 
-    return stps, tpls, trips
-
-
-def _generate_trips_user(df, gap_threshold):
-    # function called after groupby: should only contain records of one user
-    user_id = df["user_id"].unique()
-    assert len(user_id) == 1
-    user_id = user_id[0]
-
-    unknown_activity = {"user_id": user_id, "activity": True, "id": np.nan}
-    origin_activity = unknown_activity
-    temp_trip_stack = []
-    in_trip = False
-    trip_ls = []
-
-    for _, row in df.iterrows():
-
-        # check if we can start a new trip
-        # (we make sure that we start the trip with the most recent activity)
-        if in_trip is False:
-            # If there are several activities in a row, we skip until the last one
-            if row["activity"] and row["activity_next"]:
-                continue
-
-            # if this is the last activity before the trip starts, reset the origin
-            elif row["activity"]:
-                origin_activity = row
-                in_trip = True
-                continue
-
-            # if for non-activities we simply start the trip
-            else:
-                in_trip = True
-
-        if in_trip is True:
-            # during trip generation/recording
-
-            # check if trip ends regularly
-            if row["activity"] is True:
-
-                # if there are no triplegs in the trip, set the current activity as origin and start over
-                if not _check_trip_stack_has_tripleg(temp_trip_stack):
-                    origin_activity = row
-                    temp_trip_stack = list()
-                    in_trip = True
-
-                else:
-                    # record trip
-                    destination_activity = row
-                    trip_ls.append(_create_trip_from_stack(temp_trip_stack, origin_activity, destination_activity))
-
-                    # set values for next trip
-                    if row["started_at_next"] - row["finished_at"] > datetime.timedelta(minutes=gap_threshold):
-                        # if there is a gap after this trip the origin of the next trip is unknown
-                        origin_activity = unknown_activity
-                        destination_activity = None
-                        temp_trip_stack = list()
-                        in_trip = False
-
-                    else:
-                        # if there is no gap after this trip the origin of the next trip is the destination of the
-                        # current trip
-                        origin_activity = destination_activity
-                        destination_activity = None
-                        temp_trip_stack = list()
-                        in_trip = False
-
-            # check if gap during the trip
-            elif row["started_at_next"] - row["finished_at"] > datetime.timedelta(minutes=gap_threshold):
-                # in case of a gap, the destination of the current trip and the origin of the next trip
-                # are unknown.
-
-                # add current item to trip
-                temp_trip_stack.append(row)
-
-                # if the trip has no recored triplegs, we do not generate the current trip.
-                if not _check_trip_stack_has_tripleg(temp_trip_stack):
-                    origin_activity = unknown_activity
-                    in_trip = True
-                    temp_trip_stack = list()
-
-                else:
-                    # add tripleg to trip, generate trip, start new trip with unknown origin
-                    destination_activity = unknown_activity
-
-                    trip_ls.append(_create_trip_from_stack(temp_trip_stack, origin_activity, destination_activity))
-                    origin_activity = unknown_activity
-                    destination_activity = None
-                    temp_trip_stack = list()
-                    in_trip = True
-
-            else:
-                temp_trip_stack.append(row)
-
-    # if user ends generate last trip with unknown destination
-    if (len(temp_trip_stack) > 0) and (_check_trip_stack_has_tripleg(temp_trip_stack)):
-        destination_activity = unknown_activity
-        trip_ls.append(
-            _create_trip_from_stack(
-                temp_trip_stack,
-                origin_activity,
-                destination_activity,
-            )
-        )
-
-    # print(trip_ls)
-    trips = pd.DataFrame(trip_ls)
-    return trips
-
-
-def _check_trip_stack_has_tripleg(temp_trip_stack):
-    """
-    Check if a trip has at least 1 tripleg.
-
-    Parameters
-    ----------
-    temp_trip_stack : list
-        list of dictionary like elements (either pandas series or python dictionary).
-        Contains all elements that will be aggregated into a trip
-
-    Returns
-    -------
-    has_tripleg: Bool
-    """
-    has_tripleg = False
-    for row in temp_trip_stack:
-        if row["type"] == "tripleg":
-            has_tripleg = True
-            break
+    return spts, tpls, trips
 
-    return has_tripleg
 
+def _get_activity_masks(df):
+    """Split activities into three groups depending if other activities.
 
-def _create_trip_from_stack(temp_trip_stack, origin_activity, destination_activity):
-    """
-    Aggregate information of trip elements in a structured dictionary.
+    Tell if activity is first (trip end), intermediate (can be deleted), or last (trip starts).
+    First and last are intended to overlap.
 
     Parameters
     ----------
-    temp_trip_stack : list
-        list of dictionary like elements (either pandas series or python dictionary).
-        Contains all elements that will be aggregated into a trip
-
-    origin_activity : dictionary like
-        Either dictionary or pandas series
-
-    destination_activity : dictionary like
-        Either dictionary or pandas series
+    df : DataFrame
+        DataFrame with boolean column "activity".
 
     Returns
     -------
-    trip_dict_entry: dictionary
-
+    is_first, is_inter, is_last
+        Three boolean Series
     """
-    # this function return and empty dict if no tripleg is in the stack
-    first_trip_element = temp_trip_stack[0]
-    last_trip_element = temp_trip_stack[-1]
-
-    # all data has to be from the same user
-    assert origin_activity["user_id"] == last_trip_element["user_id"]
-
-    # double check if trip requirements are fulfilled
-    assert origin_activity["activity"] == True
-    assert destination_activity["activity"] == True
-    assert first_trip_element["activity"] == False
-
-    trip_dict_entry = {
-        "user_id": origin_activity["user_id"],
-        "started_at": first_trip_element["started_at"],
-        "finished_at": last_trip_element["finished_at"],
-        "origin_staypoint_id": origin_activity["id"],
-        "destination_staypoint_id": destination_activity["id"],
-        "tpls": [tripleg["id"] for tripleg in temp_trip_stack if tripleg["type"] == "tripleg"],
-        "stps": [tripleg["id"] for tripleg in temp_trip_stack if tripleg["type"] == "staypoint"],
-    }
-
-    return trip_dict_entry
+    prev_activity = df["activity"].shift(1, fill_value=False)
+    next_activity = df["activity"].shift(-1, fill_value=False)
+    is_first = df["activity"] & ~prev_activity
+    is_last = df["activity"] & ~next_activity
+    is_inter = df["activity"] & ~is_first & ~is_last
+    return is_first, is_inter, is_last


Test patch:
diff --git a/tests/analysis/test_location_identification.py b/tests/analysis/test_location_identification.py
index 6b31b34..9788293 100644
--- a/tests/analysis/test_location_identification.py
+++ b/tests/analysis/test_location_identification.py
@@ -6,6 +6,7 @@ import pandas as pd
 import pytest
 import trackintel as ti
 from geopandas.testing import assert_geodataframe_equal
+from pandas.testing import assert_index_equal
 from shapely.geometry import Point
 from trackintel.analysis.location_identification import (
     _freq_assign,
@@ -106,6 +107,13 @@ class TestPre_Filter:
         with pytest.raises(ValueError):
             pre_filter_locations(example_staypoints, **default_kwargs)
 
+    def test_non_continous_index(self, example_staypoints, default_kwargs):
+        """Test if function works with non-continous index."""
+        # issue-#247
+        example_staypoints.index = [0, 999, 1, 15]
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert_index_equal(f.index, example_staypoints.index)
+
 
 @pytest.fixture
 def example_freq():
diff --git a/tests/preprocessing/test_positionfixes.py b/tests/preprocessing/test_positionfixes.py
index f5a9d14..c1ec21d 100644
--- a/tests/preprocessing/test_positionfixes.py
+++ b/tests/preprocessing/test_positionfixes.py
@@ -45,12 +45,17 @@ def example_positionfixes():
 
 
 @pytest.fixture
-def isolated_positionfixes():
-    """Positionfixes with two isolated positionfixes that have the same geometry but different timestamps."""
+def example_positionfixes_isolated():
+    """
+    Positionfixes with isolated positionfixes.
+
+    User1 has the same geometry but different timestamps.
+    User2 has different geometry and timestamps.
+    User3 has only one isolated positionfix.
+    """
     p1 = Point(8.5067847, 47.4)
     p2 = Point(8.5067847, 47.5)
     p3 = Point(8.5067847, 47.6)
-    p4 = Point(8.5067847, 47.6)
 
     t1 = pd.Timestamp("1971-01-01 00:00:00", tz="utc")
     t2 = pd.Timestamp("1971-01-02 01:01:00", tz="utc")
@@ -65,7 +70,10 @@ def isolated_positionfixes():
         {"user_id": 1, "tracked_at": t1, "geometry": p1, "staypoint_id": 2},
         {"user_id": 1, "tracked_at": t2, "geometry": p2, "staypoint_id": pd.NA},
         {"user_id": 1, "tracked_at": t3, "geometry": p3, "staypoint_id": pd.NA},
-        {"user_id": 1, "tracked_at": t4, "geometry": p4, "staypoint_id": 3},
+        {"user_id": 1, "tracked_at": t4, "geometry": p3, "staypoint_id": 3},
+        {"user_id": 2, "tracked_at": t1, "geometry": p1, "staypoint_id": 4},
+        {"user_id": 2, "tracked_at": t2, "geometry": p2, "staypoint_id": pd.NA},
+        {"user_id": 2, "tracked_at": t4, "geometry": p3, "staypoint_id": 5},
     ]
     pfs = gpd.GeoDataFrame(data=list_dict, geometry="geometry", crs="EPSG:4326")
     pfs.index.name = "id"
@@ -228,9 +236,31 @@ class TestGenerate_staypoints:
 class TestGenerate_triplegs:
     """Tests for generate_triplegs() method."""
 
-    def test_invalid_isolates(self, isolated_positionfixes):
+    def test_noncontinuous_unordered_index(self, example_positionfixes_isolated):
+        """The unordered and noncontinuous index of pfs shall not affect the generate_triplegs() result."""
+        pfs = example_positionfixes_isolated
+
+        # regenerate noncontinuous index
+        pfs.index = range(0, pfs.shape[0] * 2, 2)
+        pfs.index.name = "id"
+        # shuffle index
+        pfs = pfs.sample(frac=1)
+
+        # a warning shall raise due to the duplicated positionfixes
+        warn_string = "The positionfixes with ids .* lead to invalid tripleg geometries."
+        with pytest.warns(UserWarning, match=warn_string):
+            pfs, tpls = pfs.as_positionfixes.generate_triplegs()
+
+        # the index shall be reordered
+        assert np.all(pfs.index == range(0, pfs.shape[0] * 2, 2))
+
+        # only user 1 has generated a tripleg.
+        # user 0 and 2 tripleg has invalid geometry (two identical points and only one point respectively)
+        assert (tpls["user_id"].unique() == [1]) and (len(tpls) == 1)
+
+    def test_invalid_isolates(self, example_positionfixes_isolated):
         """Triplegs generated from isolated duplicates are dropped."""
-        pfs = isolated_positionfixes
+        pfs = example_positionfixes_isolated
 
         # a warning shall raise due to the duplicated positionfixes
         warn_string = "The positionfixes with ids .* lead to invalid tripleg geometries."
@@ -242,7 +272,7 @@ class TestGenerate_triplegs:
         assert tpls.user_id.iloc[0] == 1
 
     def test_duplicate_columns(self, geolife_pfs_stps_long):
-        """Test if running the function twice, the generated column does not yield exception in join statement"""
+        """Test if running the function twice, the generated column does not yield exception in join statement."""
 
         # we run generate_triplegs twice in order to check that the extra column (tripleg_id) does
         # not cause any problems in the second run
@@ -255,15 +285,16 @@ class TestGenerate_triplegs:
     def test_user_without_stps(self, geolife_pfs_stps_long):
         """Check if it is safe to have users that have pfs but no stps."""
         pfs, stps = geolife_pfs_stps_long
-        # test for case 1
         # manually change the first pfs' user_id, which has no stp correspondence
         pfs.loc[0, "user_id"] = 5000
+
+        ## test for case 1
         _, tpls_1 = pfs.as_positionfixes.generate_triplegs(stps, method="between_staypoints")
         # result should be the same ommiting the first row
         _, tpls_2 = pfs.iloc[1:].as_positionfixes.generate_triplegs(stps, method="between_staypoints")
         assert_geodataframe_equal(tpls_1, tpls_2)
 
-        # test for case 2
+        ## test for case 2
         pfs.drop(columns="staypoint_id", inplace=True)
         # manually change the first pfs' user_id, which has no stp correspondence
         _, tpls_1 = pfs.as_positionfixes.generate_triplegs(stps, method="between_staypoints")
@@ -278,7 +309,6 @@ class TestGenerate_triplegs:
         _, tpls_case1 = pfs.as_positionfixes.generate_triplegs(stps, method="between_staypoints")
         # only keep pfs where staypoint id is nan
         pfs_nostps = pfs[pd.isna(pfs["staypoint_id"])].drop(columns="staypoint_id")
-        print(pfs_nostps)
         _, tpls_case2 = pfs_nostps.as_positionfixes.generate_triplegs(stps, method="between_staypoints")
 
         assert_geodataframe_equal(tpls_case1, tpls_case2)
diff --git a/tests/preprocessing/test_staypoints.py b/tests/preprocessing/test_staypoints.py
index 691a3e4..463cf5a 100644
--- a/tests/preprocessing/test_staypoints.py
+++ b/tests/preprocessing/test_staypoints.py
@@ -1,8 +1,10 @@
+import datetime
 import os
 
-import numpy as np
 import geopandas as gpd
+import numpy as np
 import pandas as pd
+import pytest
 from shapely.geometry import Point
 from sklearn.cluster import DBSCAN
 
@@ -10,6 +12,48 @@ import trackintel as ti
 from trackintel.geogr.distances import calculate_distance_matrix
 
 
+@pytest.fixture
+def example_staypoints():
+    """Staypoints for location generation.
+    Staypoints have non-continous ids and should result in noise and several locations per user.
+    The following staypoint ids should form a location (1, 15), (5,6), (80, 3)
+    The following staypoint ids should be noise (2, 7)
+
+    for agg_level="dataset"
+    The following staypoint ids should form a location (1, 15), (5,6, 80, 3),
+    The following staypoint ids should be noise (2, 7)
+    """
+    p1 = Point(8.5067847, 47.4)
+    p2 = Point(8.5067847, 47.5)
+    p3 = Point(8.5067847, 47.6)
+    p4 = Point(8.5067847, 47.7)
+
+    t1 = pd.Timestamp("1971-01-01 00:00:00", tz="utc")
+    t2 = pd.Timestamp("1971-01-01 05:00:00", tz="utc")
+    t3 = pd.Timestamp("1971-01-02 07:00:00", tz="utc")
+    t4 = pd.Timestamp("1971-01-02 08:00:00", tz="utc")
+    t5 = pd.Timestamp("1971-01-02 09:00:00", tz="utc")
+    t6 = pd.Timestamp("1971-01-02 10:00:00", tz="utc")
+    t6 = pd.Timestamp("1971-01-02 10:00:00", tz="utc")
+    t6 = pd.Timestamp("1971-01-02 10:00:00", tz="utc")
+    one_hour = datetime.timedelta(hours=1)
+
+    list_dict = [
+        {"id": 1, "user_id": 0, "started_at": t1, "finished_at": t2, "geom": p1},
+        {"id": 5, "user_id": 0, "started_at": t2, "finished_at": t3, "geom": p2},
+        {"id": 2, "user_id": 0, "started_at": t3, "finished_at": t4, "geom": p3},
+        {"id": 6, "user_id": 0, "started_at": t4, "finished_at": t5, "geom": p2},
+        {"id": 15, "user_id": 0, "started_at": t5, "finished_at": t6, "geom": p1},
+        {"id": 7, "user_id": 1, "started_at": t3, "finished_at": t4, "geom": p4},
+        {"id": 80, "user_id": 1, "started_at": t4, "finished_at": t5, "geom": p2},
+        {"id": 3, "user_id": 1, "started_at": t5, "finished_at": t6, "geom": p2},
+    ]
+    sp = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:4326")
+    sp = sp.set_index("id")
+    assert sp.as_staypoints
+    return sp
+
+
 class TestGenerate_locations:
     """Tests for generate_locations() method."""
 
@@ -216,3 +260,40 @@ class TestGenerate_locations:
         staypoints.as_staypoints.generate_locations(print_progress=False)
         captured_print = capsys.readouterr()
         assert captured_print.err == ""
+
+    def test_index_stability(self, example_staypoints):
+        """Test if the index of the staypoints remains stable"""
+        sp = example_staypoints
+        sp2, locs = sp.as_staypoints.generate_locations(
+            method="dbscan", epsilon=10, num_samples=2, distance_metric="haversine", agg_level="user"
+        )
+        assert sp.index.equals(sp2.index)
+
+    def test_location_ids_and_noise(self, example_staypoints):
+        """Test if all test cases in the example_staypoints dataset get identified correctly.
+        See docstring of example_staypoints for more information"""
+        sp = example_staypoints
+        sp2, locs = sp.as_staypoints.generate_locations(
+            method="dbscan", epsilon=10, num_samples=2, distance_metric="haversine", agg_level="user"
+        )
+        assert sp2.loc[1, "location_id"] == sp2.loc[15, "location_id"]
+        assert sp2.loc[5, "location_id"] == sp2.loc[6, "location_id"]
+        assert sp2.loc[80, "location_id"] == sp2.loc[3, "location_id"]
+        assert sp2.loc[1, "location_id"] != sp2.loc[6, "location_id"]
+        assert sp2.loc[1, "location_id"] != sp2.loc[80, "location_id"]
+
+        assert sp2.loc[[2, 7], "location_id"].isnull().all()
+
+    def test_location_ids_and_noise_dataset(self, example_staypoints):
+        """Test if all test cases in the example_staypoints dataset get identified correctly.
+        See docstring of example_staypoints for more information"""
+        sp = example_staypoints
+        sp2, locs = sp.as_staypoints.generate_locations(
+            method="dbscan", epsilon=10, num_samples=2, distance_metric="haversine", agg_level="dataset"
+        )
+
+        assert (sp2.loc[[5, 6, 80, 3], "location_id"] == sp2.loc[5, "location_id"]).all()
+        assert sp2.loc[1, "location_id"] == sp2.loc[15, "location_id"]
+        assert sp2.loc[1, "location_id"] != sp2.loc[5, "location_id"]
+
+        assert sp2.loc[[2, 7], "location_id"].isnull().all()
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 5d59730..4a7aa1d 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -1,3 +1,4 @@
+import datetime
 import os
 
 import geopandas as gpd
@@ -5,9 +6,11 @@ import numpy as np
 import pandas as pd
 import pytest
 from geopandas.testing import assert_geodataframe_equal
-from shapely.geometry import Point, LineString
+from pandas.testing import assert_frame_equal, assert_series_equal
+from shapely.geometry import LineString, Point
 
 import trackintel as ti
+from trackintel.preprocessing.triplegs import generate_trips
 
 
 class TestSmoothen_triplegs:
@@ -33,9 +36,6 @@ class TestGenerate_trips:
 
     def test_duplicate_columns(self):
         """Test if running the function twice, the generated column does not yield exception in join statement"""
-        # load pregenerated trips
-        trips_loaded = ti.read_trips_csv(os.path.join("tests", "data", "geolife_long", "trips.csv"), index_col="id")
-
         # create trips from geolife (based on positionfixes)
         pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
         pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
@@ -43,8 +43,9 @@ class TestGenerate_trips:
         pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        stps_run_1, tpls_run_1, _ = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
-        stps_run_2, tpls_run_2, _ = ti.preprocessing.triplegs.generate_trips(stps_run_1, tpls_run_1, gap_threshold=15)
+        stps_run_1, tpls_run_1, _ = generate_trips(stps, tpls, gap_threshold=15)
+        with pytest.warns(UserWarning):
+            stps_run_2, tpls_run_2, _ = generate_trips(stps_run_1, tpls_run_1, gap_threshold=15)
 
         assert set(tpls_run_1.columns) == set(tpls_run_2.columns)
         assert set(stps_run_1.columns) == set(stps_run_2.columns)
@@ -63,9 +64,9 @@ class TestGenerate_trips:
         pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+        stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
         # test if generated trips are equal
-        pd.testing.assert_frame_equal(trips_loaded, trips)
+        assert_frame_equal(trips_loaded, trips)
 
     def test_accessor(self):
         """Test if the accessor leads to the same results as the explicit function."""
@@ -96,22 +97,22 @@ class TestGenerate_trips:
 
         # prepare data
         pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
-        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
-        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
-        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
+        spts = spts.as_staypoints.create_activity_flag(time_threshold=15)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
 
         # accessor with only arguments (not allowed)
         with pytest.raises(AssertionError):
-            _, _, _ = tpls.as_triplegs.generate_trips(stps, 15)
+            _, _, _ = tpls.as_triplegs.generate_trips(spts, 15)
 
         # accessor with only keywords
-        stps_1, tpls_1, trips_1 = tpls.as_triplegs.generate_trips(stps_input=stps, gap_threshold=15)
+        spts_1, tpls_1, trips_1 = tpls.as_triplegs.generate_trips(spts=spts, gap_threshold=15)
 
         # accessor with mixed arguments/keywords
-        stps_2, tpls_2, trips_2 = tpls.as_triplegs.generate_trips(stps, gap_threshold=15)
+        spts_2, tpls_2, trips_2 = tpls.as_triplegs.generate_trips(spts, gap_threshold=15)
 
         # test if generated trips are equal (1,2)
-        assert_geodataframe_equal(stps_1, stps_2)
+        assert_geodataframe_equal(spts_1, spts_2)
         assert_geodataframe_equal(tpls_1, tpls_2)
         pd.testing.assert_frame_equal(trips_1, trips_2)
 
@@ -124,7 +125,7 @@ class TestGenerate_trips:
         pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        stps, tpls, _ = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+        stps, tpls, _ = generate_trips(stps, tpls, gap_threshold=15)
         assert pd.isna(stps["trip_id"]).any()
         assert pd.isna(stps["prev_trip_id"]).any()
         assert pd.isna(stps["next_trip_id"]).any()
@@ -138,7 +139,7 @@ class TestGenerate_trips:
         pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        stps, tpls, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+        stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
 
         assert stps["user_id"].dtype == trips["user_id"].dtype
         assert trips.index.dtype == "int64"
@@ -148,6 +149,26 @@ class TestGenerate_trips:
         assert stps["next_trip_id"].dtype == "Int64"
         assert tpls["trip_id"].dtype == "Int64"
 
+    def test_compare_to_old_trip_function(self):
+        """Test if we can generate the example trips based on example data."""
+        # load pregenerated trips
+
+        # create trips from geolife (based on positionfixes)
+        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
+        pfs, stps = pfs.as_positionfixes.generate_staypoints(method="sliding", dist_threshold=25, time_threshold=5)
+        stps = stps.as_staypoints.create_activity_flag(time_threshold=15)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
+
+        # generate trips and a joint staypoint/triplegs dataframe
+        stps, tpls, trips = generate_trips(stps, tpls, gap_threshold=15)
+        stps_, tpls_, trips_ = _generate_trips_old(stps, tpls, gap_threshold=15)
+
+        # test if generated trips are equal
+        # ignore column order and index dtype
+        assert_frame_equal(trips, trips_, check_like=True, check_index_type=False)
+        assert_frame_equal(stps, stps_, check_like=True, check_index_type=False)
+        assert_frame_equal(tpls, tpls_, check_like=True, check_index_type=False)
+
     def test_generate_trips_index_start(self):
         """Test the generated index start from 0 for different methods."""
         pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife_long"))
@@ -156,7 +177,7 @@ class TestGenerate_trips:
         pfs, tpls = pfs.as_positionfixes.generate_triplegs(stps)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        _, _, trips = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=15)
+        _, _, trips = generate_trips(stps, tpls, gap_threshold=15)
 
         assert (trips.index == np.arange(len(trips))).any()
 
@@ -215,14 +236,14 @@ class TestGenerate_trips:
         stps_tpls_loaded["finished_at"] = pd.to_datetime(stps_tpls_loaded["finished_at"], utc=True)
 
         # generate trips and a joint staypoint/triplegs dataframe
-        stps_proc, tpls_proc, trips = ti.preprocessing.triplegs.generate_trips(stps_in, tpls_in, gap_threshold=15)
+        stps_proc, tpls_proc, trips = generate_trips(stps_in, tpls_in, gap_threshold=gap_threshold)
         stps_tpls = _create_debug_stps_tpls_data(stps_proc, tpls_proc, gap_threshold=gap_threshold)
 
         # test if generated trips are equal
-        pd.testing.assert_frame_equal(trips_loaded, trips)
+        assert_frame_equal(trips_loaded, trips)
 
         # test if generated staypoints/triplegs are equal (especially important for trip ids)
-        pd.testing.assert_frame_equal(stps_tpls_loaded, stps_tpls, check_dtype=False)
+        assert_frame_equal(stps_tpls_loaded, stps_tpls, check_dtype=False)
 
     def test_generate_trips_id_management(self):
         """Test if we can generate the example trips based on example data."""
@@ -241,11 +262,37 @@ class TestGenerate_trips:
 
         # generate trips and a joint staypoint/triplegs dataframe
         gap_threshold = 15
-        stps, tpls, _ = ti.preprocessing.triplegs.generate_trips(stps, tpls, gap_threshold=gap_threshold)
+        stps, tpls, _ = generate_trips(stps, tpls, gap_threshold=gap_threshold)
         stps_tpls = _create_debug_stps_tpls_data(stps, tpls, gap_threshold=gap_threshold)
 
         # test if generated staypoints/triplegs are equal (especially important for trip ids)
-        pd.testing.assert_frame_equal(stps_tpls_loaded, stps_tpls, check_dtype=False)
+        assert_frame_equal(stps_tpls_loaded, stps_tpls, check_dtype=False)
+
+    def test_only_staypoints_in_trip(self):
+        """Test that trips with only staypoints (non-activities) are deleted."""
+        start = pd.Timestamp("2021-07-11 8:00:00")
+        h = pd.to_timedelta("1h")
+        spts_tpls = [
+            {"activity": True, "type": "staypoint"},
+            {"activity": False, "type": "staypoint"},
+            {"activity": True, "type": "staypoint"},
+            {"activity": False, "type": "tripleg"},
+            {"activity": False, "type": "staypoint"},
+            {"activity": True, "type": "staypoint"},
+        ]
+        for n, d in enumerate(spts_tpls):
+            d["user_id"] = 0
+            d["started_at"] = start + n * h
+            d["finished_at"] = d["started_at"] + h
+        spts_tpls = pd.DataFrame(spts_tpls)
+        spts = spts_tpls[spts_tpls["type"] == "staypoint"]
+        tpls = spts_tpls[spts_tpls["type"] == "tripleg"]
+        spts_, tpls_, trips = generate_trips(spts, tpls)
+        trip_id_truth = pd.Series([None, None, None, 0, None], dtype="Int64")
+        trip_id_truth.index = spts_.index  # don't check index
+        assert_series_equal(spts_["trip_id"], trip_id_truth, check_names=False)
+        assert (tpls_["trip_id"] == 0).all()
+        assert len(trips) == 1
 
 
 def _create_debug_stps_tpls_data(stps, tpls, gap_threshold):
@@ -266,3 +313,330 @@ def _create_debug_stps_tpls_data(stps, tpls, gap_threshold):
     stps_tpls["gap"] = (stps_tpls["started_at_next"] - stps_tpls["finished_at"]).dt.seconds / 60 > gap_threshold
 
     return stps_tpls
+
+
+def _generate_trips_old(stps_input, tpls_input, gap_threshold=15, print_progress=False):
+    """Generate trips based on staypoints and triplegs.
+
+    Parameters
+    ----------
+    stps_input : GeoDataFrame (as trackintel staypoints)
+        The staypoints have to follow the standard definition for staypoints DataFrames.
+
+    tpls_input : GeoDataFrame (as trackintel triplegs)
+        The triplegs have to follow the standard definition for triplegs DataFrames.
+
+    gap_threshold : float, default 15 (minutes)
+        Maximum allowed temporal gap size in minutes. If tracking data is missing for more than
+        `gap_threshold` minutes, then a new trip begins after the gap.
+
+    Returns
+    -------
+    staypoints: GeoDataFrame (as trackintel staypoints)
+        the original staypoints with new columns ``[`trip_id`, `prev_trip_id`, `next_trip_id`]``.
+
+    triplegs: GeoDataFrame (as trackintel triplegs)
+        The original triplegs with a new column ``[`trip_id`]``.
+
+    trips: GeoDataFrame (as trackintel trips)
+        The generated trips.
+
+    Notes
+    -----
+    Trips are an aggregation level in transport planning that summarize all movement and all non-essential actions
+    (e.g., waiting) between two relevant activities.
+    The function returns altered versions of the input staypoints and triplegs. Staypoints receive the fields
+    [`trip_id` `prev_trip_id` and `next_trip_id`], triplegs receive the field [`trip_id`].
+    The following assumptions are implemented
+
+        - All movement before the first and after the last activity is omitted
+        - If we do not record a person for more than `gap_threshold` minutes,
+          we assume that the person performed an activity in the recording gap and split the trip at the gap.
+        - Trips that start/end in a recording gap can have an unknown origin/destination
+        - There are no trips without a (recored) tripleg
+
+    Examples
+    --------
+    >>> staypoints, triplegs, trips = generate_trips(staypoints, triplegs)
+    """
+    assert "activity" in stps_input.columns, "staypoints need the column 'activities' to be able to generate trips"
+
+    # we copy the input because we need to add a temporary column
+    tpls = tpls_input.copy()
+    stps = stps_input.copy()
+
+    # if the triplegs already have a column "trip_id", we drop it
+    if "trip_id" in tpls:
+        tpls.drop(columns="trip_id", inplace=True)
+
+    # if the staypoints already have any of the columns "trip_id", "prev_trip_id", "next_trip_id", we drop them
+    for col in ["trip_id", "prev_trip_id", "next_trip_id"]:
+        if col in stps:
+            stps.drop(columns=col, inplace=True)
+
+    tpls["type"] = "tripleg"
+    stps["type"] = "staypoint"
+
+    # create table with relevant information from triplegs and staypoints.
+    stps_tpls = pd.concat(
+        [
+            stps[["started_at", "finished_at", "user_id", "type", "activity"]],
+            tpls[["started_at", "finished_at", "user_id", "type"]],
+        ]
+    )
+
+    # create ID field from index
+    stps_tpls["id"] = stps_tpls.index
+
+    # transform nan to bool
+    stps_tpls["activity"] = stps_tpls["activity"] == True
+
+    stps_tpls.sort_values(by=["user_id", "started_at"], inplace=True)
+    stps_tpls["started_at_next"] = stps_tpls["started_at"].shift(-1)
+    stps_tpls["activity_next"] = stps_tpls["activity"].shift(-1)
+
+    if print_progress:
+        tqdm.pandas(desc="User trip generation")
+        trips = (
+            stps_tpls.groupby(["user_id"], group_keys=False, as_index=False)
+            .progress_apply(_generate_trips_user, gap_threshold=gap_threshold)
+            .reset_index(drop=True)
+        )
+    else:
+        trips = (
+            stps_tpls.groupby(["user_id"], group_keys=False, as_index=False)
+            .apply(_generate_trips_user, gap_threshold=gap_threshold)
+            .reset_index(drop=True)
+        )
+
+    # index management
+    trips["id"] = np.arange(len(trips))
+    trips.set_index("id", inplace=True)
+
+    # assign trip_id to tpls
+    trip2tpl_map = trips[["tpls"]].to_dict()["tpls"]
+    ls = []
+    for key, values in trip2tpl_map.items():
+        for value in values:
+            ls.append([value, key])
+    temp = pd.DataFrame(ls, columns=[tpls.index.name, "trip_id"]).set_index(tpls.index.name)
+    tpls = tpls.join(temp, how="left")
+
+    # assign trip_id to stps, for non-activity stps
+    trip2spt_map = trips[["stps"]].to_dict()["stps"]
+    ls = []
+    for key, values in trip2spt_map.items():
+        for value in values:
+            ls.append([value, key])
+    temp = pd.DataFrame(ls, columns=[stps.index.name, "trip_id"]).set_index(stps.index.name)
+    stps = stps.join(temp, how="left")
+
+    # assign prev_trip_id to stps
+    temp = trips[["destination_staypoint_id"]].copy()
+    temp.rename(columns={"destination_staypoint_id": stps.index.name}, inplace=True)
+    temp.index.name = "prev_trip_id"
+    temp = temp.reset_index().set_index(stps.index.name)
+    stps = stps.join(temp, how="left")
+
+    # assign next_trip_id to stps
+    temp = trips[["origin_staypoint_id"]].copy()
+    temp.rename(columns={"origin_staypoint_id": stps.index.name}, inplace=True)
+    temp.index.name = "next_trip_id"
+    temp = temp.reset_index().set_index(stps.index.name)
+    stps = stps.join(temp, how="left")
+
+    # final cleaning
+    tpls.drop(columns=["type"], inplace=True)
+    stps.drop(columns=["type"], inplace=True)
+    trips.drop(columns=["tpls", "stps"], inplace=True)
+
+    ## dtype consistency
+    # trips id (generated by this function) should be int64
+    trips.index = trips.index.astype("int64")
+    # trip id of stps and tpls can only be in Int64 (missing values)
+    stps["trip_id"] = stps["trip_id"].astype("Int64")
+    stps["prev_trip_id"] = stps["prev_trip_id"].astype("Int64")
+    stps["next_trip_id"] = stps["next_trip_id"].astype("Int64")
+    tpls["trip_id"] = tpls["trip_id"].astype("Int64")
+
+    # user_id of trips should be the same as tpls
+    trips["user_id"] = trips["user_id"].astype(tpls["user_id"].dtype)
+
+    return stps, tpls, trips
+
+
+def _generate_trips_user(df, gap_threshold):
+    # function called after groupby: should only contain records of one user
+    user_id = df["user_id"].unique()
+    assert len(user_id) == 1
+    user_id = user_id[0]
+
+    unknown_activity = {"user_id": user_id, "activity": True, "id": np.nan}
+    origin_activity = unknown_activity
+    temp_trip_stack = []
+    in_trip = False
+    trip_ls = []
+
+    for _, row in df.iterrows():
+
+        # check if we can start a new trip
+        # (we make sure that we start the trip with the most recent activity)
+        if in_trip is False:
+            # If there are several activities in a row, we skip until the last one
+            if row["activity"] and row["activity_next"]:
+                continue
+
+            # if this is the last activity before the trip starts, reset the origin
+            elif row["activity"]:
+                origin_activity = row
+                in_trip = True
+                continue
+
+            # if for non-activities we simply start the trip
+            else:
+                in_trip = True
+
+        if in_trip is True:
+            # during trip generation/recording
+
+            # check if trip ends regularly
+            is_gap = row["started_at_next"] - row["finished_at"] > datetime.timedelta(minutes=gap_threshold)
+            if row["activity"] is True:
+
+                # if there are no triplegs in the trip, set the current activity as origin and start over
+                if not _check_trip_stack_has_tripleg(temp_trip_stack):
+                    origin_activity = row
+                    temp_trip_stack = list()
+                    in_trip = True
+
+                else:
+                    # record trip
+                    destination_activity = row
+                    trip_ls.append(_create_trip_from_stack(temp_trip_stack, origin_activity, destination_activity))
+
+                    # set values for next trip
+                    if is_gap:
+                        # if there is a gap after this trip the origin of the next trip is unknown
+                        origin_activity = unknown_activity
+                        destination_activity = None
+                        temp_trip_stack = list()
+                        in_trip = False
+
+                    else:
+                        # if there is no gap after this trip the origin of the next trip is the destination of the
+                        # current trip
+                        origin_activity = destination_activity
+                        destination_activity = None
+                        temp_trip_stack = list()
+                        in_trip = False
+
+            # check if gap during the trip
+            elif is_gap:
+                # in case of a gap, the destination of the current trip and the origin of the next trip
+                # are unknown.
+
+                # add current item to trip
+                temp_trip_stack.append(row)
+
+                # if the trip has no recored triplegs, we do not generate the current trip.
+                if not _check_trip_stack_has_tripleg(temp_trip_stack):
+                    origin_activity = unknown_activity
+                    in_trip = True
+                    temp_trip_stack = list()
+
+                else:
+                    # add tripleg to trip, generate trip, start new trip with unknown origin
+                    destination_activity = unknown_activity
+
+                    trip_ls.append(_create_trip_from_stack(temp_trip_stack, origin_activity, destination_activity))
+                    origin_activity = unknown_activity
+                    destination_activity = None
+                    temp_trip_stack = list()
+                    in_trip = True
+
+            else:
+                temp_trip_stack.append(row)
+
+    # if user ends generate last trip with unknown destination
+    if (len(temp_trip_stack) > 0) and (_check_trip_stack_has_tripleg(temp_trip_stack)):
+        destination_activity = unknown_activity
+        trip_ls.append(
+            _create_trip_from_stack(
+                temp_trip_stack,
+                origin_activity,
+                destination_activity,
+            )
+        )
+
+    # print(trip_ls)
+    trips = pd.DataFrame(trip_ls)
+    return trips
+
+
+def _check_trip_stack_has_tripleg(temp_trip_stack):
+    """
+    Check if a trip has at least 1 tripleg.
+
+    Parameters
+    ----------
+    temp_trip_stack : list
+        list of dictionary like elements (either pandas series or python dictionary).
+        Contains all elements that will be aggregated into a trip
+
+    Returns
+    -------
+    has_tripleg: Bool
+    """
+    has_tripleg = False
+    for row in temp_trip_stack:
+        if row["type"] == "tripleg":
+            has_tripleg = True
+            break
+
+    return has_tripleg
+
+
+def _create_trip_from_stack(temp_trip_stack, origin_activity, destination_activity):
+    """
+    Aggregate information of trip elements in a structured dictionary.
+
+    Parameters
+    ----------
+    temp_trip_stack : list
+        list of dictionary like elements (either pandas series or python dictionary).
+        Contains all elements that will be aggregated into a trip
+
+    origin_activity : dictionary like
+        Either dictionary or pandas series
+
+    destination_activity : dictionary like
+        Either dictionary or pandas series
+
+    Returns
+    -------
+    trip_dict_entry: dictionary
+
+    """
+    # this function return and empty dict if no tripleg is in the stack
+    first_trip_element = temp_trip_stack[0]
+    last_trip_element = temp_trip_stack[-1]
+
+    # all data has to be from the same user
+    assert origin_activity["user_id"] == last_trip_element["user_id"]
+
+    # double check if trip requirements are fulfilled
+    assert origin_activity["activity"] == True
+    assert destination_activity["activity"] == True
+    assert first_trip_element["activity"] == False
+
+    trip_dict_entry = {
+        "user_id": origin_activity["user_id"],
+        "started_at": first_trip_element["started_at"],
+        "finished_at": last_trip_element["finished_at"],
+        "origin_staypoint_id": origin_activity["id"],
+        "destination_staypoint_id": destination_activity["id"],
+        "tpls": [tripleg["id"] for tripleg in temp_trip_stack if tripleg["type"] == "tripleg"],
+        "stps": [tripleg["id"] for tripleg in temp_trip_stack if tripleg["type"] == "staypoint"],
+    }
+
+    return trip_dict_entry