Repository: GAA-UAM/scikit-fda

Issue:
Problem with pandas methods
After update the neighbors brach (#112), I have an error on this line:

https://github.com/GAA-UAM/scikit-fda/blob/91d96eceee49400962515b8b10ee21d842fdc97f/examples/plot_k_neighbors_classification.py#L61

> Traceback (most recent call last):
  File "plot_k_neighbors_classification.py", line 63, in <module>
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)
  File "/Users/pablomm/anaconda3/lib/python3.6/site-packages/sklearn/model_selection/_split.py", line 2124, in train_test_split
    safe_indexing(a, test)) for a in arrays))
  File "/Users/pablomm/anaconda3/lib/python3.6/site-packages/sklearn/model_selection/_split.py", line 2124, in <genexpr>
    safe_indexing(a, test)) for a in arrays))
  File "/Users/pablomm/anaconda3/lib/python3.6/site-packages/sklearn/utils/__init__.py", line 219, in safe_indexing
    return X.take(indices, axis=0)
TypeError: take() got an unexpected keyword argument 'axis'

It seems that the sklearn API detects that the FData has the ``take`` method and uses it to slice the samples, but using the axis argument.

In fact, the take method also does not work properly without the axis parameter.

```python
>>> import skfda
>>> a = skfda.datasets.make_sinusoidal_process()
>>> a.take([1,2,3])
```
> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/pablomm/anaconda3/lib/python3.6/site-packages/scikit_fda-0.2.3-py3.6-macosx-10.7-x86_64.egg/skfda/representation/_functional_data.py", line 1288, in take
    return self._from_sequence(result, dtype=self.dtype)
  File "/Users/pablomm/anaconda3/lib/python3.6/site-packages/scikit_fda-0.2.3-py3.6-macosx-10.7-x86_64.egg/skfda/representation/_functional_data.py", line 1224, in _from_sequence
    return cls(scalars, dtype=dtype)
TypeError: __init__() got an unexpected keyword argument 'dtype'

Discussion:

Patch:
diff --git a/skfda/representation/_functional_data.py b/skfda/representation/_functional_data.py
index a1458587..e423b205 100644
--- a/skfda/representation/_functional_data.py
+++ b/skfda/representation/_functional_data.py
@@ -6,16 +6,15 @@ objects of the package and contains some commons methods.
 
 from abc import ABC, abstractmethod, abstractproperty
 
-import numpy as np
-
-import matplotlib.patches as mpatches
-
-import matplotlib.pyplot as plt
 from matplotlib.axes import Axes
 import mpl_toolkits.mplot3d
 import pandas.api.extensions
 
-from skfda.representation.extrapolation import _parse_extrapolation
+import matplotlib.patches as mpatches
+import matplotlib.pyplot as plt
+import numpy as np
+from .extrapolation import _parse_extrapolation
+
 from .._utils import _coordinate_list, _list_of_arrays, constants
 
 
@@ -997,7 +996,7 @@ class FData(ABC, pandas.api.extensions.ExtensionArray):
 
             # Selects the number of points
             if npoints is None:
-                npoints = 2*(constants.N_POINTS_SURFACE_PLOT_AX,)
+                npoints = 2 * (constants.N_POINTS_SURFACE_PLOT_AX,)
             elif np.isscalar(npoints):
                 npoints = (npoints, npoints)
             elif len(npoints) != 2:
@@ -1218,22 +1217,29 @@ class FData(ABC, pandas.api.extensions.ExtensionArray):
 
     @classmethod
     def _from_sequence(cls, scalars, dtype=None, copy=False):
-        return cls(scalars, dtype=dtype)
+        if copy:
+            scalars = [f.copy() for f in scalars]
+
+        if dtype is not None and dtype != cls.dtype.fget(None):
+            raise ValueError(f"Invalid dtype {dtype}")
+
+        return cls._concat_same_type(scalars)
 
     @classmethod
     def _from_factorized(cls, values, original):
-        return cls(values)
+        raise NotImplementedError("Factorization does not make sense for "
+                                  "functional data")
 
     def isna(self):
         """
         A 1-D array indicating if each value is missing.
 
         Returns:
-            na_values (np.ndarray): Array full of True values.
+            na_values (np.ndarray): Array full of False values.
         """
-        return np.ones(self.nsamples, dtype=bool)
+        return np.zeros(self.nsamples, dtype=bool)
 
-    def take(self, indices, allow_fill=False, fill_value=None):
+    def take(self, indices, allow_fill=False, fill_value=None, axis=0):
         """Take elements from an array.
 
         Parameters:
@@ -1278,6 +1284,12 @@ class FData(ABC, pandas.api.extensions.ExtensionArray):
             pandas.api.extensions.take
         """
         from pandas.core.algorithms import take
