From cacccb60e588041ebed6a2897fbf54562bd948df Mon Sep 17 00:00:00 2001
From: Le Roux Erwan <erwan.le-roux@irstea.fr>
Date: Mon, 12 Nov 2018 15:31:06 +0100
Subject: [PATCH] [SPATIAL COORDINATES] add anisotropy and add tests

---
 .../spatial_coordinates/__init__.py           |  0
 ...tes.py => abstract_spatial_coordinates.py} | 59 ++++++++++++++-----
 .../alps_station_2D_coordinates.py            | 30 ++++++++++
 .../alps_station_3D_coordinates.py            | 47 +++++++++++++++
 .../alps_station_coordinates.py               | 59 -------------------
 .../generated_coordinates.py                  | 10 +---
 .../normalized_coordinates.py                 | 59 -------------------
 .../transformations/__init__.py               |  0
 .../abstract_transformation.py                | 12 ++++
 .../transformations/tranformation_3D.py       | 50 ++++++++++++++++
 .../transformations/transformation_2D.py      | 41 +++++++++++++
 .../transformed_coordinates.py                | 14 +++++
 test/extreme_estimator/test_gev_fit.py        |  6 +-
 test/{ => extreme_estimator}/test_pipeline.py | 12 +---
 .../test_spatial_coordinates.py               | 34 +++++++++++
 15 files changed, 278 insertions(+), 155 deletions(-)
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/__init__.py
 rename spatio_temporal_dataset/spatial_coordinates/{abstract_coordinates.py => abstract_spatial_coordinates.py} (57%)
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/alps_station_2D_coordinates.py
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/alps_station_3D_coordinates.py
 delete mode 100644 spatio_temporal_dataset/spatial_coordinates/alps_station_coordinates.py
 delete mode 100644 spatio_temporal_dataset/spatial_coordinates/normalized_coordinates.py
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/transformations/__init__.py
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/transformations/abstract_transformation.py
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/transformations/tranformation_3D.py
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/transformations/transformation_2D.py
 create mode 100644 spatio_temporal_dataset/spatial_coordinates/transformed_coordinates.py
 rename test/{ => extreme_estimator}/test_pipeline.py (73%)
 create mode 100644 test/spatio_temporal_dataset/test_spatial_coordinates.py

diff --git a/spatio_temporal_dataset/spatial_coordinates/__init__.py b/spatio_temporal_dataset/spatial_coordinates/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/spatio_temporal_dataset/spatial_coordinates/abstract_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/abstract_spatial_coordinates.py
similarity index 57%
rename from spatio_temporal_dataset/spatial_coordinates/abstract_coordinates.py
rename to spatio_temporal_dataset/spatial_coordinates/abstract_spatial_coordinates.py
index 3591e97e..187e0635 100644
--- a/spatio_temporal_dataset/spatial_coordinates/abstract_coordinates.py
+++ b/spatio_temporal_dataset/spatial_coordinates/abstract_spatial_coordinates.py
@@ -1,44 +1,61 @@
 import os.path as op
+from typing import List
+
 import numpy as np
 import pandas as pd
 import matplotlib.pyplot as plt
+from mpl_toolkits.mplot3d import Axes3D
 
 
 class AbstractSpatialCoordinates(object):
-
     # Columns
     COORD_X = 'coord_x'
     COORD_Y = 'coord_y'
+    COORD_Z = 'coord_z'
     COORD_SPLIT = 'coord_split'
-    COOR_ID = 'coord_id'
     # Constants
     TRAIN_SPLIT_STR = 'train_split'
     TEST_SPLIT_STR = 'test_split'
 
     def __init__(self, df_coord: pd.DataFrame, s_split: pd.Series = None):
-        self.s_split = s_split
         self.df_coord = df_coord
+        self.s_split = s_split
 
     @classmethod
-    def from_df(cls, df):
+    def from_df(cls, df: pd.DataFrame):
+        #  X and Y coordinates must be defined
         assert cls.COORD_X in df.columns and cls.COORD_Y in df.columns
-        df_coord = df.loc[:, [cls.COORD_X, cls.COORD_Y]]
+        df_coord = df.loc[:, cls.coord_columns(df)]
+        # Potentially, a split column can be specified
         s_split = df[cls.COORD_SPLIT] if cls.COORD_SPLIT in df.columns else None
         return cls(df_coord=df_coord, s_split=s_split)
 
