Repository: alteryx/featuretools

Issue:
Move `update_data` from Entity to EntitySet
Featuretools should provide an interface for users to update the data that makes up an "entity" in an EntitySet. This is currently done via a method on the `ft.Entity` object, but Woodwork dataframes do not have a corresponding method. To resolve this problem, we can move the `update_data` method from `Entity` to `EntitySet`.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=h1) Report
> Merging [#1398](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=desc) (f626d68) into [main](https://codecov.io/gh/alteryx/featuretools/commit/dd6c6f7fa84898ab280849ea1720ced913c44a8c?el=desc) (dd6c6f7) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head f626d68 differs from pull request most recent head 432d2dc. Consider uploading reports for the commit 432d2dc to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/alteryx/featuretools/pull/1398/graphs/tree.svg?width=650&height=150&src=pr&token=PqDLD96eXJ)](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##             main    #1398   +/-   ##
=======================================
  Coverage   98.58%   98.58%           
=======================================
  Files         135      135           
  Lines       14578    14579    +1     
=======================================
+ Hits        14372    14373    +1     
  Misses        206      206           
```


| [Impacted Files](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [featuretools/entityset/entity.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL2VudGl0eXNldC9lbnRpdHkucHk=) | `94.82% <ø> (-0.30%)` | :arrow_down: |
| [featuretools/entityset/entityset.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL2VudGl0eXNldC9lbnRpdHlzZXQucHk=) | `98.81% <100.00%> (+0.03%)` | :arrow_up: |
| [...utational\_backend/test\_calculate\_feature\_matrix.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL3Rlc3RzL2NvbXB1dGF0aW9uYWxfYmFja2VuZC90ZXN0X2NhbGN1bGF0ZV9mZWF0dXJlX21hdHJpeC5weQ==) | `99.44% <100.00%> (ø)` | |
| [.../tests/computational\_backend/test\_dask\_features.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL3Rlc3RzL2NvbXB1dGF0aW9uYWxfYmFja2VuZC90ZXN0X2Rhc2tfZmVhdHVyZXMucHk=) | `100.00% <100.00%> (ø)` | |
| [featuretools/tests/entityset\_tests/test\_entity.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL3Rlc3RzL2VudGl0eXNldF90ZXN0cy90ZXN0X2VudGl0eS5weQ==) | `100.00% <100.00%> (ø)` | |
| [featuretools/tests/entityset\_tests/test\_es.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL3Rlc3RzL2VudGl0eXNldF90ZXN0cy90ZXN0X2VzLnB5) | `100.00% <100.00%> (ø)` | |
| [...ools/tests/entityset\_tests/test\_last\_time\_index.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL3Rlc3RzL2VudGl0eXNldF90ZXN0cy90ZXN0X2xhc3RfdGltZV9pbmRleC5weQ==) | `100.00% <100.00%> (ø)` | |
| [...s/tests/primitive\_tests/test\_transform\_features.py](https://codecov.io/gh/alteryx/featuretools/pull/1398/diff?src=pr&el=tree#diff-ZmVhdHVyZXRvb2xzL3Rlc3RzL3ByaW1pdGl2ZV90ZXN0cy90ZXN0X3RyYW5zZm9ybV9mZWF0dXJlcy5weQ==) | `98.80% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=footer). Last update [dd6c6f7...432d2dc](https://codecov.io/gh/alteryx/featuretools/pull/1398?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

thehomebrewnerd: @jeff-hernandez Couple things here. I think we probably want to submit this PR against the `woodwork-integration` branch instead of `main`. Can you make that update and confirm that this branch is up to date with the `woodwork-integration` branch?

With that change, I think you might have a conflict in release notes to clean up. Also, in the release notes, please add this as a breaking change and include as sample code block that demonstrates the new API as we have done with the other breaking changes in the `woodwork-integration` branch.

After those updates are made, I can take a closer look, but based on a quick scan, things are looking pretty good.
rwedge: Given this PR is woodowork-aware, what if we changed the method name to `update_dataframe`?  `update_data` seems a little too ambiguous as an entityset method, like it be applied to multiple dataframes at once
jeff-hernandez: @rwedge that makes sense
thehomebrewnerd: > Given this PR is woodowork-aware, what if we changed the method name to `update_dataframe`? `update_data` seems a little too ambiguous as an entityset method, like it be applied to multiple dataframes at once

Seems like a good change to me

Patch:
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 041550eb..6773c218 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -12,6 +12,7 @@ Future Release
         * Move ``set_secondary_time_index`` method from ``Entity`` to ``EntitySet`` (:pr:`1280`)
         * Refactor Relationship creation process (:pr:`1370`)
         * Add auto assign bot on GitHub (:pr:`1380`)
+        * Replaced ``Entity.update_data`` with ``EntitySet.update_dataframe`` (:pr:`1398`)
     * Documentation Changes
         * Improve formatting of release notes (:pr:`1396`)
     * Testing Changes
@@ -21,7 +22,7 @@ Future Release
         * Update boto3 and urllib3 version requirements (:pr:`1394`)
 
     Thanks to the following people for contributing to this release:
-    :user:`gsheni`, :user:`rwedge`, :user:`thehomebrewnerd`
+    :user:`gsheni`, :user:`jeff-hernandez`, :user:`rwedge`, :user:`thehomebrewnerd`
 
 Breaking Changes
 ++++++++++++++++
@@ -36,6 +37,7 @@ Breaking Changes
 * ``EntitySet.add_relationship`` has been updated to accept dataframe and column id values or a
   ``Relationship`` object. Adding a relationship from a ``Relationship`` object now requires passing
   the relationship as a keyword argument.
+* ``Entity.update_data`` has been removed. To update the dataframe, call ``EntitySet.update_dataframe`` and use the ``entity_id`` paramter.
 
 What's New in this Release
 ++++++++++++++++++++++++++
@@ -96,6 +98,14 @@ this approach the ``relationship`` parameter name must be included.
 
     >>> es.add_relationship(relationship=new_relationship)
 
+**Update DataFrame**
+
+To update the dataframe for a single entity, call ``EntitySet.update_dataframe`` and pass in the id of the entity for which dataframe should be updated.
+
+.. code-block:: python
+
+    >>> es.update_dataframe(entity_id='log', df=df)
+
 v0.23.3 Mar 31, 2021
 ====================
     .. warning::
diff --git a/featuretools/entityset/entity.py b/featuretools/entityset/entity.py
index 5c5032c8..3ac06cf0 100644
--- a/featuretools/entityset/entity.py
+++ b/featuretools/entityset/entity.py
@@ -261,30 +261,6 @@ class Entity(object):
         self.variables = [index_variable] + [v for v in variables
                                              if v.id != index]
 
-    def update_data(self, df, already_sorted=False,
-                    recalculate_last_time_indexes=True):
-        '''Update entity's internal dataframe, optionaly making sure data is sorted,
-        reference indexes to other entities are consistent, and last_time_indexes
-        are consistent.
-        '''
-        if len(df.columns) != len(self.variables):
-            raise ValueError("Updated dataframe contains {} columns, expecting {}".format(len(df.columns),
-                                                                                          len(self.variables)))
-        for v in self.variables:
-            if v.id not in df.columns:
-                raise ValueError("Updated dataframe is missing new {} column".format(v.id))
-
-        # Make sure column ordering matches variable ordering
-        self.df = df[[v.id for v in self.variables]]
-        self.set_index(self.index)
-        if self.time_index is not None:
-            self.set_time_index(self.time_index, already_sorted=already_sorted)
-
-        self.entityset.set_secondary_time_index(self, self.secondary_time_index)
-        if recalculate_last_time_indexes and self.last_time_index is not None:
-            self.entityset.add_last_time_indexes(updated_entities=[self.id])
-        self.entityset.reset_data_description()
-
     def delete_variables(self, variable_ids):
         """
         Remove variables from entity's dataframe and from
diff --git a/featuretools/entityset/entityset.py b/featuretools/entityset/entityset.py
index 8f4915c7..88c0fd68 100644
--- a/featuretools/entityset/entityset.py
+++ b/featuretools/entityset/entityset.py
@@ -797,8 +797,12 @@ class EntitySet(object):
             if (entity.last_time_index is not None or
                     other[entity.id].last_time_index is not None):
                 has_last_time_index.append(entity.id)
-            combined_es[entity.id].update_data(df=combined_df,
-                                               recalculate_last_time_indexes=False)
+
+            combined_es.update_dataframe(
+                entity_id=entity.id,
+                df=combined_df,
+                recalculate_last_time_indexes=False,
+            )
 
         combined_es.add_last_time_indexes(updated_entities=has_last_time_index)
         self.reset_data_description()
@@ -1183,6 +1187,30 @@ class EntitySet(object):
 
         return df
 
+    def update_dataframe(self, entity_id, df, already_sorted=False, recalculate_last_time_indexes=True):
+        '''Update entity's internal dataframe, optionaly making sure data is sorted,
+        reference indexes to other entities are consistent, and last_time_indexes
+        are consistent.
+        '''
+        variables = self[entity_id].variables
+        if len(df.columns) != len(variables):
+            raise ValueError("Updated dataframe contains {} columns, expecting {}".format(len(df.columns),
+                                                                                          len(variables)))
+        for v in variables:
+            if v.id not in df.columns:
+                raise ValueError("Updated dataframe is missing new {} column".format(v.id))
+
+        # Make sure column ordering matches variable ordering
+        self[entity_id].df = df[[v.id for v in variables]]
+        self[entity_id].set_index(self[entity_id].index)
+        if self[entity_id].time_index is not None:
+            self[entity_id].set_time_index(self[entity_id].time_index, already_sorted=already_sorted)
+
+        self.set_secondary_time_index(self[entity_id], self[entity_id].secondary_time_index)
+        if recalculate_last_time_indexes and self[entity_id].last_time_index is not None:
+            self.add_last_time_indexes(updated_entities=[self[entity_id].id])
+        self.reset_data_description()
+
 
 def _vals_to_series(instance_vals, variable_id):
     """


Test patch:
diff --git a/featuretools/tests/computational_backend/test_calculate_feature_matrix.py b/featuretools/tests/computational_backend/test_calculate_feature_matrix.py
index a62e06aa..77940f26 100644
--- a/featuretools/tests/computational_backend/test_calculate_feature_matrix.py
+++ b/featuretools/tests/computational_backend/test_calculate_feature_matrix.py
@@ -661,7 +661,7 @@ def test_training_window_recent_time_index(pd_es):
     df.index = df.index.astype("category")
     df["id"] = df["id"].astype("category")
 
-    pd_es['customers'].update_data(df=df, recalculate_last_time_indexes=False)
+    pd_es.update_dataframe(entity_id='customers', df=df, recalculate_last_time_indexes=False)
     pd_es.add_last_time_indexes()
 
     property_feature = ft.Feature(pd_es['log']['id'], parent_entity=pd_es['customers'], primitive=Count)
diff --git a/featuretools/tests/computational_backend/test_dask_features.py b/featuretools/tests/computational_backend/test_dask_features.py
index bb661483..e4b19ff2 100644
--- a/featuretools/tests/computational_backend/test_dask_features.py
+++ b/featuretools/tests/computational_backend/test_dask_features.py
@@ -26,5 +26,5 @@ def test_tokenize_entityset(pd_es, int_es):
                            columns=['cohort', 'cohort_name', 'cohort_end'],
                            index=[2])
     more_cohorts = cohorts_df.append(new_row, ignore_index=True, sort=True)
