Repository: vaexio/vaex

Issue:
[BUG-REPORT] df.diff() raises error with list of columns
**Description**
df.diff() raising error when a list of columns is passed to the column argument.

**Software information**
 - Vaex version: 4.5.0
 - Vaex was installed via: pip3
 - OS: Ubuntu 20.04.3 LTS

**Example**

```
import vaex
import pandas as pd
import numpy as np

df = pd.DataFrame()
df['A'] = np.linspace(1,10,10)
df['B'] = np.ones(10)
df['C'] = 'a'

df = vaex.from_pandas(df)
df.diff(column = ['A', 'B'])
```

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-185-a1979ea4a2bd> in <module>
      8 
      9 df = vaex.from_pandas(df)
---> 10 df.diff(column = list(['A', 'B']))

/usr/local/lib/python3.8/dist-packages/vaex/dataframe.py in diff(self, periods, column, fill_value, trim, inplace, reverse)
   4476         for column in columns:
   4477             new_name = df._find_valid_name(f'__{column}_original')
-> 4478             df[new_name] = df[column]
   4479             originals[column] = new_name
   4480         df = df.shift(periods, columns, fill_value=fill_value, trim=trim, inplace=inplace)

/usr/local/lib/python3.8/dist-packages/vaex/dataframe.py in __setitem__(self, name, value)
   4971                 self.add_column(name, value)
   4972             else:
-> 4973                 self.add_virtual_column(name, value)
   4974         else:
   4975             raise TypeError('__setitem__ only takes strings as arguments, not {}'.format(type(name)))

/usr/local/lib/python3.8/dist-packages/vaex/dataframe.py in add_virtual_column(self, name, expression, unique)
   3570             # we rewrite all existing expressions (including the passed down expression argument)
   3571             self._rename(name, renamed)
-> 3572         expression = _ensure_string_from_expression(expression)
   3573 
   3574         if vaex.utils.find_valid_name(name) != name:

/usr/local/lib/python3.8/dist-packages/vaex/utils.py in _ensure_string_from_expression(expression)
    773         return expression._label
    774     else:
--> 775         raise ValueError('%r is not of string or Expression type, but %r' % (expression, type(expression)))
    776 
    777 

ValueError:   #    A    B
  0    1    1
  1    2    1
  2    3    1
  3    4    1
  4    5    1
  5    6    1
  6    7    1
  7    8    1
  8    9    1
  9   10    1 is not of string or Expression type, but <class 'vaex.dataframe.DataFrameLocal'>

```




Discussion:

Resolving patch:
diff --git a/packages/vaex-core/vaex/dataframe.py b/packages/vaex-core/vaex/dataframe.py
--- a/packages/vaex-core/vaex/dataframe.py
+++ b/packages/vaex-core/vaex/dataframe.py
@@ -4451,7 +4451,13 @@ def diff(self, periods=1, column=None, fill_value=None, trim=False, inplace=Fals
         :param inplace: {inplace}
         """
         df = self.trim(inplace=inplace)
-        columns = self.get_column_names() if column is None else [column]
+        if column is None:
+            columns = self.get_column_names()
+        else:
+            if isinstance(column, (list, tuple)):
+                columns = column
+            else:
+                columns = [column]
         originals = {}
         for column in columns:
             new_name = df._find_valid_name(f'__{column}_original')


Test patch:
diff --git a/tests/shift_test.py b/tests/shift_test.py
--- a/tests/shift_test.py
+++ b/tests/shift_test.py
@@ -366,3 +366,17 @@ def test_diff(df_factory, periods):
     assert np.all(np.isnan(result) == np.isnan(expected))
     mask = ~np.isnan(result)
     assert result[mask].tolist() == expected[mask].tolist()
+
+def test_diff_list():
+    periods = 2
+    x = np.arange(10, dtype='f8')
+    y = x**2
+    df = vaex.from_arrays(x=x, y=y)
+    dfp = df.to_pandas_df(array_type='numpy')
+    df = df.diff(periods, fill_value=np.nan, column=['x', 'y'])
+    dfp = dfp.diff(periods)
+    result = df['x'].to_numpy()
+    expected = dfp['x'].to_numpy()
+    assert np.all(np.isnan(result) == np.isnan(expected))
+    mask = ~np.isnan(result)
+    assert result[mask].tolist() == expected[mask].tolist()