Repository: alteryx/woodwork (python)

Issue:
Add df.ww.value_counts to Accessor
Another method that will overwrite the Pandas operation

Pull request description:
Add Value counts to table accesor
- Moves over value counts method from DataTables
- Closes #561 

Patch:
diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst
index c592b32..b58f70f 100644
--- a/docs/source/api_reference.rst
+++ b/docs/source/api_reference.rst
@@ -24,6 +24,7 @@ WoodworkTableAccessor
     WoodworkTableAccessor.to_dictionary
     WoodworkTableAccessor.to_parquet
     WoodworkTableAccessor.to_pickle
+    WoodworkTableAccessor.value_counts
 
 WoodworkColumnAccessor
 ======================
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 1edc9b0..0aadc04 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -31,6 +31,7 @@ Release Notes
         * Allow serialization of WoodworkColumnAccessor to csv, pickle, and parquet (:pr:`624`)
         * Add DaskColumnAccessor (:pr:`625`)
         * Allow deserialization from csv, pickle, and parquet to Woodwork table (:pr:`626`)
+        * Add ``value_counts`` to WoodworkTableAccessor (:pr:`632`)
     * Fixes
         * Create new Schema object when performing pandas operation on Accessors (:pr:`595`)
     * Changes
diff --git a/woodwork/statistics_utils.py b/woodwork/statistics_utils.py
index 9dc24f8..689f7d6 100644
--- a/woodwork/statistics_utils.py
+++ b/woodwork/statistics_utils.py
@@ -232,3 +232,48 @@ def _get_mutual_information_dict(dataframe, num_bins=10, nrows=None):
                 )
     mutual_info.sort(key=lambda mi: mi['mutual_info'], reverse=True)
     return mutual_info
+
+
+def _get_value_counts(dataframe, ascending=False, top_n=10, dropna=False):
+    """Returns a list of dictionaries with counts for the most frequent values in each column (only
+        for columns with `category` as a standard tag).
+
+
+    Args:
+        dataframe (pd.DataFrame, dd.DataFrame, ks.DataFrame): Data from which to count values.
+        ascending (bool): Defines whether each list of values should be sorted most frequent
+            to least frequent value (False), or least frequent to most frequent value (True).
+            Defaults to False.
+
+        top_n (int): the number of top values to retrieve. Defaults to 10.
+
+        dropna (bool): determines whether to remove NaN values when finding frequency. Defaults
+            to False.
+
+    Returns:
+        list(dict): a list of dictionaries for each categorical column with keys `count`
+            and `value`.
+    """
+    val_counts = {}
+    valid_cols = [col for col, column in dataframe.ww.columns.items() if _is_col_categorical(column)]
+    data = dataframe[valid_cols]
+    is_ks = False
+    if dd and isinstance(data, dd.DataFrame):
+        data = data.compute()
+    if ks and isinstance(data, ks.DataFrame):
+        data = data.to_pandas()
+        is_ks = True
+
+    for col in valid_cols:
+        if dropna and is_ks:
+            # Koalas categorical columns will have missing values replaced with the string 'None'
+            # Replace them with np.nan so dropna work
+            datacol = data[col].replace(to_replace='None', value=np.nan)
+        else:
+            datacol = data[col]
+        frequencies = datacol.value_counts(ascending=ascending, dropna=dropna)
+        df = frequencies[:top_n].reset_index()
+        df.columns = ["value", "count"]
+        dt_list = list(df.to_dict(orient="index").values())
+        val_counts[col] = dt_list
+    return val_counts
diff --git a/woodwork/table_accessor.py b/woodwork/table_accessor.py
index 46327d3..560842a 100644
--- a/woodwork/table_accessor.py
+++ b/woodwork/table_accessor.py
@@ -13,7 +13,8 @@ from woodwork.logical_types import Datetime
 from woodwork.schema import Schema
 from woodwork.statistics_utils import (
     _get_describe_dict,
-    _get_mutual_information_dict
+    _get_mutual_information_dict,
+    _get_value_counts
 )
 from woodwork.type_sys.utils import (
     _get_ltype_class,
@@ -530,6 +531,27 @@ class WoodworkTableAccessor:
         ]
         return pd.DataFrame(results).reindex(index_order)
 