-    dupe['cohorts'].update_data(more_cohorts)
+    dupe.update_dataframe(entity_id='cohorts', df=more_cohorts)
     assert tokenize(pd_es) == tokenize(dupe)
diff --git a/featuretools/tests/entityset_tests/test_entity.py b/featuretools/tests/entityset_tests/test_entity.py
index 73ee928e..9d76632f 100644
--- a/featuretools/tests/entityset_tests/test_entity.py
+++ b/featuretools/tests/entityset_tests/test_entity.py
@@ -95,7 +95,7 @@ def test_eq(es):
     assert not other_es['stores'].__eq__(es['stores'], deep=True)
 
 
-def test_update_data(es):
+def test_update_dataframe(es):
     df = es['customers'].df.copy()
     if ks and isinstance(df, ks.DataFrame):
         df['new'] = [1, 2, 3]
@@ -104,11 +104,11 @@ def test_update_data(es):
 
     error_text = 'Updated dataframe is missing new cohort column'
     with pytest.raises(ValueError, match=error_text):
-        es['customers'].update_data(df.drop(columns=['cohort']))
+        es.update_dataframe(entity_id='customers', df=df.drop(columns=['cohort']))
 
     error_text = 'Updated dataframe contains 16 columns, expecting 15'
     with pytest.raises(ValueError, match=error_text):
