Repository: pandas-dev/pandas

Issue:
clip dataframe column-wise
I have 

```
In [67]: a
Out[67]:

   0  1  2
0  1  2  3
1  4  5  6

```
when I run
```
In [69]: a.clip(lower=[1.5,2.5,3.5],axis=1)
```

I got 
```
----> 1 a.clip(lower=[1.5,2.5,3.5],axis=1)

/home/.../anaconda3/lib/python3.5/site-packages/pandas/core/generic.py in clip(self, lower, upper, axis, *args, **kwargs)
   3668         result = self
   3669         if lower is not None:
-> 3670             result = result.clip_lower(lower, axis)
   3671         if upper is not None:
   3672             result = result.clip_upper(upper, axis)

/home/.../anaconda3/lib/python3.5/site-packages/pandas/core/generic.py in clip_lower(self, threshold, axis)
   3720
   3721         subset = self.ge(threshold, axis=axis) | isnull(self)
-> 3722         return self.where(subset, threshold, axis=axis)
   3723
   3724     def groupby(self, by=None, axis=0, level=None, as_index=True, sort=True,

/home/.../anaconda3/lib/python3.5/site-packages/pandas/core/generic.py in where(self, cond, other, inplace, axis, level, try\
_cast, raise_on_error)
   4458
   4459                 else:
-> 4460                     raise ValueError('other must be the same shape as self '                                           
   4461                                      'when an ndarray')
   4462

ValueError: other must be the same shape as self when an ndarray

```

Is that expected?
I was expecting to get something like:
```
Out[72]:

     0    1    2
0  1.5  2.5  3.5
1  4.0  5.0  6.0

```

<details>
# Paste the output here pd.show_versions() here
In [73]: pd.show_versions()

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.2.final.0
python-bits: 64
OS: Linux
OS-release: 4.4.0-59-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

pandas: 0.18.1
nose: 1.3.7
pip: 8.1.2
setuptools: 27.2.0
Cython: 0.24.1
numpy: 1.11.1
scipy: 0.18.1
statsmodels: 0.6.1
xarray: None
IPython: 5.1.0
sphinx: 1.4.6
patsy: 0.4.1
dateutil: 2.5.3
pytz: 2016.6.1
blosc: None
bottleneck: 1.1.0
tables: 3.2.3.1
numexpr: 2.6.1
matplotlib: 1.5.3
openpyxl: 2.3.2
xlrd: 1.0.0
xlwt: 1.1.2
xlsxwriter: 0.9.3
lxml: 3.6.4
bs4: 4.5.1
html5lib: None
httplib2: None
apiclient: None
sqlalchemy: 1.0.13
pymysql: None
psycopg2: None
jinja2: 2.8
boto: 2.42.0
pandas_datareader: None

</details>



Discussion:
Using a Series instead of a list seems to work:

```
In [28]: a = pd.DataFrame([[1,2,3],[4,5,6]])

In [29]: a.clip(lower=pd.Series([1.5,2.5,3.5]), axis=1)
Out[29]: 
     0    1    2
0  1.5  2.5  3.5
1  4.0  5.0  6.0
```

With an array instead of list, it also gives the error.

So given the docs (which say it can be 'array-like'), this is a bug. Or otherwise the docs in not correct.
Hi, can I work on this?
Looks like good thing to start contributing to OSS
Yes sure. If you have any questions, just ask here or on the gitter chat: https://gitter.im/pydata/pandas
I started to work on this as well.

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -52,6 +52,7 @@
 from pandas.compat.numpy import function as nv
 from pandas.compat import (map, zip, lzip, lrange, string_types,
                            isidentifier, set_function_name, cPickle as pkl)
+from pandas.core.ops import _align_method_FRAME
 import pandas.core.nanops as nanops
 from pandas.util._decorators import Appender, Substitution, deprecate_kwarg
 from pandas.util._validators import validate_bool_kwarg
@@ -4413,6 +4414,34 @@ def _clip_with_scalar(self, lower, upper, inplace=False):
         else:
             return result
 
+    def _clip_with_one_bound(self, threshold, method, axis, inplace):
+
+        inplace = validate_bool_kwarg(inplace, 'inplace')
+        if axis is not None:
+            axis = self._get_axis_number(axis)
+
+        if np.any(isnull(threshold)):
+            raise ValueError("Cannot use an NA value as a clip threshold")
+
+        # method is self.le for upper bound and self.ge for lower bound
+        if is_scalar(threshold) and is_number(threshold):
+            if method.__name__ == 'le':
+                return self._clip_with_scalar(None, threshold, inplace=inplace)
+            return self._clip_with_scalar(threshold, None, inplace=inplace)
+
+        subset = method(threshold, axis=axis) | isnull(self)
+
+        # GH #15390
+        # In order for where method to work, the threshold must
+        # be transformed to NDFrame from other array like structure.
+        if (not isinstance(threshold, ABCSeries)) and is_list_like(threshold):
+            if isinstance(self, ABCSeries):
+                threshold = pd.Series(threshold, index=self.index)
+            else:
+                threshold = _align_method_FRAME(self, np.asarray(threshold),
+                                                axis)
+        return self.where(subset, threshold, axis=axis, inplace=inplace)
+
     def clip(self, lower=None, upper=None, axis=None, inplace=False,
              *args, **kwargs):
         """
@@ -4515,16 +4544,8 @@ def clip_upper(self, threshold, axis=None, inplace=False):
         -------
         clipped : same type as input
         """
