Repository: alteryx/woodwork (python)

Issue:
Setting new logical type with set_types drops user set semantic tags
Setting a new logical type with `set_types` can cause previously set semantic tags to be lost. This behavior seems a bit counter-intuitive when users do not specify new tags. If users do not specify new values for semantic tags during the call to `set_types` any previously set semantic tags should be retained.

### Current Behavior
Custom tag on `vals` column is lost
```python
import pandas as pd
import woodwork as ww

df = pd.DataFrame({'id': [0, 1, 2], 'vals': [True, False, True]})
df.ww.init(semantic_tags={'vals': 'custom_tag'})

df.ww
```
```
       Physical Type Logical Type Semantic Tag(s)
Column                                           
id             int64      Integer     ['numeric']
vals            bool      Boolean  ['custom_tag']
```
```python
df.ww.set_types({'vals': 'boolean_nullable'})
df.ww
```
```
       Physical Type     Logical Type Semantic Tag(s)
Column                                               
id             int64          Integer     ['numeric']
vals         boolean  BooleanNullable              []
```

### Expected Behavior
Custom tag on `vals` column is retained
```python
import pandas as pd
import woodwork as ww

df = pd.DataFrame({'id': [0, 1, 2], 'vals': [True, False, True]})
df.ww.init(semantic_tags={'vals': 'custom_tag'})

df.ww
```
```
       Physical Type Logical Type Semantic Tag(s)
Column                                           
id             int64      Integer     ['numeric']
vals            bool      Boolean  ['custom_tag']
```
```python
df.ww.set_types({'vals': 'boolean_nullable'})
df.ww
```
```
       Physical Type     Logical Type Semantic Tag(s)
Column                                               
id             int64          Integer     ['numeric']
vals         boolean  BooleanNullable  ['custom_tag']
```


Pull request description:
Preserve custom semantic tags when changing logical type
Closes #1130 

Patch:
diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst
index b5dc970..d9e68f3 100644
--- a/docs/source/api_reference.rst
+++ b/docs/source/api_reference.rst
@@ -103,6 +103,7 @@ ColumnSchema
     :toctree: generated/
 
     ColumnSchema
+    ColumnSchema.custom_tags
     ColumnSchema.description
     ColumnSchema.origin
     ColumnSchema.is_boolean
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 2ba2fe1..822d1bb 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -3,15 +3,17 @@
 Release Notes
 -------------
 
-.. Future Release
-  ==============
+Future Release
+==============
     * Enhancements
     * Fixes
+        * Preserve custom semantic tags when changing column logical type (:pr:`1300`)
     * Changes
     * Documentation Changes
     * Testing Changes
 
-.. Thanks to the following people for contributing to this release:
+    Thanks to the following people for contributing to this release:
+    :user:`jeff-hernandez`
 
 v0.13.0 Feb 16, 2022
 ====================
diff --git a/woodwork/column_schema.py b/woodwork/column_schema.py
index d9abbea..19a9e6d 100644
--- a/woodwork/column_schema.py
+++ b/woodwork/column_schema.py
@@ -218,6 +218,14 @@ class ColumnSchema(object):
 
         self.semantic_tags = semantic_tags
 
+    @property
+    def custom_tags(self):
+        """The custom semantic tag(s) specified for the column."""
+        standard_tags = set()
+        if self.use_standard_tags:
+            standard_tags |= self.logical_type.standard_tags
+        return self.semantic_tags - standard_tags - {"index", "time_index"}
+
 
 def _validate_logical_type(logical_type):
     if type(logical_type) not in ww.type_system.registered_types:
diff --git a/woodwork/table_schema.py b/woodwork/table_schema.py
index 7ceb961..c15c537 100644
--- a/woodwork/table_schema.py
+++ b/woodwork/table_schema.py
@@ -232,16 +232,18 @@ class TableSchema(object):
 
         for col_name in logical_types.keys() | semantic_tags.keys():
             original_tags = self.semantic_tags[col_name]
