Repository: mie-lab/trackintel (python)

Issue:
change default for geom_col argument in read_*_gpd to geometry.name
When we use a `read_*_gpd` (e.g., `read_staypoints_gpd`) with a valid geodataframe it throws an error if the geometry column is not named 'geom'. I think if the geometry dataframe is already valid it should default to the geometry.name argument.


Pull request description:
ENH: Default to geometry for `read_*_gpd` functions.
closes #278 

Default of `read_*_gpd` functions now opt for the geometry of the GeoDataFrame if available.
Also added a function `_trackintel_model` that the functions call instead of doing everything themselves.
Added tests for this new function.
Added crs keyword to all `read_*_gpd` functions.
Added "extent" to `read_locations_gpd`.
Added geometry to `read_trips_gpd`
Set default from `mapper` from `{}` to `None` to avoid bugs with mutable datatypes.

~~Two test are still missing~~
~~1. checking if `read_trips_gpd` correctly return either DataFrame or GeoDataFrame.~~
~~2. checking if `read_locations_gpd` can handle an "extent" column.~~

I just wanted to to have a small and clean PR and now we have already 400 changed lines. :]

Patch:
diff --git a/trackintel/io/from_geopandas.py b/trackintel/io/from_geopandas.py
index adb7636..8fecdea 100644
--- a/trackintel/io/from_geopandas.py
+++ b/trackintel/io/from_geopandas.py
@@ -1,9 +1,12 @@
 import pandas as pd
+import geopandas as gpd
 
 from trackintel.io.file import _localize_timestamp
 
 
-def read_positionfixes_gpd(gdf, tracked_at="tracked_at", user_id="user_id", geom_col="geom", tz=None, mapper={}):
+def read_positionfixes_gpd(
+    gdf, tracked_at="tracked_at", user_id="user_id", geom_col=None, crs=None, tz=None, mapper=None
+):
     """
     Read positionfixes from GeoDataFrames.
 
@@ -15,19 +18,24 @@ def read_positionfixes_gpd(gdf, tracked_at="tracked_at", user_id="user_id", geom
         GeoDataFrame with valid point geometry, containing the positionfixes to import
 
     tracked_at : str, default 'tracked_at'
-        name of the column storing the timestamps.
+        Name of the column storing the timestamps.
 
     user_id : str, default 'user_id'
-        name of the column storing the user_id.
+        Name of the column storing the user_id.
 
-    geom_col : str, default 'geom'
-        name of the column storing the geometry.
+    geom_col : str, optional
+        Name of the column storing the geometry. If None assumes geometry is already set.
+
+    crs : pyproj.crs or str, optional
+        Set coordinate reference system. The value can be anything accepted
+        by pyproj.CRS.from_user_input(), such as an authority string
+        (eg "EPSG:4326") or a WKT string.
 
     tz : str, optional
         pytz compatible timezone string. If None UTC will be assumed
 
     mapper : dict, optional
-        further columns that should be renamed.
+        Further columns that should be renamed.
 
     Returns
     -------
@@ -39,23 +47,24 @@ def read_positionfixes_gpd(gdf, tracked_at="tracked_at", user_id="user_id", geom
     >>> trackintel.read_positionfixes_gpd(gdf, user_id='User', geom_col='geom', tz='utc')
     """
     columns = {tracked_at: "tracked_at", user_id: "user_id"}
-    columns.update(mapper)
-
-    pfs = gdf.rename(columns=columns)
-    pfs = pfs.set_geometry(geom_col)
-
-    # check and/or set timezone
-    for col in ["tracked_at"]:
-        if not pd.api.types.is_datetime64tz_dtype(pfs[col]):
-            pfs[col] = _localize_timestamp(dt_series=pfs[col], pytz_tzinfo=tz, col_name=col)
+    if mapper is not None:
+        columns.update(mapper)
 
+    pfs = _trackintel_model(gdf, columns, geom_col, crs, ["tracked_at"], tz)
     # assert validity of positionfixes
     pfs.as_positionfixes
     return pfs
 
 
 def read_staypoints_gpd(
-    gdf, started_at="started_at", finished_at="finished_at", user_id="user_id", geom_col="geom", tz=None, mapper={}
+    gdf,
+    started_at="started_at",
+    finished_at="finished_at",
+    user_id="user_id",
+    geom_col=None,
+    crs=None,
+    tz=None,
+    mapper=None,
 ):
     """
     Read staypoints from GeoDataFrames.
@@ -68,22 +77,27 @@ def read_staypoints_gpd(
         GeoDataFrame with valid point geometry, containing the staypoints to import
 
     started_at : str, default 'started_at'
-        name of the column storing the starttime of the staypoints.
+        Name of the column storing the starttime of the staypoints.
 
     finished_at : str, default 'finished_at'
-        name of the column storing the endtime of the staypoints.
+        Name of the column storing the endtime of the staypoints.
 
     user_id : str, default 'user_id'
-        name of the column storing the user_id.
+        Name of the column storing the user_id.
+
+    geom_col : str
+        Name of the column storing the geometry. If None assumes geometry is already set.
 
-    geom_col : str, default 'geom'
-        name of the column storing the geometry.
+    crs : pyproj.crs or str, optional
+        Set coordinate reference system. The value can be anything accepted
+        by pyproj.CRS.from_user_input(), such as an authority string
+        (eg "EPSG:4326") or a WKT string.
 
     tz : str, optional
         pytz compatible timezone string. If None UTC is assumed.
 
     mapper : dict, optional
-        further columns that should be renamed.
+        Further columns that should be renamed.
 
     Returns
     -------
@@ -95,15 +109,10 @@ def read_staypoints_gpd(
     >>> trackintel.read_staypoints_gpd(gdf, started_at='start_time', finished_at='end_time', tz='utc')
     """
     columns = {started_at: "started_at", finished_at: "finished_at", user_id: "user_id"}