+
+        # The axis parameter must exist, because sklearn tries to use take
+        # instead of __getitem__
+        if axis != 0:
+            raise ValueError(f"Axis must be 0, not {axis}")
+
         # If the ExtensionArray is backed by an ndarray, then
         # just pass that here instead of coercing to object.
         data = self.astype(object)
@@ -1307,10 +1319,4 @@ class FData(ABC, pandas.api.extensions.ExtensionArray):
 
         first, *others = to_concat
 
-        for o in others:
-            first = first.concatenate(o)
-
-        # When #101 is ready
-        # return first.concatenate(others)
-
-        return first
+        return first.concatenate(*others)
diff --git a/skfda/representation/grid.py b/skfda/representation/grid.py
index e28b69e2..e9a3f279 100644
--- a/skfda/representation/grid.py
+++ b/skfda/representation/grid.py
@@ -6,18 +6,18 @@ list of discretisation points.
 
 """
 
+import copy
 import numbers
 
-import copy
-import numpy as np
-import scipy.stats.mstats
 import pandas.api.extensions
+import scipy.stats.mstats
 
+import numpy as np
 
-from . import basis as fdbasis
-from .interpolation import SplineInterpolator
 from . import FData
+from . import basis as fdbasis
 from .._utils import _list_of_arrays, constants
+from .interpolation import SplineInterpolator
 
 
 __author__ = "Miguel Carbajo Berrocal"
@@ -302,16 +302,6 @@ class FDataGrid(FData):
 
         return FDataGrid._CoordinateIterator(self)
 
-    @property
-    def ndim(self):
-        """Return number of dimensions of the data matrix.
-
-        Returns:
-            int: Number of dimensions of the data matrix.
-
-        """
-        return self.data_matrix.ndim
-
     @property
     def nsamples(self):
         """Return number of rows of the data_matrix. Also the number of samples.
@@ -580,6 +570,47 @@ class FDataGrid(FData):
         return self.copy(data_matrix=[
             scipy.stats.mstats.gmean(self.data_matrix, 0)])
 
+    def __eq__(self, other):
+        """Comparison of FDataGrid objects"""
+        if not isinstance(other, FDataGrid):
+            return NotImplemented
+
+        if not np.array_equal(self.data_matrix, other.data_matrix):
+            return False
+
+        if len(self.sample_points) != len(other.sample_points):
+            return False
+
+        for a, b in zip(self.sample_points, other.sample_points):
+            if not np.array_equal(a, b):
+                return False
+
+        if not np.array_equal(self.domain_range, other.domain_range):
+            return False
+
+        if self.dataset_label != other.dataset_label:
+            return False
+
+        if self.axes_labels is None or other.axes_labels is None:
+            # Both must be None
+            if self.axes_labels is not other.axes_labels:
+                return False
+        else:
+            if len(self.axes_labels) != len(other.axes_labels):
+                return False
+
+            for a, b in zip(self.axes_labels, other.axes_labels):
+                if a != b:
+                    return False
+
+        if self.extrapolation != other.extrapolation:
+            return False
+
+        if self.interpolator != other.interpolator:
+            return False
+
+        return True
+
     def __add__(self, other):
         """Addition for FDataGrid object.
 
diff --git a/skfda/representation/interpolation.py b/skfda/representation/interpolation.py
index 435973bf..a8ec37f4 100644
--- a/skfda/representation/interpolation.py
+++ b/skfda/representation/interpolation.py
@@ -3,15 +3,15 @@ Module to interpolate functional data objects.
 """
 
 
-import numpy as np
-
-# Scipy interpolator methods used internally
 from scipy.interpolate import (PchipInterpolator, UnivariateSpline,
                                RectBivariateSpline, RegularGridInterpolator)
 
+import numpy as np
+
 from .evaluator import Evaluator, EvaluatorConstructor
 
 
+# Scipy interpolator methods used internally
 class SplineInterpolator(EvaluatorConstructor):
     r"""Spline interpolator of :class:`FDataGrid`.
 
@@ -85,7 +85,7 @@ class SplineInterpolator(EvaluatorConstructor):
         return (super().__eq__(other) and
                 self.interpolation_order == other.interpolation_order and
                 self.smoothness_parameter == other.smoothness_parameter and
-                self.monoton == other.monotone)
+                self.monotone == other.monotone)
 
     def evaluator(self, fdatagrid):
         """Construct a SplineInterpolatorEvaluator used in the evaluation.