-        es['customers'].update_data(df)
+        es.update_dataframe(entity_id='customers', df=df)
 
     # test already_sorted on entity without time index
     df = es["sessions"].df.copy()
@@ -121,10 +121,10 @@ def test_update_data(es):
         df = df.sort_index()
     else:
         df["id"] = updated_id
-    es["sessions"].update_data(df.copy())
+    es.update_dataframe(entity_id='sessions', df=df.copy())
     sessions_df = to_pandas(es['sessions'].df)
     assert sessions_df["id"].iloc[1] == 2  # no sorting since time index not defined
-    es["sessions"].update_data(df.copy(), already_sorted=True)
+    es.update_dataframe(entity_id='sessions', df=df.copy(), already_sorted=True)
     sessions_df = to_pandas(es['sessions'].df)
     assert sessions_df["id"].iloc[1] == 2
 
@@ -138,13 +138,13 @@ def test_update_data(es):
         df = df.sort_index()
     else:
         df['signup_date'] = updated_signup
-    es["customers"].update_data(df.copy(), already_sorted=True)
+    es.update_dataframe(entity_id='customers', df=df.copy(), already_sorted=True)
     customers_df = to_pandas(es['customers'].df)
     assert customers_df["id"].iloc[0] == 2
 
     # only pandas allows for sorting:
     if isinstance(df, pd.DataFrame):