+    @classmethod
+    def coord_columns(cls, df_coord: pd.DataFrame) -> List[str]:
+        # If a Z coordinate is in the DataFrame, then
+        coord_columns = [cls.COORD_X, cls.COORD_Y]
+        if cls.COORD_Z in df_coord.columns:
+            coord_columns.append(cls.COORD_Z)
+        return coord_columns
+
     @property
-    def df(self):
+    def columns(self):
+        return self.coord_columns(df_coord=self.df_coord)
+
+    @property
+    def df(self) -> pd.DataFrame:
+        # Merged DataFrame of df_coord and s_split
         return self.df_coord if self.s_split is None else self.df_coord.join(self.s_split)
 
     @classmethod
-    def from_csv(cls, csv_path=None):
+    def from_csv(cls, csv_path: str = None):
         assert csv_path is not None
         assert op.exists(csv_path)
         df = pd.read_csv(csv_path)
         return cls.from_df(df)
 
     @classmethod
-    def from_nb_points(cls, nb_points, **kwargs):
+    def from_nb_points(cls, nb_points: int, **kwargs):
         # Call the default class method from csv
         coordinates = cls.from_csv()  # type: AbstractSpatialCoordinates
         # Sample randomly nb_points coordinates
@@ -49,35 +66,45 @@ class AbstractSpatialCoordinates(object):
             df_sample = pd.DataFrame.sample(coordinates.df, n=nb_points)
             return cls.from_df(df=df_sample)
 
-    def coord_x_y_values(self, df_coord: pd.DataFrame) -> np.ndarray:
-        return df_coord.loc[:, [self.COORD_X, self.COORD_Y]].values
-
     def df_coord_split(self, split_str: str) -> pd.DataFrame:
         assert self.s_split is not None
         ind = self.s_split == split_str
         return self.df_coord.loc[ind]
 
+    def coord_values(self, df_coord: pd.DataFrame) -> np.ndarray:
+        return df_coord.loc[:, self.coord_columns(df_coord)].values
+
     @property
     def coord(self) -> np.ndarray:
-        return self.coord_x_y_values(df_coord=self.df_coord)
+        return self.coord_values(df_coord=self.df_coord)
 
     @property
     def coord_train(self) -> np.ndarray:
-        return self.coord_x_y_values(df_coord=self.df_coord_split(self.TRAIN_SPLIT_STR))
+        return self.coord_values(df_coord=self.df_coord_split(self.TRAIN_SPLIT_STR))
 
     @property
     def coord_test(self) -> np.ndarray:
-        return self.coord_x_y_values(df_coord=self.df_coord_split(self.TEST_SPLIT_STR))
+        return self.coord_values(df_coord=self.df_coord_split(self.TEST_SPLIT_STR))
 
     @property
     def index(self):
         return self.df_coord.index
 
-    def visualization(self):
+    #  Visualization
+
+    def visualization_2D(self):
         x, y = self.coord[:, 0], self.coord[:, 1]
         plt.scatter(x, y)
         plt.show()
 
+    def visualization_3D(self):
+        assert len(self.coord_columns(self.df_coord)) == 3
+        fig = plt.figure()
+        ax = fig.add_subplot(111, projection='3d')  # type: Axes3D
+        x, y, z = self.coord[:, 0], self.coord[:, 1], self.coord[:, 2]
+        ax.scatter(x, y, z, marker='^')
+        plt.show()
+
     #  Magic Methods
 
     def __len__(self):
@@ -88,4 +115,4 @@ class AbstractSpatialCoordinates(object):
         return self
 
     def __rmul__(self, other):