+            custom_tags = self.columns[col_name].custom_tags
 
             # Update Logical Type for the TableSchema, getting new semantic tags
             new_logical_type = logical_types.get(col_name)
             if new_logical_type is not None:
                 self.columns[col_name].logical_type = new_logical_type
 
-            # Set new semantic tags, removing existing tags
+            # Retain custom tags if no tags are given, otherwise set to new semantic tags
             new_semantic_tags = semantic_tags.get(col_name)
             if new_semantic_tags is None:
                 self.columns[col_name]._reset_semantic_tags()
+                self.columns[col_name]._set_semantic_tags(custom_tags)
             else:
                 self.columns[col_name]._set_semantic_tags(new_semantic_tags)
                 _validate_not_setting_index_tags(self.semantic_tags[col_name], col_name)


Test patch:
diff --git a/woodwork/tests/accessor/test_table_accessor.py b/woodwork/tests/accessor/test_table_accessor.py
index e86070b..b8796de 100644
--- a/woodwork/tests/accessor/test_table_accessor.py
+++ b/woodwork/tests/accessor/test_table_accessor.py
@@ -1941,7 +1941,11 @@ def test_accessor_set_index_errors(sample_df):
 
 
 def test_set_types(sample_df):
-    sample_df.ww.init(index="full_name", time_index="signup_date")
+    sample_df.ww.init(
+        index="full_name",
+        time_index="signup_date",
+        semantic_tags={"is_registered": "custom_tag"},
+    )
 
     original_df = sample_df.ww.copy()
 
@@ -1951,6 +1955,7 @@ def test_set_types(sample_df):
 
     sample_df.ww.set_types(logical_types={"is_registered": "IntegerNullable"})
     assert sample_df["is_registered"].dtype == "Int64"
+    assert "custom_tag" in sample_df.ww.semantic_tags["is_registered"]
 
     sample_df.ww.set_types(
         semantic_tags={"signup_date": ["new_tag"]},
diff --git a/woodwork/tests/schema/test_table_schema.py b/woodwork/tests/schema/test_table_schema.py
index 439d25d..fbb534b 100644
--- a/woodwork/tests/schema/test_table_schema.py
+++ b/woodwork/tests/schema/test_table_schema.py
@@ -554,10 +554,10 @@ def test_set_logical_types(sample_column_names, sample_inferred_logical_types):
     assert schema.logical_types["phone_number"] == PhoneNumber
     assert schema.logical_types["age"] == Double
 
-    # Verify semantic tags were reset to standard tags in columns with Logical Type changes
-    assert schema.semantic_tags["full_name"] == {"category"}
-    assert schema.semantic_tags["email"] == set()
-    assert schema.semantic_tags["phone_number"] == set()
+    # Verify custom tags were not removed after changing logical types
+    assert schema.semantic_tags["full_name"] == {"category", "tag1"}
+    assert schema.semantic_tags["email"] == {"tag2"}
+    assert schema.semantic_tags["phone_number"] == {"tag3", "tag2"}
     assert schema.semantic_tags["age"] == {"numeric"}
 
     # Verify signup date column was unchanged
@@ -663,7 +663,7 @@ def test_set_types_combined(sample_column_names, sample_inferred_logical_types):
         retain_index_tags=False,
     )
 
-    assert schema.semantic_tags["id"] == {"numeric"}
+    assert schema.semantic_tags["id"] == {"numeric", "tag1"}
     assert schema.semantic_tags["age"] == {"numeric", "new_tag"}
 
     # don't use standard tags and lose index tags
@@ -688,7 +688,7 @@ def test_set_types_combined(sample_column_names, sample_inferred_logical_types):
     assert schema.semantic_tags["id"] == {"new_tag"}
     assert schema.semantic_tags["age"] == {"new_tag"}
     assert schema.semantic_tags["is_registered"] == {"new_tag"}
-    assert schema.semantic_tags["email"] == set()
+    assert schema.semantic_tags["email"] == {"tag2"}
     assert schema.semantic_tags["signup_date"] == {"time_index", "secondary_time_index"}
 
     assert schema.logical_types["id"] == Double