Repository: mie-lab/trackintel
Pull request #54 (2020-11-15 22:15:07)

reimplement preprocessing.cluster_staypoints function
- Update the naming of 'places' to 'locations'
- use the `dissolve()` function from `geopandas `to speed up the location data frame creation
- function returns staypoints together with locations 
- update the test functions accordingly

codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=h1) Report
> Merging [#54](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=desc) (42ffdf5) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/b67b92b4e45bfa9ee3c9f4830bff1c15f3b366f2?el=desc) (b67b92b) will **decrease** coverage by `0.17%`.
> The diff coverage is `62.71%`.

[![Impacted file tree graph](https://codecov.io/gh/mie-lab/trackintel/pull/54/graphs/tree.svg?width=650&height=150&src=pr&token=ITDZEEE1MO)](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #54      +/-   ##
==========================================
- Coverage   49.39%   49.21%   -0.18%     
==========================================
  Files          27       27              
  Lines        1148     1150       +2     
  Branches      171      174       +3     
==========================================
- Hits          567      566       -1     
- Misses        527      528       +1     
- Partials       54       56       +2     
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [trackintel/io/postgis.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9pby9wb3N0Z2lzLnB5) | `13.59% <16.66%> (ø)` | |
| [trackintel/visualization/locations.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC92aXN1YWxpemF0aW9uL2xvY2F0aW9ucy5weQ==) | `15.15% <16.66%> (ø)` | |
| [trackintel/model/locations.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9tb2RlbC9sb2NhdGlvbnMucHk=) | `71.42% <50.00%> (ø)` | |
| [trackintel/preprocessing/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3N0YXlwb2ludHMucHk=) | `77.55% <85.71%> (-5.43%)` | :arrow_down: |
| [trackintel/io/\_\_init\_\_.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9pby9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [trackintel/io/file.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9pby9maWxlLnB5) | `97.05% <100.00%> (ø)` | |
| [trackintel/model/\_\_init\_\_.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9tb2RlbC9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [trackintel/model/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/54/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9tb2RlbC9zdGF5cG9pbnRzLnB5) | `90.32% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=footer). Last update [b67b92b...42ffdf5](https://codecov.io/gh/mie-lab/trackintel/pull/54?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

henrymartin1: Looks good!

Patch:
diff --git a/examples/import_export_postgis.py b/examples/import_export_postgis.py
index d04381c..b677e50 100644
--- a/examples/import_export_postgis.py
+++ b/examples/import_export_postgis.py
@@ -10,14 +10,14 @@ import matplotlib.pyplot as plt
 import trackintel as ti
 
 
-logging.basicConfig(filename='log/import_export_postgis.log', level=logging.INFO, filemode='w')
+logging.basicConfig(filename='examples/log/import_export_postgis.log', level=logging.INFO, filemode='w')
 
 conn_string = 'postgresql://test:1234@localhost:5432/trackintel-tests'
 
 # Geolife trajectory to PostGIS.
-pfs = ti.read_positionfixes_csv('data/geolife_trajectory.csv', sep=';')
-pfs.as_positionfixes.to_postgis(conn_string, 'positionfixes')
+pfs = ti.read_positionfixes_csv('examples/data/geolife_trajectory.csv', sep=';')
+# pfs.as_positionfixes.to_postgis(conn_string, 'positionfixes')
 
 # Geolife trajectory from PostGIS.
-pfs = ti.read_positionfixes_postgis(conn_string, 'positionfixes')
+# pfs = ti.read_positionfixes_postgis(conn_string, 'positionfixes')
 pfs.as_positionfixes.plot()
diff --git a/examples/preprocess_trajectories.py b/examples/preprocess_trajectories.py
index 2df2062..dc2b35a 100644
--- a/examples/preprocess_trajectories.py
+++ b/examples/preprocess_trajectories.py
@@ -12,24 +12,24 @@ import trackintel as ti
 from trackintel.geogr.distances import meters_to_decimal_degrees
 
 
-logging.basicConfig(filename='log/preprocessing.log', level=logging.INFO, filemode='w')
+logging.basicConfig(filename='examples/log/preprocessing.log', level=logging.INFO, filemode='w')
 
 # GPSies trajectory.
-pfs = ti.read_positionfixes_csv('data/geolife_trajectory.csv', sep=';')
-pfs.as_positionfixes.plot(out_filename='out/gpsies_trajectory_positionfixes.png', plot_osm=True)
+pfs = ti.read_positionfixes_csv('examples/data/geolife_trajectory.csv', sep=';')
+pfs.as_positionfixes.plot(out_filename='examples/out/gpsies_trajectory_positionfixes.png', plot_osm=True)
 
 spts = pfs.as_positionfixes.extract_staypoints(method='sliding', dist_threshold=100, time_threshold=5*60)
-spts.as_staypoints.plot(out_filename='out/gpsies_trajectory_staypoints.png',
+spts.as_staypoints.plot(out_filename='examples/out/gpsies_trajectory_staypoints.png',
                         radius=meters_to_decimal_degrees(100, 47.5), positionfixes=pfs, plot_osm=True)
 
 plcs = spts.as_staypoints.extract_places(method='dbscan', epsilon=meters_to_decimal_degrees(120, 47.5), 
                                          num_samples=3)
-plcs.as_places.plot(out_filename='out/gpsies_trajectory_places.png', radius=meters_to_decimal_degrees(120, 47.5), 
+plcs.as_places.plot(out_filename='examples/out/gpsies_trajectory_places.png', radius=meters_to_decimal_degrees(120, 47.5), 
                     positionfixes=pfs, staypoints=spts, staypoints_radius=meters_to_decimal_degrees(100, 47.5), 
                     plot_osm=True)
 
 tpls = pfs.as_positionfixes.extract_triplegs()
-tpls.as_triplegs.plot(out_filename='out/gpsies_trajectory_triplegs.png', staypoints=spts, 
+tpls.as_triplegs.plot(out_filename='examples/out/gpsies_trajectory_triplegs.png', staypoints=spts, 
                       staypoints_radius=meters_to_decimal_degrees(100, 47.5))
 
 sys.exit(0)
diff --git a/trackintel/io/__init__.py b/trackintel/io/__init__.py
index fd5b424..67c2257 100644
--- a/trackintel/io/__init__.py
+++ b/trackintel/io/__init__.py
@@ -13,10 +13,10 @@ from .file import write_staypoints_csv
 from .postgis import read_staypoints_postgis
 from .postgis import write_staypoints_postgis
 
-from .file import read_places_csv
-from .file import write_places_csv
-from .postgis import read_places_postgis
-from .postgis import write_places_postgis
+from .file import read_locations_csv
+from .file import write_locations_csv
+from .postgis import read_locations_postgis
+from .postgis import write_locations_postgis
 
 from .file import read_trips_csv
 from .file import write_trips_csv
diff --git a/trackintel/io/file.py b/trackintel/io/file.py
index a2819ad..2864a31 100644
--- a/trackintel/io/file.py
+++ b/trackintel/io/file.py
@@ -129,42 +129,42 @@ def write_staypoints_csv(staypoints, filename, *args, **kwargs):
     gdf.to_csv(filename, index=False, *args, **kwargs)
 
 
-def read_places_csv(*args, **kwargs):
-    """Wraps the pandas read_csv function, extracts a WKT for the place 
+def read_locations_csv(*args, **kwargs):
+    """Wraps the pandas read_csv function, extracts a WKT for the location 
     center (and extent) and builds a geopandas GeoDataFrame. This also 
     validates that the ingested data conforms to the trackintel understanding 
-    of places (see :doc:`/modules/model`).
+    of locations (see :doc:`/modules/model`).
 
     Returns
     -------
     GeoDataFrame
-        A GeoDataFrame containing the places.
+        A GeoDataFrame containing the locations.
     """
     df = pd.read_csv(*args, **kwargs)
     df['center'] = df['center'].apply(wkt.loads)
     if 'extent' in df.columns:
         df['extent'] = df['extent'].apply(wkt.loads)
     gdf = gpd.GeoDataFrame(df, geometry='center')
-    assert gdf.as_places
+    assert gdf.as_locations
     return gdf
 
 
-def write_places_csv(places, filename, *args, **kwargs):
+def write_locations_csv(locations, filename, *args, **kwargs):
     """Wraps the pandas to_csv function, but transforms the center (and 
     extent) into WKT before writing.
 
     Parameters
     ----------
-    places : GeoDataFrame
-        The places to store to the CSV file.
+    locations : GeoDataFrame
+        The locations to store to the CSV file.
     
     filename : str
         The file to write to.
     """
-    gdf = places.copy()
-    gdf['center'] = places['center'].apply(wkt.dumps)
+    gdf = locations.copy()
+    gdf['center'] = locations['center'].apply(wkt.dumps)
     if 'extent' in gdf.columns:
-        gdf['extent'] = places['extent'].apply(wkt.dumps)
+        gdf['extent'] = locations['extent'].apply(wkt.dumps)
     gdf.to_csv(filename, index=False, *args, **kwargs)
 
 
diff --git a/trackintel/io/postgis.py b/trackintel/io/postgis.py
index 09eb5c6..a761ad5 100644
--- a/trackintel/io/postgis.py
+++ b/trackintel/io/postgis.py
@@ -274,8 +274,8 @@ def write_staypoints_postgis(staypoints, conn_string, table_name, schema=None,
         conn.close()
         
         
-def read_places_postgis(conn_string, table_name, geom_col='geom', *args, **kwargs):
-    """Reads places from a PostGIS database.
+def read_locations_postgis(conn_string, table_name, geom_col='geom', *args, **kwargs):
+    """Reads locations from a PostGIS database.
 
     Parameters
     ----------
@@ -284,7 +284,7 @@ def read_places_postgis(conn_string, table_name, geom_col='geom', *args, **kwarg
         ``postgresql://username:password@host:socket/database``.
     
     table_name : str
-        The table to read the places from.
+        The table to read the locations from.
 
     geom_col : str, default 'geom'
         The geometry column of the table. 
@@ -292,23 +292,23 @@ def read_places_postgis(conn_string, table_name, geom_col='geom', *args, **kwarg
     Returns
     -------
     GeoDataFrame
-        A GeoDataFrame containing the places.
+        A GeoDataFrame containing the locations.
     """
     engine = create_engine(conn_string)
     conn = engine.connect()
     try:
-        plcs = gpd.GeoDataFrame.from_postgis("SELECT * FROM %s" % table_name, conn, 
+        locs = gpd.GeoDataFrame.from_postgis("SELECT * FROM %s" % table_name, conn, 
                                             geom_col=geom_col, index_col='id',
                                             *args, **kwargs)
     finally:
         conn.close()
-    assert plcs.as_places
-    return plcs
+    assert locs.as_locations
+    return locs
 
 
-def write_places_postgis(places, conn_string, table_name, schema=None,
+def write_locations_postgis(locations, conn_string, table_name, schema=None,
                          sql_chunksize=None, if_exists='replace'):
-    """Stores places to PostGIS. Usually, this is directly called on a places 
+    """Stores locations to PostGIS. Usually, this is directly called on a locations 
     GeoDataFrame (see example below).
 
     **Attention!** This replaces the table if it already exists!
@@ -316,7 +316,7 @@ def write_places_postgis(places, conn_string, table_name, schema=None,
     Parameters
     ----------
     places : GeoDataFrame
-        The places to store to the database.
+        The locations to store to the database.
 
     conn_string : str
         A connection string to connect to a database, e.g., 
@@ -336,24 +336,24 @@ def write_places_postgis(places, conn_string, table_name, schema=None,
 
     Examples
     --------
-    >>> df.as_places.to_postgis(conn_string, table_name)
+    >>> df.as_locations.to_postgis(conn_string, table_name)
     """
     
     # make a copy in order to avoid changing the geometry of the original array
-    places_postgis = places.copy()
+    locations_postgis = locations.copy()
     
-    srid = int(places_postgis.crs['init'].split(':')[1])
-    places_postgis['center'] = \
-        places_postgis['center'].apply(lambda x: WKTElement(x.wkt, srid=srid))
-    places_postgis['extent'] = \
-        places_postgis['extent'].apply(lambda x: WKTElement(x.wkt, srid=srid))
-    if 'id' not in places_postgis.columns:
-        places_postgis['id'] = places_postgis.index
+    srid = int(locations_postgis.crs['init'].split(':')[1])
+    locations_postgis['center'] = \
+        locations_postgis['center'].apply(lambda x: WKTElement(x.wkt, srid=srid))
+    locations_postgis['extent'] = \
+        locations_postgis['extent'].apply(lambda x: WKTElement(x.wkt, srid=srid))
+    if 'id' not in locations_postgis.columns:
+        locations_postgis['id'] = locations_postgis.index
 
     engine = create_engine(conn_string)
     conn = engine.connect()
     try:
-        places_postgis.to_sql(table_name, engine, schema=schema,
+        locations_postgis.to_sql(table_name, engine, schema=schema,
                               if_exists=if_exists, index=False, 
                               dtype={'center': Geometry('POINT', srid=srid),
                                      'extent': Geometry('GEOMETRY', srid=srid)},
diff --git a/trackintel/model/__init__.py b/trackintel/model/__init__.py
index 2f8fca6..fe39746 100644
--- a/trackintel/model/__init__.py
+++ b/trackintel/model/__init__.py
@@ -1,5 +1,5 @@
 from .positionfixes import *
 from .staypoints import *
 from .triplegs import *
-from .places import *
+from .locations import *
 from .trips import *
\ No newline at end of file
diff --git a/trackintel/model/places.py b/trackintel/model/locations.py
similarity index 57%
rename from trackintel/model/places.py
rename to trackintel/model/locations.py
index 862c2ef..2853089 100644
--- a/trackintel/model/places.py
+++ b/trackintel/model/locations.py
@@ -2,12 +2,12 @@ import pandas as pd
 import trackintel as ti
 
 import trackintel.visualization.staypoints
-import trackintel.visualization.places
+import trackintel.visualization.locations
 
 
-@pd.api.extensions.register_dataframe_accessor("as_places")
-class PlacesAccessor(object):
-    """A pandas accessor to treat (Geo)DataFrames as collections of places. This
+@pd.api.extensions.register_dataframe_accessor("as_locations")
+class LocationsAccessor(object):
+    """A pandas accessor to treat (Geo)DataFrames as collections of locations. This
     will define certain methods and accessors, as well as make sure that the DataFrame
     adheres to some requirements.
 
@@ -30,29 +30,29 @@ class PlacesAccessor(object):
 
     @staticmethod
     def _validate(obj):
-        if any([c not in obj.columns for c in PlacesAccessor.required_columns]):
-            raise AttributeError("To process a DataFrame as a collection of staypoints, " \
+        if any([c not in obj.columns for c in LocationsAccessor.required_columns]):
+            raise AttributeError("To process a DataFrame as a collection of locations, " \
                 + "it must have the properties [%s], but it has [%s]." \
-                % (', '.join(PlacesAccessor.required_columns), ', '.join(obj.columns)))
+                % (', '.join(LocationsAccessor.required_columns), ', '.join(obj.columns)))
 
         if not (obj.shape[0] > 0 and obj['center'].iloc[0].geom_type is 'Point'):
-            # todo: We could think about allowing both geometry types for places (point and polygon)
+            # todo: We could think about allowing both geometry types for locations (point and polygon)
             # One for extend and one for the center
             raise AttributeError("The center geometry must be a Point (only first checked).")
 
     def plot(self, *args, **kwargs):
-        """Plots this collection of places. 
-        See :func:`trackintel.visualization.places.plot_places`."""
-        ti.visualization.places.plot_center_of_places(self._obj, *args, **kwargs)
+        """Plots this collection of locations. 
+        See :func:`trackintel.visualization.locations.plot_locations`."""
+        ti.visualization.locations.plot_center_of_locations(self._obj, *args, **kwargs)
 
     def to_csv(self, filename, *args, **kwargs):
-        """Stores this collection of places as a CSV file.
-        See :func:`trackintel.io.file.write_places_csv`."""
-        ti.io.file.write_places_csv(self._obj, filename, *args, **kwargs)
+        """Stores this collection of locations as a CSV file.
+        See :func:`trackintel.io.file.write_locations_csv`."""
+        ti.io.file.write_locations_csv(self._obj, filename, *args, **kwargs)
 
     def to_postgis(self, conn_string, table_name, schema=None,
             sql_chunksize=None, if_exists='replace'):
-        """Stores this collection of places to PostGIS.
-        See :func:`trackintel.io.postgis.write_places_postgis`."""
-        ti.io.postgis.write_places_postgis(self._obj, conn_string, table_name, 
+        """Stores this collection of locations to PostGIS.
+        See :func:`trackintel.io.postgis.write_locations_postgis`."""
+        ti.io.postgis.write_locations_postgis(self._obj, conn_string, table_name, 
             schema, sql_chunksize, if_exists)
diff --git a/trackintel/model/staypoints.py b/trackintel/model/staypoints.py
index 0e1b1fe..15e707a 100644
--- a/trackintel/model/staypoints.py
+++ b/trackintel/model/staypoints.py
@@ -47,8 +47,8 @@ class StaypointsAccessor(object):
         lon = self._obj.geometry.x
         return (float(lon.mean()), float(lat.mean()))
 
-    def extract_places(self, *args, **kwargs):
-        """Extracts places from this collection of staypoints.
+    def extract_locations(self, *args, **kwargs):
+        """Extracts locations from this collection of staypoints.
         See :func:`trackintel.preprocessing.staypoints.cluster_staypoints`."""
         return ti.preprocessing.staypoints.cluster_staypoints(self._obj, *args, **kwargs)
 
diff --git a/trackintel/preprocessing/staypoints.py b/trackintel/preprocessing/staypoints.py
index c79a5a1..b051c4e 100644
--- a/trackintel/preprocessing/staypoints.py
+++ b/trackintel/preprocessing/staypoints.py
@@ -8,11 +8,11 @@ import datetime
 from shapely.geometry import Point, MultiPoint
 from sklearn.cluster import DBSCAN
 
-from trackintel.geogr.distances import calculate_distance_matrix
+from trackintel.geogr.distances import calculate_distance_matrix, meters_to_decimal_degrees
 
 def cluster_staypoints(staypoints, method='dbscan',
-                       epsilon=100, num_samples=3, distance_matrix_metric=None):
-    """Clusters staypoints to get places.
+                       epsilon=100, num_samples=1, distance_matrix_metric=None):
+    """Clusters staypoints to get locations.
 
     Parameters
     ----------
@@ -39,14 +39,14 @@ def cluster_staypoints(staypoints, method='dbscan',
     Returns
     -------
     GeoDataFrame
-        A new GeoDataFrame containing places that a person visited multiple times.
+        A new GeoDataFrame containing locations that a person visited multiple times.
         
     Examples
     --------
-    >>> spts.as_staypoints.cluster_staypoints(method='dbscan', epsilon=50, num_samples=3)
+    >>> spts.as_staypoints.cluster_staypoints(method='dbscan', epsilon=100, num_samples=1)
     """
-    ret_places = pd.DataFrame(columns=['user_id', 'place_id','center', 'extent'])
-
+    
+    ret_staypoints = staypoints.copy()
     if method=='dbscan':
 
         if distance_matrix_metric is not None:
@@ -57,9 +57,9 @@ def cluster_staypoints(staypoints, method='dbscan',
             
         place_id_counter = 0
             
-        for user_id_this in staypoints["user_id"].unique():
+        for user_id_this in ret_staypoints["user_id"].unique():
             # Slice staypoints array by user. This is not a copy!
-            user_staypoints = staypoints[staypoints["user_id"] == user_id_this]  
+            user_staypoints = ret_staypoints[ret_staypoints["user_id"] == user_id_this]  
             
             if distance_matrix_metric is not None:
                 sp_distance_matrix = calculate_distance_matrix(
@@ -78,34 +78,42 @@ def cluster_staypoints(staypoints, method='dbscan',
                 place_id_counter = place_id_counter + max_label + 1
             
             # add staypoint - place matching to original staypoints
-            staypoints.loc[user_staypoints.index, 'place_id'] = labels
+            ret_staypoints.loc[user_staypoints.index, 'place_id'] = labels
+
 
-            
-    
         # create places as grouped staypoints
-        grouped_df = staypoints.groupby(['user_id', 'place_id'])
-        for combined_id, group in grouped_df:
-            user_id, place_id = combined_id
-            group.set_geometry(staypoints.geometry.name, inplace=True)
-    
-            if int(place_id) != -1:
-                ret_place = {}
-                ret_place['user_id'] = user_id
-                ret_place['place_id'] = place_id
-                
-                # point geometry of place
-                ret_place['center'] = Point(group.geometry.x.mean(),
-                     group.geometry.y.mean())
-                # polygon geometry of place
-                ret_place['extent'] = MultiPoint(points=list(group.geometry)).convex_hull
-    
-                ret_places = ret_places.append(ret_place, ignore_index=True)
-    
-        ret_places = gpd.GeoDataFrame(ret_places, geometry='center',
-                                      crs=staypoints.crs)
-        ret_places['place_id'] = ret_places['place_id'].astype('int')
+        temp_sp = ret_staypoints[['user_id', 'place_id', ret_staypoints.geometry.name]]
+        ret_loc = temp_sp.dissolve(by=['user_id', 'place_id'],as_index=False)
+        # filter outlier
+        ret_loc = ret_loc.loc[ret_loc['place_id'] != -1]
+        
+        # locations with only one staypoints is of type "Point"
+        point_idx = ret_loc.geom_type == 'Point'
+        ret_loc['center'] = 0 # initialize
+        ret_loc.loc[point_idx, 'center'] = ret_loc.loc[point_idx, 'geom']
+        # locations with multiple staypoints is of type "MultiPoint"
+        ret_loc.loc[~point_idx, 'center'] = ret_loc.loc[~point_idx, 'geom'].apply(lambda p: Point(np.array(p)[:,0].mean(), 
+                                                                                                np.array(p)[:,1].mean()))
+        
+        # extent is the convex hull of the geometry
+        ret_loc['extent'] = ret_loc['geom'].apply(lambda p: p.convex_hull)                                                                                        
+        # convex_hull of one point would be a Point and two points a Linestring, 
+        # we change them into Polygon by creating a buffer of epsilon around them.
+        pointLine_idx = (ret_loc['extent'].geom_type == 'LineString') | (ret_loc['extent'].geom_type == 'Point')
+        
+        # Perform meter to decimal conversion if the distance metric is haversine
+        if distance_matrix_metric == 'haversine':
+            ret_loc.loc[pointLine_idx, 'extent'] = ret_loc.loc[pointLine_idx].apply(
+                lambda p: p['extent'].buffer(meters_to_decimal_degrees(epsilon, p['center'].y)), axis=1)
+        else:
+            ret_loc.loc[pointLine_idx, 'extent'] = ret_loc.loc[pointLine_idx].apply(
+                lambda p: p['extent'].buffer(epsilon), axis=1)
+        
+        ret_loc = ret_loc.set_geometry('center')
+        ret_loc = ret_loc[['user_id', 'place_id', 'center', 'extent']]
+        ret_loc['place_id'] = ret_loc['place_id'].astype('int')
         
-    return ret_places
+    return ret_staypoints, ret_loc
 
 
 def create_activity_flag(staypoints, method='time_threshold', time_threshold=5, activity_column_name='activity'):
diff --git a/trackintel/visualization/places.py b/trackintel/visualization/locations.py
similarity index 74%
rename from trackintel/visualization/places.py
rename to trackintel/visualization/locations.py
index b36ff18..7a37092 100644
--- a/trackintel/visualization/places.py
+++ b/trackintel/visualization/locations.py
@@ -5,21 +5,21 @@ from trackintel.visualization.util import regular_figure, save_fig
 from trackintel.visualization.osm import plot_osm_streets
 
 
-def plot_center_of_places(places, out_filename=None, radius=None, positionfixes=None, 
+def plot_center_of_locations(locations, out_filename=None, radius=None, positionfixes=None, 
                           staypoints=None, staypoints_radius=None, plot_osm=False):
-    """Plots places (optionally to a file). Optionally, you can specify several other
-    datasets to be plotted beneath the places.
+    """Plots locations (optionally to a file). Optionally, you can specify several other
+    datasets to be plotted beneath the locations.
 
     Parameters
     ----------
-    places : GeoDataFrame
-        The places to plot.
+    locations : GeoDataFrame
+        The locations to plot.
     
     out_filename : str, optional
         The file to plot to, if this is not set, the plot will simply be shown.
 
     radius : float, optional
-        The radius with which circles around places should be drawn.
+        The radius with which circles around locations should be drawn.
 
     positionfixes : GeoDataFrame, optional
         If available, some positionfixes that can additionally be plotted.
@@ -33,7 +33,7 @@ def plot_center_of_places(places, out_filename=None, radius=None, positionfixes=
 
     Examples
     --------
-    >>> df.as_places.plot('output.png', radius=10, positionfixes=pdf, 
+    >>> df.as_locations.plot('output.png', radius=10, positionfixes=pdf, 
     >>>                   staypoints=spf, staypoints_radius=8, plot_osm=True)
     """
     _, ax = regular_figure()
@@ -45,10 +45,10 @@ def plot_center_of_places(places, out_filename=None, radius=None, positionfixes=
             north = positionfixes['geom'].y.max()
             south = positionfixes['geom'].y.min()
         else:
-            west = places['center'].x.min() - 0.03
-            east = places['center'].x.max() + 0.03
-            north = places['center'].y.max() + 0.03
-            south = places['center'].y.min() - 0.03
+            west = locations['center'].x.min() - 0.03
+            east = locations['center'].x.max() + 0.03
+            north = locations['center'].y.max() + 0.03
+            south = locations['center'].y.min() - 0.03
         plot_osm_streets(north, south, east, west, ax)
 
     if positionfixes is not None:
@@ -64,7 +64,7 @@ def plot_center_of_places(places, out_filename=None, radius=None, positionfixes=
 
     if radius is None:
         radius = 5
-    for pt in places.to_dict('records'):
+    for pt in locations.to_dict('records'):
         circle = mpatches.Circle((pt['center'].x, pt['center'].y), radius, 
                                   facecolor='none', edgecolor='r', zorder=4)
         ax.add_artist(circle)


Test patch:
diff --git a/tests/data/places.csv b/tests/data/locations.csv
similarity index 100%
rename from tests/data/places.csv
rename to tests/data/locations.csv
diff --git a/tests/preprocessing/test_preprocessing.py b/tests/preprocessing/test_preprocessing.py
index 7373d22..b7a0942 100644
--- a/tests/preprocessing/test_preprocessing.py
+++ b/tests/preprocessing/test_preprocessing.py
@@ -31,13 +31,13 @@ class TestPreprocessing():
     def test_cluster_staypoints_dbscan_min(self):
         pfs = ti.read_positionfixes_csv('tests/data/positionfixes.csv', sep=';')
         spts = pfs.as_positionfixes.extract_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
-        clusters = spts.as_staypoints.extract_places(method='dbscan', epsilon=1e-18, num_samples=0)
+        _, clusters = spts.as_staypoints.extract_locations(method='dbscan', epsilon=1e-18, num_samples=0)
         assert len(clusters) == len(spts), "With small hyperparameters, clustering should not reduce the number"
 
     def test_cluster_staypoints_dbscan_max(self):
         pfs = ti.read_positionfixes_csv('tests/data/positionfixes.csv', sep=';')
         spts = pfs.as_positionfixes.extract_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
-        clusters = spts.as_staypoints.extract_places(method='dbscan', epsilon=1e18, num_samples=1000)
+        _, clusters = spts.as_staypoints.extract_locations(method='dbscan', epsilon=1e18, num_samples=1000)
         assert len(clusters) == 0, "With large hyperparameters, everything is an outlier"
 
 
diff --git a/tests/test_io.py b/tests/test_io.py
index e69fef1..c80c533 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -92,16 +92,16 @@ class TestIO:
         # TODO Implement some tests for PostGIS.
         pass
 
-    def test_places_from_to_csv(self):
-        orig_file = 'tests/data/places.csv'
-        tmp_file = 'tests/data/places_test.csv'
-        plcs = ti.read_places_csv(orig_file, sep=';')
-        plcs.as_places.to_csv(tmp_file, sep=';', 
+    def test_locations_from_to_csv(self):
+        orig_file = 'tests/data/locations.csv'
+        tmp_file = 'tests/data/locations_test.csv'
+        plcs = ti.read_locations_csv(orig_file, sep=';')
+        plcs.as_locations.to_csv(tmp_file, sep=';', 
             columns=['user_id', 'elevation', 'center', 'extent'])
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
         
-    def test_places_from_to_postgis(self):
+    def test_locations_from_to_postgis(self):
         # TODO Implement some tests for PostGIS.
         pass