-        return self * other
\ No newline at end of file
+        return self * other
diff --git a/spatio_temporal_dataset/spatial_coordinates/alps_station_2D_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/alps_station_2D_coordinates.py
new file mode 100644
index 00000000..aa88ff30
--- /dev/null
+++ b/spatio_temporal_dataset/spatial_coordinates/alps_station_2D_coordinates.py
@@ -0,0 +1,30 @@
+from spatio_temporal_dataset.spatial_coordinates.alps_station_3D_coordinates import AlpsStation3DCoordinates
+from spatio_temporal_dataset.spatial_coordinates.transformations.transformation_2D import \
+    BetweenZeroAndOne2DNormalization
+from spatio_temporal_dataset.spatial_coordinates.transformed_coordinates import TransformedCoordinates
+
+
+class AlpsStation2DCoordinates(AlpsStation3DCoordinates):
+
+    @classmethod
+    def from_csv(cls, csv_file='coord-lambert2'):
+        # Remove the Z coordinates from df_coord
+        spatial_coordinates = super().from_csv(csv_file)
+        spatial_coordinates.df_coord.drop(cls.COORD_Z, axis=1, inplace=True)
+        return spatial_coordinates
+
+
+class AlpsStation2DCoordinatesBetweenZeroAndOne(AlpsStation2DCoordinates):
+
+    @classmethod
+    def from_csv(cls, csv_file='coord-lambert2'):
+        coord = super().from_csv(csv_file)
+        return TransformedCoordinates.from_coordinates(spatial_coordinates=coord,
+                                                       transformation_function=BetweenZeroAndOne2DNormalization())
+
+
+class AlpsStationCoordinatesBetweenZeroAndTwo(AlpsStation2DCoordinatesBetweenZeroAndOne):
+
+    @classmethod
+    def from_csv(cls, csv_file='coord-lambert2'):
+        return 2 * super().from_csv(csv_file)
diff --git a/spatio_temporal_dataset/spatial_coordinates/alps_station_3D_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/alps_station_3D_coordinates.py
new file mode 100644
index 00000000..f45c94b6
--- /dev/null
+++ b/spatio_temporal_dataset/spatial_coordinates/alps_station_3D_coordinates.py
@@ -0,0 +1,47 @@
+import os.path as op
+
+import pandas as pd
+
+from spatio_temporal_dataset.spatial_coordinates.abstract_spatial_coordinates import AbstractSpatialCoordinates
+from spatio_temporal_dataset.spatial_coordinates.transformations.tranformation_3D import AnisotropyTransformation
+from spatio_temporal_dataset.spatial_coordinates.transformed_coordinates import TransformedCoordinates
+from utils import get_full_path
+
+
+class AlpsStation3DCoordinates(AbstractSpatialCoordinates):
+    """
+    For the Alps Stations, X, Y coordinates are in Lambert 2. Altitude is in meters
+    """
+    RELATIVE_PATH = r'local/spatio_temporal_datasets/Gilles  - precipitations'
+    FULL_PATH = get_full_path(relative_path=RELATIVE_PATH)
+
+    @classmethod
+    def from_csv(cls, csv_file='coord-lambert2'):
+        csv_path = op.join(cls.FULL_PATH, csv_file + '.csv')
+        return super().from_csv(csv_path)
+
+    @classmethod
+    def transform_txt_into_csv(cls):
+        filepath = op.join(cls.FULL_PATH, 'original data', 'coord-lambert2.txt')
+        station_to_coordinates = {}
+        with open(filepath, 'r') as f:
+            for l in f:
+                _, station_name, coordinates = l.split('"')
+                coordinates = coordinates.split()
+                assert len(coordinates) == 3
+                station_to_coordinates[station_name] = coordinates
+        df = pd.DataFrame.from_dict(data=station_to_coordinates, orient='index',
+                                    columns=[cls.COORD_X, cls.COORD_Y, cls.COORD_Z])
+        print(df.head())
+        filepath = op.join(cls.FULL_PATH, 'coord-lambert2.csv')
+        assert not op.exists(filepath)
+        df.to_csv(filepath)
+
+
+class AlpsStation3DCoordinatesWithAnisotropy(AlpsStation3DCoordinates):
+
+    @classmethod
+    def from_csv(cls, csv_file='coord-lambert2'):
+        coord = super().from_csv(csv_file)
+        return TransformedCoordinates.from_coordinates(spatial_coordinates=coord,
+                                                       transformation_function=AnisotropyTransformation())
diff --git a/spatio_temporal_dataset/spatial_coordinates/alps_station_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/alps_station_coordinates.py
deleted file mode 100644
index fb07791f..00000000
--- a/spatio_temporal_dataset/spatial_coordinates/alps_station_coordinates.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import pandas as pd
-import os.path as op
-
-from spatio_temporal_dataset.spatial_coordinates.abstract_coordinates import AbstractSpatialCoordinates
-from spatio_temporal_dataset.spatial_coordinates.normalized_coordinates import BetweenZeroAndOneNormalization, \
-    NormalizedCoordinates
-from utils import get_full_path
-
-
-class AlpsStationCoordinates(AbstractSpatialCoordinates):
-    RELATIVE_PATH = r'local/spatio_temporal_datasets/Gilles  - precipitations'
-    FULL_PATH = get_full_path(relative_path=RELATIVE_PATH)
-
-    @classmethod
-    def from_csv(cls, csv_file='coord-lambert2'):
-        csv_path = op.join(cls.FULL_PATH, csv_file + '.csv')
-        return super().from_csv(csv_path)
-
-    @classmethod
-    def transform_txt_into_csv(cls):
-        filepath = op.join(cls.FULL_PATH, 'original data', 'coord-lambert2.txt')
-        station_to_coordinates = {}
-        with open(filepath, 'r') as f:
-            for l in f:
-                _, station_name, coordinates = l.split('"')
-                coordinates = coordinates.split()
-                assert len(coordinates) == 3
-                station_to_coordinates[station_name] = coordinates
-        df = pd.DataFrame.from_dict(data=station_to_coordinates, orient='index',
-                                    columns=[cls.COORD_X, cls.COORD_Y, cls.COOR_ID])
-        df.to_csv(op.join(cls.FULL_PATH, 'coord-lambert2.csv'))
-        print(df.head())
-        print(df.index)
-
-
-class AlpsStationCoordinatesBetweenZeroAndOne(AlpsStationCoordinates):
-
-    @classmethod
-    def from_csv(cls, csv_file='coord-lambert2'):
-        coord = super().from_csv(csv_file)
-        return NormalizedCoordinates.from_coordinates(spatial_coordinates=coord,
-                                                      normalizing_function=BetweenZeroAndOneNormalization())
-
-
-class AlpsStationCoordinatesBetweenZeroAndTwo(AlpsStationCoordinatesBetweenZeroAndOne):
-
-    @classmethod
-    def from_csv(cls, csv_file='coord-lambert2'):
-        return 2 * super().from_csv(csv_file)
-
-
-if __name__ == '__main__':
-    # AlpsStationCoordinate.transform_txt_into_csv()
-    # coord = AlpsStationCoordinates.from_csv()
-    # coord = AlpsStationCoordinates.from_nb_points(nb_points=60)
-    # coord = AlpsStationCoordinatesBetweenZeroAndOne.from_csv()
-    coord = AlpsStationCoordinatesBetweenZeroAndTwo.from_nb_points(nb_points=60)
-    # coord = coord * 2
-    coord.visualization()
diff --git a/spatio_temporal_dataset/spatial_coordinates/generated_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/generated_coordinates.py
index 7acc626d..6cf57904 100644
--- a/spatio_temporal_dataset/spatial_coordinates/generated_coordinates.py
+++ b/spatio_temporal_dataset/spatial_coordinates/generated_coordinates.py
@@ -3,7 +3,7 @@ import numpy as np
 import pandas as pd
 
 from extreme_estimator.R_fit.utils import get_loaded_r