-        es["customers"].update_data(df.copy())
+        es.update_dataframe(entity_id='customers', df=df.copy())
         assert es['customers'].df["id"].iloc[0] == 0
 
 
diff --git a/featuretools/tests/entityset_tests/test_es.py b/featuretools/tests/entityset_tests/test_es.py
index 7904595b..ec4f70fd 100644
--- a/featuretools/tests/entityset_tests/test_es.py
+++ b/featuretools/tests/entityset_tests/test_es.py
@@ -723,7 +723,7 @@ def test_concat_entitysets(es):
     for i, _es in enumerate([es_1, es_2]):
         for entity, rows in emap.items():
             df = _es[entity].df
-            _es[entity].update_data(df=df.loc[rows[i]])
+            _es.update_dataframe(entity_id=entity, df=df.loc[rows[i]])
 
     assert 10 not in es_1['log'].last_time_index.index
     assert 10 in es_2['log'].last_time_index.index
diff --git a/featuretools/tests/entityset_tests/test_last_time_index.py b/featuretools/tests/entityset_tests/test_last_time_index.py
index 26efd647..d84d9461 100644
--- a/featuretools/tests/entityset_tests/test_last_time_index.py
+++ b/featuretools/tests/entityset_tests/test_last_time_index.py
@@ -126,7 +126,7 @@ class TestLastTimeIndex(object):
         df = values.df.append(row, sort=True)
         df = df[['value', 'value_time']].sort_values(by='value')
         df.index.name = 'values_id'
-        values.update_data(df)
+        values_es.update_dataframe(entity_id='values', df=df)
         values_es.add_last_time_indexes()
         # lti value should default to instance's time index
         true_values_lti[10] = pd.Timestamp("2011-04-10 11:10:02")
