Repository: mie-lab/trackintel (python)

Issue:
read_csv function should have an option to set the crs
At the moment it is not possible to define a crs while reading trackintel data directly from .csv. I think this would be a nice enhancement. 

Example code:
```python 
tpls_file = os.path.join('tests', 'data', 'geolife', 'geolife_triplegs_short.csv')
tpls = ti.read_triplegs_csv(tpls_file, tz='utc', index_col='id')
print(tpls.crs)
```

Pull request:
ENH: Added option to set CRS in read_xx_csv function #123
Closes #123


Patch:
diff --git a/trackintel/io/file.py b/trackintel/io/file.py
index 9d9cac6..b3b3106 100644
--- a/trackintel/io/file.py
+++ b/trackintel/io/file.py
@@ -47,12 +47,16 @@ def read_positionfixes_csv(*args, **kwargs):
     Parameters
     ----------
     columns : dict
-        The columnnames to rename in the format {'old_name':'trackintel_standard_name'}.
+        The column names to rename in the format {'old_name':'trackintel_standard_name'}.
     tz : str
         pytz compatible timezone string. If None UTC is assumed.
     index_col : str
         column name to be used as index. If None the default index is assumed
         as unique identifier.
+    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.
 
     Returns
     -------
@@ -72,11 +76,12 @@ def read_positionfixes_csv(*args, **kwargs):
     """
     columns = kwargs.pop('columns', {})
     tz = kwargs.pop('tz', None)
+    crs = kwargs.pop('crs', None)
     
     # Warning if no 'index_col' parameter is provided
-    if not 'index_col' in kwargs:
-        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" + 
-            "argument to avoid a warning when reading csv files.")
+    if 'index_col' not in kwargs:
+        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" +
+                      "argument to avoid a warning when reading csv files.")
 
     df = pd.read_csv(*args, **kwargs)
     df = df.rename(columns=columns)
@@ -95,6 +100,8 @@ def read_positionfixes_csv(*args, **kwargs):
 
     df = df.drop(['longitude', 'latitude'], axis=1)
     gdf = gpd.GeoDataFrame(df, geometry='geom')
+    if crs:
+        gdf.set_crs(crs, inplace=True)
     assert gdf.as_positionfixes
     return gdf
 
@@ -127,12 +134,16 @@ def read_triplegs_csv(*args, **kwargs):
     Parameters
     ----------
     columns : dict
-        The columnnames to rename in the format {'old_name':'trackintel_standard_name'}.
+        The column names to rename in the format {'old_name':'trackintel_standard_name'}.
     tz : str
         pytz compatible timezone string. If None UTC is assumed.
     index_col : str
         column name to be used as index. If None the default index is assumed 
         as unique identifier.
+    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.
 
     Returns
     -------
@@ -147,11 +158,12 @@ def read_triplegs_csv(*args, **kwargs):
 
     columns = kwargs.pop('columns', {})
     tz = kwargs.pop('tz', None)
-    
+    crs = kwargs.pop('crs', None)
+
     # Warning if no 'index_col' parameter is provided
-    if not 'index_col' in kwargs:
-        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" + 
-            "argument to avoid a warning when reading csv files.")
+    if 'index_col' not in kwargs:
+        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" +
+                      "argument to avoid a warning when reading csv files.")
 
     df = pd.read_csv(*args, **kwargs)
     df = df.rename(columns=columns)
@@ -169,6 +181,8 @@ def read_triplegs_csv(*args, **kwargs):
             df[col] = df[col].dt.tz_convert(tz)
 
     gdf = gpd.GeoDataFrame(df, geometry='geom')
+    if crs:
+        gdf.set_crs(crs, inplace=True)
     assert gdf.as_triplegs
     return gdf
 
@@ -200,12 +214,16 @@ def read_staypoints_csv(*args, **kwargs):
     Parameters
     ----------
     columns : dict
-        The columnnames to rename in the format {'old_name':'trackintel_standard_name'}.
+        The column names to rename in the format {'old_name':'trackintel_standard_name'}.
     tz : str
         pytz compatible timezone string. If None UTC is assumed.
     index_col : str
         column name to be used as index. If None the default index is assumed 
         as unique identifier.
+    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.
 
     Returns
     -------
@@ -220,11 +238,12 @@ def read_staypoints_csv(*args, **kwargs):
 
     columns = kwargs.pop('columns', {})
     tz = kwargs.pop('tz', None)
+    crs = kwargs.pop('crs', None)
     
     # Warning if no 'index_col' parameter is provided
-    if not 'index_col' in kwargs:
-        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" + 
-            "argument to avoid a warning when reading csv files.")
+    if 'index_col' not in kwargs:
+        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" +
+                      "argument to avoid a warning when reading csv files.")
 
     df = pd.read_csv(*args, **kwargs)
     df = df.rename(columns=columns)
@@ -242,6 +261,8 @@ def read_staypoints_csv(*args, **kwargs):
             df[col] = df[col].dt.tz_convert(tz)
         
     gdf = gpd.GeoDataFrame(df, geometry='geom')
+    if crs:
+        gdf.set_crs(crs, inplace=True)
     assert gdf.as_staypoints
     return gdf
 
@@ -273,28 +294,32 @@ def read_locations_csv(*args, **kwargs):
     Parameters
     ----------
     columns : dict
-        The columnnames to rename in the format {'old_name':'trackintel_standard_name'}.
+        The column names to rename in the format {'old_name':'trackintel_standard_name'}.
+    index_col : str
+        column name to be used as index. If None the default index is assumed
+        as unique identifier.
+    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.
 
     Returns
     -------
     GeoDataFrame
         A GeoDataFrame containing the locations.
-    index_col : str
-        column name to be used as index. If None the default index is assumed 
-        as unique identifier.
-        
-            
+
     Examples
     --------
     >>> trackintel.read_locations_csv('data.csv')
     >>> trackintel.read_locations_csv('data.csv', columns={'start_time':'started_at', 'User':'user_id'})
     """
     columns = kwargs.pop('columns', {})
