Repository: mie-lab/trackintel
Pull request #393 (2022-05-01 22:28:19)

ENH: Simplified logic in `_split_overlaps`
When I updated `split_overlaps` in #384, I had an idea for an alternative implementation.
I am not completely happy with mine either because I had to use the `apply` method. I did not find another way with the two datetime columns ["started_at", "finished_at"]. The logic is simpler, but I haven`t looked at the performance.
So this is more a suggestion, if you think this is a bad idea, we can drop it.

This PR contains:
- removed unused method `_get_all_quality`
- removed `max_iter` keyword argument.
- created new function `_get_times` that returns list of start-end times.
- applied this function to all rows for a more elegant split.
- wrote test for new `_get_times` function.


codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/393?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
> Merging [#393](https://codecov.io/gh/mie-lab/trackintel/pull/393?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (4ceb608) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/61a70ca949359968269d953bc62457107cea44ff?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) (61a70ca) will **increase** coverage by `0.33%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #393      +/-   ##
==========================================
+ Coverage   96.37%   96.71%   +0.33%     
==========================================
  Files          39       39              
  Lines        1905     1888      -17     
  Branches      284      283       -1     
==========================================
- Hits         1836     1826      -10     
+ Misses         34       27       -7     
  Partials       35       35              
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/393?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/analysis/tracking\_quality.py](https://codecov.io/gh/mie-lab/trackintel/pull/393/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9hbmFseXNpcy90cmFja2luZ19xdWFsaXR5LnB5) | `100.00% <100.00%> (+7.95%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/393?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/393?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab). Last update [61a70ca...4ceb608](https://codecov.io/gh/mie-lab/trackintel/pull/393?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/docs/modules/model.rst b/docs/modules/model.rst
index 139b2a1..25c7a4b 100644
--- a/docs/modules/model.rst
+++ b/docs/modules/model.rst
@@ -123,13 +123,6 @@ a user, but also (potentially, if this link has already been made) to a tripleg
         -- Temporal attributes.
         tracked_at timestamp with time zone NOT NULL,
 
-        -- Specific attributes.
-        accuracy double precision,
-        tracking_tech character(12),
-        -- The context contains additional information that might be filled in by trackintel.
-        -- This could include things such as the temperature, public transport stops in vicinity, etc.
-        context json,
-
         -- Spatial attributes.
         elevation double precision,
         geom geometry(Point, 4326),
@@ -163,14 +156,6 @@ i.e., a meaningful destination of movement::
         validated_at timestamp with time zone,
         activity boolean,
 
-        -- Specific attributes.
-        -- The radius is an approximation of how far the positionfixes that made up this staypoint
-        -- are scattered around the center (geom) of it.
-        radius double precision,
-        -- The context contains additional information that might be filled in by trackintel.
-        -- This could include things such as the temperature, public transport stops in vicinity, etc.
-        context json,
-
         -- Spatial attributes.
         elevation double precision,
         geom geometry(Point, 4326),
@@ -201,11 +186,6 @@ and if applicable, a public transport case::
         validated boolean,
         validated_at timestamp with time zone,
 
-        -- Specific attributes.
-        -- The context contains additional information that might be filled in by trackintel.
-        -- This could include things such as the temperature, public transport stops in vicinity, etc.
-        context json,
-
         -- Spatial attributes.
         -- The raw geometry is unprocessed, directly made up from the positionfixes. The column
         -- 'geom' contains processed (e.g., smoothened, map matched, etc.) data.
@@ -223,11 +203,6 @@ staypoints) meaningful locations::
         -- Common to all tables.
         id bigint NOT NULL,
         user_id bigint,
-
-        -- Specific attributes.
-        -- The context contains additional information that might be filled in by trackintel.
-        -- This could include things such as the temperature, public transport stops in vicinity, etc.
-        context json,
         
         -- Spatial attributes.
         elevation double precision,
@@ -253,11 +228,6 @@ activity (staypoint with ``activity==True``) to another. They are simply linked
         -- Temporal attributes.
         started_at timestamp with time zone NOT NULL,
         finished_at timestamp with time zone NOT NULL,