@@ -152,7 +152,7 @@ class TestLastTimeIndex(object):
         sessions = es['sessions']
 
         # add session instance with no associated log instances
-        sessions.update_data(extra_session_df)
+        es.update_dataframe(entity_id='sessions', df=extra_session_df)
         es.add_last_time_indexes()
         # since sessions has no time index, default value is NaT
         true_sessions_lti[6] = pd.NaT
@@ -229,7 +229,7 @@ class TestLastTimeIndex(object):
         sessions = es['sessions']
 
         # add row to sessions so not all session instances are in log
-        sessions.update_data(extra_session_df)
+        es.update_dataframe(entity_id='sessions', df=extra_session_df)
 
         # add row to wishlist df so new session instance in in wishlist_log
         row_values = {'session_id': 6,
@@ -270,7 +270,7 @@ class TestLastTimeIndex(object):
         sessions = es['sessions']
 
         # add row to sessions so not all session instances are in log
-        sessions.update_data(extra_session_df)
+        es.update_dataframe(entity_id='sessions', df=extra_session_df)
 
         # add row to wishlist_log so extra session has child instance
         row_values = {'session_id': 6,
@@ -322,7 +322,7 @@ class TestLastTimeIndex(object):
                           'datetime': ft.variable_types.variable.DatetimeTimeIndex,
                           'product_id': ft.variable_types.variable.Categorical}
         # add row to sessions to create session with no events
-        sessions.update_data(extra_session_df)
+        es.update_dataframe(entity_id='sessions', df=extra_session_df)
 
         es.entity_from_dataframe(entity_id="wishlist_log",
                                  dataframe=wishlist_df,
@@ -361,7 +361,7 @@ class TestLastTimeIndex(object):
             df = dd.from_pandas(df, npartitions=2)
         if ks and isinstance(log.df, ks.DataFrame):
             df = ks.from_pandas(df)
-        log.update_data(df)
+        es.update_dataframe(entity_id='log', df=df)
         es.add_last_time_indexes()
 
         true_customers_lti = pd.Series([datetime(2011, 4, 9, 10, 40, 1),
diff --git a/featuretools/tests/primitive_tests/test_transform_features.py b/featuretools/tests/primitive_tests/test_transform_features.py
index 842b4167..9a9247b5 100644
--- a/featuretools/tests/primitive_tests/test_transform_features.py
+++ b/featuretools/tests/primitive_tests/test_transform_features.py
@@ -642,7 +642,7 @@ def test_latlong_with_nan(pd_es):
     df['latlong'][1] = (10, np.nan)
     df['latlong'][2] = (np.nan, 4)
     df['latlong'][3] = (np.nan, np.nan)
-    pd_es['log'].update_data(df)
+    pd_es.update_dataframe(entity_id='log', df=df)
     log_latlong_feat = pd_es['log']['latlong']
     latitude = ft.Feature(log_latlong_feat, primitive=Latitude)
     longitude = ft.Feature(log_latlong_feat, primitive=Longitude)
@@ -695,7 +695,7 @@ def test_haversine_with_nan(pd_es):
     df = pd_es['log'].df
     df['latlong'][0] = np.nan
     df['latlong'][1] = (10, np.nan)
-    pd_es['log'].update_data(df)
+    pd_es.update_dataframe(entity_id='log', df=df)
     log_latlong_feat = pd_es['log']['latlong']
     log_latlong_feat2 = pd_es['log']['latlong2']
     haversine = ft.Feature([log_latlong_feat, log_latlong_feat2],
@@ -713,7 +713,7 @@ def test_haversine_with_nan(pd_es):
     # Check all `nan` values
     df = pd_es['log'].df
     df['latlong2'] = np.nan
-    pd_es['log'].update_data(df)
+    pd_es.update_dataframe(entity_id='log', df=df)
     log_latlong_feat = pd_es['log']['latlong']
     log_latlong_feat2 = pd_es['log']['latlong2']
     haversine = ft.Feature([log_latlong_feat, log_latlong_feat2],