+    crs = kwargs.pop('crs', None)
     
     # Warning if no 'index_col' parameter is provided
-    if not 'index_col' in kwargs:
-        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" + 
-            "argument to avoid a warning when reading csv files.")
+    if 'index_col' not in kwargs:
+        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" +
+                      "argument to avoid a warning when reading csv files.")
     
     df = pd.read_csv(*args, **kwargs)
     df = df.rename(columns=columns)
@@ -302,6 +327,8 @@ def read_locations_csv(*args, **kwargs):
     if 'extent' in df.columns:
         df['extent'] = df['extent'].apply(wkt.loads)
     gdf = gpd.GeoDataFrame(df, geometry='center')
+    if crs:
+        gdf.set_crs(crs, inplace=True)
     assert gdf.as_locations
     return gdf
 
@@ -326,14 +353,14 @@ def write_locations_csv(locations, filename, *args, **kwargs):
 
 
 def read_trips_csv(*args, **kwargs):
-    """Wraps the pandas read_csv function and extraces proper datetimes. This also 
+    """Wraps the pandas read_csv function and extracts proper datetimes. This also
     validates that the ingested data conforms to the trackintel understanding 
     of trips (see :doc:`/modules/model`).
     
     Parameters
     ----------
     columns : dict
-        The columnnames to rename in the format {'old_name':'trackintel_standard_name'}.
+        The column names to rename in the format {'old_name':'trackintel_standard_name'}.
     tz : str
         pytz compatible timezone string. If None UTC is assumed.
     index_col : str
@@ -355,9 +382,9 @@ def read_trips_csv(*args, **kwargs):
     columns = kwargs.pop('columns', {})
     tz = kwargs.pop('tz', None)
     
-    if not 'index_col' in kwargs:
-        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" + 
-            "argument to avoid a warning when reading csv files.")
+    if 'index_col' not in kwargs:
+        warnings.warn("Assuming default index as unique identifier. Pass 'index_col=None' as explicit" +
+                      "argument to avoid a warning when reading csv files.")
     
     df = pd.read_csv(*args, **kwargs)
     df = df.rename(columns=columns)
@@ -393,12 +420,12 @@ def write_trips_csv(trips, filename, *args, **kwargs):
 
 
 def read_tours_csv(*args, **kwargs):
-    """Wraps the pandas read_csv function and extraces proper datetimes. This also 
+    """Wraps the pandas read_csv function and extracts proper datetimes. This also
     validates that the ingested data conforms to the trackintel understanding 
     of tours (see :doc:`/modules/model`).
 
     columns : dict
-        The columnnames to rename in the format {'old_name':'trackintel_standard_name'}.
+        The column names to rename in the format {'old_name':'trackintel_standard_name'}.
     tz : str
         pytz compatible timezone string. If None UTC is assumed.
 
diff --git a/trackintel/model/tours.py b/trackintel/model/tours.py
index 3f5c285..378809b 100644
--- a/trackintel/model/tours.py
+++ b/trackintel/model/tours.py
@@ -34,8 +34,8 @@ class ToursAccessor(object):
     @staticmethod
     def _validate(obj):
         if any([c not in obj.columns for c in ToursAccessor.required_columns]):
