Repository: mie-lab/trackintel

Issue:
Remove `_check` from trackintel classes
Currently we use `_check` to test if the class invariants still hold and if not fallback to `GeoDataFrame`/`DataFrame`.
This is nice one hand as we always know that the data will be correct (up to a certain point &rarr; e.g. we don't check for geometry in the `_constructor`)
The negative is that for `TripsGeoDataFrame` a line of code can have 4 different output types and you have to keep in mind what is what, such that you don't call `generate_tours` on a `GeoDataframe`. This is can be a significant cognitive load. 
In total we would have three different class types (`Positionfixes`, `TripsGeoDataFrames`, `Tours`) that have three different patterns how they fallback and to what. This is quite hard to explain. 
If we would remove `_check` then we would only have to explain that `TripsGeoDataFrame` fallback to `TripsDataFrame` and the rest would be equivalent to `GeoPandas`.
Additionally we could call the `validate` in each method to assure the data is in the right shape.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`a55e2e2`)](https://app.codecov.io/gh/mie-lab/trackintel/commit/a55e2e2be9b385d2683b0db7a77362e44d27039c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) 93.24% compared to head [(`32a3aee`)](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) 92.82%.
> Report is 1 commits behind head on master.

> :exclamation: Current head 32a3aee differs from pull request most recent head 22bbfac. Consider uploading reports for the commit 22bbfac to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #550      +/-   ##
==========================================
- Coverage   93.24%   92.82%   -0.42%     
==========================================
  Files          32       32              
  Lines        2101     2048      -53     
  Branches      399      372      -27     
==========================================
- Hits         1959     1901      -58     
- Misses        129      131       +2     
- Partials       13       16       +3     
```


| [Files](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/geogr/distances.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9nZW9nci9kaXN0YW5jZXMucHk=) | `95.39% <100.00%> (-1.28%)` | :arrow_down: |
| [trackintel/model/locations.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9sb2NhdGlvbnMucHk=) | `100.00% <100.00%> (ø)` | |
| [trackintel/model/positionfixes.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9wb3NpdGlvbmZpeGVzLnB5) | `97.61% <100.00%> (-2.39%)` | :arrow_down: |
| [trackintel/model/staypoints.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC9zdGF5cG9pbnRzLnB5) | `100.00% <100.00%> (ø)` | |
| [trackintel/model/tours.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90b3Vycy5weQ==) | `100.00% <100.00%> (ø)` | |
| [trackintel/model/triplegs.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwbGVncy5weQ==) | `100.00% <100.00%> (ø)` | |
| [trackintel/model/trips.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC90cmlwcy5weQ==) | `95.74% <100.00%> (-4.26%)` | :arrow_down: |
| [trackintel/model/util.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9tb2RlbC91dGlsLnB5) | `100.00% <100.00%> (ø)` | |
| [trackintel/preprocessing/positionfixes.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3Bvc2l0aW9uZml4ZXMucHk=) | `98.83% <100.00%> (+0.03%)` | :arrow_up: |
| [trackintel/preprocessing/staypoints.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3N0YXlwb2ludHMucHk=) | `99.09% <100.00%> (+0.01%)` | :arrow_up: |
| ... and [2 more](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/mie-lab/trackintel/pull/550/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mie-lab/trackintel/pull/550?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).

bifbof: > I like the changes to call validate in all methods, I only had a few questions. One more general question: Why is it not done in other methods, like in the `analysis` module?

Many functions do not depend on everything that e.g. `position fixes` provide, so the validation of the class is sometimes too strong.Then test functions also depend on not having to input real Positionfixes, thus those tests would have to be updated as well.
So I decided to keep this PR minimal, but they can still be added easily. 
Is there a function that you think `validation` is necessary?


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2d6a5ec..aef9115 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -13,7 +13,7 @@ ci:
 
 repos: 
 - repo: https://github.com/psf/black 
-  rev: 23.9.1 
+  rev: 23.10.0 
   hooks:
   - id: black
     args: [
diff --git a/trackintel/geogr/distances.py b/trackintel/geogr/distances.py
index 7f49a39..5421160 100644
--- a/trackintel/geogr/distances.py
+++ b/trackintel/geogr/distances.py
@@ -11,6 +11,8 @@ import similaritymeasures
 from scipy.spatial.distance import cdist
 from sklearn.metrics import pairwise_distances
 
+from trackintel import Positionfixes, Triplegs
+
 
 def point_haversine_dist(lon_1, lat_1, lon_2, lat_2, r=6371000, float_flag=False):
     """
@@ -388,6 +390,7 @@ def get_speed_triplegs(triplegs, positionfixes=None, method="tpls_speed"):
     tpls: GeoDataFrame (as trackintel triplegs)
         The original triplegs with a new column ``[`speed`]``. The speed is given in m/s.
     """
+    Triplegs.validate(triplegs)
     # Simple method: Divide overall tripleg distance by overall duration
     if method == "tpls_speed":
         if check_gdf_planar(triplegs):
diff --git a/trackintel/model/locations.py b/trackintel/model/locations.py
index 7554d2d..d83ad35 100644
--- a/trackintel/model/locations.py
+++ b/trackintel/model/locations.py
@@ -32,16 +32,17 @@ class Locations(TrackintelBase, TrackintelGeoDataFrame):
     >>> df.as_locations.to_csv("filename.csv")
     """
 
-    def __init__(self, *args, validate_geometry=True, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         super().__init__(*args, **kwargs)
-        self._validate(self, validate_geometry=validate_geometry)
+        if validate:
+            self.validate(self)
 
     @property
     def as_locations(self):
         return self
 
     @staticmethod
-    def _validate(obj, validate_geometry):
+    def validate(obj):
         if any([c not in obj.columns for c in _required_columns]):
             raise AttributeError(
                 "To process a DataFrame as a collection of locations, it must have the properties"
@@ -50,22 +51,11 @@ class Locations(TrackintelBase, TrackintelGeoDataFrame):
         if obj.shape[0] <= 0:
             raise ValueError(f"GeoDataFrame is empty with shape: {obj.shape}")
 
-        if validate_geometry and obj["center"].iloc[0].geom_type != "Point":
+        if obj["center"].iloc[0].geom_type != "Point":
             # todo: We could think about allowing both geometry types for locations (point and polygon)
             # One for extend and one for the center
             raise ValueError("The center geometry must be a Point (only first checked).")
 
-    @staticmethod
-    def _check(obj, validate_geometry=True):
-        """Check does the same as _validate but returns bool instead of potentially raising an error."""
-        if any([c not in obj.columns for c in _required_columns]):
-            return False
-        if obj.shape[0] <= 0:
-            return False
-        if validate_geometry:
-            return obj.geometry.iloc[0].geom_type == "Point"
-        return True
-
     @doc(_shared_docs["write_csv"], first_arg="", long="locations", short="locs")
     def to_csv(self, filename, *args, **kwargs):
         ti.io.file.write_locations_csv(self, filename, *args, **kwargs)
diff --git a/trackintel/model/positionfixes.py b/trackintel/model/positionfixes.py
index 278c792..3407f81 100644
--- a/trackintel/model/positionfixes.py
+++ b/trackintel/model/positionfixes.py
@@ -41,12 +41,13 @@ class Positionfixes(TrackintelBase, TrackintelGeoDataFrame, gpd.GeoDataFrame):
     >>> df.as_positionfixes.generate_staypoints()
     """
 
-    def __init__(self, *args, validate_geometry=True, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         # could be moved to super class
         # validate kwarg is necessary as the object is not fully initialised if we call it from _constructor
         # (geometry-link is missing). thus we need a way to stop validating too early.
         super().__init__(*args, **kwargs)
-        self._validate(self, validate_geometry=validate_geometry)
+        if validate:
+            self.validate(self)
 
     # create circular reference directly -> avoid second call of init via accessor
     @property
@@ -54,7 +55,7 @@ class Positionfixes(TrackintelBase, TrackintelGeoDataFrame, gpd.GeoDataFrame):
         return self
 
     @staticmethod
-    def _validate(obj, validate_geometry=True):
+    def validate(obj):
         assert obj.shape[0] > 0, f"Geodataframe is empty with shape: {obj.shape}"
         # check columns
         if any([c not in obj.columns for c in _required_columns]):
@@ -68,26 +69,12 @@ class Positionfixes(TrackintelBase, TrackintelGeoDataFrame, gpd.GeoDataFrame):
         ), f"dtype of tracked_at is {obj['tracked_at'].dtype} but has to be datetime64 and timezone aware"
 
         # check geometry
-        if validate_geometry:
-            assert (
-                obj.geometry.is_valid.all()
-            ), "Not all geometries are valid. Try x[~ x.geometry.is_valid] where x is you GeoDataFrame"
+        assert (
+            obj.geometry.is_valid.all()
+        ), "Not all geometries are valid. Try x[~ x.geometry.is_valid] where x is you GeoDataFrame"
 
-            if obj.geometry.iloc[0].geom_type != "Point":
-                raise AttributeError("The geometry must be a Point (only first checked).")
-
-    @staticmethod
-    def _check(obj, validate_geometry=True):
-        """Check does the same as _validate but returns bool instead of potentially raising an error."""
-        if any([c not in obj.columns for c in _required_columns]):
-            return False
-        if obj.shape[0] <= 0:
-            return False
-        if not isinstance(obj["tracked_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if validate_geometry:
-            return obj.geometry.is_valid.all() and obj.geometry.iloc[0].geom_type == "Point"
-        return True
+        if obj.geometry.iloc[0].geom_type != "Point":
+            raise AttributeError("The geometry must be a Point (only first checked).")
 
     @property
     def center(self):
diff --git a/trackintel/model/staypoints.py b/trackintel/model/staypoints.py
index ff394e8..322a0db 100644
--- a/trackintel/model/staypoints.py
+++ b/trackintel/model/staypoints.py
@@ -43,9 +43,10 @@ class Staypoints(TrackintelBase, TrackintelGeoDataFrame):
     >>> df.as_staypoints.generate_locations()
     """
 
-    def __init__(self, *args, validate_geometry=True, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         super().__init__(*args, **kwargs)
-        self._validate(self, validate_geometry=validate_geometry)
+        if validate:
+            self.validate(self)
 
     # create circular reference directly -> avoid second call of init via accessor
     @property
@@ -53,7 +54,7 @@ class Staypoints(TrackintelBase, TrackintelGeoDataFrame):
         return self
 
     @staticmethod
-    def _validate(obj, validate_geometry=True):
+    def validate(obj):
         # check columns
         if any([c not in obj.columns for c in _required_columns]):
             raise AttributeError(
@@ -68,26 +69,12 @@ class Staypoints(TrackintelBase, TrackintelGeoDataFrame):
             obj["finished_at"].dtype, pd.DatetimeTZDtype
         ), f"dtype of finished_at is {obj['finished_at'].dtype} but has to be tz aware datetime64"
 
-        if validate_geometry:
-            # check geometry
-            assert (
-                obj.geometry.is_valid.all()
-            ), "Not all geometries are valid. Try x[~ x.geometry.is_valid] where x is you GeoDataFrame"
-            if obj.geometry.iloc[0].geom_type != "Point":
-                raise AttributeError("The geometry must be a Point (only first checked).")
-
-    @staticmethod
-    def _check(obj, validate_geometry=True):
-        """Check does the same as _validate but returns bool instead of potentially raising an error."""
-        if any([c not in obj.columns for c in _required_columns]):
-            return False
-        if not isinstance(obj["started_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if not isinstance(obj["finished_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if validate_geometry:
-            return obj.geometry.is_valid.all() and obj.geometry.iloc[0].geom_type == "Point"
-        return True
+        # check geometry
+        assert (
+            obj.geometry.is_valid.all()
+        ), "Not all geometries are valid. Try x[~ x.geometry.is_valid] where x is you GeoDataFrame"
+        if obj.geometry.iloc[0].geom_type != "Point":
+            raise AttributeError("The geometry must be a Point (only first checked).")
 
     @property
     def center(self):
diff --git a/trackintel/model/tours.py b/trackintel/model/tours.py
index f758d74..947759c 100644
--- a/trackintel/model/tours.py
+++ b/trackintel/model/tours.py
@@ -36,9 +36,10 @@ class Tours(TrackintelBase, TrackintelDataFrame):
     >>> df.as_tours.to_csv("filename.csv")
     """
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         super().__init__(*args, **kwargs)
-        self._validate(self)
+        if validate:
+            self.validate(self)
 
     # createte circular reference directly -> avoid second call of init via accessor
     @property
@@ -46,7 +47,7 @@ class Tours(TrackintelBase, TrackintelDataFrame):
         return self
 
     @staticmethod
-    def _validate(obj):
+    def validate(obj):
         if any([c not in obj.columns for c in _required_columns]):
             raise AttributeError(
                 "To process a DataFrame as a collection of tours, it must have the properties"
@@ -61,17 +62,6 @@ class Tours(TrackintelBase, TrackintelDataFrame):
             obj["finished_at"].dtype, pd.DatetimeTZDtype
         ), f"dtype of finished_at is {obj['finished_at'].dtype} but has to be datetime64 and timezone aware"
 
-    @staticmethod
-    def _check(obj):
-        """Check does the same as _validate but returns bool instead of potentially raising an error."""
-        if any([c not in obj.columns for c in _required_columns]):
-            return False
-        if not isinstance(obj["started_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if not isinstance(obj["finished_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        return True
-
     @doc(_shared_docs["write_csv"], first_arg="", long="tours", short="tours")
     def to_csv(self, filename, *args, **kwargs):
         ti.io.file.write_tours_csv(self, filename, *args, **kwargs)
diff --git a/trackintel/model/triplegs.py b/trackintel/model/triplegs.py
index 9b249ba..97d2cae 100644
--- a/trackintel/model/triplegs.py
+++ b/trackintel/model/triplegs.py
@@ -39,9 +39,10 @@ class Triplegs(TrackintelBase, TrackintelGeoDataFrame):
     >>> df.as_triplegs.generate_trips()
     """
 
-    def __init__(self, *args, validate_geometry=True, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         super().__init__(*args, **kwargs)
-        self._validate(self, validate_geometry=validate_geometry)
+        if validate:
+            self.validate(self)
 
     # create circular reference directly -> avoid second call of init via accessor
     @property
@@ -49,7 +50,7 @@ class Triplegs(TrackintelBase, TrackintelGeoDataFrame):
         return self
 
     @staticmethod
-    def _validate(obj, validate_geometry=True):
+    def validate(obj):
         assert obj.shape[0] > 0, f"Geodataframe is empty with shape: {obj.shape}"
         # check columns
         if any([c not in obj.columns for c in _required_columns]):
@@ -67,27 +68,11 @@ class Triplegs(TrackintelBase, TrackintelGeoDataFrame):
         ), f"dtype of finished_at is {obj['finished_at'].dtype} but has to be datetime64 and timezone aware"
 
         # check geometry
-        if validate_geometry:
-            assert (
-                obj.geometry.is_valid.all()
-            ), "Not all geometries are valid. Try x[~ x.geometry.is_valid] where x is you GeoDataFrame"
-            if obj.geometry.iloc[0].geom_type != "LineString":
-                raise AttributeError("The geometry must be a LineString (only first checked).")
-
-    @staticmethod
-    def _check(obj, validate_geometry=True):
-        """Check does the same as _validate but returns bool instead of potentially raising an error."""
-        if any([c not in obj.columns for c in _required_columns]):
-            return False
-        if obj.shape[0] <= 0:
-            return False
-        if not isinstance(obj["started_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if not isinstance(obj["finished_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if validate_geometry:
-            return obj.geometry.is_valid.all() and obj.geometry.iloc[0].geom_type == "LineString"
-        return True
+        assert (
+            obj.geometry.is_valid.all()
+        ), "Not all geometries are valid. Try x[~ x.geometry.is_valid] where x is you GeoDataFrame"
+        if obj.geometry.iloc[0].geom_type != "LineString":
+            raise AttributeError("The geometry must be a LineString (only first checked).")
 
     @doc(_shared_docs["write_csv"], first_arg="", long="triplegs", short="tpls")
     def to_csv(self, filename, *args, **kwargs):
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index 8ecb8b3..e97cfb5 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -89,12 +89,13 @@ class TripsDataFrame(TrackintelBase, TrackintelDataFrame):
     >>> df.as_trips.generate_tours()
     """
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         super().__init__(*args, **kwargs)
-        TripsDataFrame._validate(self)  # static call
+        if validate:
+            TripsDataFrame.validate(self)  # static call
 
     @staticmethod
-    def _validate(obj):
+    def validate(obj):
         if any([c not in obj.columns for c in _required_columns]):
             raise AttributeError(
                 "To process a DataFrame as a collection of trips, it must have the properties"
@@ -109,16 +110,6 @@ class TripsDataFrame(TrackintelBase, TrackintelDataFrame):
             obj["finished_at"].dtype, pd.DatetimeTZDtype
         ), f"dtype of finished_at is {obj['finished_at'].dtype} but has to be datetime64 and timezone aware"
 
-    @staticmethod
-    def _check(obj):
-        if any([c not in obj.columns for c in _required_columns]):
-            return False
-        if not isinstance(obj["started_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        if not isinstance(obj["finished_at"].dtype, pd.DatetimeTZDtype):
-            return False
-        return True
-
     @doc(_shared_docs["write_csv"], first_arg="", long="trips", short="trips")
     def to_csv(self, filename, *args, **kwargs):
         ti.io.file.write_trips_csv(self, filename, *args, **kwargs)
@@ -180,25 +171,16 @@ class TripsGeoDataFrame(TrackintelGeoDataFrame, TripsDataFrame, gpd.GeoDataFrame
 
     fallback_class = TripsDataFrame
 
-    def __init__(self, *args, validate_geometry=True, **kwargs):
+    def __init__(self, *args, validate=True, **kwargs):
         super().__init__(*args, **kwargs)
-        TripsGeoDataFrame._validate(self, validate_geometry=validate_geometry)
+        if validate:
+            TripsGeoDataFrame.validate(self)
 
     @staticmethod
-    def _validate(self, validate_geometry=True):
-        if not validate_geometry:
-            return
-        # _validate should not be called from the outside -> we can use fact that it is called only from __init__
-        # therefore TrackintelDataFrame validated all the columns and here we only have to validate the geometry
+    def validate(self):
+        TripsDataFrame.validate(self)
         assert (
             self.geometry.is_valid.all()
         ), "Not all geometries are valid. Try x[~x.geometry.is_valid] where x is you GeoDataFrame"
         if self.geometry.iloc[0].geom_type != "MultiPoint":
             raise ValueError("The geometry must be a MultiPoint (only first checked).")
-
-    @staticmethod
-    def _check(self, validate_geometry=True):
-        val = TripsDataFrame._check(self)
-        if not validate_geometry:
-            return val
-        return val and self.geometry.is_valid.all() and self.geometry.iloc[0].geom_type == "MultiPoint"
diff --git a/trackintel/model/util.py b/trackintel/model/util.py
index b166b7e..2e16b46 100644
--- a/trackintel/model/util.py
+++ b/trackintel/model/util.py
@@ -1,5 +1,5 @@
 import warnings
-from functools import wraps
+from functools import wraps, partial
 from textwrap import dedent
 
 import pandas as pd
@@ -12,7 +12,7 @@ def _wrapped_gdf_method(func):
     @wraps(func)  # copy all metadata
     def wrapper(self, *args, **kwargs):
         result = func(self, *args, **kwargs)
-        if isinstance(result, pd.DataFrame) and self._check(result, validate_geometry=False):
+        if isinstance(result, pd.DataFrame):
             if isinstance(result, GeoDataFrame):
                 result.__class__ = self.__class__
             elif self.fallback_class is not None:
@@ -27,18 +27,14 @@ class TrackintelGeoDataFrame(GeoDataFrame):
     Helper class to subtype GeoDataFrame correctly and fallback to custom class.
 
     Three possible outcomes for the _constructor
-    - If check fails or is DataFrame without fallback_class set, return GeoDataFrame/DataFrame instance w/o changes
-    - If check succeeds and has geometry, change class to current class and return
-    - If check succeeds and has no geometry, change class to fallback_class and return
+    - If GeoDataFrame, keep it a TrackintelGeoDataFrame
+    - If DataFrame without fallback class, just return the DataFrame
+    - If DataFrame with fallback class, fall back to it.
     """
 
-    # set fallback_class for succeeding _check and missing geometry, if None default to pd.DataFrame
+    # set fallback_class for missing geometry, if None default to pd.DataFrame
     fallback_class = None
 
-    @staticmethod
-    def _check(self, validate_geometry=True):
-        raise NotImplementedError
-
     @property
     def _constructor(self):
         """Interface to subtype pandas properly"""
@@ -46,14 +42,11 @@ class TrackintelGeoDataFrame(GeoDataFrame):
 
         def _constructor_with_fallback(*args, **kwargs):
             result = super_cons(*args, **kwargs)
-            if not self._check(result, validate_geometry=False):
-                # check fails -> not one of our classes
-                return result
             if isinstance(result, GeoDataFrame):
-                # cannot validate geometry as geometry column is maybe not set
-                return self.__class__(result, validate_geometry=False)
+                return self.__class__(result, validate=False)
+            # uses DataFrame constructor -> must be DataFrame
             if self.fallback_class is not None:
-                return self.fallback_class(result)
+                return self.fallback_class(result, validate=False)
             return result
 
         return _constructor_with_fallback
@@ -76,22 +69,11 @@ class TrackintelGeoDataFrame(GeoDataFrame):
 class TrackintelDataFrame(pd.DataFrame):
     """Helper class to subtype DataFrame and handle fallback"""
 
-    @staticmethod
-    def _check(self):  # has no geometry to check
-        raise NotImplementedError
-
+    # subclassing DataFrames is significant simpler.
     @property
     def _constructor(self):
         """Interface to subtype pandas properly"""
-        super_cons = super()._constructor
-
-        def _constructor_with_fallback(*args, **kwargs):
-            result = super_cons(*args, **kwargs)
-            if not self._check(result):
-                return result
-            return self.__class__(result)
-
-        return _constructor_with_fallback
+        return partial(self.__class__, validate=False)
 
 
 class TrackintelBase(object):
diff --git a/trackintel/preprocessing/positionfixes.py b/trackintel/preprocessing/positionfixes.py
index cab132a..3282721 100644
--- a/trackintel/preprocessing/positionfixes.py
+++ b/trackintel/preprocessing/positionfixes.py
@@ -7,6 +7,7 @@ import pandas as pd
 from shapely.geometry import LineString
 from tqdm import tqdm
 
+from trackintel import Positionfixes, Staypoints
 from trackintel.geogr import check_gdf_planar, point_haversine_dist
 from trackintel.preprocessing.util import _explode_agg, angle_centroid_multipoints, applyParallel
 
@@ -96,6 +97,7 @@ def generate_staypoints(
     similarity based on location history. In Proceedings of the 16th ACM SIGSPATIAL international
     conference on Advances in geographic information systems (p. 34). ACM.
     """
+    Positionfixes.validate(positionfixes)
     # copy the original pfs for adding 'staypoint_id' column
     pfs = positionfixes.copy()
 
@@ -220,6 +222,9 @@ def generate_triplegs(
     --------
     >>> pfs.as_positionfixes.generate_triplegs('between_staypoints', gap_threshold=15)
     """
+    Positionfixes.validate(positionfixes)
+    if staypoints is not None:
+        Staypoints.validate(staypoints)
     # copy the original pfs for adding 'tripleg_id' column
     pfs = positionfixes.copy()
 
diff --git a/trackintel/preprocessing/staypoints.py b/trackintel/preprocessing/staypoints.py
index 6c180c0..547f71c 100644
--- a/trackintel/preprocessing/staypoints.py
+++ b/trackintel/preprocessing/staypoints.py
@@ -4,6 +4,7 @@ import pandas as pd
 from sklearn.cluster import DBSCAN
 import warnings
 
+from trackintel import Staypoints
 from trackintel.geogr.distances import meters_to_decimal_degrees, check_gdf_planar
 from trackintel.preprocessing.util import applyParallel, angle_centroid_multipoints
 
@@ -71,6 +72,7 @@ def generate_locations(
     --------
     >>> sp.as_staypoints.generate_locations(method='dbscan', epsilon=100, num_samples=1)
     """
+    Staypoints.validate(staypoints)
     if agg_level not in ["user", "dataset"]:
         raise AttributeError(f"agg_level '{agg_level}' is unknown. Supported values are ['user', 'dataset'].")
     if method not in ["dbscan"]:
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index 43e2989..be3f57a 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -5,6 +5,7 @@ import numpy as np
 import pandas as pd
 from shapely.geometry import MultiPoint, Point
 
+from trackintel import Staypoints, Triplegs
 from trackintel.preprocessing.util import _explode_agg
 
 
@@ -66,6 +67,8 @@ def generate_trips(staypoints, triplegs, gap_threshold=15, add_geometry=True):
     trips can also be directly generated using the tripleg accessor
     >>> staypoints, triplegs, trips = triplegs.as_triplegs.generate_trips(staypoints)
     """
+    Triplegs.validate(triplegs)
+    Staypoints.validate(staypoints)
     gap_threshold = pd.to_timedelta(gap_threshold, unit="min")
     sp_tpls = _concat_staypoints_triplegs(staypoints, triplegs, add_geometry)
 
diff --git a/trackintel/preprocessing/trips.py b/trackintel/preprocessing/trips.py
index 2121ad0..3563841 100644
--- a/trackintel/preprocessing/trips.py
+++ b/trackintel/preprocessing/trips.py
@@ -119,12 +119,14 @@ def generate_tours(
         ), "Staypoints with location ID is required, otherwise tours are generated without location using max_dist"
         geom_col = None  # not used
         crs_is_projected = False  # not used
+        ti.Staypoints.validate(staypoints)
     else:
         # if no location is given, we need the trips table to have a geometry column
         assert isinstance(trips, gpd.geodataframe.GeoDataFrame), "Trips table must be a GeoDataFrame"
         geom_col = trips.geometry.name
         # get crs
         crs_is_projected = ti.geogr.check_gdf_planar(trips)
+        ti.TripsGeoDataFrame.validate(trips)
 
     # convert max_time to timedelta
     if isinstance(max_time, str):


Test patch:
diff --git a/tests/model/test_locations.py b/tests/model/test_locations.py
index 8c0a33f..84da561 100644
--- a/tests/model/test_locations.py
+++ b/tests/model/test_locations.py
@@ -45,29 +45,3 @@ class TestLocations:
         locs = testdata_locs.as_locations
         assert type(locs) is Locations
         assert id(locs) == id(locs.as_locations)
-
-    def test_check_suceeding(self, testdata_locs):
-        """Test if check returns True on valid pfs"""
-        assert Locations._check(testdata_locs)
-
-    def test_check_missing_columns(self, testdata_locs):
-        """Test if check returns False if column is missing"""
-        assert not Locations._check(testdata_locs.drop(columns="user_id"))
-
-    def test_check_empty_df(self, testdata_locs):
-        """Test if check returns False if DataFrame is empty"""
-        assert not Locations._check(testdata_locs.drop(testdata_locs.index))
-
-    def test_check_false_geometry_type(self, testdata_locs):
-        """Test if check returns False if geometry type is wrong"""
-        testdata_locs["center"] = LineString(
-            [(13.476808430, 48.573711823), (13.506804, 48.939008), (13.4664690, 48.5706414)]
-        )
-        assert not Locations._check(testdata_locs)
-
-    def test_check_ignore_false_geometry_type(self, testdata_locs):
-        """Test if check returns True if geometry type is wrong but validate_geometry is set to False"""
-        testdata_locs["center"] = LineString(
-            [(13.476808430, 48.573711823), (13.506804, 48.939008), (13.4664690, 48.5706414)]
-        )
-        assert Locations._check(testdata_locs, validate_geometry=False)
diff --git a/tests/model/test_positionfixes.py b/tests/model/test_positionfixes.py
index 9337794..7239580 100644
--- a/tests/model/test_positionfixes.py
+++ b/tests/model/test_positionfixes.py
@@ -56,34 +56,3 @@ class TestPositionfixes:
         accessor_result = pfs.as_positionfixes.calculate_distance_matrix(dist_metric="haversine", n_jobs=1)
         function_result = ti.geogr.distances.calculate_distance_matrix(pfs, dist_metric="haversine", n_jobs=1)
         assert np.allclose(accessor_result, function_result)
-
-    def test_check_suceeding(self, testdata_geolife):
-        """Test if check returns True on valid pfs"""
-        assert Positionfixes._check(testdata_geolife)
-
-    def test_check_missing_columns(self, testdata_geolife):
-        """Test if check returns False if column is missing"""
-        assert not Positionfixes._check(testdata_geolife.drop(columns="user_id"))
-
-    def test_check_empty_df(self, testdata_geolife):
-        """Test if check returns False if DataFrame is empty"""
-        assert not Positionfixes._check(testdata_geolife.drop(testdata_geolife.index))
-
-    def test_check_no_tz(self, testdata_geolife):
-        """Test if check returns False if tracked at column has no tz"""
-        testdata_geolife["tracked_at"] = testdata_geolife["tracked_at"].dt.tz_localize(None)
-        assert not Positionfixes._check(testdata_geolife)
-
-    def test_check_false_geometry_type(self, testdata_geolife):
-        """Test if check returns False if geometry type is wrong"""
-        testdata_geolife["geom"] = LineString(
-            [(13.476808430, 48.573711823), (13.506804, 48.939008), (13.4664690, 48.5706414)]
-        )
-        assert not Positionfixes._check(testdata_geolife)
-
-    def test_check_ignore_false_geometry_type(self, testdata_geolife):
-        """Test if check returns True if geometry type is wrong but validate_geometry is set to False"""
-        testdata_geolife["geom"] = LineString(
-            [(13.476808430, 48.573711823), (13.506804, 48.939008), (13.4664690, 48.5706414)]
-        )
-        assert Positionfixes._check(testdata_geolife, validate_geometry=False)
diff --git a/tests/model/test_staypoints.py b/tests/model/test_staypoints.py
index a21c233..323aaae 100644
--- a/tests/model/test_staypoints.py
+++ b/tests/model/test_staypoints.py
@@ -47,34 +47,3 @@ class TestStaypoints:
         """Check if sp has center method and returns (lat, lon) pairs as geometry."""
         sp = testdata_sp.copy()
         assert len(sp.as_staypoints.center) == 2
-
-    def test_check_suceeding(self, testdata_sp):
-        """Test if check returns True on valid sp"""
-        assert Staypoints._check(testdata_sp)
-
-    def test_check_missing_columns(self, testdata_sp):
-        """Test if check returns False if column is missing"""
-        assert not Staypoints._check(testdata_sp.drop(columns="user_id"))
-
-    def test_check_no_tz(self, testdata_sp):
-        """Test if check returns False if datetime columns have no tz"""
-        tmp = testdata_sp["started_at"]
-        testdata_sp["started_at"] = testdata_sp["started_at"].dt.tz_localize(None)
-        assert not Staypoints._check(testdata_sp)
-        testdata_sp["started_at"] = tmp
-        testdata_sp["finished_at"] = testdata_sp["finished_at"].dt.tz_localize(None)
-        assert not Staypoints._check(testdata_sp)
-
-    def test_check_false_geometry_type(self, testdata_sp):
-        """Test if check returns False if geometry type is wrong"""
-        testdata_sp["geom"] = LineString(
-            [(13.476808430, 48.573711823), (13.506804, 48.939008), (13.4664690, 48.5706414)]
-        )
-        assert not Staypoints._check(testdata_sp)
-
-    def test_check_ignore_false_geometry_type(self, testdata_sp):
-        """Test if check returns True if geometry type is wrong but validate_geometry is set to False"""
-        testdata_sp["geom"] = LineString(
-            [(13.476808430, 48.573711823), (13.506804, 48.939008), (13.4664690, 48.5706414)]
-        )
-        assert Staypoints._check(testdata_sp, validate_geometry=False)
diff --git a/tests/model/test_tours.py b/tests/model/test_tours.py
index 281a244..573d582 100644
--- a/tests/model/test_tours.py
+++ b/tests/model/test_tours.py
@@ -25,20 +25,3 @@ class TestTours:
         tours = test_tours.as_tours
         assert type(tours) is Tours
         assert id(tours) == id(tours.as_tours)
-
-    def test_check_succeeding(self, test_tours):
-        """Test if check returns True on valid tours"""
-        assert Tours._check(test_tours)
-
-    def test_check_missing_columns(self, test_tours):
-        """Test if check returns False if column is missing"""
-        assert not Tours._check(test_tours.drop(columns="user_id"))
-
-    def test_check_no_tz(self, test_tours):
-        """Test if check returns False if datetime columns have no tz"""
-        tmp = test_tours["started_at"]
-        test_tours["started_at"] = test_tours["started_at"].dt.tz_localize(None)
-        assert not Tours._check(test_tours)
-        test_tours["started_at"] = tmp
-        test_tours["finished_at"] = test_tours["finished_at"].dt.tz_localize(None)
-        assert not Tours._check(test_tours)
diff --git a/tests/model/test_triplegs.py b/tests/model/test_triplegs.py
index 451b19d..a94fb5c 100644
--- a/tests/model/test_triplegs.py
+++ b/tests/model/test_triplegs.py
@@ -51,34 +51,3 @@ class TestTriplegs:
         tpls = testdata_tpls.as_triplegs
         assert type(tpls) is Triplegs
         assert id(tpls) == id(tpls.as_triplegs)
-
-    def test_check_suceeding(self, testdata_tpls):
-        """Test if check returns True on valid pfs"""
-        assert Triplegs._check(testdata_tpls)
-
-    def test_check_missing_columns(self, testdata_tpls):
-        """Test if check returns False if column is missing"""
-        assert not Triplegs._check(testdata_tpls.drop(columns="user_id"))
-
-    def test_check_empty_df(self, testdata_tpls):
-        """Test if check returns False if DataFrame is empty"""
-        assert not Triplegs._check(testdata_tpls.drop(testdata_tpls.index))
-
-    def test_check_no_tz(self, testdata_tpls):
-        """Test if check returns False if tracked at column has no tz"""
-        tmp = testdata_tpls["started_at"]
-        testdata_tpls["started_at"] = testdata_tpls["started_at"].dt.tz_localize(None)
-        assert not Triplegs._check(testdata_tpls)
-        testdata_tpls["started_at"] = tmp
-        testdata_tpls["finished_at"] = testdata_tpls["finished_at"].dt.tz_localize(None)
-        assert not Triplegs._check(testdata_tpls)
-
-    def test_check_false_geometry_type(self, testdata_tpls):
-        """Test if check returns False if geometry type is wrong"""
-        testdata_tpls["geom"] = Point([(13.476808430, 48.573711823)])
-        assert not Triplegs._check(testdata_tpls)
-
-    def test_check_ignore_false_geometry_type(self, testdata_tpls):
-        """Test if check returns True if geometry type is wrong but validate_geometry is set to False"""
-        testdata_tpls["geom"] = Point([(13.476808430, 48.573711823)])
-        assert Triplegs._check(testdata_tpls, validate_geometry=False)
diff --git a/tests/model/test_trips.py b/tests/model/test_trips.py
index 3678e75..dbf1633 100644
--- a/tests/model/test_trips.py
+++ b/tests/model/test_trips.py
@@ -46,38 +46,3 @@ class TestTrips:
         # check that it works with other geometry name
         with pytest.raises(ValueError, match="The geometry must be a MultiPoint"):
             trips_other_geom_name.as_trips
-
-
-class TestTripsDataFrame:
-    """Test TripsDataFrame class"""
-
-    def test_check_suceeding(self, testdata_trips):
-        """Test if check returns True on valid trips"""
-        assert TripsDataFrame._check(testdata_trips)
-
-    def test_check_missing_columns(self, testdata_trips):
-        """Test if check returns False if column is missing"""
-        assert not TripsDataFrame._check(testdata_trips.drop(columns="user_id"))
-
-    def test_check_no_tz(self, testdata_trips):
-        """Test if check returns False if datetime columns have no tz"""
-        tmp = testdata_trips["started_at"]
-        testdata_trips["started_at"] = testdata_trips["started_at"].dt.tz_localize(None)
-        assert not TripsDataFrame._check(testdata_trips)
-        testdata_trips["started_at"] = tmp
-        testdata_trips["finished_at"] = testdata_trips["finished_at"].dt.tz_localize(None)
-        assert not TripsDataFrame._check(testdata_trips)
-
-
-class TestTripsGeoDataFrame:
-    """Test TripsGeoDataFrame class"""
-
-    def test_check_false_geometry_type(self, testdata_trips):
-        """Test if check returns False if geometry type is wrong"""
-        testdata_trips["geom"] = Point((13.476808430, 48.573711823))
-        assert not TripsGeoDataFrame._check(testdata_trips)
-
-    def test_check_ignore_false_geometry_type(self, testdata_trips):
-        """Test if check returns True if geometry type is wrong but validate_geometry is set to False"""
-        testdata_trips["geom"] = Point((13.476808430, 48.573711823))
-        assert TripsGeoDataFrame._check(testdata_trips, validate_geometry=False)
diff --git a/tests/model/test_util.py b/tests/model/test_util.py
index 61e5d45..69d8e39 100644
--- a/tests/model/test_util.py
+++ b/tests/model/test_util.py
@@ -50,33 +50,12 @@ class Test_wrapped_gdf_method:
 
         assert type(foo(example_positionfixes)) is pd.Series
 
-    def test_failed_check(self, example_positionfixes):
-        """Test if _check fails then __class__ is not touched"""
-
-        class A(GeoDataFrame):
-            fallback_class = None
-
-            @staticmethod
-            def _check(obj, validate_geometry=True):
-                return False
-
-        @_wrapped_gdf_method
-        def foo(a: A) -> GeoDataFrame:
-            return GeoDataFrame(a)
-
-        a = A(example_positionfixes)
-        assert type(foo(a)) is GeoDataFrame
-
     def test_keep_class(self, example_positionfixes):
-        """Test if original class is restored if return value subclasses GeoDataFarme and fulfills _check"""
+        """Test if original class is restored if return value subclasses GeoDataFrame"""
 
         class A(GeoDataFrame):
             fallback_class = None
 
-            @staticmethod
-            def _check(obj, validate_geometry=True):
-                return True
-
         @_wrapped_gdf_method
         def foo(a: A) -> GeoDataFrame:
             return GeoDataFrame(a)
@@ -85,7 +64,7 @@ class Test_wrapped_gdf_method:
         assert type(foo(a)) is A
 
     def test_fallback(self, example_positionfixes):
-        """Test if fallback to fallback_class if _check succeeds but not subclasses GeoDataFrame"""
+        """Test if fallback to fallback_class if no GeoDataFrame"""
 
         class B(pd.DataFrame):
             pass
@@ -93,10 +72,6 @@ class Test_wrapped_gdf_method:
         class A(GeoDataFrame):
             fallback_class = B
 
-            @staticmethod
-            def _check(obj, validate_geometry=True):
-                return True
-
         @_wrapped_gdf_method
         def foo(a: A) -> pd.DataFrame:
             return pd.DataFrame(a)
@@ -110,10 +85,6 @@ class Test_wrapped_gdf_method:
         class A(GeoDataFrame):
             fallback_class = None
 
-            @staticmethod
-            def _check(obj, validate_geometry=True):
-                return True
-
         @_wrapped_gdf_method
         def foo(a: A) -> pd.DataFrame:
             return pd.DataFrame(a)
@@ -129,17 +100,14 @@ class TestTrackintelGeoDataFrame:
         """Mimic TrackintelGeoDataFrame subclass by taking the same arguments"""
 
         class AFallback(TrackintelDataFrame):
-            pass
+            def __init__(self, *args, validate=True, **kwargs):
+                super().__init__(*args, **kwargs)
 
         fallback_class = AFallback
 
-        def __init__(self, *args, validate_geometry=True, **kwargs):
+        def __init__(self, *args, validate=True, validate_geometry=True, **kwargs):
             super().__init__(*args, **kwargs)
 
-        @staticmethod
-        def _check(obj, validate_geometry=True):
-            return "user_id" in obj.columns
-
     def test_getitem(self, example_positionfixes):
         """Test if loc on all columns returns original class."""
         a = self.A(example_positionfixes)
@@ -158,15 +126,6 @@ class TestTrackintelGeoDataFrame:
         b = a.merge(a, on="user_id", suffixes=("", "_other"))
         assert type(b) is self.A
 
-    def test_constructor_fails_check(self, example_positionfixes):
-        """Test if falls through if _check fails"""
-        a = self.A(example_positionfixes)
-        # a.drop(columns=geometryname, inplace=True) fails
-        # this is also true for geopandas their motivation can be found here:
-        # https://github.com/geopandas/geopandas/pull/2060#issuecomment-899802955
-        a = a.drop(columns="user_id")
-        assert type(a) is GeoDataFrame
-
     def test_constructor_fallback_class(self, example_positionfixes):
         """Test if _constructor gets can fallback to fallback_class"""
         a = self.A(example_positionfixes)
@@ -192,18 +151,11 @@ class TestTrackintelDataFrame:
     class A(TrackintelDataFrame):
         """Mimic TrackintelDataFrame subclass by taking the same arguments"""
 
-        @staticmethod
-        def _check(obj):
-            return "user_id" in obj.columns
-
-    def test_constructor_fails_check(self, example_positionfixes):
-        """Test if falls through if _check fails"""
-        a = self.A(example_positionfixes)
-        a = a.drop(columns="user_id")
-        assert type(a) is pd.DataFrame
+        def __init__(self, *args, validate=True, **kwargs):
+            super().__init__(*args, **kwargs)
 
     def test_constructor_calls_init(self, example_positionfixes):
-        """Test if _constructor gets GeoDataFrame and fulfills test then builds class"""
+        """Test if _constructor gets DataFrame and fulfills test then builds class"""
         a = self.A(example_positionfixes)
         assert type(a._constructor(a)) is self.A
 
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 834b1c0..45e2a33 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -263,7 +263,7 @@ class TestGenerate_trips:
 
     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")
+        start = pd.Timestamp("2021-07-11 8:00:00", tz="utc")
         h = pd.to_timedelta("1h")
         sp_tpls = [
             {"is_activity": True, "type": "staypoint"},
@@ -278,8 +278,12 @@ class TestGenerate_trips:
             d["started_at"] = start + n * h
             d["finished_at"] = d["started_at"] + h
         sp_tpls = pd.DataFrame(sp_tpls)
-        sp = sp_tpls[sp_tpls["type"] == "staypoint"]
-        tpls = sp_tpls[sp_tpls["type"] == "tripleg"]
+        sp = sp_tpls[sp_tpls["type"] == "staypoint"].copy()
+        tpls = sp_tpls[sp_tpls["type"] == "tripleg"].copy()
+        sp["geom"] = Point(0, 0)
+        tpls["geom"] = LineString([[1, 1], [2, 2]])
+        sp = gpd.GeoDataFrame(sp, geometry="geom")
+        tpls = gpd.GeoDataFrame(tpls, geometry="geom")
         sp_, tpls_, trips = generate_trips(sp, tpls, add_geometry=False)
         trip_id_truth = pd.Series([None, None, None, 0, None], dtype="Int64")
         trip_id_truth.index = sp_.index  # don't check index
@@ -289,7 +293,7 @@ class TestGenerate_trips:
 
     def test_sp_tpls_index(self):
         """Test if staypoint and tripleg index are identical before and after generating trips."""
-        start = pd.Timestamp("2021-07-11 8:00:00")
+        start = pd.Timestamp("2021-07-11 8:00:00", tz="utc")
         h = pd.to_timedelta("1h")
         sp_tpls = [
             {"is_activity": True, "type": "staypoint"},
@@ -304,8 +308,12 @@ class TestGenerate_trips:
             d["finished_at"] = d["started_at"] + h
 
         sp_tpls = pd.DataFrame(sp_tpls)
-        sp = sp_tpls[sp_tpls["type"] == "staypoint"]
-        tpls = sp_tpls[sp_tpls["type"] != "staypoint"]
+        sp = sp_tpls[sp_tpls["type"] == "staypoint"].copy()
+        tpls = sp_tpls[sp_tpls["type"] != "staypoint"].copy()
+        sp["geom"] = Point(0, 0)
+        tpls["geom"] = LineString([(0, 0), (1, 1)])
+        sp = gpd.GeoDataFrame(sp, geometry="geom")
+        tpls = gpd.GeoDataFrame(tpls, geometry="geom")
         tpls.index.name = "something_long_and_obscure"
         sp.index.name = "even_obscurer"
         sp_, tpls_, _ = generate_trips(sp, tpls, add_geometry=False)
diff --git a/tests/preprocessing/test_trips.py b/tests/preprocessing/test_trips.py
index d5bb6c0..e1b830d 100644
--- a/tests/preprocessing/test_trips.py
+++ b/tests/preprocessing/test_trips.py
@@ -108,6 +108,10 @@ def example_trip_data():
 
     # assign location IDs (only for testing, not a trackintel staypoints standard!)
     sp_locs = pd.DataFrame({"location_id": [1, 1, 2, 3, 1, 2, 4]}, index=[1, 2, 3, 4, 5, 6, 7])
+    sp_locs["user_id"] = 0
+    sp_locs["started_at"] = sp_locs["finished_at"] = pd.Timestamp("2021-07-11 8:00:00", tz="utc")
+    sp_locs["geom"] = Point(0.0, 0.0)
+    sp_locs = gpd.GeoDataFrame(sp_locs, geometry="geom")
 
     trips = gpd.GeoDataFrame(data=trips_list_dict, geometry="geom", crs="EPSG:4326")
     trips = trips.set_index("id")