-        
-        -- Specific attributes.
-        -- The context contains additional information that might be filled in by trackintel.
-        -- This could include things such as the temperature, public transport stops in vicinity, etc.
-        context json,
 
         -- Constraints.
         CONSTRAINT trips_pkey PRIMARY KEY (id)
@@ -281,9 +251,6 @@ They are linked to a user::
         
         -- Specific attributes.
         journey bool,
-        -- The context contains additional information that might be filled in by trackintel.
-        -- This could include things such as the temperature, public transport stops in vicinity, etc.
-        context json,
 
         -- Constraints.
         CONSTRAINT tours_pkey PRIMARY KEY (id)
diff --git a/sql/create_tables_pg.sql b/sql/create_tables_pg.sql
index 1da4756..cc55a9e 100644
--- a/sql/create_tables_pg.sql
+++ b/sql/create_tables_pg.sql
@@ -15,13 +15,6 @@ CREATE TABLE positionfixes (
     -- Temporal attributes.
     tracked_at timestamp without time zone NOT NULL,
 
-    -- Specific attributes.
-    accuracy double precision,
-    tracking_tech character(12),
-    -- The context contains additional information that might be filled in by trackintel.
-    -- This could include things such as the temperature, public transport stops in vicinity, etc.
-    context json,
-
     -- Spatial attributes.
     elevation double precision,
     geom geometry(Point, 4326),
@@ -50,14 +43,6 @@ CREATE TABLE staypoints (
     validated_at timestamp without time zone,
     activity boolean,
 
-    -- Specific attributes.
-    -- The radius is an approximation of how far the positionfixes that made up this staypoint
-    -- are scattered around the center (geom) of it.
-    radius double precision,
-    -- The context contains additional information that might be filled in by trackintel.
-    -- This could include things such as the temperature, public transport stops in vicinity, etc.
-    context json,
-
     -- Spatial attributes.
     elevation double precision,
     geom geometry(Point, 4326),
@@ -84,11 +69,6 @@ CREATE TABLE triplegs (
     validated boolean,
     validated_at timestamp without time zone,
 
-    -- Specific attributes.
-    -- The context contains additional information that might be filled in by trackintel.
-    -- This could include things such as the temperature, public transport stops in vicinity, etc.
-    context json,
-
     -- Spatial attributes.
     -- The raw geometry is unprocessed, directly made up from the positionfixes. The column
     -- 'geom' contains processed (e.g., smoothened, map matched, etc.) data.
@@ -103,11 +83,6 @@ CREATE TABLE locations (
     -- Common to all tables.
     id bigint NOT NULL,
     user_id bigint,
-
-    -- Specific attributes.
-    -- The context contains additional information that might be filled in by trackintel.
-    -- This could include things such as the temperature, public transport stops in vicinity, etc.
-    context json,
     
     -- Spatial attributes.
     elevation double precision,
@@ -130,11 +105,6 @@ CREATE TABLE trips (
     -- Temporal attributes.
     started_at timestamp without time zone NOT NULL,
     finished_at timestamp without time zone NOT NULL,
-    
-    -- Specific attributes.
-    -- The context contains additional information that might be filled in by trackintel.
-    -- This could include things such as the temperature, public transport stops in vicinity, etc.
-    context json,
 
     -- Constraints.
     CONSTRAINT trips_pkey PRIMARY KEY (id)
@@ -151,10 +121,6 @@ CREATE TABLE tours (
     -- Temporal attributes.
     started_at timestamp without time zone NOT NULL,
     finished_at timestamp without time zone NOT NULL,
-    
-    -- The context contains additional information that might be filled in by trackintel.
-    -- This could include things such as the temperature, public transport stops in vicinity, etc.
-    context json,
 
     -- Constraints.
     CONSTRAINT tours_pkey PRIMARY KEY (id)
diff --git a/trackintel/analysis/tracking_quality.py b/trackintel/analysis/tracking_quality.py
index 20e3e3a..11a91ee 100644
--- a/trackintel/analysis/tracking_quality.py
+++ b/trackintel/analysis/tracking_quality.py
@@ -5,7 +5,7 @@ import numpy as np
 import pandas as pd
 
 
-def temporal_tracking_quality(source, granularity="all", max_iter=60):
+def temporal_tracking_quality(source, granularity="all"):
     """
     Calculate per-user temporal tracking quality (temporal coverage).
 
@@ -20,13 +20,6 @@ def temporal_tracking_quality(source, granularity="all", max_iter=60):
         by weeks; "weekday" the quality by day of the week (e.g, Mondays, Tuesdays, etc.) and
         "hour" the quality by hours.
 
-    max_iter: int, default 60
-        The maximum iteration when spliting the df per granularity to calculate tracking quality.
-        The unit is the same as the "granularity", e.g., if "max_iter" = 60 and "granularity" = "day",
-        df that contains records with a duration more than 60 days cannot be processed and the quality
-        result is likely to be wrong. if "max_iter" is reached, consider pre-filter the df instead of
-        changing the "max_iter" parameter.
-
     Returns
     -------
     quality: DataFrame
@@ -99,7 +92,7 @@ def temporal_tracking_quality(source, granularity="all", max_iter=60):
         return quality
 
     # split records that span several days
-    df = _split_overlaps(df, granularity="day", max_iter=max_iter)
+    df = _split_overlaps(df, granularity="day")
     if granularity == "day":
         grouper = pd.Grouper(key="started_at", freq="D")
         column_name = "day"
@@ -140,39 +133,6 @@ def temporal_tracking_quality(source, granularity="all", max_iter=60):
     return quality
 
 
-def _get_all_quality(df, raw_quality, granularity):
-    """
-    Add tracking quality values for empty bins.
-
-    raw_quality is calculated using `groupby` and does not report bins (=granularties) with
-    quality = 0. This function adds these values.
-
-    Parameters
-    ----------
-    df : GeoDataFrame (as trackintel datamodels)
-
-    raw_quality: DataFrame
-        The calculated raw tracking quality directly from the groupby operations.
-
-    granularity : {"all", "day", "weekday", "week", "hour"}
-        Used for accessing the column in raw_quality.
-
-    Returns
-    -------
-    quality: pandas.Series
-        A pandas.Series object containing the tracking quality
-    """
-    all_users = df["user_id"].unique()
-    all_granularity = np.arange(df[granularity].max() + 1)
-    # construct array containing all user and granularity combinations
-    all_combi = np.array(np.meshgrid(all_users, all_granularity)).T.reshape(-1, 2)
-    # the records with no corresponding raw_quality is nan, and transformed into 0
-    all_combi = pd.DataFrame(all_combi, columns=["user_id", granularity])
-    quality = all_combi.merge(raw_quality, how="left", on=["user_id", granularity], validate="one_to_one")
-    quality.fillna(0, inplace=True)
-    return quality
-
-
 def _get_tracking_quality_user(df, granularity="all"):
     """
     Tracking quality per-user per-granularity.
@@ -216,84 +176,57 @@ def _get_tracking_quality_user(df, granularity="all"):
     return pd.Series([tracked_duration / extent], index=["quality"])
 
 
-def _split_overlaps(source, granularity="day", max_iter=60):
+def _split_overlaps(source, granularity="day"):
     """
     Split input df that have a duration of several days or hours.
 
     Parameters
     ----------
     source : GeoDataFrame (as trackintel datamodels)
-        The source to perform the split
+        The GeoDataFrame to perform the split on.
 
     granularity : {'day', 'hour'}, default 'day'
         The criteria of splitting. "day" splits records that have duration of several
         days and "hour" splits records that have duration of several hours.
 
-    max_iter: int, default 60
-        The maximum iteration when spliting the source per granularity.
-        See :func:`trackintel.analysis.tracking_quality.temporal_tracking_quality` for a more detailed explaination.
-
     Returns
     -------
     GeoDataFrame (as trackintel datamodels)
         The GeoDataFrame object after the splitting
     """
-    if granularity == "hour":
-        # every split over hour splits also over day
-        # this way to split of an entry over a month takes 30+24 iterations instead of 30*24.
-        df = _split_overlaps(source, granularity="day", max_iter=max_iter)
-    else:
-        df = source.copy()
-
-    change_flag = _get_split_index(df, granularity=granularity)
-    iter_count = 0
-
-    freq = "D" if granularity == "day" else "H"
-    # Iteratively split one day/hour from multi day/hour entries until no entry spans over multiple days/hours
-    while change_flag.sum() > 0:
-        # calculate new finished_at timestamp (00:00 midnight)
-        new_df = df.loc[change_flag].copy()
-        df.loc[change_flag, "finished_at"] = (df.loc[change_flag, "started_at"] + pd.Timestamp.resolution).dt.ceil(freq)
-
-        # create new entries with remaining timestamp
-        new_df.loc[change_flag, "started_at"] = df.loc[change_flag, "finished_at"]
+    freq = "H" if granularity == "hour" else "D"
+    gdf = source.copy()
+    gdf[["started_at", "finished_at"]] = gdf.apply(_get_times, axis="columns", result_type="expand", freq=freq)
+    # must call DataFrame.explode directly because GeoDataFrame.explode cannot be used on multiple columns
+    gdf = super(type(gdf), gdf).explode(["started_at", "finished_at"], ignore_index=True)
+    if "duration" in gdf.columns:
+        gdf["duration"] = gdf["finished_at"] - gdf["started_at"]
+    return gdf
 
-        df = pd.concat((df, new_df), ignore_index=True, sort=True)
 
-        change_flag = _get_split_index(df, granularity=granularity)
-        iter_count += 1
-        if iter_count >= max_iter:
-            warnings.warn(
-                f"Maximum iteration {max_iter} reached when splitting the input dataframe by {granularity}. "
-                "Consider checking the timeframe of the input or parsing a higher 'max_iter' parameter."
-            )
-            break
-
-    if "duration" in df.columns:
-        df["duration"] = df["finished_at"] - df["started_at"]
-    return df
-
-
-def _get_split_index(df, granularity="day"):
+def _get_times(row, freq="D"):
     """
-    Get the index that needs to be splitted.
+    Returns the times for splitting range (start-finish) at frequency borders.
+
+    Use it with `.apply()` for a single row of a DataFrame. Set result_type="expand".
 
     Parameters
     ----------
-    df : GeoDataFrame (as trackintel datamodels)
-        The source to perform the split.
+    row : Series
+        Row of dataframe with columns ["started_at", "finished_at"].
 
-    granularity : {'day', 'hour'}, default 'day'
-        The criteria of spliting. "day" splits records that have duration of several
-        days and "hour" splits records that have duration of several hours.
+    freq : str or DateOffset, default 'D'
+        Pandas frequency string.
 
     Returns
     -------
-    change_flag: pd.Series
-        Boolean index indicating which records needs to be splitted
+    Tuple of lists
+        Tuple of (start, end) times.
     """
-    freq = "D" if granularity == "day" else "H"
-    cond1 = df["started_at"].dt.floor(freq) != (df["finished_at"] - pd.Timedelta.resolution).dt.floor(freq)
-    # catch corner case where both on same border and subtracting would lead to error
-    cond2 = df["started_at"] != df["finished_at"]
-    return cond1 & cond2
+    result = []
+    if row["started_at"] != row["started_at"].ceil(freq):
+        result.append(row["started_at"])  # is not on border -> not included in date_range
+    result.extend(pd.date_range(row["started_at"].ceil(freq), row["finished_at"], freq=freq).to_list())
+    if (row["finished_at"] != result[-1]) or (len(result) == 1):  # len check for started_at == finished_at
+        result.append(row["finished_at"])  # is not on border -> not included in data_range
+    return result[:-1], result[1:]
diff --git a/trackintel/model/locations.py b/trackintel/model/locations.py
index 7d1e901..c8dd578 100644
--- a/trackintel/model/locations.py
+++ b/trackintel/model/locations.py
@@ -19,7 +19,7 @@ class LocationsAccessor(object):
     ['user_id', 'center']
 
     For several usecases, the following additional columns are required:
-    ['elevation', 'context', 'extent']
+    ['elevation', 'extent']
 
     Notes
     -----
diff --git a/trackintel/model/positionfixes.py b/trackintel/model/positionfixes.py
index 73a3a63..45ccb4a 100644
--- a/trackintel/model/positionfixes.py
+++ b/trackintel/model/positionfixes.py
@@ -23,7 +23,7 @@ class PositionfixesAccessor(object):
     of the `Positionfixes`.
 
     For several usecases, the following additional columns are required:
-    ['elevation', 'accuracy', 'tracking_tech', 'context', 'staypoint_id', 'tripleg_id']
+    ['elevation', 'accuracy' 'staypoint_id', 'tripleg_id']
 
     Notes
     -----
diff --git a/trackintel/model/staypoints.py b/trackintel/model/staypoints.py
index 85f4a52..1d0d4a5 100644
--- a/trackintel/model/staypoints.py
+++ b/trackintel/model/staypoints.py
@@ -24,14 +24,14 @@ class StaypointsAccessor(object):
     of the `Staypoints`.
 
     For several usecases, the following additional columns are required:
-    ['elevation', 'radius', 'context', 'purpose', 'is_activity', 'next_trip_id', 'prev_trip_id', 'trip_id',
+    ['elevation', 'purpose', 'is_activity', 'next_trip_id', 'prev_trip_id', 'trip_id',
     location_id]
 
     Notes
     -----
     `Staypoints` are defined as location were a person did not move for a while.
     Under consideration of location uncertainty this means that a person stays within
-    a certain radius for a certain amount of time.
+    a close proximity for a certain amount of time.
     The exact definition is use-case dependent.
 
     'started_at' and 'finished_at' are timezone aware pandas datetime objects.
diff --git a/trackintel/model/tours.py b/trackintel/model/tours.py
index f36eabc..67a6b40 100644
--- a/trackintel/model/tours.py
+++ b/trackintel/model/tours.py
@@ -12,7 +12,7 @@ class ToursAccessor(object):
     The 'index' of the DataFrame will be treated as unique identifier of the `Tours`
 
     For several usecases, the following additional columns are required:
-    ['location_id', 'journey', 'context', 'origin_staypoint_id', 'destination_staypoint_id']
+    ['location_id', 'journey', 'origin_staypoint_id', 'destination_staypoint_id']
 
     Notes
     -----
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index ff46471..0675a15 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -23,7 +23,7 @@ class TripsAccessor(object):
     Trips have an optional geometry of type MultiPoint which describes the start and the end point of the trip
 
     For several usecases, the following additional columns are required:
-    ['context', 'origin_purpose', 'destination_purpose', 'modes', 'primary_mode', 'tour_id']
+    ['origin_purpose', 'destination_purpose', 'modes', 'primary_mode', 'tour_id']
 
     Notes
     -----
diff --git a/trackintel/preprocessing/trips.py b/trackintel/preprocessing/trips.py
index bb74cf0..0b378e9 100644
--- a/trackintel/preprocessing/trips.py
+++ b/trackintel/preprocessing/trips.py
@@ -257,8 +257,8 @@ def _generate_tours_user(
             else:
                 # If no locations are available, check whether the distance is smaller than max_dist
                 end_start_at_same_loc = _check_max_dist(
-                    user_trip_df.loc[start_candidates[-1], geom_col][1],  # destination point of previous trip
-                    row[geom_col][0],  # start point of current trip
+                    user_trip_df.loc[start_candidates[-1], geom_col].geoms[1],  # destination point of previous trip
+                    row[geom_col].geoms[0],  # start point of current trip
                     max_dist,
                     crs_is_projected,
                 )
@@ -318,8 +318,8 @@ def _generate_tours_user(
             else:
                 # if no locations are available, check whether the distance is smaller than max_dist
                 end_start_at_same_loc = _check_max_dist(
-                    user_trip_df.loc[cand, geom_col][0],  # start point of first trip
-                    row[geom_col][1],  # destination point of current trip
+                    user_trip_df.loc[cand, geom_col].geoms[0],  # start point of first trip
+                    row[geom_col].geoms[1],  # destination point of current trip
                     max_dist,
                     crs_is_projected=crs_is_projected,
                 )


Test patch:
diff --git a/tests/analysis/test_tracking_quality.py b/tests/analysis/test_tracking_quality.py
index 4511ec4..f87300a 100644
--- a/tests/analysis/test_tracking_quality.py
+++ b/tests/analysis/test_tracking_quality.py
@@ -7,7 +7,7 @@ import geopandas as gpd
 from shapely.geometry import Point
 
 import trackintel as ti
-from trackintel.analysis.tracking_quality import _get_split_index
+from trackintel.analysis.tracking_quality import _get_times
 
 
 @pytest.fixture
@@ -302,7 +302,7 @@ class TestSplit_overlaps:
     def test_split_overlaps_hours_case2(self, testdata_sp_tpls_geolife_long):
         """Test if _split_overlaps() function can split record that have the same hour but different days."""
         # get the first two records
-        head2 = testdata_sp_tpls_geolife_long.head(2)
+        head2 = testdata_sp_tpls_geolife_long.head(2).copy()
 
         # construct the finished_at exactly one day after started_at
         head2["finished_at"] = head2["started_at"] + pd.Timedelta("1d")
@@ -339,22 +339,6 @@ class TestSplit_overlaps:
         assert splitted_day["duration"].sum() == sp_tpls["duration"].sum()
         assert splitted_hour["duration"].sum() == sp_tpls["duration"].sum()
 
-    def test_max_iter_warning(self):
-        """Test if a warning is raised when maximum iteration is reached."""
-        p1 = Point(8.5067847, 47.4)
-        # construct time that is far apart - exceeding the default max_iter
-        t1 = pd.Timestamp("1971-01-01 00:00:00", tz="utc")
-        t2 = pd.Timestamp("1981-01-01 00:00:00", tz="utc")
-
-        list_dict = [
-            {"user_id": 0, "started_at": t1, "finished_at": t2, "geom": p1},
-        ]
-        sp = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:4326")
-        sp.index.name = "id"
-
-        with pytest.warns(UserWarning):
-            ti.analysis.tracking_quality._split_overlaps(sp, granularity="day")
-
     def test_exact_midnight_split(self):
         """Test if split finishes and starts on midnight on the ns (pandas resolution)."""
         midnight = pd.Timestamp("2022-03-18 00:00:00", tz="utc")
@@ -373,27 +357,21 @@ class TestSplit_overlaps:
         assert (sp_res["finished_at"] == [midnight, end, midnight, end, midnight]).all()
 
 
-class TestGet_split_index:
-    """Test if __get_split_index splits correctly"""
+class TestGet_times:
+    """Test if _get_times splits correctly"""
 
     def test_midnight_ns(self):
         """Test datetimes 1 ns around midnight."""
-        # 9 possibilities, 3 per starts before, on and after an hour point
-        #     h1          h2
-        # --  | -- ... -- |  --
-        # s1 s2 s3 ... e1 e2 e3
+        #  Create 3 ranges for this split t1-t2, t2-t3, t3-t4
+        #     mn         mn + h
+        # --  | -- ... --  |  --
+        # t1 t2 -- ... -- t3 t4
         midnight = pd.Timestamp("2022-03-18 00:00:00", tz="utc")
-        start1 = midnight - pd.Timestamp.resolution
-        start2 = midnight
-        start3 = midnight + pd.Timestamp.resolution
-        starts = [start1, start2, start3]
-        ends = [s + pd.Timedelta("1h") for s in starts]
-
-        data = [
-            {"started_at": start, "finished_at": end, "res": (start == starts[0]) or (end == ends[2])}
-            for start in starts
-            for end in ends
-        ]
-        df = pd.DataFrame(data=data)
-        calculated_result = _get_split_index(df, "hour")
-        assert (calculated_result == df["res"]).all()
+        time1 = midnight - pd.Timestamp.resolution
+        time2 = midnight
+        time3 = midnight + pd.Timedelta("1h")
+        time4 = time3 + pd.Timestamp.resolution
+        times = [time1, time2, time3, time4]
+        result = (times[:-1], times[1:])
+        row = pd.Series({"index": 0, "started_at": times[0], "finished_at": times[-1]})
+        assert _get_times(row, freq="H") == result
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 625eda1..7e68da5 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -115,7 +115,7 @@ class TestGenerate_trips:
 
         # Check start and destination points of all rows
         for i, row in trips.iterrows():
-            start_point_trips = row["geom"][0]  # get origin Point in generated trips
+            start_point_trips = row["geom"].geoms[0]  # get origin Point in generated trips
             if not pd.isna(row["origin_staypoint_id"]):
                 # compare to the Point in the staypoints
                 correct_start_point = sp.loc[row["origin_staypoint_id"], "geom"]
@@ -124,11 +124,11 @@ class TestGenerate_trips:
                 # get all triplegs on this trip
                 tpls_on_trip = tpls[tpls["trip_id"] == row.name]
                 # correct point is the first point on the tripleg
-                correct_start_point, _ = tpls_on_trip.iloc[0]["geom"].boundary
+                correct_start_point, _ = tpls_on_trip.iloc[0]["geom"].boundary.geoms
 
             assert correct_start_point == start_point_trips
 
-            dest_point_trips = row["geom"][1]  # get destination Point in generated trips
+            dest_point_trips = row["geom"].geoms[1]  # get destination Point in generated trips
             if not pd.isna(row["destination_staypoint_id"]):
                 correct_dest_point = sp.loc[row["destination_staypoint_id"], "geom"]
                 # compare to the Point in the staypoints
@@ -137,7 +137,7 @@ class TestGenerate_trips:
                 # get all triplegs on this trip
                 tpls_on_trip = tpls[tpls["trip_id"] == row.name]
                 # correct point is the first point on the tripleg
-                _, correct_dest_point = tpls_on_trip.iloc[-1]["geom"].boundary
+                _, correct_dest_point = tpls_on_trip.iloc[-1]["geom"].boundary.geoms
 
             assert correct_dest_point == dest_point_trips
 
@@ -364,7 +364,7 @@ class TestGenerate_trips:
         sp, tpls, trips = ti.preprocessing.triplegs.generate_trips(sp, tpls, gap_threshold=15)
 
         # test if start of first trip is (0,0)
-        assert trips.loc[0, "geom"][0] == Point(0, 0)
+        assert trips.loc[0, "geom"].geoms[0] == Point(0, 0)
 
 
 def _create_debug_sp_tpls_data(sp, tpls, gap_threshold):
diff --git a/tests/preprocessing/test_trips.py b/tests/preprocessing/test_trips.py
index 0aa69f4..e1416ac 100644
--- a/tests/preprocessing/test_trips.py
+++ b/tests/preprocessing/test_trips.py
@@ -124,8 +124,8 @@ def example_nested_tour(example_trip_data):
 
     # construct trips that lie between trips 6 and 15 and form a tour on their own
     # define start and end points of these trips
-    first_trip_subtour = MultiPoint((trips.loc[15, "geom"][0], Point(9.5067847, 47.20001)))
-    second_trip_subtour = MultiPoint((Point(9.5067847, 47.20001), trips.loc[15, "geom"][0]))
+    first_trip_subtour = MultiPoint((trips.loc[15, "geom"].geoms[0], Point(9.5067847, 47.20001)))
+    second_trip_subtour = MultiPoint((Point(9.5067847, 47.20001), trips.loc[15, "geom"].geoms[0]))
     # define time of start and time at the intermediate point
     start_time_subtour = pd.Timestamp("1971-01-02 08:45:00", tz="utc")
     middle_time = pd.Timestamp("1971-01-02 08:55:00", tz="utc")