Repository: mie-lab/trackintel
Pull request #88 (2021-01-26 17:12:02)

Fix: dtype consistency and function outputs
For generate functions:
- functions do not change the original input df but instead returns the modified one (with the corresponding generated ids).
- The generated id is in int64. Exception: the dtype for [`trip_id` `prev_trip_id` and `next_trip_id`] for spts and [`trip_id`] for triplegs in 'generate_trips' function is float, as int64 does not support missing values ('NaN').
- generated df has column 'id', while original df receives a new column '{generated}_id'. Example: loc_df has ['id'] field, while original stps_df has ['location_id'] field. (Partly solves Issue #56 )
- 'user_id' dtype of the generated df is consistent with the original one. (Issue #70  )
- modified tests to account for these changes 

codecov[bot]: # [Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/88?src=pr&el=h1) Report
> Merging [#88](https://codecov.io/gh/mie-lab/trackintel/pull/88?src=pr&el=desc) (0625f0d) into [master](https://codecov.io/gh/mie-lab/trackintel/commit/359d5b3ab1780b025112e5a783284475a8647483?el=desc) (359d5b3) will **increase** coverage by `0.44%`.
> The diff coverage is `86.79%`.

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

```diff
@@            Coverage Diff             @@
##           master      #88      +/-   ##
==========================================
+ Coverage   64.42%   64.86%   +0.44%     
==========================================
  Files          31       31              
  Lines        1116     1130      +14     
  Branches      183      183              
==========================================
+ Hits          719      733      +14     
  Misses        336      336              
  Partials       61       61              
```


| [Impacted Files](https://codecov.io/gh/mie-lab/trackintel/pull/88?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [trackintel/io/\_\_init\_\_.py](https://codecov.io/gh/mie-lab/trackintel/pull/88/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9pby9fX2luaXRfXy5weQ==) | `100.00% <ø> (ø)` | |
| [trackintel/preprocessing/positionfixes.py](https://codecov.io/gh/mie-lab/trackintel/pull/88/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3Bvc2l0aW9uZml4ZXMucHk=) | `66.45% <78.78%> (+1.07%)` | :arrow_up: |
| [trackintel/preprocessing/staypoints.py](https://codecov.io/gh/mie-lab/trackintel/pull/88/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3N0YXlwb2ludHMucHk=) | `91.93% <100.00%> (+0.41%)` | :arrow_up: |
| [trackintel/preprocessing/triplegs.py](https://codecov.io/gh/mie-lab/trackintel/pull/88/diff?src=pr&el=tree#diff-dHJhY2tpbnRlbC9wcmVwcm9jZXNzaW5nL3RyaXBsZWdzLnB5) | `91.47% <100.00%> (+0.34%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mie-lab/trackintel/pull/88?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/88?src=pr&el=footer). Last update [359d5b3...0625f0d](https://codecov.io/gh/mie-lab/trackintel/pull/88?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index e34f6f4..14a68a9 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
 # The trackintel Framework
 
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/mie-lab/trackintel/master?filepath=%2Fexamples%2Fexample_geolife%2FTrackintel_introduction.ipynb)
-![Version](https://img.shields.io/badge/version-v0.2.0-red.svg)
+[![PyPI version](https://badge.fury.io/py/trackintel.svg)](https://badge.fury.io/py/trackintel)
 [![Build Status](https://travis-ci.org/mie-lab/trackintel.svg?branch=master)](https://travis-ci.org/mie-lab/trackintel)
-[![codecov.io](https://codecov.io/gh/mie-lab/trackintel/coverage.svg?branch=master)](https://codecov.io/gh/mie-lab/trackintel)
 [![Documentation Status](https://readthedocs.org/projects/trackintel/badge/?version=latest)](https://trackintel.readthedocs.io/en/latest/?badge=latest)
+[![codecov.io](https://codecov.io/gh/mie-lab/trackintel/coverage.svg?branch=master)](https://codecov.io/gh/mie-lab/trackintel)
           
 Focusing on human mobility data, *trackintel* provides functionalities for data quality enhancement, integrating data from various sources, performing quantitative analysis and mining tasks, and visualizing the data and/or analysis results.
 In addition to these core functionalities, packages are provided for user mobility profiling and trajectory-based learning analytics.
diff --git a/ROADMAP.md b/ROADMAP.md
index b82f43f..c811374 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -37,7 +37,7 @@ Steps until we get to version 1.0.0.
 
 
 ### v0.6.0
-* Support flexible geometry columns [Issue15](https://github.com/mie-lab/trackintel/issues/15)
+* :heavy_check_mark: Support flexible geometry columns [Issue15](https://github.com/mie-lab/trackintel/issues/15)
 * :heavy_check_mark: Filter data by geographic location (e.g., all data within Switzerland)
 * Enhance tripleg extraction to be _gap aware_ [issue27](https://github.com/mie-lab/trackintel/issues/27)
 
diff --git a/Readme_developers.md b/Readme_developers.md
new file mode 100644
index 0000000..68342d1
--- /dev/null
+++ b/Readme_developers.md
@@ -0,0 +1,14 @@
+## trackintel: version release checklist
+
+Before you release a new version you should check/modify the following files:
+
+
+
+- trackintel/__ version__.py -> update the version number
+
+- docs/index.rst -> update the version number
+
+- check setup.py if new dependencies need greater python version
+
+After the release is tagged, __no new release with the same version number__ can be published on pypi!
+
diff --git a/docs/index.rst b/docs/index.rst
index b5dde29..fb67b8f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -4,7 +4,8 @@ The *trackintel* Documentation
 
 .. image:: https://mybinder.org/badge_logo.svg
  :target: https://mybinder.org/v2/gh/mie-lab/trackintel/master?filepath=%2Fexamples%2Fexample_geolife%2FTrackintel_introduction.ipynb
-.. image:: https://img.shields.io/badge/version-v0.2.0-red.svg
+.. image:: https://badge.fury.io/py/trackintel.svg
+    :target: https://badge.fury.io/py/trackintel
 
 Focusing on human mobility data, *trackintel* provides functionalities for data quality 
 enhancement, integrating data from various sources, performing quantitative analysis and 
diff --git a/setup.py b/setup.py
index 654f8ad..7eb2650 100644
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,20 @@ REQUIRED = [
     'tqdm'
 ]
 
+install_requires = [
+    'pandas',
+    'matplotlib',
+    'numpy',
+    'pint',
+    'shapely',
+    'networkx',
+    'geoalchemy2',
+    'osmnx',
+    'scikit-learn',
+    'tqdm',
+    'geopandas'
+]
+
 # What packages are optional?
 EXTRAS = {
     # 'fancy feature': ['django'],
@@ -96,4 +110,4 @@ setup(
     cmdclass={
         # 'upload': UploadCommand,
     },
-)
\ No newline at end of file
+)
diff --git a/trackintel/__version__.py b/trackintel/__version__.py
index d1ee583..3cb459a 100644
--- a/trackintel/__version__.py
+++ b/trackintel/__version__.py
@@ -1,3 +1,3 @@
-VERSION = (0, 2, 0)
+VERSION = (0, 4, 1)
 
-__version__ = '.'.join(map(str, VERSION))
\ No newline at end of file
+__version__ = '.'.join(map(str, VERSION))
diff --git a/trackintel/io/__init__.py b/trackintel/io/__init__.py
index a646a5b..e230162 100644
--- a/trackintel/io/__init__.py
+++ b/trackintel/io/__init__.py
@@ -21,4 +21,6 @@ from .postgis import write_locations_postgis
 from .file import read_trips_csv
 from .file import write_trips_csv
 from .postgis import read_trips_postgis
-from .postgis import write_trips_postgis
\ No newline at end of file
+from .postgis import write_trips_postgis
+
+from .dataset_reader import read_geolife
\ No newline at end of file
diff --git a/trackintel/preprocessing/positionfixes.py b/trackintel/preprocessing/positionfixes.py
index 08496a9..80e2ff7 100644
--- a/trackintel/preprocessing/positionfixes.py
+++ b/trackintel/preprocessing/positionfixes.py
@@ -8,16 +8,13 @@ from trackintel.geogr.distances import haversine_dist
 
 
 def generate_staypoints(positionfixes, method='sliding',
-                        dist_threshold=50, time_threshold=5 * 60, epsilon=100,
+                        dist_threshold=50, time_threshold= 300, epsilon=100,
                         dist_func=haversine_dist, num_samples=None):
     """Generates staypoints from positionfixes.
 
-    This function modifies the positionfixes and adds staypoint_ids.
-
     Parameters
     ----------
     
-    
     positionfixes : GeoDataFrame
         The positionfixes have to follow the standard definition for positionfixes DataFrames.
 
@@ -25,30 +22,31 @@ def generate_staypoints(positionfixes, method='sliding',
         - 'sliding' : Applies a sliding window over the data.
         - 'dbscan' : Uses the DBSCAN algorithm to find clusters of staypoints.
 
-    dist_threshold : float
+    dist_threshold : float, default 50
         The distance threshold for the 'sliding' method, i.e., how far someone has to travel to
         generate a new staypoint.
 
-    time_threshold : float
+    time_threshold : float, default 300 (seconds)
         The time threshold for the 'sliding' method in seconds, i.e., how long someone has to 
         stay within an area to consider it as a staypoint.
 
-    epsilon : float
+    epsilon : float, default 100
         The epsilon for the 'dbscan' method.
 
-    dist_func : function
+    dist_func : function, defaut haversine_dist
         A function that expects (lon_1, lat_1, lon_2, lat_2) and computes a distance in meters.
         
     num_samples :
     
     Returns
     -------
-    GeoDataFrame
-        A new GeoDataFrame containing points where a person spent some time.
+    (GeoDataFrame, GeoDataFrame)
+        Tuple of (positionfixes, staypoints). Positionfixes is the original GeoDataFrame with 
+        a new column 'staypoint_id'. 
 
     Examples
     --------
-    >>> psfs.as_positionfixes.generate_staypoints('sliding', dist_threshold=100)
+    >>> pfs.as_positionfixes.generate_staypoints('sliding', dist_threshold=100)
 
     References
     ----------
@@ -59,23 +57,25 @@ def generate_staypoints(positionfixes, method='sliding',
     similarity based on location history. In Proceedings of the 16th ACM SIGSPATIAL international 
     conference on Advances in geographic information systems (p. 34). ACM.
     """
-    if 'id' not in positionfixes.columns:
-        positionfixes['id'] = positionfixes.index
+    # copy the original pfs for adding 'staypoint_id' column
+    ret_pfs = positionfixes.copy()
+    
+    if 'id' not in ret_pfs.columns:
+        ret_pfs['id'] = ret_pfs.index
     
-    elevation_flag = 'elevation' in positionfixes.columns # if there is elevation data
+    elevation_flag = 'elevation' in ret_pfs.columns # if there is elevation data
 
-    name_geocol = positionfixes.geometry.name
-    ret_staypoints = pd.DataFrame(columns=['started_at', 'finished_at', 'geom', 'id'])
+    name_geocol = ret_pfs.geometry.name
+    ret_spts = pd.DataFrame(columns=['id', 'user_id', 'started_at', 'finished_at', 'geom'])
 
     if method == 'sliding':
         # Algorithm from Li et al. (2008). For details, please refer to the paper.
         staypoint_id_counter = 0
-        positionfixes['staypoint_id'] = -1  # this marks all that are not part of a SP
+        ret_pfs['staypoint_id'] = -1  # this marks all that are not part of a SP
 
-        for user_id_this in positionfixes['user_id'].unique():
+        for user_id_this in ret_pfs['user_id'].unique():
 
-            positionfixes_user_this = positionfixes.loc[
-                positionfixes['user_id'] == user_id_this]  # this is no copy
+            positionfixes_user_this = ret_pfs.loc[ret_pfs['user_id'] == user_id_this]  # this is no copy
 
             pfs = positionfixes_user_this.sort_values('tracked_at').to_dict('records')
             num_pfs = len(pfs)
@@ -115,7 +115,7 @@ def generate_staypoints(positionfixes, method='sliding',
                             staypoint_id_counter += 1
 
                             # add staypoint
-                            ret_staypoints = ret_staypoints.append(staypoint, ignore_index=True)
+                            ret_spts = ret_spts.append(staypoint, ignore_index=True)
 
                             # TODO Discussion: Is this last point really a staypoint? As we don't know if the
                             #      person "moves on" afterwards...
@@ -133,7 +133,7 @@ def generate_staypoints(positionfixes, method='sliding',
                                 posfix_staypoint_matching[staypoint_id_counter] = [
                                     pfs[j]['id']]  # rather [k for k in range(i, j)]?
                                 staypoint_id_counter += 1
-                                ret_staypoints = ret_staypoints.append(staypoint, ignore_index=True)
+                                ret_spts = ret_spts.append(staypoint, ignore_index=True)
                         i = j
                         break
                     j = j + 1
@@ -143,7 +143,7 @@ def generate_staypoints(positionfixes, method='sliding',
             for staypoints_id, posfix_idlist in posfix_staypoint_matching.items():
                 # note that we use .loc because above we have saved the id 
                 # of the positionfixes not thier absolut position
-                positionfixes.loc[posfix_idlist, 'staypoint_id'] = staypoints_id
+                ret_pfs.loc[posfix_idlist, 'staypoint_id'] = staypoints_id
 
 
     elif method == 'dbscan':
@@ -152,9 +152,9 @@ def generate_staypoints(positionfixes, method='sliding',
         # adjust the distance metric e.g. chebychev
 
         db = DBSCAN(eps=epsilon, min_samples=num_samples)
-        for user_id_this in positionfixes['user_id'].unique():
+        for user_id_this in ret_pfs['user_id'].unique():
 
-            user_positionfixes = positionfixes[positionfixes['user_id'] == user_id_this]  # this is not a copy!
+            user_positionfixes = ret_pfs[ret_pfs['user_id'] == user_id_this]  # this is not a copy!
 
             # TODO: enable transformations to temporary (metric) system
             transform_crs = None
@@ -166,10 +166,10 @@ def generate_staypoints(positionfixes, method='sliding',
             labels = db.fit_predict(coordinates)
 
             # add positionfixes - staypoint matching to original positionfixes
-            positionfixes.loc[user_positionfixes.index, 'staypoint_id'] = labels
+            ret_pfs.loc[user_positionfixes.index, 'staypoint_id'] = labels
 
         # create staypoints as the center of the grouped positionfixes
-        grouped_df = positionfixes.groupby(['user_id', 'staypoint_id'])
+        grouped_df = ret_pfs.groupby(['user_id', 'staypoint_id'])
         for combined_id, group in grouped_df:
             user_id, staypoint_id = combined_id
 
@@ -182,13 +182,19 @@ def generate_staypoints(positionfixes, method='sliding',
                 staypoint.geometry = Point(group[name_geocol].x.mean(),
                                            group[name_geocol].y.mean())
 
-                ret_staypoints = ret_staypoints.append(staypoint, ignore_index=True)
+                ret_spts = ret_spts.append(staypoint, ignore_index=True)
 
-    ret_staypoints = gpd.GeoDataFrame(ret_staypoints, geometry='geom',
-                                      crs=positionfixes.crs)
-    ret_staypoints['id'] = ret_staypoints['id'].astype('int')
+    ret_spts = gpd.GeoDataFrame(ret_spts, geometry='geom',crs=ret_pfs.crs)
+    
+    ## ensure dtype consistency 
+    # stps id (generated by this function) should be int64
+    ret_spts['id'] = ret_spts['id'].astype('int64')
+    ret_pfs['staypoint_id'] = ret_pfs['staypoint_id'].astype('int64')
+    
+    # user_id of spts should be the same as ret_pfs
+    ret_spts['user_id'] = ret_spts['user_id'].astype(ret_pfs['user_id'].dtype)
 
-    return ret_staypoints
+    return ret_pfs, ret_spts
 
 
 def generate_triplegs(positionfixes, staypoints=None, *args, **kwargs):
@@ -199,8 +205,6 @@ def generate_triplegs(positionfixes, staypoints=None, *args, **kwargs):
     positionfixes or passing some staypoints that correspond to the positionfixes! 
     This means you usually should call ``extract_staypoints()`` first.
 
-    This function modifies the positionfixes and adds a ``tripleg_id``.
-
     Parameters
     ----------
     positionfixes : GeoDataFrame
@@ -208,33 +212,36 @@ def generate_triplegs(positionfixes, staypoints=None, *args, **kwargs):
 
     staypoints : GeoDataFrame, optional
         The staypoints (corresponding to the positionfixes). If this is not passed, the 
-        positionfixes need staypoint_ids associated with them.
+        positionfixes need staypoint_id associated with them.
 
     Returns
     -------
-    GeoDataFrame
-        A new GeoDataFrame containing triplegs.
+    (GeoDataFrame, GeoDataFrame)
+        Tuple of (positionfixes, triplegs). Positionfixes is the original GeoDataFrame with 
+        a new column 'tripleg_id'.
 
     Examples
     --------
-    >>> psfs.as_positionfixes.generate_triplegs(staypoints)
+    >>> pfs.as_positionfixes.generate_triplegs(staypoints)
     """
-    name_geocol = positionfixes.geometry.name
+    # copy the original pfs for adding 'staypoint_id' column
+    ret_pfs = positionfixes.copy()
+    
+    name_geocol = ret_pfs.geometry.name
     # Check that data adheres to contract.
-    if staypoints is None and len(positionfixes['staypoint_id'].unique()) < 2:
+    if staypoints is None and len(ret_pfs['staypoint_id'].unique()) < 2:
         raise ValueError("If staypoints is not defined, positionfixes must have more than 1 staypoint_id.")
 
     # if staypoints is not None:
     #     raise NotImplementedError("Splitting up positionfixes by timestamp is not available yet. " + \
     #         "Use extract_staypoints and the thus generated staypoint_ids.")
 
-    ret_triplegs = pd.DataFrame(columns=['id', 'user_id', 'started_at', 'finished_at', 'geom'])
+    ret_tpls = pd.DataFrame(columns=['id', 'user_id', 'started_at', 'finished_at', 'geom'])
     curr_tripleg_id = 0
     # Do this for each user.
-    for user_id_this in positionfixes['user_id'].unique():
+    for user_id_this in ret_pfs['user_id'].unique():
 
-        positionfixes_user_this = positionfixes.loc[
-            positionfixes['user_id'] == user_id_this]  # this is no copy
+        positionfixes_user_this = ret_pfs.loc[ret_pfs['user_id'] == user_id_this]  # this is no copy
         pfs = positionfixes_user_this.sort_values('tracked_at')
         generated_triplegs = []
 
@@ -362,10 +369,20 @@ def generate_triplegs(positionfixes, staypoints=None, *args, **kwargs):
 
                 prev_pf = pf
         if len(generated_triplegs) > 0:
-            ret_triplegs = ret_triplegs.append(generated_triplegs)
+            ret_tpls = ret_tpls.append(generated_triplegs)
 
-    ret_triplegs = gpd.GeoDataFrame(ret_triplegs, geometry='geom', crs=positionfixes.crs)
-    ret_triplegs['id'] = ret_triplegs['id'].astype('int')
+    ret_tpls = gpd.GeoDataFrame(ret_tpls, geometry='geom', crs=ret_pfs.crs)
+    
+    ## ensure dtype consistency 
+    # tpls id (generated by this function) should be int64
+    ret_tpls['id'] = ret_tpls['id'].astype('int64')
+    # ISSUE 56: no tripleg_id is assigned for case 1
+    # ret_pfs['tripleg_id'] = ret_pfs['tripleg_id'].astype('int64')
+    
+    # user_id of tpls should be the same as ret_pfs
+    ret_tpls['user_id'] = ret_tpls['user_id'].astype(ret_pfs['user_id'].dtype)
+    
+    
     # todo: triplegs dataframe has use the index as id
     # todo: proposed fix: ret_triplegs = ret_triplegs.set_index('id')
-    return ret_triplegs
+    return ret_pfs, ret_tpls
diff --git a/trackintel/preprocessing/staypoints.py b/trackintel/preprocessing/staypoints.py
index 4e347b8..7b54e9e 100644
--- a/trackintel/preprocessing/staypoints.py
+++ b/trackintel/preprocessing/staypoints.py
@@ -57,8 +57,8 @@ def generate_locations(staypoints,
         raise AttributeError("The parameter agg_level must be one of ['user', 'dataset'].")
     
     # initialize the return GeoDataFrames
-    ret_sp = staypoints.copy()
-    ret_loc = gpd.GeoDataFrame([], columns=['user_id', 'location_id', 'center', 'extent'])
+    ret_stps = staypoints.copy()
+    ret_loc = gpd.GeoDataFrame([], columns=['user_id', 'id', 'center', 'extent'])
     
     if method=='dbscan':
 
@@ -73,9 +73,9 @@ def generate_locations(staypoints,
             
         if agg_level == 'user':
             location_id_counter = 0
-            for user_id_this in ret_sp["user_id"].unique():
+            for user_id_this in ret_stps["user_id"].unique():
                 # Slice staypoints array by user. This is not a copy!
-                user_staypoints = ret_sp[ret_sp["user_id"] == user_id_this]  
+                user_staypoints = ret_stps[ret_stps["user_id"] == user_id_this]  
                 
                 if distance_matrix_metric == 'haversine':
                     # the input is converted to list of (lat, lon) tuples in radians unit
@@ -91,19 +91,19 @@ def generate_locations(staypoints,
                     location_id_counter = location_id_counter + max_label + 1
                 
                 # add staypoint - location matching to original staypoints
-                ret_sp.loc[user_staypoints.index, 'location_id'] = labels
+                ret_stps.loc[user_staypoints.index, 'location_id'] = labels
         else:
             if distance_matrix_metric == 'haversine':
                 # the input is converted to list of (lat, lon) tuples in radians unit
-                p = np.array([[radians(g.y), radians(g.x)] for g in ret_sp.geometry])
+                p = np.array([[radians(g.y), radians(g.x)] for g in ret_stps.geometry])
             else:
-                p = np.array([[g.x, g.y] for g in ret_sp.geometry])
+                p = np.array([[g.x, g.y] for g in ret_stps.geometry])
             labels = db.fit_predict(p)
             
-            ret_sp['location_id'] = labels
+            ret_stps['location_id'] = labels
             
         # create locations as grouped staypoints
-        temp_sp = ret_sp[['user_id', 'location_id', ret_sp.geometry.name]]
+        temp_sp = ret_stps[['user_id', 'location_id', ret_stps.geometry.name]]
         if agg_level == 'user':
             # directly dissolve by 'user_id' and 'location_id'
             ret_loc = temp_sp.dissolve(by=['user_id', 'location_id'],as_index=False)
@@ -143,9 +143,16 @@ def generate_locations(staypoints,
         
         ret_loc = ret_loc.set_geometry('center')
         ret_loc = ret_loc[['user_id', 'location_id', 'center', 'extent']]
-        ret_loc['location_id'] = ret_loc['location_id'].astype('int')
+        ret_loc.rename(columns={'location_id':'id'}, inplace=True)
         
-    return ret_sp, ret_loc
+    ## ensure dtype consistency 
+    # locs id (generated by this function) should be int64
+    ret_loc['id'] = ret_loc['id'].astype('int64')
+    ret_stps['location_id'] = ret_stps['location_id'].astype('int64')
+    # user_id of ret_loc should be the same as ret_stps
+    ret_loc['user_id'] = ret_loc['user_id'].astype(ret_stps['user_id'].dtype)
+    
+    return ret_stps, ret_loc
 
 
 def create_activity_flag(staypoints, method='time_threshold', time_threshold=5, activity_column_name='activity'):
diff --git a/trackintel/preprocessing/triplegs.py b/trackintel/preprocessing/triplegs.py
index 4a9b86f..a9e28a9 100644
--- a/trackintel/preprocessing/triplegs.py
+++ b/trackintel/preprocessing/triplegs.py
@@ -147,8 +147,21 @@ def generate_trips(stps_input, tpls_input, gap_threshold=15, id_offset=0, print_
     tpls.drop(columns=['type'], inplace=True)
     spts.drop(columns=['type'], inplace=True)
     trips.drop(columns = ['tpls', 'spts'], inplace=True)
-    trips.set_index('id', inplace=True)
     
+    ## ensure dtype consistency 
+    # trips id (generated by this function) should be int64
+    trips['id'] = trips['id'].astype('int64')
+    # spts and tpls trip id can only be in float
+    spts['trip_id'] = spts['trip_id'].astype('float')
+    spts['prev_trip_id'] = spts['prev_trip_id'].astype('float')
+    spts['next_trip_id'] = spts['next_trip_id'].astype('float')
+    tpls['trip_id'] = tpls['trip_id'].astype('float')
+    
+    # user_id of trips should be the same as tpls
+    trips['user_id'] = trips['user_id'].astype(tpls['user_id'].dtype)
+    
+    
+    # trips.set_index('id', inplace=True)
     return spts, tpls, trips
 
 


Test patch:
diff --git a/.github/workflows/publish_test_pypi.yml b/.github/workflows/publish_test_pypi.yml
new file mode 100644
index 0000000..5bf7cd8
--- /dev/null
+++ b/.github/workflows/publish_test_pypi.yml
@@ -0,0 +1,30 @@
+name: pypi-publish
+on: release
+jobs:
+  build-n-publish:
+    name: Build and publish Python distr to TestPyPI
+    runs-on: ubuntu-18.04
+    steps:
+    - uses: actions/checkout@master
+    - name: Set up Python 3.8
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.8
+    - name: Install pypa/build
+      run: >-
+        python -m
+        pip install
+        build
+        --user
+    - name: Build a binary wheel and a source tarball
+      run: >-
+        python -m
+        build
+        --sdist
+        --wheel
+        --outdir dist/
+    - name: Publish distribution 📦 to PyPI
+      if: startsWith(github.ref, 'refs/tags')
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        password: ${{ secrets.pypikey }}
diff --git a/tests/preprocessing/test_positionfixes.py b/tests/preprocessing/test_positionfixes.py
index 281c189..9f3a78c 100644
--- a/tests/preprocessing/test_positionfixes.py
+++ b/tests/preprocessing/test_positionfixes.py
@@ -9,24 +9,30 @@ import trackintel as ti
 class TestGenerate_staypoints():
     def test_generate_staypoints_sliding_min(self):
         pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
         assert len(spts) == len(pfs), "With small thresholds, staypoint extraction should yield each positionfix"
         
     def test_generate_staypoints_sliding_max(self):
         pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=sys.maxsize, 
+        _, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=sys.maxsize, 
                                                        time_threshold=sys.maxsize)
         assert len(spts) == 0, "With large thresholds, staypoint extraction should not yield positionfixes"
         
-            
-
+    def test_generate_staypoints_dtype_consistent(self):
+        pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', 
+                                                             dist_threshold=25, 
+                                                             time_threshold=5 * 60)
+        assert pfs['user_id'].dtype == spts['user_id'].dtype
+        assert pfs['staypoint_id'].dtype == spts['id'].dtype
+        
 
 class TestGenerate_triplegs():
     def test_generate_triplegs_global(self):
         # generate triplegs from raw-data
         pfs = ti.io.dataset_reader.read_geolife(os.path.join('tests', 'data', 'geolife'))
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25, time_threshold=5 * 60)
-        tpls = pfs.as_positionfixes.generate_triplegs(spts)
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25, time_threshold=5 * 60)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
 
         # load pregenerated test-triplegs
         tpls_test = ti.read_triplegs_csv(os.path.join('tests', 'data', 'geolife', 'geolife_triplegs_short.csv'))
@@ -39,12 +45,21 @@ class TestGenerate_triplegs():
             distance = tpls.geom.iloc[i].distance(tpls_test.geom.iloc[i])
             distance_sum = distance_sum + distance
         np.testing.assert_almost_equal(distance_sum, 0.0)
+    
+    def test_generate_triplegs_dtype_consistent(self):
+        pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', 
+                                                             dist_threshold=25, 
+                                                             time_threshold=5 * 60)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
+        assert pfs['user_id'].dtype == tpls['user_id'].dtype
+        # assert pfs['tripleg_id'].dtype == tpls['id'].dtype
         
     def test_generate_staypoint_triplegs(self):
         pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
-        tpls1 = pfs.as_positionfixes.generate_triplegs()
-        tpls2 = pfs.as_positionfixes.generate_triplegs(spts)
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
+        _, tpls1 = pfs.as_positionfixes.generate_triplegs()
+        _, tpls2 = pfs.as_positionfixes.generate_triplegs(spts)
         assert len(tpls1) > 0, "There should be more than zero triplegs"
         assert len(tpls2) > 0, "There should be more than zero triplegs"
         assert len(tpls1) == len(tpls2), "If we extract the staypoints in the same way, it should lead to " + \
@@ -58,8 +73,8 @@ class TestGenerate_triplegs():
         the next one started.
         """
         pfs = ti.io.dataset_reader.read_geolife(os.path.join('tests', 'data', 'geolife_long'))
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25, time_threshold=5 * 60)
-        tpls = pfs.as_positionfixes.generate_triplegs(spts)
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25, time_threshold=5 * 60)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
 
         spts_tpls = spts[['started_at', 'finished_at', 'user_id']].append(
             tpls[['started_at', 'finished_at', 'user_id']])
diff --git a/tests/preprocessing/test_staypoints.py b/tests/preprocessing/test_staypoints.py
index 0ec09ac..92a2a60 100644
--- a/tests/preprocessing/test_staypoints.py
+++ b/tests/preprocessing/test_staypoints.py
@@ -41,7 +41,7 @@ class TestGenerate_locations():
         db = DBSCAN(eps=10, min_samples=0, metric="precomputed")
         labels = db.fit_predict(sp_distance_matrix)
         
-        assert len(set(locs['location_id'])) == len(set(labels)) , "The #location should be the same"
+        assert len(set(locs['id'])) == len(set(labels)) , "The #location should be the same"
     
     def test_generate_locations_dbscan_loc(self):
         spts = ti.read_staypoints_csv(os.path.join('tests', 'data', 'geolife', 'geolife_staypoints.csv'))
@@ -50,7 +50,7 @@ class TestGenerate_locations():
                                                           agg_level='dataset')
 
         # create locations as grouped staypoints, another way to create locations
-        other_locs = pd.DataFrame(columns=['user_id', 'location_id','center'])
+        other_locs = pd.DataFrame(columns=['user_id', 'id','center'])
         grouped_df = spts.groupby(['user_id', 'location_id'])
         for combined_id, group in grouped_df:
             user_id, location_id = combined_id
@@ -59,7 +59,7 @@ class TestGenerate_locations():
             if int(location_id) != -1:
                 temp_loc = {}
                 temp_loc['user_id'] = user_id
-                temp_loc['location_id'] = location_id
+                temp_loc['id'] = location_id
                 
                 # point geometry of place
                 temp_loc['center'] = Point(group.geometry.x.mean(), group.geometry.y.mean())
@@ -68,7 +68,7 @@ class TestGenerate_locations():
         other_locs = gpd.GeoDataFrame(other_locs, geometry='center', crs=spts.crs)
         
         assert all(other_locs['center'] == locs['center']), "The location geometry should be the same"
-        assert all(other_locs['location_id'] == locs['location_id']), "The location id should be the same"
+        assert all(other_locs['id'] == locs['id']), "The location id should be the same"
     
     def test_generate_locations_dbscan_user_dataset(self):
         spts = ti.read_staypoints_csv(os.path.join('tests', 'data', 'geolife', 'geolife_staypoints.csv'))
@@ -85,14 +85,14 @@ class TestGenerate_locations():
         _, locs_us = spts.as_staypoints.generate_locations(method='dbscan', epsilon=10, 
                                                           num_samples=0, distance_matrix_metric='haversine',
                                                           agg_level='user')
-        loc_ds_num = locs_ds['location_id'].unique().shape[0]
-        loc_us_num = locs_us['location_id'].unique().shape[0]
+        loc_ds_num = locs_ds['id'].unique().shape[0]
+        loc_us_num = locs_us['id'].unique().shape[0]
         assert loc_ds_num == 1, "Considering all staypoints at once, there should be only one location"
         assert loc_us_num == 2, "Considering user staypoints separately, there should be two locations"
     
     def test_generate_locations_dbscan_min(self):
         pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
+        _, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
         _, locs_user = spts.as_staypoints.generate_locations(method='dbscan', epsilon=1e-18, 
                                                             num_samples=0, agg_level='user')
         _, locs_data = spts.as_staypoints.generate_locations(method='dbscan', epsilon=1e-18, 
@@ -102,13 +102,34 @@ class TestGenerate_locations():
 
     def test_generate_locations_dbscan_max(self):
         pfs = ti.read_positionfixes_csv(os.path.join('tests','data','positionfixes.csv'), sep=';')
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
+        _, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=0, time_threshold=0)
         _, locs_user = spts.as_staypoints.generate_locations(method='dbscan', epsilon=1e18, 
                                                             num_samples=1000, agg_level='user')
         _, locs_data = spts.as_staypoints.generate_locations(method='dbscan', epsilon=1e18, 
                                                             num_samples=1000, agg_level='dataset')
         assert len(locs_user) == 0, "With large hyperparameters, every user location is an outlier"
         assert len(locs_data) == 0, "With large hyperparameters, every dataset location is an outlier"
+    
+    def test_generate_locations_dtype_consistent(self):
+        spts = ti.read_staypoints_csv(os.path.join('tests', 'data', 'geolife', 'geolife_staypoints.csv'))
+        # 
+        spts, locs = spts.as_staypoints.generate_locations(method='dbscan', 
+                                                           epsilon=10, 
+                                                           num_samples=0, 
+                                                           distance_matrix_metric='haversine',
+                                                           agg_level='dataset')
+        assert spts['user_id'].dtype == locs['user_id'].dtype
+        assert spts['location_id'].dtype == locs['id'].dtype
+        # change the user_id to string
+        spts['user_id'] = spts['user_id'].apply(lambda x: str(x))
+        spts, locs = spts.as_staypoints.generate_locations(method='dbscan', 
+                                                           epsilon=10, 
+                                                           num_samples=0, 
+                                                           distance_matrix_metric='haversine',
+                                                           agg_level='dataset')
+        assert spts['user_id'].dtype == locs['user_id'].dtype
+        assert spts['location_id'].dtype == locs['id'].dtype
+        
         
 class TestCreate_activity_flag():
     pass
\ No newline at end of file
diff --git a/tests/preprocessing/test_triplegs.py b/tests/preprocessing/test_triplegs.py
index 0b9b8b8..c43d37a 100644
--- a/tests/preprocessing/test_triplegs.py
+++ b/tests/preprocessing/test_triplegs.py
@@ -31,13 +31,14 @@ class TestGenerate_trips():
         trips_loaded = pd.read_csv(os.path.join('tests', 'data', 'geolife_long', 'trips.csv'), index_col='id')
         trips_loaded['started_at'] = pd.to_datetime(trips_loaded['started_at'])
         trips_loaded['finished_at'] = pd.to_datetime(trips_loaded['finished_at'])
+        trips_loaded['user_id'] = trips_loaded['user_id'].astype("int64")
 
         # create trips from geolife (based on positionfixes)
         pfs = ti.io.dataset_reader.read_geolife(os.path.join('tests', 'data', 'geolife_long'))
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25,
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25,
                                                         time_threshold=5 * 60)
         spts = spts.as_staypoints.create_activity_flag()
-        tpls = pfs.as_positionfixes.generate_triplegs(spts)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
 
         # temporary fix ID bug (issue  #56) so that we work with valid staypoint/tripleg files
         spts = spts.set_index('id')
@@ -45,9 +46,32 @@ class TestGenerate_trips():
 
         # generate trips and a joint staypoint/triplegs dataframe
         spts, tpls, trips = ti.preprocessing.triplegs.generate_trips(spts, tpls, gap_threshold=gap_threshold, id_offset=0)
+        trips.set_index('id', inplace=True)
         # test if generated trips are equal
         pd.testing.assert_frame_equal(trips_loaded, trips)
-    
+        
+    def test_generate_trips_dtype_consistent(self):
+        # create trips from geolife (based on positionfixes)
+        pfs = ti.io.dataset_reader.read_geolife(os.path.join('tests', 'data', 'geolife_long'))
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', 
+                                                             dist_threshold=25,
+                                                             time_threshold=5 * 60)
+        spts = spts.as_staypoints.create_activity_flag()
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
+
+        # temporary fix ID bug (issue  #56) so that we work with valid staypoint/tripleg files
+        spts = spts.set_index('id')
+        tpls = tpls.set_index('id')
+
+        # generate trips and a joint staypoint/triplegs dataframe
+        spts, tpls, trips = ti.preprocessing.triplegs.generate_trips(spts, 
+                                                                     tpls, 
+                                                                     gap_threshold=15, 
+                                                                     id_offset=0)
+        
+        assert spts['user_id'].dtype == trips['user_id'].dtype
+        assert trips['id'].dtype == "int64"
+        
     def test_generate_trips_gap_detection(self):
         """
         Test different gap cases:
@@ -94,7 +118,8 @@ class TestGenerate_trips():
                                                                                gap_threshold=15, 
                                                                                id_offset=0)
         spts_tpls = _create_debug_spts_tpls_data(spts_proc, tpls_proc, gap_threshold=gap_threshold)
-
+        
+        trips.set_index('id', inplace=True)
         # test if generated trips are equal
         pd.testing.assert_frame_equal(trips_loaded, trips)
 
@@ -114,9 +139,9 @@ class TestGenerate_trips():
 
         # create trips from geolife (based on positionfixes)
         pfs = ti.io.dataset_reader.read_geolife(os.path.join('tests', 'data', 'geolife_long'))
-        spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25, time_threshold=5 * 60)
+        pfs, spts = pfs.as_positionfixes.generate_staypoints(method='sliding', dist_threshold=25, time_threshold=5 * 60)
         spts = spts.as_staypoints.create_activity_flag()
-        tpls = pfs.as_positionfixes.generate_triplegs(spts)
+        pfs, tpls = pfs.as_positionfixes.generate_triplegs(spts)
 
         # temporary fix ID bug (issue  #56) so that we work with valid staypoint/tripleg files
         spts = spts.set_index('id')