+    def value_counts(self, ascending=False, top_n=10, dropna=False):
+        """Returns a list of dictionaries with counts for the most frequent values in each column (only
+            for columns with `category` as a standard tag).
+
+
+        Args:
+            ascending (bool): Defines whether each list of values should be sorted most frequent
+                to least frequent value (False), or least frequent to most frequent value (True).
+                Defaults to False.
+
+            top_n (int): the number of top values to retrieve. Defaults to 10.
+
+            dropna (bool): determines whether to remove NaN values when finding frequency. Defaults
+                to False.
+
+        Returns:
+           list(dict): a list of dictionaries for each categorical column with keys `count`
+                and `value`.
+        """
+        return _get_value_counts(self._dataframe, ascending, top_n, dropna)
+
 
 def _validate_accessor_params(dataframe, index, make_index, time_index, logical_types, schema):
     _check_unique_column_names(dataframe)


Test patch:
diff --git a/woodwork/tests/accessor/test_statistics.py b/woodwork/tests/accessor/test_statistics.py
index 471b462..3804cee 100644
--- a/woodwork/tests/accessor/test_statistics.py
+++ b/woodwork/tests/accessor/test_statistics.py
@@ -533,3 +533,49 @@ def test_describe_with_include(sample_df):
         assert col_name in multi_params_df.columns
     multi_params_df['full_name'].equals(col_name_df['full_name'])
     multi_params_df['full_name'].equals(sample_df.ww.describe()['full_name'])
+
+
+def test_value_counts(categorical_df):
+    xfail_dask_and_koalas(categorical_df)
+
+    logical_types = {
+        'ints': Integer,
+        'categories1': Categorical,
+        'bools': Boolean,
+        'categories2': Categorical,
+        'categories3': Categorical,
+    }
+    categorical_df.ww.init(logical_types=logical_types)
+    val_cts = categorical_df.ww.value_counts()
+    for col in categorical_df.ww.columns:
+        if col in ['ints', 'bools']:
+            assert col not in val_cts
+        else:
+            assert col in val_cts
+
+    none_val = np.nan
+    expected_cat1 = [{'value': 200, 'count': 4}, {'value': 100, 'count': 3}, {'value': 1, 'count': 2}, {'value': 3, 'count': 1}]
+    # Koalas converts numeric categories to strings, so we need to update the expected values for this
+    # Koalas will result in `None` instead of `np.nan` in categorical columns
+    if ks and isinstance(categorical_df, ks.DataFrame):
+        updated_results = []
+        for items in expected_cat1:
+            updated_results.append({k: (str(v) if k == 'value' else v) for k, v in items.items()})
+        expected_cat1 = updated_results
+        none_val = 'None'
+
+    assert val_cts['categories1'] == expected_cat1
+    assert val_cts['categories2'] == [{'value': none_val, 'count': 6}, {'value': 'test', 'count': 3}, {'value': 'test2', 'count': 1}]
+    assert val_cts['categories3'] == [{'value': none_val, 'count': 7}, {'value': 'test', 'count': 3}]
+
+    val_cts_descending = categorical_df.ww.value_counts(ascending=True)
+    for col, vals in val_cts_descending.items():
+        for i in range(len(vals)):
+            assert vals[i]['count'] == val_cts[col][-i - 1]['count']
+
+    val_cts_dropna = categorical_df.ww.value_counts(dropna=True)
+    assert val_cts_dropna['categories3'] == [{'value': 'test', 'count': 3}]
+
+    val_cts_2 = categorical_df.ww.value_counts(top_n=2)
+    for col in val_cts_2:
+        assert len(val_cts_2[col]) == 2