-from spatio_temporal_dataset.spatial_coordinates.abstract_coordinates import AbstractSpatialCoordinates
+from spatio_temporal_dataset.spatial_coordinates.abstract_spatial_coordinates import AbstractSpatialCoordinates
 import matplotlib.pyplot as plt
 
 
@@ -18,13 +18,13 @@ class CircleCoordinatesRadius1(AbstractSpatialCoordinates):
         df = pd.DataFrame.from_dict({cls.COORD_X: radius * np.cos(angles), cls.COORD_Y: radius * np.sin(angles)})
         return cls.from_df(df)
 
-    def visualization(self):
+    def visualization_2D(self):
         r = 1.0
         circle1 = plt.Circle((0, 0), r, color='r', fill=False)
         plt.gcf().gca().set_xlim((-r, r))
         plt.gcf().gca().set_ylim((-r, r))
         plt.gcf().gca().add_artist(circle1)
-        super().visualization()
+        super().visualization_2D()
 
 
 class CircleCoordinatesRadius2(CircleCoordinatesRadius1):
@@ -33,7 +33,3 @@ class CircleCoordinatesRadius2(CircleCoordinatesRadius1):
     def from_nb_points(cls, nb_points, max_radius=1.0):
         return 2 * super().from_nb_points(nb_points, max_radius)
 