-            raise AttributeError("To process a DataFrame as a collection of staypoints, " \
-                                 + "it must have the properties [%s], but it has [%s]." \
+            raise AttributeError("To process a DataFrame as a collection of tours, "
+                                 + "it must have the properties [%s], but it has [%s]."
                                  % (', '.join(ToursAccessor.required_columns), ', '.join(obj.columns)))
 
         # check timestamp dtypes
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index d0b6b79..9e573da 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -48,7 +48,7 @@ class TripsAccessor(object):
     @staticmethod
     def _validate(obj):
         if any([c not in obj.columns for c in TripsAccessor.required_columns]):
-            raise AttributeError("To process a DataFrame as a collection of staypoints, "
+            raise AttributeError("To process a DataFrame as a collection of trips, "
                                  + "it must have the properties [%s], but it has [%s]."
                                  % (', '.join(TripsAccessor.required_columns), ', '.join(obj.columns)))
 


Test patch:
diff --git a/tests/io/test_file.py b/tests/io/test_file.py
index b83f24f..856990e 100644
--- a/tests/io/test_file.py
+++ b/tests/io/test_file.py
@@ -8,12 +8,12 @@ import trackintel as ti
 class TestFile:
     def test_positionfixes_from_to_csv(self):
         orig_file = os.path.join('tests', 'data', 'positionfixes.csv')
-        mod_file = os.path.join('tests','data', 'positionfixes_mod_columns.csv')
+        mod_file = os.path.join('tests', 'data', 'positionfixes_mod_columns.csv')
         tmp_file = os.path.join('tests', 'data', 'positionfixes_test.csv')
 
         pfs = ti.read_positionfixes_csv(orig_file, sep=';', index_col="id")
         
-        column_mapping = {'lat':'latitude', 'lon':'longitude', 'time':'tracked_at'}
+        column_mapping = {'lat': 'latitude', 'lon': 'longitude', 'time': 'tracked_at'}
         mod_pfs = ti.read_positionfixes_csv(mod_file, sep=';', index_col="id", columns=column_mapping)
         assert mod_pfs.equals(pfs)
         pfs['tracked_at'] = pfs['tracked_at'].apply(lambda d: d.isoformat().replace('+00:00', 'Z'))
@@ -22,9 +22,17 @@ class TestFile:
         pfs.as_positionfixes.to_csv(tmp_file, sep=';', columns=columns)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
-        
+
+    def test_read_positionfixes_csv_crs_parameter(self):
+        file = os.path.join('tests', 'data', 'positionfixes.csv')
+        pfs = ti.read_positionfixes_csv(file, sep=';', index_col="id")
+        crs = "EPSG:2056"
+        assert pfs.crs is None
+        pfs = ti.read_positionfixes_csv(file, sep=';', index_col="id", crs=crs)
+        assert pfs.crs == crs
+
     def test_positionfixes_csv_index_warning(self):
-        """Test if a warning is raised when not parsing the index_col arguement."""
+        """Test if a warning is raised when not parsing the index_col argument."""
         file = os.path.join('tests', 'data', 'positionfixes.csv')
         with pytest.warns(UserWarning):
             ti.read_positionfixes_csv(file, sep=';')
@@ -32,12 +40,10 @@ class TestFile:
     def test_positionfixes_from_to_postgis(self):
         # TODO Implement some tests for PostGIS.
         pass
-    
-
 
     def test_triplegs_from_to_csv(self):
         orig_file = os.path.join('tests', 'data', 'triplegs.csv')
-        mod_file = os.path.join('tests','data','triplegs_mod_columns.csv')
+        mod_file = os.path.join('tests', 'data', 'triplegs_mod_columns.csv')
         tmp_file = os.path.join('tests', 'data', 'triplegs_test.csv')
         tpls = ti.read_triplegs_csv(orig_file, sep=';', tz='utc', index_col="id")
         
@@ -47,14 +53,22 @@ class TestFile:
         assert mod_tpls.equals(tpls)
         tpls['started_at'] = tpls['started_at'].apply(lambda d: d.isoformat().replace('+00:00', 'Z'))
         tpls['finished_at'] = tpls['finished_at'].apply(lambda d: d.isoformat().replace('+00:00', 'Z'))
-        
+
         columns = ['user_id', 'started_at', 'finished_at', 'geom']
         tpls.as_triplegs.to_csv(tmp_file, sep=';', columns=columns)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
+
+    def test_triplegs_read_csv_crs_parameter(self):
+        file = os.path.join('tests', 'data', 'triplegs.csv')
+        crs = "EPSG:2056"
+        tpls = ti.read_triplegs_csv(file, sep=';', tz='utc', index_col="id")
+        assert tpls.crs is None
+        tpls = ti.read_triplegs_csv(file, sep=';', tz='utc', index_col="id", crs=crs)
+        assert tpls.crs == crs
         
     def test_triplegs_csv_index_warning(self):
-        """Test if a warning is raised when not parsing the index_col arguement."""
+        """Test if a warning is raised when not parsing the index_col argument."""
         file = os.path.join('tests', 'data', 'triplegs.csv')
         with pytest.warns(UserWarning):
             ti.read_triplegs_csv(file, sep=';')
@@ -62,8 +76,6 @@ class TestFile:
     def test_triplegs_from_to_postgis(self):
         # TODO Implement some tests for PostGIS.
         pass
-    
-    
 
     def test_staypoints_from_to_csv(self):
         orig_file = os.path.join('tests', 'data', 'staypoints.csv')
@@ -79,31 +91,46 @@ class TestFile:
         stps.as_staypoints.to_csv(tmp_file, sep=';', columns=columns)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
+
+    def test_read_staypoints_csv_crs_parameter(self):
+        file = os.path.join('tests', 'data', 'staypoints.csv')
+        crs = "EPSG:2056"
+        stps = ti.read_staypoints_csv(file, sep=';', tz='utc', index_col="id")
+        assert stps.crs is None
+        stps = ti.read_staypoints_csv(file, sep=';', tz='utc', index_col="id", crs=crs)
+        assert stps.crs == crs
     
     def test_staypoints_csv_index_warning(self):
-        """Test if a warning is raised when not parsing the index_col arguement."""
+        """Test if a warning is raised when not parsing the index_col argument."""
         file = os.path.join('tests', 'data', 'staypoints.csv')
         with pytest.warns(UserWarning):
             ti.read_staypoints_csv(file, sep=';')
-            
+
     def test_staypoints_from_to_postgis(self):
         # TODO Implement some tests for PostGIS.
         pass
-    
 
     def test_locations_from_to_csv(self):
         orig_file = os.path.join('tests', 'data', 'locations.csv')
-        mod_file = os.path.join('tests','data','locations_mod_columns.csv')
+        mod_file = os.path.join('tests', 'data', 'locations_mod_columns.csv')
         tmp_file = os.path.join('tests', 'data', 'locations_test.csv')
-        mod_locs = ti.read_locations_csv(mod_file, columns={'geom':'center'},sep=';', index_col="id")
+        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)
         locs.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_read_locations_csv_crs_parameter(self):
+        file = os.path.join('tests', 'data', 'locations.csv')
+        crs = "EPSG:2056"
+        locs = ti.read_locations_csv(file, sep=';', index_col="id")
+        assert locs.crs is None
+        locs = ti.read_locations_csv(file, sep=';', index_col="id", crs=crs)
+        assert locs.crs == crs
+
     def test_locations_csv_index_warning(self):
-        """Test if a warning is raised when not parsing the index_col arguement."""
+        """Test if a warning is raised when not parsing the index_col argument."""
         file = os.path.join('tests', 'data', 'locations.csv')
         with pytest.warns(UserWarning):
             ti.read_locations_csv(file, sep=';')
@@ -112,15 +139,13 @@ class TestFile:
         # TODO Implement some tests for PostGIS.
         pass
 
-    
-
     def test_trips_from_to_csv(self):
         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')
         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")
+        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")
         assert mod_trips.equals(trips)
         
         trips['started_at'] = trips['started_at'].apply(lambda d: d.isoformat().replace('+00:00', 'Z'))
@@ -131,7 +156,7 @@ class TestFile:
         os.remove(tmp_file)
     
     def test_trips_csv_index_warning(self):
-        """Test if a warning is raised when not parsing the index_col arguement."""
+        """Test if a warning is raised when not parsing the index_col argument."""
         file = os.path.join('tests', 'data', 'trips.csv')
         with pytest.warns(UserWarning):
             ti.read_trips_csv(file, sep=';')
@@ -139,9 +164,7 @@ class TestFile:
     def test_trips_from_to_postgis(self):
         # TODO Implement some tests for PostGIS.
         pass
-    
-    
+
     def test_tours_from_to_csv(self):
         # TODO Implement some tests for reading and writing tours.
         pass
-