Test patch:
diff --git a/tests/test_grid.py b/tests/test_grid.py
index 483a5cab..52a31d98 100644
--- a/tests/test_grid.py
+++ b/tests/test_grid.py
@@ -1,10 +1,10 @@
 import unittest
 
-import numpy as np
 import scipy.stats.mstats
 
-from skfda.exploratory import stats
+import numpy as np
 from skfda import FDataGrid
+from skfda.exploratory import stats
 
 
 class TestFDataGrid(unittest.TestCase):
@@ -20,6 +20,10 @@ class TestFDataGrid(unittest.TestCase):
         np.testing.assert_array_equal(
             fd.sample_points, np.array([[0., 0.25, 0.5, 0.75, 1.]]))
 
+    def test_copy_equals(self):
+        fd = FDataGrid([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]])
+        self.assertEqual(fd, fd.copy())
+
     def test_mean(self):
         fd = FDataGrid([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]])
         mean = stats.mean(fd)
@@ -69,21 +73,6 @@ class TestFDataGrid(unittest.TestCase):
                                        [3, 4, 5, 6, 7], [4, 5, 6, 7, 8]])
         np.testing.assert_array_equal(fd1.axes_labels, fd.axes_labels)
 
-    def test_concatenate(self):
-        fd1 = FDataGrid([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]])
-        fd2 = FDataGrid([[3, 4, 5, 6, 7], [4, 5, 6, 7, 8]])
-
-        fd1.axes_labels = ["x", "y"]
-        fd = fd1.concatenate(fd2)
-
-        np.testing.assert_equal(fd.nsamples, 4)
-        np.testing.assert_equal(fd.ndim_image, 1)
-        np.testing.assert_equal(fd.ndim_domain, 1)
-        np.testing.assert_array_equal(fd.data_matrix[..., 0],
-                                      [[1, 2, 3, 4, 5], [2, 3, 4, 5, 6],
-                                       [3, 4, 5, 6, 7], [4, 5, 6, 7, 8]])
-        np.testing.assert_array_equal(fd1.axes_labels, fd.axes_labels)
-
     def test_concatenate_coordinates(self):
         fd1 = FDataGrid([[1, 2, 3, 4], [2, 3, 4, 5]])
         fd2 = FDataGrid([[3, 4, 5, 6], [4, 5, 6, 7]])
diff --git a/tests/test_pandas.py b/tests/test_pandas.py
new file mode 100644
index 00000000..cf25e9b3
--- /dev/null
+++ b/tests/test_pandas.py
@@ -0,0 +1,45 @@
+import unittest
+
+import pandas as pd
+import skfda
+
+
+class TestPandas(unittest.TestCase):
+
+    def setUp(self):
+        self.fd = skfda.FDataGrid(
+            [[1, 2, 3, 4, 5, 6, 7], [2, 3, 4, 5, 6, 7, 9]])
+        self.fd_basis = self.fd.to_basis(skfda.representation.basis.BSpline(
+            domain_range=self.fd.domain_range, nbasis=5))
+
+    def test_fdatagrid_series(self):
+        series = pd.Series(self.fd)
+        self.assertEqual(
+            series.dtype, skfda.representation.grid.FDataGridDType)
+        self.assertEqual(len(series), self.fd.nsamples)
+        self.assertEqual(series[0], self.fd[0])
+
+    def test_fdatabasis_series(self):
+        series = pd.Series(self.fd_basis)
+        self.assertEqual(
+            series.dtype, skfda.representation.basis.FDataBasisDType)
+        self.assertEqual(len(series), self.fd_basis.nsamples)
+        self.assertEqual(series[0], self.fd_basis[0])
+
+    def test_fdatagrid_dataframe(self):
+        df = pd.DataFrame({"function": self.fd})
+        self.assertEqual(
+            df["function"].dtype, skfda.representation.grid.FDataGridDType)
+        self.assertEqual(len(df["function"]), self.fd.nsamples)
+        self.assertEqual(df["function"][0], self.fd[0])
+
+    def test_fdatabasis_dataframe(self):
+        df = pd.DataFrame({"function": self.fd_basis})
+        self.assertEqual(
+            df["function"].dtype, skfda.representation.basis.FDataBasisDType)
+        self.assertEqual(len(df["function"]), self.fd_basis.nsamples)
+        self.assertEqual(df["function"][0], self.fd_basis[0])
+
+    def test_take(self):
+        self.assertEqual(self.fd.take(0), self.fd[0])
+        self.assertEqual(self.fd.take(0, axis=0), self.fd[0])