-        if np.any(isnull(threshold)):
-            raise ValueError("Cannot use an NA value as a clip threshold")
-
-        if is_scalar(threshold) and is_number(threshold):
-            return self._clip_with_scalar(None, threshold, inplace=inplace)
-
-        inplace = validate_bool_kwarg(inplace, 'inplace')
-
-        subset = self.le(threshold, axis=axis) | isnull(self)
-        return self.where(subset, threshold, axis=axis, inplace=inplace)
+        return self._clip_with_one_bound(threshold, method=self.le,
+                                         axis=axis, inplace=inplace)
 
     def clip_lower(self, threshold, axis=None, inplace=False):
         """
@@ -4547,16 +4568,8 @@ def clip_lower(self, threshold, axis=None, inplace=False):
         -------
         clipped : same type as input
         """
-        if np.any(isnull(threshold)):
-            raise ValueError("Cannot use an NA value as a clip threshold")
-
-        if is_scalar(threshold) and is_number(threshold):
-            return self._clip_with_scalar(threshold, None, inplace=inplace)
-
-        inplace = validate_bool_kwarg(inplace, 'inplace')
-
-        subset = self.ge(threshold, axis=axis) | isnull(self)
-        return self.where(subset, threshold, axis=axis, inplace=inplace)
+        return self._clip_with_one_bound(threshold, method=self.ge,
+                                         axis=axis, inplace=inplace)
 
     def groupby(self, by=None, axis=0, level=None, as_index=True, sort=True,
                 group_keys=True, squeeze=False, **kwargs):


Test patch:
diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py
--- a/pandas/tests/frame/test_analytics.py
+++ b/pandas/tests/frame/test_analytics.py
@@ -1892,12 +1892,33 @@ def test_clip_against_series(self, inplace):
 
             tm.assert_series_equal(clipped_df.loc[mask, i], df.loc[mask, i])
 
-    def test_clip_against_frame(self):
+    @pytest.mark.parametrize("inplace", [True, False])
+    @pytest.mark.parametrize("lower", [[2, 3, 4], np.asarray([2, 3, 4])])
+    @pytest.mark.parametrize("axis,res", [
+        (0, [[2., 2., 3.], [4., 5., 6.], [7., 7., 7.]]),
+        (1, [[2., 3., 4.], [4., 5., 6.], [5., 6., 7.]])
+    ])
+    def test_clip_against_list_like(self, inplace, lower, axis, res):
+        # GH #15390
+        original = self.simple.copy(deep=True)
+
+        result = original.clip(lower=lower, upper=[5, 6, 7],
+                               axis=axis, inplace=inplace)
+
+        expected = pd.DataFrame(res,
+                                columns=original.columns,
+                                index=original.index)
+        if inplace:
+            result = original
+        tm.assert_frame_equal(result, expected, check_exact=True)
+
+    @pytest.mark.parametrize("axis", [0, 1, None])
+    def test_clip_against_frame(self, axis):
         df = DataFrame(np.random.randn(1000, 2))
         lb = DataFrame(np.random.randn(1000, 2))
         ub = lb + 1
 
-        clipped_df = df.clip(lb, ub)
+        clipped_df = df.clip(lb, ub, axis=axis)
 
         lb_mask = df <= lb
         ub_mask = df >= ub
diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py
--- a/pandas/tests/series/test_analytics.py
+++ b/pandas/tests/series/test_analytics.py
@@ -1015,6 +1015,18 @@ def test_clip_against_series(self):
         assert_series_equal(s.clip(lower, upper), Series([1.0, 2.0, 3.5]))
         assert_series_equal(s.clip(1.5, upper), Series([1.5, 1.5, 3.5]))
 
+    @pytest.mark.parametrize("inplace", [True, False])
+    @pytest.mark.parametrize("upper", [[1, 2, 3], np.asarray([1, 2, 3])])
+    def test_clip_against_list_like(self, inplace, upper):
+        # GH #15390
+        original = pd.Series([5, 6, 7])
+        result = original.clip(upper=upper, inplace=inplace)
+        expected = pd.Series([1, 2, 3])
+
+        if inplace:
+            result = original
+        tm.assert_series_equal(result, expected, check_exact=True)
+
     def test_clip_with_datetimes(self):
 
         # GH 11838