-
-if __name__ == '__main__':
-    coord = CircleCoordinatesRadius1.from_nb_points(nb_points=500, max_radius=1)
-    coord.visualization()
diff --git a/spatio_temporal_dataset/spatial_coordinates/normalized_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/normalized_coordinates.py
deleted file mode 100644
index 36608d6f..00000000
--- a/spatio_temporal_dataset/spatial_coordinates/normalized_coordinates.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import pandas as pd
-from spatio_temporal_dataset.spatial_coordinates.abstract_coordinates import AbstractSpatialCoordinates
-
-
-class AbstractNormalizingFunction(object):
-
-    def normalize(self, df_coord: pd.DataFrame) -> pd.DataFrame:
-        assert len(df_coord.columns) == 2
-        return df_coord
-
-
-class NormalizedCoordinates(AbstractSpatialCoordinates):
-
-    @classmethod
-    def from_coordinates(cls, spatial_coordinates: AbstractSpatialCoordinates,
-                         normalizing_function: AbstractNormalizingFunction):
-        df_coord_normalized = spatial_coordinates.df_coord.copy()
-        coord_XY = [spatial_coordinates.COORD_X, spatial_coordinates.COORD_Y]
-        df_coord_normalized.loc[:, coord_XY] = normalizing_function.normalize(df_coord_normalized.loc[:, coord_XY])
-        return cls(df_coord=df_coord_normalized, s_split=spatial_coordinates.s_split)
-
-
-"""
-Define various types of normalizing functions
-"""
-
-
-class UniformNormalization(AbstractNormalizingFunction):
-    """Normalize similarly the X and Y axis with a single function so as to conserve proportional distances"""
-
-    def normalize(self, df_coord: pd.DataFrame) -> pd.DataFrame:
-        df_coord = super().normalize(df_coord)
-        for i in range(2):
-            df_coord.iloc[:, i] = self.uniform_normalization(df_coord.iloc[:, i])
-        return df_coord
-
-    def uniform_normalization(self, s_coord: pd.Series) -> pd.Series:
-        return s_coord
-
-
-class BetweenZeroAndOneNormalization(UniformNormalization):
-    """Normalize such that min(coord) >= (0,0) and max(coord) <= (1,1)"""
-
-    def __init__(self) -> None:
-        self.min_coord = None
-        self.max_coord = None
-
-    def normalize(self, df_coord: pd.DataFrame) -> pd.DataFrame:
-        # Compute the min and max globally
-        self.min_coord, self.max_coord = df_coord.min().min(), df_coord.max().max()
-        #  Then, call the super method that will call the uniform_normalization method
-        return super().normalize(df_coord)
-
-    def uniform_normalization(self, s_coord: pd.Series) -> pd.Series:
-        s_coord_shifted = s_coord - self.min_coord
-        s_coord_scaled = s_coord_shifted / (self.max_coord - self.min_coord)
-        return s_coord_scaled
-
-
diff --git a/spatio_temporal_dataset/spatial_coordinates/transformations/__init__.py b/spatio_temporal_dataset/spatial_coordinates/transformations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/spatio_temporal_dataset/spatial_coordinates/transformations/abstract_transformation.py b/spatio_temporal_dataset/spatial_coordinates/transformations/abstract_transformation.py
new file mode 100644
index 00000000..8c822f4f
--- /dev/null
+++ b/spatio_temporal_dataset/spatial_coordinates/transformations/abstract_transformation.py
@@ -0,0 +1,12 @@
+import pandas as pd
+
+
+class AbstractTransformation(object):
+
+    def __init__(self, nb_dimensions):
+        self.nb_dimensions = nb_dimensions
+
+    def transform(self, df_coord: pd.DataFrame) -> pd.DataFrame:
+        assert len(df_coord.columns) == self.nb_dimensions, "columns={}, nb_dimensions={}".format(df_coord.columns,
+                                                                                                  self.nb_dimensions)
+        return df_coord
diff --git a/spatio_temporal_dataset/spatial_coordinates/transformations/tranformation_3D.py b/spatio_temporal_dataset/spatial_coordinates/transformations/tranformation_3D.py
new file mode 100644
index 00000000..42659ee2
--- /dev/null
+++ b/spatio_temporal_dataset/spatial_coordinates/transformations/tranformation_3D.py
@@ -0,0 +1,50 @@
+import pandas as pd
+import numpy as np
+from spatio_temporal_dataset.spatial_coordinates.transformations.abstract_transformation import AbstractTransformation
+import math
+
+
+class Transformation3D(AbstractTransformation):
+
+    def __init__(self):
+        super().__init__(nb_dimensions=3)
+
+    def transform(self, df_coord: pd.DataFrame) -> pd.DataFrame:
+        df_coord = super().transform(df_coord=df_coord)
+        normalized_values = self.transform_values(df_coord.values)
+        return pd.DataFrame(data=normalized_values, index=df_coord.index, columns=df_coord.columns)
+
+    def transform_values(self, coord_arr: np.ndarray) -> np.ndarray:
+        return coord_arr
+
+
+class AnisotropyTransformation(Transformation3D):
+
+    def __init__(self, phi: float = 0.0, w1: float = 1.0, w2: float = 1.0):
+        """
+        Anisotropy transformation
+        :param phi: Between 0 and Pi, it corresponds to the angle of strongest dependence
+        :param w1: > 0, it corresponds to the anisotropy ratio
+        :param w2:  it corresponds to the weight for the altitude
+        """
+        super().__init__()
+        self.phi = phi
+        self.w1 = w1
+        self.w2 = w2
+        assert 0 <= self.phi < math.pi
+        assert self.w1 > 0
+
+    def transform_values(self, coord_arr: np.ndarray) -> np.ndarray:
+        cosinus, sinus = math.cos(self.phi), math.sin(self.phi)
+        inverse_w1 = 1 / self.w1
+        V = np.array([
+            [cosinus, -sinus, 0],
+            [inverse_w1 * sinus, inverse_w1 * cosinus, 0],
+            [0, 0, self.w2],
+        ])
+        coord_arr = np.transpose(coord_arr)
+        coord_arr = np.dot(V, coord_arr)
+        return np.transpose(coord_arr)
+
+
+
diff --git a/spatio_temporal_dataset/spatial_coordinates/transformations/transformation_2D.py b/spatio_temporal_dataset/spatial_coordinates/transformations/transformation_2D.py
new file mode 100644
index 00000000..2b55ae53
--- /dev/null
+++ b/spatio_temporal_dataset/spatial_coordinates/transformations/transformation_2D.py
@@ -0,0 +1,41 @@
+from spatio_temporal_dataset.spatial_coordinates.transformations.abstract_transformation import AbstractTransformation
+import pandas as pd
+
+
+class Transformation2D(AbstractTransformation):
+
+    def __init__(self):
+        super().__init__(nb_dimensions=2)
+
+
+class Uniform2DNormalization(Transformation2D):
+    """Normalize similarly the X and Y axis with a single function so as to conserve proportional distances"""
+
+    def transform(self, df_coord: pd.DataFrame) -> pd.DataFrame:
+        df_coord = super().transform(df_coord)
+        for i in range(2):
+            df_coord.iloc[:, i] = self.uniform_normalization(df_coord.iloc[:, i])
+        return df_coord
+
+    def uniform_normalization(self, s_coord: pd.Series) -> pd.Series:
+        return s_coord
+
+
+class BetweenZeroAndOne2DNormalization(Uniform2DNormalization):
+    """Normalize such that min(coord) >= (0,0) and max(coord) <= (1,1)"""
+
+    def __init__(self) -> None:
+        super().__init__()
+        self.min_coord = None
+        self.max_coord = None
+
+    def transform(self, df_coord: pd.DataFrame) -> pd.DataFrame:
+        # Compute the min and max globally
+        self.min_coord, self.max_coord = df_coord.min().min(), df_coord.max().max()
+        #  Then, call the super method that will call the uniform_normalization method
+        return super().transform(df_coord)
+
+    def uniform_normalization(self, s_coord: pd.Series) -> pd.Series:
+        s_coord_shifted = s_coord - self.min_coord
+        s_coord_scaled = s_coord_shifted / (self.max_coord - self.min_coord)
+        return s_coord_scaled
diff --git a/spatio_temporal_dataset/spatial_coordinates/transformed_coordinates.py b/spatio_temporal_dataset/spatial_coordinates/transformed_coordinates.py
new file mode 100644
index 00000000..6b19e87a
--- /dev/null
+++ b/spatio_temporal_dataset/spatial_coordinates/transformed_coordinates.py
@@ -0,0 +1,14 @@
+from spatio_temporal_dataset.spatial_coordinates.abstract_spatial_coordinates import AbstractSpatialCoordinates
+from spatio_temporal_dataset.spatial_coordinates.transformations.abstract_transformation import AbstractTransformation
+
+
+class TransformedCoordinates(AbstractSpatialCoordinates):
+
+    @classmethod
+    def from_coordinates(cls, spatial_coordinates: AbstractSpatialCoordinates,
+                         transformation_function: AbstractTransformation):
+        df_coord_transformed = spatial_coordinates.df_coord.copy()
+        df_coord_transformed = transformation_function.transform(df_coord=df_coord_transformed)
+        return cls(df_coord=df_coord_transformed, s_split=spatial_coordinates.s_split)
+
+
diff --git a/test/extreme_estimator/test_gev_fit.py b/test/extreme_estimator/test_gev_fit.py
index 17b96365..4f9fd23e 100644
--- a/test/extreme_estimator/test_gev_fit.py
+++ b/test/extreme_estimator/test_gev_fit.py
@@ -1,16 +1,14 @@
 import unittest