-    columns.update(mapper)
-
-    stps = gdf.rename(columns=columns)
-    stps = stps.set_geometry(geom_col)
+    if mapper is not None:
+        columns.update(mapper)
 
-    # check and/or set timezone
-    for col in ["started_at", "finished_at"]:
-        if not pd.api.types.is_datetime64tz_dtype(stps[col]):
-            stps[col] = _localize_timestamp(dt_series=stps[col], pytz_tzinfo=tz, col_name=col)
+    stps = _trackintel_model(gdf, columns, geom_col, crs, ["started_at", "finished_at"], tz)
 
     # assert validity of staypoints
     stps.as_staypoints
@@ -111,7 +120,14 @@ def read_staypoints_gpd(
 
 
 def read_triplegs_gpd(
-    gdf, started_at="started_at", finished_at="finished_at", user_id="user_id", geom_col="geom", tz=None, mapper={}
+    gdf,
+    started_at="started_at",
+    finished_at="finished_at",
+    user_id="user_id",
+    geom_col=None,
+    crs=None,
+    tz=None,
+    mapper=None,
 ):
     """
     Read triplegs from GeoDataFrames.
@@ -124,22 +140,27 @@ def read_triplegs_gpd(
         GeoDataFrame with valid line geometry, containing the triplegs to import.
 
     started_at : str, default 'started_at'
-        name of the column storing the starttime of the triplegs.
+        Name of the column storing the starttime of the triplegs.
 
     finished_at : str, default 'finished_at'
-        name of the column storing the endtime of the triplegs.
+        Name of the column storing the endtime of the triplegs.
 
     user_id : str, default 'user_id'
-        name of the column storing the user_id.
+        Name of the column storing the user_id.
 
-    geom_col : str, default 'geom'
-        name of the column storing the geometry.
+    geom_col : str, optional
+        Name of the column storing the geometry. If None assumes geometry is already set.
+
+    crs : pyproj.crs or str, optional
+        Set coordinate reference system. The value can be anything accepted
+        by pyproj.CRS.from_user_input(), such as an authority string
+        (eg "EPSG:4326") or a WKT string.
 
     tz : str, optional
         pytz compatible timezone string. If None UTC is assumed.
 
     mapper : dict, optional
-        further columns that should be renamed.
+        Further columns that should be renamed.
 
     Returns
     -------
@@ -151,16 +172,10 @@ def read_triplegs_gpd(
     >>> trackintel.read_triplegs_gpd(gdf, user_id='User', geom_col='geom', tz='utc')
     """
     columns = {started_at: "started_at", finished_at: "finished_at", user_id: "user_id"}
-    columns.update(mapper)
-
-    tpls = gdf.rename(columns=columns)
-    tpls = tpls.set_geometry(geom_col)
-
-    # check and/or set timezone
-    for col in ["started_at", "finished_at"]:
-        if not pd.api.types.is_datetime64tz_dtype(tpls[col]):
-            tpls[col] = _localize_timestamp(dt_series=tpls[col], pytz_tzinfo=tz, col_name=col)
+    if mapper is not None:
+        columns.update(mapper)
 
+    tpls = _trackintel_model(gdf, columns, geom_col, crs, ["started_at", "finished_at"], tz)
     # assert validity of triplegs
     tpls.as_triplegs
     return tpls
@@ -173,8 +188,10 @@ def read_trips_gpd(
     user_id="user_id",
     origin_staypoint_id="origin_staypoint_id",
     destination_staypoint_id="destination_staypoint_id",
+    geom_col=None,
+    crs=None,
     tz=None,
-    mapper={},
+    mapper=None,
 ):
     """
     Read trips from GeoDataFrames/DataFrames.
@@ -184,33 +201,41 @@ def read_trips_gpd(
     Parameters
     ----------
     gdf : GeoDataFrame or DataFrame
-        GeoDataFrame/DataFrame containing the trips to import.
+        (Geo)DataFrame containing the trips to import.
 
     started_at : str, default 'started_at'
-        name of the column storing the starttime of the staypoints.
+        Name of the column storing the starttime of the staypoints.
 
     finished_at : str, default 'finished_at'
-        name of the column storing the endtime of the staypoints.
+        Name of the column storing the endtime of the staypoints.
 
     user_id : str, default 'user_id'
-        name of the column storing the user_id.
+        Name of the column storing the user_id.
 
     origin_staypoint_id : str, default 'origin_staypoint_id'
-        name of the column storing the staypoint_id of the start of the tripleg
+        Name of the column storing the staypoint_id of the start of the tripleg.
 
     destination_staypoint_id : str, default 'destination_staypoint_id'
-        name of the column storing the staypoint_id of the end of the tripleg
+        Name of the column storing the staypoint_id of the end of the tripleg
+
+    geom_col : str, optional
+        Name of the column storing the geometry. If None assumes has no geometry!
+
+    crs : pyproj.crs or str, optional
+        Set coordinate reference system. The value can be anything accepted
+        by pyproj.CRS.from_user_input(), such as an authority string
+        (eg "EPSG:4326") or a WKT string. Ignored if "geom_col" is None.
 
     tz : str, optional
         pytz compatible timezone string. If None UTC is assumed.
 
     mapper : dict, optional
-        further columns that should be renamed.
+        Further columns that should be renamed.
 
     Returns
     -------
-    trips : GeoDataFrame/DataFrame (as trackintel trips)
-        A GeoDataFrame/DataFrame containing the trips.
+    trips : (Geo)DataFrame (as trackintel trips)
+        A (Geo)DataFrame containing the trips.
 
     Examples
     --------
@@ -223,21 +248,24 @@ def read_trips_gpd(
         origin_staypoint_id: "origin_staypoint_id",
         destination_staypoint_id: "destination_staypoint_id",
     }
-    columns.update(mapper)
-
-    trips = gdf.rename(columns=columns)
-
-    # check and/or set timezone
-    for col in ["started_at", "finished_at"]:
-        if not pd.api.types.is_datetime64tz_dtype(trips[col]):
-            trips[col] = _localize_timestamp(dt_series=trips[col], pytz_tzinfo=tz, col_name=col)
+    if mapper is not None:
+        columns.update(mapper)
+
+    if geom_col is not None:  # handle (Geo)DataFrame modes.
+        trips = _trackintel_model(gdf, columns, geom_col, crs, ["started_at", "finished_at"], tz)
+    else:
+        trips = gdf.rename(columns=columns)
+        # check and/or set timezone
+        for col in ["started_at", "finished_at"]:
+            if not pd.api.types.is_datetime64tz_dtype(trips[col]):
+                trips[col] = _localize_timestamp(dt_series=trips[col], pytz_tzinfo=tz, col_name=col)
 
     # assert validity of trips
     trips.as_trips
     return trips
 
 
-def read_locations_gpd(gdf, user_id="user_id", center="center", mapper={}):
+def read_locations_gpd(gdf, user_id="user_id", center="center", extent=None, crs=None, mapper=None):
     """
     Read locations from GeoDataFrames.
 
@@ -249,16 +277,21 @@ def read_locations_gpd(gdf, user_id="user_id", center="center", mapper={}):
         GeoDataFrame with valid point geometry, containing the locations to import.
 
     user_id : str, default 'user_id'
-        name of the column storing the user_id.
+        Name of the column storing the user_id.
 
     center : str, default 'center'
-        name of the column storing the geometry (Center of the location).
+        Name of the column storing the geometry (center of the location).
 
-    tz : str, optional
-        pytz compatible timezone string. If None UTC is assumed.
+    extent : str, optional
+        Name of the column storing the additionaly geometry (extent of location).
+
+    crs : pyproj.crs or str, optional
+        Set coordinate reference system. The value can be anything accepted
+        by pyproj.CRS.from_user_input(), such as an authority string
+        (eg "EPSG:4326") or a WKT string.
 
     mapper : dict, optional
-        further columns that should be renamed.
+        Further columns that should be renamed.
 
     Returns
     -------
@@ -270,10 +303,15 @@ def read_locations_gpd(gdf, user_id="user_id", center="center", mapper={}):
     >>> trackintel.read_locations_gpd(df, user_id='User', center='geometry')
     """
     columns = {user_id: "user_id", center: "center"}
-    columns.update(mapper)
+    if extent is not None:
+        columns[extent] = "extent"
+    if mapper is not None:
+        columns.update(mapper)
+
+    locs = _trackintel_model(gdf, columns, "center", crs)
 
-    locs = gdf.rename(columns=columns)
-    locs = locs.set_geometry("center")
+    if extent is not None:
+        locs["extent"] = gpd.GeoSeries(locs["extent"])
 
     # assert validity of locations
     locs.as_locations
@@ -340,3 +378,55 @@ def read_tours_gpd(
     # trs.as_tours
     # return trs
     pass
+
+
+def _trackintel_model(gdf, set_names=None, geom_col=None, crs=None, tz_cols=None, tz=None):
+    """Help function to assure the trackintel model on a GeoDataFrame.
+
+    Parameters
+    ----------
+    gdf : GeoDataFrame
+        Input GeoDataFrame
+
+    set_names : dict, optional
+        Renaming dictionary for the columns of the GeoDataFrame.
+
+    set_geometry : str, optional
+        Set geometry of GeoDataFrame.
+
+    crs : pyproj.crs or str, optional
+        Set coordinate reference system. The value can be anything accepted
+        by pyproj.CRS.from_user_input(), such as an authority string
+        (eg "EPSG:4326") or a WKT string.
+
+    tz_cols : list, optional
+        List of timezone aware datetime columns.
+
+    tz : str, optional
+        pytz compatible timezone string. If None UTC will be assumed
+
+    Returns
+    -------
+    gdf : GeoDataFrame
+        The input GeoDataFrame transformed to match the trackintel format.
+    """
+    if set_names is not None:
+        gdf = gdf.rename(columns=set_names)
+
+    if geom_col is not None:
+        gdf = gdf.set_geometry(geom_col)
+    else:
+        try:
+            gdf.geometry
+        except AttributeError:
+            raise AttributeError("GeoDataFrame has no geometry, set it with keyword argument.")
+
+    if crs is not None:
+        gdf = gdf.set_crs(crs)
+
+    if tz_cols is not None:
+        for col in tz_cols:
+            if not pd.api.types.is_datetime64tz_dtype(gdf[col]):
+                gdf[col] = _localize_timestamp(dt_series=gdf[col], pytz_tzinfo=tz, col_name=col)
+
+    return gdf
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index 776c3b3..a27ad58 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -1,11 +1,9 @@
 import warnings
 
+import geopandas as gpd
 import numpy as np
 import pandas as pd
-from shapely import geometry
-from tqdm import tqdm
-from shapely.geometry import MultiPoint, Point
-import geopandas as gpd
+from shapely.geometry import MultiPoint
 
 
 def smoothen_triplegs(triplegs, tolerance=1.0, preserve_topology=True):
@@ -164,7 +162,7 @@ def generate_trips(spts, tpls, gap_threshold=15, add_geometry=True):
 
     trips_grouper = spts_tpls_no_act.groupby("temp_trip_id")
     trips = trips_grouper.agg(
-        {"user_id": "mean", "started_at": min, "finished_at": max, "type": list, "spts_tpls_id": list}
+        {"user_id": "first", "started_at": min, "finished_at": max, "type": list, "spts_tpls_id": list}
     )
 
     def _seperate_ids(row):


Test patch:
diff --git a/tests/io/test_file.py b/tests/io/test_file.py
index 67e027b..1478c14 100644
--- a/tests/io/test_file.py
+++ b/tests/io/test_file.py
@@ -13,7 +13,7 @@ class TestPositionfixes:
         """Test basic reading and writing functions."""
         orig_file = os.path.join("tests", "data", "positionfixes.csv")
         mod_file = os.path.join("tests", "data", "positionfixes_mod_columns.csv")
-        tmp_file = os.path.join("tests", "data", "positionfixes_test.csv")
+        tmp_file = os.path.join("tests", "data", "positionfixes_test_1.csv")
 
         pfs = ti.read_positionfixes_csv(orig_file, sep=";", index_col="id")
 
@@ -47,7 +47,7 @@ class TestPositionfixes:
         # check if a timezone will be set after manually deleting the timezone
         pfs["tracked_at"] = pfs["tracked_at"].dt.tz_localize(None)
         assert not pd.api.types.is_datetime64tz_dtype(pfs["tracked_at"])
-        tmp_file = os.path.join("tests", "data", "positionfixes_test.csv")
+        tmp_file = os.path.join("tests", "data", "positionfixes_test_2.csv")
         pfs.as_positionfixes.to_csv(tmp_file, sep=";")
         pfs = ti.read_positionfixes_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
@@ -82,7 +82,7 @@ class TestTriplegs:
         """Test basic reading and writing functions."""
         orig_file = os.path.join("tests", "data", "triplegs.csv")
         mod_file = os.path.join("tests", "data", "triplegs_mod_columns.csv")
-        tmp_file = os.path.join("tests", "data", "triplegs_test.csv")
+        tmp_file = os.path.join("tests", "data", "triplegs_test_1.csv")
         tpls = ti.read_triplegs_csv(orig_file, sep=";", tz="utc", index_col="id")
 
         column_mapping = {"start_time": "started_at", "end_time": "finished_at", "tripleg": "geom"}
@@ -117,7 +117,7 @@ class TestTriplegs:
         # check if a timezone will be set after manually deleting the timezone
         tpls["started_at"] = tpls["started_at"].dt.tz_localize(None)
         assert not pd.api.types.is_datetime64tz_dtype(tpls["started_at"])
-        tmp_file = os.path.join("tests", "data", "triplegs_test.csv")
+        tmp_file = os.path.join("tests", "data", "triplegs_test_2.csv")
         tpls.as_triplegs.to_csv(tmp_file, sep=";")
         tpls = ti.read_triplegs_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
@@ -152,7 +152,7 @@ class TestStaypoints:
         """Test basic reading and writing functions."""
         orig_file = os.path.join("tests", "data", "staypoints.csv")
         mod_file = os.path.join("tests", "data", "staypoints_mod_columns.csv")
-        tmp_file = os.path.join("tests", "data", "staypoints_test.csv")
+        tmp_file = os.path.join("tests", "data", "staypoints_test_1.csv")
         stps = ti.read_staypoints_csv(orig_file, sep=";", tz="utc", index_col="id")
         mod_stps = ti.read_staypoints_csv(mod_file, columns={"User": "user_id"}, sep=";", index_col="id")
         assert mod_stps.equals(stps)
@@ -184,7 +184,7 @@ class TestStaypoints:
         # check if a timezone will be set after manually deleting the timezone
         stps["started_at"] = stps["started_at"].dt.tz_localize(None)
         assert not pd.api.types.is_datetime64tz_dtype(stps["started_at"])
-        tmp_file = os.path.join("tests", "data", "staypoints_test.csv")
+        tmp_file = os.path.join("tests", "data", "staypoints_test_2.csv")
         stps.as_staypoints.to_csv(tmp_file, sep=";")
         stps = ti.read_staypoints_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
@@ -219,7 +219,7 @@ class TestLocations:
         """Test basic reading and writing functions."""
         orig_file = os.path.join("tests", "data", "locations.csv")
         mod_file = os.path.join("tests", "data", "locations_mod_columns.csv")
-        tmp_file = os.path.join("tests", "data", "locations_test.csv")
+        tmp_file = os.path.join("tests", "data", "locations_test_1.csv")
         mod_locs = ti.read_locations_csv(mod_file, columns={"geom": "center"}, sep=";", index_col="id")
         locs = ti.read_locations_csv(orig_file, sep=";", index_col="id")
         assert mod_locs.equals(locs)
@@ -260,7 +260,7 @@ class TestTrips:
         """Test basic reading and writing functions."""
         orig_file = os.path.join("tests", "data", "trips.csv")
         mod_file = os.path.join("tests", "data", "trips_mod_columns.csv")
-        tmp_file = os.path.join("tests", "data", "trips_test.csv")
+        tmp_file = os.path.join("tests", "data", "trips_test_1.csv")
         trips = ti.read_trips_csv(orig_file, sep=";", index_col="id")
         column_mapping = {"orig_stp": "origin_staypoint_id", "dest_stp": "destination_staypoint_id"}
         mod_trips = ti.read_trips_csv(mod_file, columns=column_mapping, sep=";", index_col="id")
@@ -284,7 +284,7 @@ class TestTrips:
         # check if a timezone will be set after manually deleting the timezone
         trips["started_at"] = trips["started_at"].dt.tz_localize(None)
         assert not pd.api.types.is_datetime64tz_dtype(trips["started_at"])
-        tmp_file = os.path.join("tests", "data", "trips_test.csv")
+        tmp_file = os.path.join("tests", "data", "trips_test_2.csv")
         trips.as_trips.to_csv(tmp_file, sep=";")
         trips = ti.read_trips_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
diff --git a/tests/io/test_from_geopandas.py b/tests/io/test_from_geopandas.py
index a6a96c3..5a1b2bc 100644
--- a/tests/io/test_from_geopandas.py
+++ b/tests/io/test_from_geopandas.py
@@ -2,75 +2,305 @@ import os
 
 import geopandas as gpd
 import pandas as pd
-
+import pytest
 import trackintel as ti
+from geopandas.testing import assert_geodataframe_equal
+from pandas.testing import assert_frame_equal, assert_index_equal
+from shapely.geometry import Point, Polygon, MultiPoint
+from trackintel.io.from_geopandas import (
+    _trackintel_model,
+    read_locations_gpd,
+    read_positionfixes_gpd,
+    read_staypoints_gpd,
+    read_tours_gpd,
+    read_triplegs_gpd,
+    read_trips_gpd,
+)
+
+
+@pytest.fixture()
+def example_positionfixes():
+    """Model conform positionfixes to test with."""
+    p1 = Point(8.5067847, 47.4)
+    p2 = Point(8.5067847, 47.5)
+    p3 = Point(8.5067847, 47.6)
+
+    t1 = pd.Timestamp("1971-01-01 04:00:00", tz="utc")
+    t2 = pd.Timestamp("1971-01-01 05:00:00", tz="utc")
+    t3 = pd.Timestamp("1971-01-02 07:00:00", tz="utc")
+
+    list_dict = [
+        {"user_id": 0, "tracked_at": t1, "geom": p1},
+        {"user_id": 0, "tracked_at": t2, "geom": p2},
+        {"user_id": 1, "tracked_at": t3, "geom": p3},
+    ]
+    pfs = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:4326")
+    pfs.index.name = "id"
+    assert pfs.as_positionfixes
+    return pfs
+
+
+class Test_Trackintel_Model:
+    """Test `_trackintel_model()` function."""
+
+    def test_renaming(self, example_positionfixes):
+        """Test renaming of columns."""
+        example_positionfixes["additional_col"] = [11, 22, 33]
+        pfs = example_positionfixes.copy()
+        # create new column mapping and revert it
+        columns = {"user_id": "_user_id", "tracked_at": "_tracked_at", "additional_col": "_additional_col"}
+        columns_rev = {val: key for key, val in columns.items()}
+        # check if columns get renamed correctly
+        pfs.rename(columns=columns, inplace=True)
+        pfs = _trackintel_model(pfs, columns_rev)
+        assert_geodataframe_equal(example_positionfixes, pfs)
+
+    def test_setting_geometry(self, example_positionfixes):
+        """Test the setting of the geometry."""
+        # create pfs as dataframe
+        pfs = pd.DataFrame(example_positionfixes[["user_id", "tracked_at"]], copy=True)
+        pfs["geom"] = example_positionfixes.geometry
+        # check if geom column gets assigned to geometry
+        pfs = _trackintel_model(pfs, geom_col="geom")
+        assert_geodataframe_equal(example_positionfixes, pfs)
+
+    def test_set_crs(self, example_positionfixes):
+        """Test if crs will be set."""
+        pfs = example_positionfixes.copy()
+        example_positionfixes.crs = "EPSG:2056"
+        # check if the crs is correctly set
+        pfs.crs = None
+        pfs = _trackintel_model(pfs, crs="EPSG:2056")
+        assert_geodataframe_equal(example_positionfixes, pfs)
+
+    def test_already_set_geometry(self, example_positionfixes):
+        """Test if default checks if GeoDataFrame already has a geometry."""
+        pfs = _trackintel_model(example_positionfixes)
+        assert_geodataframe_equal(pfs, example_positionfixes)
 
+    def test_error_no_set_geometry(self, example_positionfixes):
+        """Test if AttributeError will be raised if no geom_col is provided and GeoDataFrame has no geometry."""
+        pfs = gpd.GeoDataFrame(example_positionfixes[["user_id", "tracked_at"]])
+        with pytest.raises(AttributeError):
+            _trackintel_model(pfs)
 
-class TestFromGeopandas:
-    """Test for all 'read_*_gpd' functions."""
+    def test_tz_cols(self, example_positionfixes):
+        """Test if columns get casted to datetimes."""
+        pfs = example_positionfixes.copy()
+        pfs["tracked_at"] = ["1971-01-01 04:00:00", "1971-01-01 05:00:00", "1971-01-02 07:00:00"]
+        pfs = _trackintel_model(pfs, tz_cols=["tracked_at"], tz="UTC")
+        assert_geodataframe_equal(pfs, example_positionfixes)
 
-    def test_read_positionfixes_gpd(self):
+
+class TestRead_Positionfixes_Gpd:
+    """Test `read_positionfixes_gpd()` function."""
+
+    def test_csv(self):
         """Test if the results of reading from gpd and csv agrees."""
+        # read from file and transform to trackintel format
         gdf = gpd.read_file(os.path.join("tests", "data", "positionfixes.geojson"))
         gdf.set_index("id", inplace=True)
-        pfs_from_gpd = ti.io.from_geopandas.read_positionfixes_gpd(gdf, user_id="User", geom_col="geometry", tz="utc")
+        pfs_from_gpd = read_positionfixes_gpd(gdf, user_id="User", geom_col="geometry", crs="EPSG:4326", tz="utc")
 
+        # read from csv file
         pfs_file = os.path.join("tests", "data", "positionfixes.csv")
-        pfs_from_csv = ti.read_positionfixes_csv(pfs_file, sep=";", tz="utc", index_col="id")
+        pfs_from_csv = ti.read_positionfixes_csv(pfs_file, sep=";", tz="utc", index_col="id", crs="EPSG:4326")
         pfs_from_csv = pfs_from_csv.rename(columns={"geom": "geometry"})
 
-        pd.testing.assert_frame_equal(pfs_from_gpd, pfs_from_csv, check_exact=False)
+        assert_frame_equal(pfs_from_gpd, pfs_from_csv, check_exact=False)
+
+    def test_mapper(self, example_positionfixes):
+        """Test if mapper argument allows for additional renaming."""
+        example_positionfixes["additional_col"] = [11, 22, 33]
+        mapper = {"additional_col": "additional_col_renamed"}
+        pfs = read_positionfixes_gpd(example_positionfixes, mapper=mapper)
+        example_positionfixes.rename(columns=mapper, inplace=True)
+        assert_geodataframe_equal(example_positionfixes, pfs)
 
-    def test_read_triplegs_gpd(self):
+
+class TestRead_Triplegs_Gpd:
+    """Test `read_triplegs_gpd()` function."""
+
+    def test_csv(self):
         """Test if the results of reading from gpd and csv agrees."""
+        # read from file and transform to trackintel format
         gdf = gpd.read_file(os.path.join("tests", "data", "triplegs.geojson"))
         gdf.set_index("id", inplace=True)
-        tpls_from_gpd = ti.io.from_geopandas.read_triplegs_gpd(gdf, user_id="User", geom_col="geometry", tz="utc")
+        tpls_from_gpd = read_triplegs_gpd(gdf, user_id="User", geom_col="geometry", crs="EPSG:4326", tz="utc")
 
+        # read from csv file
         tpls_file = os.path.join("tests", "data", "triplegs.csv")
         tpls_from_csv = ti.read_triplegs_csv(tpls_file, sep=";", tz="utc", index_col="id")
         tpls_from_csv = tpls_from_csv.rename(columns={"geom": "geometry"})
 
-        pd.testing.assert_frame_equal(tpls_from_gpd, tpls_from_csv, check_exact=False)
+        assert_frame_equal(tpls_from_gpd, tpls_from_csv, check_exact=False)
 
-    def test_read_staypoints_gpd(self):
+    def test_mapper(self):
+        """Test if mapper argument allows for additional renaming."""
+        gdf = gpd.read_file(os.path.join("tests", "data", "triplegs.geojson"))
+        gdf["additional_col"] = [11, 22]
+        gdf.rename(columns={"User": "user_id"}, inplace=True)
+        mapper = {"additional_col": "additional_col_renamed"}
+        tpls = read_triplegs_gpd(gdf, mapper=mapper, tz="utc")
+        gdf.rename(columns=mapper, inplace=True)
+
+        assert_index_equal(tpls.columns, gdf.columns)
+
+
+class TestRead_Staypoints_Gpd:
+    """Test `read_staypoints_gpd()` function."""
+
+    def test_csv(self):
         """Test if the results of reading from gpd and csv agrees."""
+        # read from file and transform to trackintel format
         gdf = gpd.read_file(os.path.join("tests", "data", "staypoints.geojson"))
         gdf.set_index("id", inplace=True)
-        stps_from_gpd = ti.io.from_geopandas.read_staypoints_gpd(
-            gdf, "start_time", "end_time", geom_col="geometry", tz="utc"
+        stps_from_gpd = read_staypoints_gpd(
+            gdf, "start_time", "end_time", geom_col="geometry", crs="EPSG:4326", tz="utc"
         )
 
+        # read from csv file
         stps_file = os.path.join("tests", "data", "staypoints.csv")
         stps_from_csv = ti.read_staypoints_csv(stps_file, sep=";", tz="utc", index_col="id")
         stps_from_csv = stps_from_csv.rename(columns={"geom": "geometry"})
 
-        pd.testing.assert_frame_equal(stps_from_gpd, stps_from_csv, check_exact=False)
+        assert_frame_equal(stps_from_gpd, stps_from_csv, check_exact=False)
 
-    def test_read_locations_gpd(self):
+    def test_mapper(self):
+        """Test if mapper argument allows for additional renaming."""
+        gdf = gpd.read_file(os.path.join("tests", "data", "staypoints.geojson"))
+        gdf["additional_col"] = [11, 22]
+        gdf.rename(columns={"start_time": "started_at", "end_time": "finished_at"}, inplace=True)
+        mapper = {"additional_col": "additional_col_renamed"}
+        stps = read_staypoints_gpd(gdf, mapper=mapper, tz="utc")
+        gdf.rename(columns=mapper, inplace=True)
+
+        assert_index_equal(gdf.columns, stps.columns)
+
+
+@pytest.fixture()
+def example_locations():
+    """Model conform locations to test with."""
+    p1 = Point(8.5067847, 47.4)
+    p2 = Point(8.5067847, 47.5)
+
+    list_dict = [
+        {"user_id": 0, "center": p1},
+        {"user_id": 0, "center": p2},
+        {"user_id": 1, "center": p2},
+    ]
+    locs = gpd.GeoDataFrame(data=list_dict, geometry="center", crs="EPSG:4326")
+    locs.index.name = "id"
+
+    coords = [[8.45, 47.6], [8.45, 47.4], [8.55, 47.4], [8.55, 47.6], [8.45, 47.6]]
+    extent = Polygon(coords)
+    locs["extent"] = extent  # broadcasting
+    locs["extent"] = gpd.GeoSeries(locs["extent"])  # dtype
+    assert locs.as_locations
+    return locs
+
+
+class TestRead_Locations_Gpd:
+    """Test `read_locations_gpd()` function."""
+
+    def test_csv(self):
         """Test if the results of reading from gpd and csv agrees."""
-        # TODO: Problem with multiple geometry columns and geojson format
+        # TODO: Problem with multiple geometry columns and csv format
         gdf = gpd.read_file(os.path.join("tests", "data", "locations.geojson"))
         gdf.set_index("id", inplace=True)
-        locs_from_gpd = ti.io.from_geopandas.read_locations_gpd(gdf, user_id="User", center="geometry")
+        locs_from_gpd = read_locations_gpd(gdf, user_id="User", center="geometry", crs="EPSG:4326")
 
         locs_file = os.path.join("tests", "data", "locations.csv")
         locs_from_csv = ti.read_locations_csv(locs_file, sep=";", index_col="id")
 
         # drop the second geometry column manually because not storable in GeoJSON (from Geopandas)
         locs_from_csv = locs_from_csv.drop(columns="extent")
-        pd.testing.assert_frame_equal(locs_from_csv, locs_from_gpd, check_exact=False)
+        assert_frame_equal(locs_from_csv, locs_from_gpd, check_exact=False)
+
+    def test_extent_col(self, example_locations):
+        """Test function with optional geom-column "extent"."""
+        locs = example_locations.copy()
+        del locs["extent"]
+        coords = [[8.45, 47.6], [8.45, 47.4], [8.55, 47.4], [8.55, 47.6], [8.45, 47.6]]
+        locs["extent_wrongname"] = Polygon(coords)
+        locs = read_locations_gpd(locs, extent="extent_wrongname")
+        assert_geodataframe_equal(locs, example_locations)
+
+    def test_mapper(self, example_locations):
+        """Test if mapper argument allows for additional renaming."""
+        example_locations["additional_col"] = [11, 22, 33]
+        mapper = {"additional_col": "additional_col_renamed"}
+        locs = read_locations_gpd(example_locations, mapper=mapper)
+        example_locations.rename(columns=mapper, inplace=True)
+        assert_geodataframe_equal(locs, example_locations)
+
 
-    def test_read_trips_gpd(self):
+@pytest.fixture
+def example_trips():
+    """Model conform trips to test with."""
+    start = pd.Timestamp("1971-01-01 00:00:00", tz="utc")
+    h = pd.Timedelta("1h")
+
+    mp1 = MultiPoint([(0.0, 0.0), (1.0, 1.0)])
+    mp2 = MultiPoint([(2.0, 2.0), (3.0, 3.0)])
+
+    list_dict = [
+        {"user_id": 0, "origin_staypoint_id": 0, "destination_staypoint_id": 1, "geom": mp1},
+        {"user_id": 0, "origin_staypoint_id": 1, "destination_staypoint_id": 2, "geom": mp2},
+        {"user_id": 1, "origin_staypoint_id": 0, "destination_staypoint_id": 1, "geom": mp2},
+    ]
+    for n, d in enumerate(list_dict):
+        d["started_at"] = start + 4 * n * h
+        d["finished_at"] = d["started_at"] + h
+    trips = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:2056")
+    trips.index.name = "id"
+    assert trips.as_trips
+    return trips
+
+
+class TestRead_Trips_Gpd:
+    """Test `read_trips_gpd()` function."""
+
+    def test_cvs(self):
         """Test if the results of reading from gpd and csv agrees."""
         df = pd.read_csv(os.path.join("tests", "data", "trips.csv"), sep=";")
         df.set_index("id", inplace=True)
-        trips_from_gpd = ti.io.from_geopandas.read_trips_gpd(df, tz="utc")
+        trips_from_gpd = read_trips_gpd(df, tz="utc")
 
         trips_file = os.path.join("tests", "data", "trips.csv")
         trips_from_csv = ti.read_trips_csv(trips_file, sep=";", tz="utc", index_col="id")
 
-        pd.testing.assert_frame_equal(trips_from_gpd, trips_from_csv, check_exact=False)
+        assert_frame_equal(trips_from_gpd, trips_from_csv, check_exact=False)
+
+    def test_with_geometry(self, example_trips):
+        """Test if optional geometry gets read."""
+        trips = example_trips.copy()
+        del trips["geom"]
+        mp1 = MultiPoint([(0.0, 0.0), (1.0, 1.0)])
+        mp2 = MultiPoint([(2.0, 2.0), (3.0, 3.0)])
+        trips["geom"] = [mp1, mp2, mp2]
+        trips = read_trips_gpd(trips, geom_col="geom", crs="EPSG:2056", tz="utc")
+        example_trips = example_trips[trips.columns]  # copy changed column order
+        assert_geodataframe_equal(trips, example_trips)
+
+    def test_without_geometry(self, example_trips):
+        """Test if DataFrame without geometry stays the same."""
+        columns_without_geom = example_trips.columns.difference(["geom"])
+        trips = pd.DataFrame(example_trips[columns_without_geom], copy=True)
+        trips_after_function = read_trips_gpd(trips)
+        assert_frame_equal(trips, trips_after_function)
+
+    def test_mapper(self, example_trips):
+        """Test if mapper argument allows for additional renaming."""
+        example_trips["additional_col"] = [11, 22, 33]
+        mapper = {"additional_col": "additional_col_renamed"}
+        trips = read_trips_gpd(example_trips, mapper=mapper)
+        example_trips.rename(columns=mapper, inplace=True)
+        assert_geodataframe_equal(trips, example_trips)
+
+
+class TestRead_Tours_Gpd:
+    """Test `read_trips_gpd()` function."""
 
     def test_read_tours_gpd(self):
         # TODO: implement tests for reading tours from Geopandas