Repository: pandas-dev/pandas
Pull request: ENH: Make Series.update() use objects coercible to Series
- [x] closes #33215 
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

Allows `Series.update()` to accept objects that can be coerced into a `Series`, like a `dict`. This mirrors the existing behavior of `DataFrame.update()`.

Problem statement:
[PROPOSAL] Make Series.update() accept a dictionary
#### Code Sample, a copy-pastable example if possible

I propose to re-write the `Series.update() ` method to accept a dictionary object. This would be useful for example when you are iterating over a DataFrame using `DataFrame.iterrows()`, and you want to update the `row` object returned by `iterrows()` in order to build another DataFrame starting from the DataFrame you are iterating over

Here is an example that doesn't work:

```python
import pandas as pd
s = pd.Series({'city':'Rome'})
s.update({'country':'Italy'})
```
#### Problem description

> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "C:\InstalledPrograms\Anaconda3\lib\site-packages\pandas\core\series.py", line 2807, in update
>     other = other.reindex_like(self)
> AttributeError: 'dict' object has no attribute 'reindex_like'

#### Simple use case

In this use case, an explicit conversion to dict() is needed

```
import random
import pandas as pd

def very_long_dictionary():
    d = dict()
    for c in range(2,10):
        d[c] = random.randint(1,101)
    return d

df = pd.DataFrame([[1,2],[3,4],[5,6]])
print(df)

outdl = list()
for index,row in df.iterrows():
    #Explicit conversion to dict is needed
    row = dict(row)
    row.update(very_long_dictionary())
    outdl.append(row)
newdf = pd.DataFrame(outdl)
print(newdf)
```
#### Expected Output

The series would update to the new value

#### Output of ``pd.show_versions()``

<details>

>>> pd.show_versions()

INSTALLED VERSIONS
------------------
commit           : None
python           : 3.7.7.final.0
python-bits      : 64
OS               : Windows
OS-release       : 10
machine          : AMD64
processor        : Intel64 Family 6 Model 158 Stepping 10, GenuineIntel
byteorder        : little
LC_ALL           : None
LANG             : None
LOCALE           : None.None

pandas           : 1.0.3
numpy            : 1.18.1
pytz             : 2019.3
dateutil         : 2.8.1
pip              : 20.0.2
setuptools       : 46.1.3.post20200330
Cython           : 0.29.15
pytest           : 5.4.1
hypothesis       : 5.5.4
sphinx           : 2.4.4
blosc            : None
feather          : None
xlsxwriter       : 1.2.8
lxml.etree       : 4.5.0
html5lib         : 1.0.1
pymysql          : None
psycopg2         : None
jinja2           : 2.11.1
IPython          : 7.13.0
pandas_datareader: None
bs4              : 4.8.2
bottleneck       : 1.3.2
fastparquet      : None
gcsfs            : None
lxml.etree       : 4.5.0
matplotlib       : 3.1.3
numexpr          : 2.7.1
odfpy            : None
openpyxl         : 3.0.3
pandas_gbq       : None
pyarrow          : None
pytables         : None
pytest           : 5.4.1
pyxlsb           : None
s3fs             : None
scipy            : 1.4.1
sqlalchemy       : 1.3.15
tables           : 3.6.1
tabulate         : None
xarray           : None
xlrd             : 1.2.0
xlwt             : 1.3.0
xlsxwriter       : 1.2.8
numba            : 0.48.0

</details>

Hints from issue discussion:
How about wrapping your dict in a Series before passing to update? (Also I'm not sure if your example s.update({'country':'Italy'}) will do what you want since 'country' isn't in the index of s.)

I do think though that this could probably use a better error message.
`DataFrame.update()` will attempt to make a DataFrame out of the input if it isn't already a DataFrame. Why doesn't `Series.update()` do the same?
> `DataFrame.update()` will attempt to make a DataFrame out of the input if it isn't already a DataFrame. Why doesn't `Series.update()` do the same?

@dericke  Hmm, that's a good point. I suppose a PR to make that change might be accepted?
As long as it wasn't a conscious design decision, I'll start working on it.
@dsaxton I submitted a PR to close this.

Commit message: Parameterize new test

Patch to review:
diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst
index a797090a83444..fdf67c6d9c666 100644
--- a/doc/source/whatsnew/v1.1.0.rst
+++ b/doc/source/whatsnew/v1.1.0.rst
@@ -98,6 +98,8 @@ Other enhancements
   This can be used to set a custom compression level, e.g.,
   ``df.to_csv(path, compression={'method': 'gzip', 'compresslevel': 1}``
   (:issue:`33196`)
+- :meth:`Series.update` now accepts objects that can be coerced to a :class:`Series`,
+  such as ``dict`` and ``list``, mirroring the behavior of :meth:`DataFrame.update` (:issue:`33215`)
 - :meth:`~pandas.core.groupby.GroupBy.transform` has gained ``engine`` and ``engine_kwargs`` arguments that supports executing functions with ``Numba`` (:issue:`32854`)
 -
 
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 9182e378fbaeb..93468a24da2d7 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -2764,7 +2764,7 @@ def combine_first(self, other) -> "Series":
 
         Parameters
         ----------
-        other : Series
+        other : Series, or object coercible into Series
             The value(s) to be combined with the `Series`.
 
         Returns
@@ -2844,6 +2844,10 @@ def update(self, other) -> None:
         2    6
         dtype: int64
         """
+
+        if not isinstance(other, Series):
+            other = Series(other)
+
         other = other.reindex_like(self)
         mask = notna(other)
 
diff --git a/pandas/tests/series/methods/test_update.py b/pandas/tests/series/methods/test_update.py
index b7f5f33294792..ed7dbe9349066 100644
--- a/pandas/tests/series/methods/test_update.py
+++ b/pandas/tests/series/methods/test_update.py
@@ -56,3 +56,20 @@ def test_update_dtypes(self, other, dtype, expected):
         ser.update(other)
 
         tm.assert_series_equal(ser, expected)
+
+    @pytest.mark.parametrize(
+        "s, d, expected",
+        [
+            # update by key
+            (
+                Series({"a": 1, "b": 2, "c": 3, "d": 4}),
+                {"b": 5, "c": np.nan},
+                Series({"a": 1, "b": 5, "c": 3, "d": 4}),
+            ),
+            # update by position
+            (Series([1, 2, 3, 4]), [np.nan, 5, 1], Series([1, 5, 1, 4])),
+        ],
+    )
+    def test_update_from_non_series(self, s, d, expected):
+        s.update(d)
+        tm.assert_series_equal(s, expected)


Code review comments:
diff_hunk: @@ -2764,7 +2764,7 @@ def combine_first(self, other) -> "Series":
 
         Parameters
         ----------
-        other : Series
+        other : Series, or object coercible into Series
path: pandas/core/series.py
text: @user2:
It looks like you're editing the docstring of combine_first, can you modify the one for update instead? @user1 do you think it'd be good to add an example to the docstring for this behavior?