Repository: mie-lab/trackintel
Pull request #216 (2021-05-18 21:45:06)

BLD: update for the v1.0 release
We can discuss the future milestones in this PR

codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/216?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
> Merging [#216](https://codecov.io/gh/mie-lab/trackintel/pull/216?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (2a9ff99) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/ddd0a9df13d5e4e0ffe962ccf261655e1c814aa4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (ddd0a9d) will **decrease** coverage by `4.41%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/216/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/216?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     #216      +/-   ##
==========================================
- Coverage   94.70%   90.29%   -4.42%     
==========================================
  Files          36       36              
  Lines        1473     1473              
  Branches      244      244              
==========================================
- Hits         1395     1330      -65     
- Misses         41      106      +65     
  Partials       37       37              
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/216?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/io/postgis.py](https://codecov.io/gh/mie-lab/trackintel/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9wb3N0Z2lzLnB5) | `17.80% <0.00%> (-82.20%)` | :arrow_down: |
| [trackintel/model/trips.py](https://codecov.io/gh/mie-lab/trackintel/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwcy5weQ==) | `95.23% <0.00%> (-4.77%)` | :arrow_down: |
| [trackintel/model/locations.py](https://codecov.io/gh/mie-lab/trackintel/pull/216/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.00% <0.00%> (-4.00%)` | :arrow_down: |
| [trackintel/model/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/216/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.05% <0.00%> (-2.95%)` | :arrow_down: |
| [trackintel/model/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwbGVncy5weQ==) | `97.22% <0.00%> (-2.78%)` | :arrow_down: |
| [trackintel/model/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9zdGF5cG9pbnRzLnB5) | `97.22% <0.00%> (-2.78%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/216?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/216?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Last update [ddd0a9d...2a9ff99](https://codecov.io/gh/mie-lab/trackintel/pull/216?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/ROADMAP.md b/ROADMAP.md
index 1d2387e..a74c214 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,69 +2,12 @@
 
 This document explains the functionality planned for various releases.
 
-## v1.0.0
-
-Steps until we get to version 1.0.0.
-
-### v0.1.0
-
-* :heavy_check_mark: Read *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* from CSV files.
-* :heavy_check_mark: Read *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* from PostGIS.
-* :heavy_check_mark: Write *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* to CSV files.
-* :heavy_check_mark: Write *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* to PostGIS.
-* :heavy_check_mark: Visualize *positionfixes*, *triplegs*, *staypoints* geographically.
-
-### v0.2.0
-
-* :heavy_check_mark: Extract *triplegs* and *staypoints* from *positionfixes*.
-* :heavy_check_mark:Functions for smoothening tripleg data (e.g., Douglas-Peucker).
-
-### v0.3.0
-
-* :heavy_check_mark: Extract *trips* from *triplegs* and *staypoints*
-* :heavy_check_mark: Extract *locations* from *staypoints*.
-
-### v0.4.0
-
-* :heavy_check_mark: Create transition graphs from *locations*.
-* :heavy_check_mark: Provide and visualize a range of (transition) graph measures.
-
-### v0.5.0
-
-* :heavy_check_mark: Augment model of *triplegs* and *staypoints* in such a way that they can
-  hold references to *trips* and *locations*.
-
-### v0.6.0
-* :heavy_check_mark: Support flexible geometry columns [Issue15](https://github.com/mie-lab/trackintel/issues/15)
-* :heavy_check_mark: Filter data by geographic location (e.g., all data within Switzerland)
-* :heavy_check_mark: Enhance tripleg extraction to be _gap aware_ [issue27](https://github.com/mie-lab/trackintel/issues/27)
-
-### v0.7.0
-
-* :heavy_check_mark: Simple transport mode prediction. This functionality is based on properties of the movement data only.
-* Simple activity inference. This functionality is based on properties of the movement data only and should include {Home, Work, Other}.
-
-### v0.8.0
-
-* :heavy_check_mark: Data quality assessments: statistical measures of temporal tracking frequency.
-
-### v0.9.0
-
-* :heavy_check_mark: Tutorial for trackintel
-
-### Final Steps for v1.0.0
-
-* Documentation
-  * :heavy_check_mark: Link a guide to the readme how to contribute to the documentation
-  * :heavy_check_mark: The documentation should be complete (all functions and methods are mentioned)
-  * The documentation should not be focused on PostGIS 
-
 
 ## v2.0.0
 
 Ideas for a later version.
 
-I/O
+I/O:
 * Read *customermovements*, *tours* from files.
 * Read *customermovements*, *tours* from PostGIS.
 * Write *customermovements*, *tours* to files.
@@ -72,7 +15,7 @@ I/O
 * Include data input/output methods for more common datasets
 
 
-Preprocessing
+Preprocessing:
 * Implement *tours* (starting and ending at a persons home location) and *customermovements* (consecutive triplegs with using transport provided by a single provider (e.g., a local bus company)).
 * Short walks to/from cars/buses/etc.: These are often not recognized by the tracking applications.
 * Map match *triplegs* (based on transport mode identification). 
@@ -106,3 +49,39 @@ Various
 
 
 
+## v1.0.0
+
+I/O and model:
+* :heavy_check_mark: Read *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* from CSV files.
+* :heavy_check_mark: Read *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* from PostGIS.
+* :heavy_check_mark: Write *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* to CSV files.
+* :heavy_check_mark: Write *positionfixes*, *triplegs*, *staypoints*, *locations*, *trips* to PostGIS.
+* :heavy_check_mark: Augment model of *triplegs* and *staypoints* in such a way that they can hold references to *trips* and *locations*.
+* :heavy_check_mark: Support flexible geometry columns [Issue15](https://github.com/mie-lab/trackintel/issues/15)
+
+
+Preprocessing
+* :heavy_check_mark: Extract *triplegs* and *staypoints* from *positionfixes*.
+* :heavy_check_mark: Functions for smoothening tripleg data (e.g., Douglas-Peucker).
+* :heavy_check_mark: Enhance tripleg extraction to be _gap aware_ [issue27](https://github.com/mie-lab/trackintel/issues/27)
+* :heavy_check_mark: Extract *trips* from *triplegs* and *staypoints*
+* :heavy_check_mark: Extract *locations* from *staypoints*.
+* :heavy_check_mark: Filter data by geographic location (e.g., all data within Switzerland)
+
+Augment tracking data:
+* :heavy_check_mark: Simple transport mode prediction. This functionality is based on properties of the movement data only.
+* :heavy_check_mark: Simple activity inference. This functionality is based on properties of the movement data only and should include {Home, Work, Other}.
+
+Analysis:
+* :heavy_check_mark: Create transition graphs from *locations*.
+* :heavy_check_mark: Provide and visualize a range of (transition) graph measures.
+* :heavy_check_mark: Data quality assessments: statistical measures of temporal tracking frequency.
+
+Visualization:
+* :heavy_check_mark: Visualize *positionfixes*, *triplegs*, *staypoints* geographically.
+
+Various:
+* :heavy_check_mark: Tutorial for trackintel
+* :heavy_check_mark: Link a guide to the readme how to contribute to the documentation
+* :heavy_check_mark: The documentation should be complete (all functions and methods are mentioned)
+* :heavy_check_mark: The documentation should not be focused on PostGIS 
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index 282997d..19cb9a7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -74,7 +74,7 @@ def setup(app):
 # The full version, including alpha/beta/rc tags
 # release = version
 
-version = "0.6.0"
+version = "1.0.0"
 release = version
 
 
diff --git a/docs/modules/analysis.rst b/docs/modules/analysis.rst
index ed4c045..49b95e5 100644
--- a/docs/modules/analysis.rst
+++ b/docs/modules/analysis.rst
@@ -19,4 +19,13 @@ Tracking Quality
 Modal Split
 ===========
 
-.. autofunction:: trackintel.analysis.modal_split.calculate_modal_split
\ No newline at end of file
+.. autofunction:: trackintel.analysis.modal_split.calculate_modal_split
+
+Location Identification
+=======================
+
+.. autofunction:: trackintel.analysis.location_identification.location_identifier
+
+.. autofunction:: trackintel.analysis.location_identification.pre_filter_locations
+
+.. autofunction:: trackintel.analysis.location_identification.freq_recipe
\ No newline at end of file
diff --git a/trackintel/__version__.py b/trackintel/__version__.py
index 30aec59..4bdba83 100644
--- a/trackintel/__version__.py
+++ b/trackintel/__version__.py
@@ -1,3 +1,3 @@
-VERSION = (0, 6, 0)
+VERSION = (1, 0, 0)
 
 __version__ = ".".join(map(str, VERSION))
diff --git a/trackintel/analysis/location_identification.py b/trackintel/analysis/location_identification.py
new file mode 100644
index 0000000..3a85af5
--- /dev/null
+++ b/trackintel/analysis/location_identification.py
@@ -0,0 +1,231 @@
+import numpy as np
+import pandas as pd
+import trackintel as ti
+
+
+def location_identifier(spts, method="FREQ", pre_filter=True, **pre_filter_kwargs):
+    """Assign "home" and "work" activity label for each user with different methods.
+
+    Parameters
+    ----------
+    spts : Geodataframe (as trackintel staypoints)
+        Staypoints with column "location_id".
+
+    method : {'FREQ'}, default "FREQ"
+        Choose which method to use.
+
+        - FREQ: "Generate a activity label per user by assigning the most visited location the label "home"
+          and the second most visited location the label "work". The remaining locations get no label.
+
+    pre_filter : bool, default True
+        Prefiltering the staypoints to exclude locations with not enough data.
+        The filter function can also be accessed via `pre_filter_locations`.
+
+    pre_filter_kwargs : dict
+        Kwargs to hand to `pre_filter_locations` if used. See function for more informations.
+
+    Returns
+    -------
+    Geodataframe (as trackintel staypoints)
+        With additional column `activity label` assigning one of three activity labels {'home', 'work', None}.
+
+    Note
+    ----
+    The methods are adapted from [1]. The original algorithms count the distinct hours at a
+    location as the home location is derived from geo-tagged tweets. We directly sum the time
+    spent at a location as our data model includes that.
+
+    References
+    ----------
+    [1] Chen, Qingqing, and Ate Poorthuis. 2021.
+    ‘Identifying Home Locations in Human Mobility Data: An Open-Source R Package for Comparison and Reproducibility’.
+    International Journal of Geographical Information Science 0 (0): 1–24.
+    https://doi.org/10.1080/13658816.2021.1887489.
+
+    Examples
+    --------
+    >>> ti.analysis.location_identifier(spts, pre_filter=True, method="FREQ")
+    """
+    assert spts.as_staypoints
+    spts = spts.copy()
+    if "location_id" not in spts.columns:
+        raise KeyError(
+            (
+                "To derive activity labels the GeoDataFrame (as trackintel staypoints) must have a column "
+                f"named 'location_id' but it has [{', '.join(spts.columns)}]"
+            )
+        )
+    if pre_filter:
+        f = pre_filter_locations(spts, **pre_filter_kwargs)
+    else:
+        f = pd.Series(np.full(len(spts.index), True))
+
+    if method == "FREQ":
+        method_val = freq_method(spts[f], "home", "work")
+    else:
+        raise ValueError(f"Method {method} does not exist.")
+
+    spts.loc[f, "activity_label"] = method_val["activity_label"]
+    return spts
+
+
+def pre_filter_locations(
+    spts,
+    agg_level="user",
+    thresh_sp=10,
+    thresh_loc=10,
+    thresh_sp_at_loc=10,
+    thresh_loc_time="1h",
+    thresh_loc_period="5h",
+):
+    """Filter locations and user out that have not enough data to do a proper analysis.
+
+    To disable a specific filter parameter set it to zero.
+
+    Parameters
+    ----------
+    spts : GeoDataFrame (as trackintel staypoints)
+        Staypoints with the column "location_id".
+
+    agg_level: {"user", "dataset"}, default "user"
+        The level of aggregation when filtering locations:
+        - 'user' : locations are filtered per-user.
+        - 'dataset' : locations are filtered over the whole dataset.
+
+    thresh_sp : int, default 10
+        Minimum staypoints a user must have to be included.
+
+    thresh_loc : int, default 10
+        Minimum locations a user must have to be included.
+
+    thresh_sp_at_loc : int, default 10
+        Minimum number of staypoints at a location must have to be included.
+
+    thresh_loc_time : str or pd.Timedelta, default "1h"
+        Minimum sum of durations that was spent at location to be included.
+        If str must be parsable by pd.to_timedelta.
+
+    thresh_loc_period : str or pd.Timedelta, default "5h"
+        Minimum timespan of first to last visit at a location to be included.
+        If str must be parsable by pd.to_timedelta.
+
+    Returns
+    -------
+    pd.Series
+        Boolean series containing the filter as a mask.
+
+    Examples
+    --------
+    >> mask = ti.analysis.pre_filter_locations(spts)
+    >> spts = spts[mask]
+    """
+    assert spts.as_staypoints
+    spts = spts.copy()
+    if isinstance(thresh_loc_time, str):
+        thresh_loc_time = pd.to_timedelta(thresh_loc_time)
+    if isinstance(thresh_loc_period, str):
+        thresh_loc_period = pd.to_timedelta(thresh_loc_period)
+
+    # filtering users
+    user = spts.groupby("user_id").nunique()
+    user_sp = user["started_at"] >= thresh_sp  # every staypoint should have a started_at -> count
+    user_loc = user["location_id"] >= thresh_loc
+    user_filter_agg = user_sp & user_loc
+    user_filter_agg.rename("user_filter", inplace=True)  # rename for merging
+    user_filter = pd.merge(spts["user_id"], user_filter_agg, left_on="user_id", right_index=True)["user_filter"]
+
+    # filtering locations
+    spts["duration"] = spts["finished_at"] - spts["started_at"]
+    if agg_level == "user":
+        groupby_loc = ["user_id", "location_id"]
+    elif agg_level == "dataset":
+        groupby_loc = ["location_id"]
+    else:
+        raise ValueError(f"Unknown agg_level '{agg_level}' use instead {{'user', 'dataset'}}.")
+    loc = spts.groupby(groupby_loc).agg({"started_at": [min, "count"], "finished_at": max, "duration": sum})
+    loc.columns = loc.columns.droplevel(0)  # remove possible multi-index
+    loc.rename(columns={"min": "started_at", "max": "finished_at", "sum": "duration"}, inplace=True)
+    # period for maximal time span first visit - last visit.
+    # duration for effective time spent at location summed up.
+    loc["period"] = loc["finished_at"] - loc["started_at"]
+    loc_sp = loc["count"] >= thresh_sp_at_loc
+    loc_time = loc["duration"] >= thresh_loc_time
+    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"]
+
+    total_filter = user_filter & loc_filter
+
+    return total_filter
+
+
+def freq_method(spts, *labels):
+    """Generate an activity label per user by assigning the most visited location the label "home"
+    and the second most visited location the label "work" or assign your own labels. The remaining
+    locations get no label.
+
+    Parameters
+    ----------
+    spts : GeoDataFrame (as trackintel staypoints)
+        Staypoints with the column "location_id".
+    labels : collection of str, default ("home", "work")
+        Labels in decreasing time of activity.
+
+    Returns
+    -------
+    GeoDataFrame
+        with column "activity_label".
+
+    Examples
+    --------
+    >> staypoints = ti.analysis.freq_method(staypoints, "home", "work")
+    """
+    spts = spts.copy()
+    if not labels:
+        labels = ("home", "work")
+    for name, group in spts.groupby("user_id"):
+        if "duration" not in group.columns:
+            group["duration"] = group["finished_at"] - group["started_at"]
+        # pandas keeps inner order of groups
+        spts.loc[spts["user_id"] == name, "activity_label"] = _freq_transform(group, *labels)
+    return spts
+
+
+def _freq_transform(group, *labels):
+    """Transform function that assigns the longest (duration) visited locations the labels in order.
+
+    Parameters
+    ----------
+    group : pd.DataFrame
+        Should have columns "location_id" and "duration".
+
+    Returns
+    -------
+    pd.Series
+        dtype : `object`
+    """
+    group_agg = group.groupby("location_id").agg({"duration": sum})
+    group_agg["activity_label"] = _freq_assign(group_agg["duration"], *labels)
+    group_merge = pd.merge(
+        group["location_id"], group_agg["activity_label"], how="left", left_on="location_id", right_index=True
+    )
+    return group_merge["activity_label"]
+
+
+def _freq_assign(duration, *labels):
+    """Assign k labels to k longest durations the rest is `None`.
+
+    Parameters
+    ----------
+    duration : pd.Series
+
+    Returns
+    -------
+    np.array
+        dtype : object
+    """
+    kth = (-duration).argsort()[: len(labels)]  # if inefficient use partial sort.
+    label_array = np.full(len(duration), fill_value=None)
+    label_array[kth] = labels
+    return label_array
diff --git a/trackintel/preprocessing/positionfixes.py b/trackintel/preprocessing/positionfixes.py
index 650c5a3..46afbf9 100644
--- a/trackintel/preprocessing/positionfixes.py
+++ b/trackintel/preprocessing/positionfixes.py
@@ -85,6 +85,10 @@ def generate_staypoints(
     # copy the original pfs for adding 'staypoint_id' column
     pfs = pfs_input.copy()
 
+    # if the positionfixes already have a column "staypoint_id", we drop it
+    if "staypoint_id" in pfs:
+        pfs.drop(columns="staypoint_id", inplace=True)
+
     elevation_flag = "elevation" in pfs.columns  # if there is elevation data
 
     geo_col = pfs.geometry.name
@@ -214,6 +218,10 @@ def generate_triplegs(pfs_input, stps_input, method="between_staypoints", gap_th
     # copy the original pfs for adding 'staypoint_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)
+
     if method == "between_staypoints":
 
         # get case:
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index 066c95b..f599a38 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -91,6 +91,15 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, print_progress=Fals
     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"
 


Test patch:
diff --git a/tests/analysis/test_location_identification.py b/tests/analysis/test_location_identification.py
new file mode 100644
index 0000000..7d47d72
--- /dev/null
+++ b/tests/analysis/test_location_identification.py
@@ -0,0 +1,232 @@
+import datetime
+
+import geopandas as gpd
+from geopandas.testing import assert_geodataframe_equal
+import numpy as np
+import pandas as pd
+import pytest
+from shapely.geometry import Point
+
+import trackintel as ti
+from trackintel.analysis.location_identification import (
+    pre_filter_locations,
+    _freq_transform,
+    freq_method,
+    _freq_assign,
+    location_identifier,
+)
+
+
+@pytest.fixture
+def example_staypoints():
+    """Staypoints to test pre_filter."""
+    p1 = Point(8.5067847, 47.4)
+    p2 = Point(8.5067847, 47.5)
+
+    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-01 07:00:00", tz="utc")
+    t4 = pd.Timestamp("1971-01-02 23:00:00", tz="utc")  # for time testin bigger gap
+    one_hour = datetime.timedelta(hours=1)
+
+    list_dict = [
+        {"user_id": 0, "started_at": t1, "finished_at": t2, "geometry": p1, "location_id": 0},
+        {"user_id": 0, "started_at": t2, "finished_at": t3, "geometry": p1, "location_id": 0},
+        {"user_id": 0, "started_at": t3, "finished_at": t4, "geometry": p2, "location_id": 1},  # longest duration
+        {"user_id": 1, "started_at": t4, "finished_at": t4 + one_hour, "geometry": p1, "location_id": 0},
+    ]
+    spts = gpd.GeoDataFrame(data=list_dict, geometry="geometry", crs="EPSG:4326")
+    spts.index.name = "id"
+    assert spts.as_staypoints
+    assert "location_id" in spts.columns
+    return spts
+
+
+@pytest.fixture
+def default_kwargs():
+    kw = {
+        "agg_level": "dataset",
+        "thresh_sp": 0,
+        "thresh_loc": 0,
+        "thresh_sp_at_loc": 0,
+        "thresh_loc_time": "0h",
+        "thresh_loc_period": "0h",
+    }
+    return kw
+
+
+class TestPre_Filter:
+    """Tests for the function `pre_filter_locations()`."""
+
+    def test_no_kw(self, example_staypoints, default_kwargs):
+        """Test that nothing gets filtered if all parameters are set to zero."""
+        assert all(pre_filter_locations(example_staypoints, **default_kwargs))
+
+    def test_thresh_sp(self, example_staypoints, default_kwargs):
+        """Test the minimum staypoint per user parameter."""
+        default_kwargs["thresh_sp"] = 2
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert all(f == (example_staypoints["user_id"] == 0))
+
+    def test_thresh_loc(self, example_staypoints, default_kwargs):
+        """Test the minimum location per user parameter."""
+        default_kwargs["thresh_loc"] = 2
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert all(f == (example_staypoints["user_id"] == 0))
+
+    def test_thresh_sp_at_loc(self, example_staypoints, default_kwargs):
+        """Test the minimum staypoint per location parameter."""
+        default_kwargs["thresh_sp_at_loc"] = 2
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert all(f == (example_staypoints["location_id"] == 0))
+
+    def test_tresh_loc_time(self, example_staypoints, default_kwargs):
+        """Test the minimum duration per location parameter."""
+        default_kwargs["thresh_loc_time"] = "14h"
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert all(f == (example_staypoints["location_id"] == 1))
+
+    def test_loc_period(self, example_staypoints, default_kwargs):
+        """Test the minimum period per location parameter."""
+        default_kwargs["thresh_loc_period"] = "2d"
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert all(f == (example_staypoints["location_id"] == 0))
+
+    def test_agg_level(self, example_staypoints, default_kwargs):
+        """Test if different aggregation works."""
+        default_kwargs["agg_level"] = "user"
+        default_kwargs["thresh_loc_period"] = pd.Timedelta("1d")
+        f = pre_filter_locations(example_staypoints, **default_kwargs)
+        assert all(f == (example_staypoints[["user_id", "location_id"]] == [0, 1]).all(axis=1))
+
+    def test_agg_level_error(self, example_staypoints, default_kwargs):
+        """Test if ValueError is raised for unknown agg_level."""
+        default_kwargs["agg_level"] = "unknown"
+        with pytest.raises(ValueError):
+            pre_filter_locations(example_staypoints, **default_kwargs)
+
+
+@pytest.fixture
+def example_freq():
+    """Example staypoints with 4 location for 2 users with [3, 2, 1, 1] staypoint per location."""
+    list_dict = [
+        {"user_id": 0, "location_id": 0},
+        {"user_id": 0, "location_id": 0},
+        {"user_id": 0, "location_id": 0},
+        {"user_id": 0, "location_id": 1},
+        {"user_id": 0, "location_id": 1},
+        {"user_id": 0, "location_id": 2},
+        {"user_id": 0, "location_id": 3},
+        {"user_id": 1, "location_id": 0},
+        {"user_id": 1, "location_id": 0},
+        {"user_id": 1, "location_id": 0},
+        {"user_id": 1, "location_id": 1},
+        {"user_id": 1, "location_id": 1},
+        {"user_id": 1, "location_id": 2},
+        {"user_id": 1, "location_id": 3},
+    ]
+    p1 = Point(8.5067847, 47.1)  # geometry isn't used
+    t1 = pd.Timestamp("1971-01-01 00:00:00", tz="utc")
+    t2 = pd.Timestamp("1971-01-01 01:00:00", tz="utc")
+    for d in list_dict:
+        d["started_at"] = t1
+        d["finished_at"] = t2
+        d["geom"] = p1
+
+    spts = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:4326")
+    spts.index.name = "id"
+    assert "location_id" in spts.columns
+    assert spts.as_staypoints
+    return spts
+
+
+class TestFreq_method:
+    """Test freq_method."""
+
+    def test_default_labels(self, example_freq):
+        """Test method with default labels"""
+        freq = freq_method(example_freq)
+        example_freq["activity_label"] = None
+        example_freq.loc[example_freq["location_id"] == 0, "activity_label"] = "home"
+        example_freq.loc[example_freq["location_id"] == 1, "activity_label"] = "work"
+        assert freq["activity_label"].count() == example_freq["activity_label"].count()
+        assert_geodataframe_equal(example_freq, freq)
+
+    def test_custom_labels(self, example_freq):
+        """Test method with custom label of a different length"""
+        custom_label = "doing_nothing"
+        freq = freq_method(example_freq, "doing_nothing")
+        example_freq["activity_label"] = None
+        example_freq.loc[example_freq["location_id"] == 0, "activity_label"] = custom_label
+        assert freq["activity_label"].count() == example_freq["activity_label"].count()
+        assert_geodataframe_equal(example_freq, freq)
+
+    def test_duration(self, example_freq):
+        """Test if function can handle only "duration" column and no columns "started_at", "finished_at"."""
+        example_freq["duration"] = example_freq["finished_at"] - example_freq["started_at"]
+        del example_freq["finished_at"]
+        del example_freq["started_at"]
+        freq = freq_method(example_freq)
+        example_freq["activity_label"] = None
+        example_freq.loc[example_freq["location_id"] == 0, "activity_label"] = "home"
+        example_freq.loc[example_freq["location_id"] == 1, "activity_label"] = "work"
+        assert freq["activity_label"].count() == example_freq["activity_label"].count()
+        assert_geodataframe_equal(example_freq, freq)
+
+
+class Test_Freq_Transform:
+    """Test help function _freq_transform."""
+
+    def test_function(self):
+        """Test if groupby assign works."""
+        list_dict = [
+            {"location_id": 0, "duration": 1},
+            {"location_id": 0, "duration": 1},
+            {"location_id": 1, "duration": 1},
+        ]
+        df = pd.DataFrame(list_dict)
+        freq = _freq_transform(df, "work")
+        sol = pd.Series(["work", "work", None])
+        assert freq.equals(sol)
+
+
+class Test_Freq_Assign:
+    """Test help function _freq_assign."""
+
+    def test_function(self):
+        """Test function with simple input."""
+        dur = pd.Series([9, 0, 8, 1, 7, 6, 5])
+        labels = ("label1", "label2", "label3")
+        freq_sol = np.array([labels[0], None, labels[1], None, labels[2], None, None])
+        freq = _freq_assign(dur, *labels)
+        assert all(freq == freq_sol)
+
+
+class TestLocation_Identifier:
+    """Test function `location_identifier`"""
+
+    def test_unkown_method(self, example_staypoints):
+        """Test if ValueError is risen if method is unknown"""
+        with pytest.raises(ValueError):
+            location_identifier(example_staypoints, method="UNKNOWN", pre_filter=False)
+
+    def test_no_location_column(self, example_staypoints):
+        """Test if key error is risen if no column `location_id`."""
+        with pytest.raises(KeyError):
+            del example_staypoints["location_id"]
+            location_identifier(example_staypoints)
+
+    def test_pre_filter(self, example_freq, default_kwargs):
+        """Test if function calls pre_filter correctly."""
+        default_kwargs["agg_level"] = "user"
+        default_kwargs["thresh_sp_at_loc"] = 2
+        li = location_identifier(example_freq, method="FREQ", pre_filter=True, **default_kwargs)
+        f = pre_filter_locations(example_freq, **default_kwargs)
+        example_freq.loc[f, "activity_label"] = freq_method(example_freq[f])["activity_label"]
+        assert_geodataframe_equal(li, example_freq)
+
+    def test_freq_method(self, example_freq):
+        """Test if function calls freq method correctly."""
+        li = location_identifier(example_freq, method="FREQ", pre_filter=False)
+        fr = freq_method(example_freq)
+        assert_geodataframe_equal(li, fr)
diff --git a/tests/preprocessing/test_positionfixes.py b/tests/preprocessing/test_positionfixes.py
index b4664ca..e16b9a9 100644
--- a/tests/preprocessing/test_positionfixes.py
+++ b/tests/preprocessing/test_positionfixes.py
@@ -23,6 +23,20 @@ def geolife_pfs_stps_long():
 class TestGenerate_staypoints:
     """Tests for generate_staypoints() method."""
 
+    def test_duplicate_columns(self):
+        """Test if running the function twice, the generated column does not yield exception in join statement"""
+
+        # we run generate_staypoints twice in order to check that the extra column(tripleg_id) does
+        # not cause any problems in the second run
+        pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife"))
+        pfs_run_1, _ = pfs.as_positionfixes.generate_staypoints(
+            method="sliding", dist_threshold=100, time_threshold=5.0, include_last=True
+        )
+        pfs_run_2, _ = pfs_run_1.as_positionfixes.generate_staypoints(
+            method="sliding", dist_threshold=100, time_threshold=5.0, include_last=True
+        )
+        assert set(pfs_run_1.columns) == set(pfs_run_2.columns)
+
     def test_sliding_min(self):
         """Test if using small thresholds, stp extraction yields each pfs."""
         pfs, _ = ti.io.dataset_reader.read_geolife(os.path.join("tests", "data", "geolife"))
@@ -123,6 +137,17 @@ class TestGenerate_staypoints:
 class TestGenerate_triplegs:
     """Tests for generate_triplegs() method."""
 
+    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"""
+
+        # we run generate_triplegs twice in order to check that the extra column (tripleg_id) does
+        # not cause any problems in the second run
+        pfs, stps = geolife_pfs_stps_long
+
+        pfs_run_1, _ = pfs.as_positionfixes.generate_triplegs(stps, method="between_staypoints")
+        pfs_run_2, _ = pfs_run_1.as_positionfixes.generate_triplegs(stps, method="between_staypoints")
+        assert set(pfs_run_1.columns) == set(pfs_run_2.columns)
+
     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
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 76d793a..40e3388 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -29,6 +29,24 @@ class TestSmoothen_triplegs:
 class TestGenerate_trips:
     """Tests for generate_trips() method."""
 
+    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)
+        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_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)
+
+        assert set(tpls_run_1.columns) == set(tpls_run_2.columns)
+        assert set(stps_run_1.columns) == set(stps_run_2.columns)
+
     def test_generate_trips(self):
         """Test if we can generate the example trips based on example data."""
         # load pregenerated trips