-import rpy2.robjects as ro
 import numpy as np
 
 from extreme_estimator.R_fit.gev_fit.gev_mle_fit import GevMleFit
+from extreme_estimator.R_fit.utils import get_loaded_r
 
 
 class TestGevFit(unittest.TestCase):
 
     def test_unitary_mle_gev_fit(self):
-        r = ro.r
-        # Generate some sample from a gev
-        r.library('SpatialExtremes')
+        r = get_loaded_r()
         r("""
         set.seed(42)
         N <- 50
diff --git a/test/test_pipeline.py b/test/extreme_estimator/test_pipeline.py
similarity index 73%
rename from test/test_pipeline.py
rename to test/extreme_estimator/test_pipeline.py
index 252f829b..d3357fef 100644
--- a/test/test_pipeline.py
+++ b/test/extreme_estimator/test_pipeline.py
@@ -1,10 +1,7 @@
 import unittest
 import pandas as pd
 
-from spatio_temporal_dataset.spatio_temporal_data_handler import SpatioTemporalDataHandler
-
-
-class TestPipeline(unittest):
+class TestPipeline(unittest.TestCase):
 
     def main_pipeline(self):
         # Select a type of marginals (either spatial, spatio temporal, temporal)
@@ -51,10 +48,5 @@ class TestPipeline(unittest):
 
         # Fit the max stable process
 
-    def test_dataframe_fit_unitary(self):
-        df = pd.DataFrame(1, index=['station1', 'station2'], columns=['200' + str(i) for i in range(18)])
-        xp = SpatioTemporalDataHandler.from_dataframe(df)
-
 if __name__ == '__main__':
-    df = pd.DataFrame(1, index=['station1', 'station2'], columns=['200' + str(i) for i in range(18)])
-    xp = SpatioTemporalDataHandler.from_dataframe(df)
\ No newline at end of file
+    unittest.main()
\ No newline at end of file
diff --git a/test/spatio_temporal_dataset/test_spatial_coordinates.py b/test/spatio_temporal_dataset/test_spatial_coordinates.py
new file mode 100644
index 00000000..cf657c21
--- /dev/null
+++ b/test/spatio_temporal_dataset/test_spatial_coordinates.py
@@ -0,0 +1,34 @@
+import unittest
+
+from spatio_temporal_dataset.spatial_coordinates.alps_station_2D_coordinates import \
+    AlpsStation2DCoordinatesBetweenZeroAndOne
+from spatio_temporal_dataset.spatial_coordinates.alps_station_3D_coordinates import \
+    AlpsStation3DCoordinatesWithAnisotropy
+from spatio_temporal_dataset.spatial_coordinates.generated_coordinates import CircleCoordinatesRadius1
+
+
+class TestSpatialCoordinates(unittest.TestCase):
+
+    DISPLAY = False
+
+    def test_circle(self):
+        coord_2D = CircleCoordinatesRadius1.from_nb_points(nb_points=500)
+        if self.DISPLAY:
+            coord_2D.visualization_2D()
+        self.assertTrue(True)
+
+    def test_anisotropy(self):
+        coord_3D = AlpsStation3DCoordinatesWithAnisotropy.from_csv()
+        if self.DISPLAY:
+            coord_3D.visualization_3D()
+        self.assertTrue(True)
+
+    def test_normalization(self):
+        coord_2D = AlpsStation2DCoordinatesBetweenZeroAndOne.from_csv()
+        if self.DISPLAY:
+            coord_2D.visualization_2D()
+        self.assertTrue(True)
+
+
+if __name__ == '__main__':
+    unittest.main()
-- 
GitLab