Repository: pandas-dev/pandas

Issue:
Add mode() function to pandas.Series
The mode function returns the most common value (http://en.wikipedia.org/wiki/Mode_%28statistics%29) and it would be nice to have it is a shortcut for Series.value_counts().idxmax() .



Discussion:
it could actually have a very fast implementation in cython as well

It would also be nice to have this on DataFrames without having to go to each column (same with value_counts).

What would it mean to do value_counts over the entire frame, given that the method returns a Series with the unique labels as Index?

that said, I'll see if I can put something together for this - seems interesting to do.

An index with the union over all series indices and NaNs or 0s in the columns if that value is not in the column.

@rockg I'm -1 on doing that for now - too easy to end up with Index with non-comparable objects and right now there's some issues with making that cross-platform anyways.  Feel free to open a separate issue or submit a PR if you want this (we just might not accept until we have all the sort issues nailed down).

@jtratner This is all I was expecting.  I see your point on non-comparable objects, but I think that would be an odd use.  I can put something together that checks for similar dtypes.  I'll open a separate issue.

```
import random
from pandas import DataFrame
a = []
b = []
for i in range(20):
    a.append(random.randint(0,5))
    b.append(random.randint(4,8))

df = DataFrame({'A': a, 'B': b})
vc = dict()
for k, v in df.iteritems():
    vc[k] = v.value_counts()

DataFrame(vc)
Out[1]: 
    A   B
0   4 NaN
1   5 NaN
2   3 NaN
3   4 NaN
4   1   5
5   3   6
6 NaN   6
7 NaN   2
8 NaN   1
```

the issue isn't implementation, it's just non-similar dtypes.  No need to raise if they aren't similar.

@rockg - I just put this together.  The problem is that if you have multiple equal counts, you get multiple modes. What's the result supposed to be if you do this at the DataFrame level?

E.g., if you have the list `[12, 12, 11, 10, 19, 11]`, the modes are `[12, 11]`. I'm thinking Series returns ndarray and DataFrame returns dict of col: ndarray.

u should return series or frame

these are like isin in that regard

`pd.Series([12, 12, 11, 10]).mode()` --> `pd.Series([12])`?

but what about:
`pd.DataFrame({"A": [12, 12, 11, 11], "B": [1, 1, 3, 5], "C": [0, 1, 2, 3]}).mode()`
should that go to (equiv of): `pd.DataFrame({"A": [12, 11], "B": [1, np.nan], "C": [np.nan, np.nan]})`?

@jreback is there some function that handles converts ragged arrays to same size arrays?  I.e., this:

`[[1], [1, 3, 5], [2, 1]]` --> `[[1, nan, nan], [1, 3, 5], [2, 1, nan]]`

maybe tile the shape you want and assign 

not sure

I guess I could also do something like:

``` python
arrays = arrays_with_diff_lengths
arr = np.empty((max(len(a) for a in arrays), len(arrays)), dtype=some_object)
for i, s in results:
    arr[0:len(s), i] = s
return DataFrame(arr, columns=df.columns)
```


Resolving patch:
diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py
--- a/pandas/core/algorithms.py
+++ b/pandas/core/algorithms.py
@@ -3,6 +3,7 @@
 intended for public consumption
 """
 
+from warnings import warn
 import numpy as np
 
 import pandas.core.common as com
@@ -221,6 +222,41 @@ def value_counts(values, sort=True, ascending=False, normalize=False, bins=None)
     return result
 
 
+def mode(values):
+    "Returns the mode or mode(s) of the passed Series or ndarray (sorted)"
+    # must sort because hash order isn't necessarily defined.
+    from pandas.core.series import Series
+
+    if isinstance(values, Series):
+        constructor = values._constructor
+        values = values.values
+    else:
+        values = np.asanyarray(values)
+        constructor = Series
+
+    dtype = values.dtype
+    if com.is_integer_dtype(values.dtype):
+        values = com._ensure_int64(values)
+        result = constructor(sorted(htable.mode_int64(values)), dtype=dtype)
+
+    elif issubclass(values.dtype.type, (np.datetime64,np.timedelta64)):
+        dtype = values.dtype
+        values = values.view(np.int64)
+        result = constructor(sorted(htable.mode_int64(values)), dtype=dtype)
+
+    else:
+        mask = com.isnull(values)
+        values = com._ensure_object(values)
+        res = htable.mode_object(values, mask)
+        try:
+            res = sorted(res)
+        except TypeError as e:
+            warn("Unable to sort modes: %s" % e)
+        result = constructor(res, dtype=dtype)
+
+    return result
+
+
 def rank(values, axis=0, method='average', na_option='keep',
          ascending=True):
     """
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -4083,6 +4083,28 @@ def _get_agg_axis(self, axis_num):
         else:
             raise ValueError('Axis must be 0 or 1 (got %r)' % axis_num)
 
+    def mode(self, axis=0, numeric_only=False):
+        """
+        Gets the mode of each element along the axis selected. Empty if nothing
+        has 2+ occurrences. Adds a row for each mode per label, fills in gaps
+        with nan.
+
+        Parameters
+        ----------
+        axis : {0, 1, 'index', 'columns'} (default 0)
+            0/'index' : get mode of each column
+            1/'columns' : get mode of each row
+        numeric_only : bool, default False
+            if True, only apply to numeric columns
+
+        Returns
+        -------
+        modes : DataFrame (sorted)
+        """
+        data = self if not numeric_only else self._get_numeric_data()
+        f = lambda s: s.mode()
+        return data.apply(f, axis=axis)
+
     def quantile(self, q=0.5, axis=0, numeric_only=True):
         """
         Return values at the given quantile over requested axis, a la
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -1127,6 +1127,26 @@ def value_counts(self, normalize=False, sort=True, ascending=False, bins=None):
         return value_counts(self.values, sort=sort, ascending=ascending,
                             normalize=normalize, bins=bins)
 
+    def mode(self):
+        """Returns the mode(s) of the dataset.
+
+        Empty if nothing occurs at least 2 times.  Always returns Series even
+        if only one value.
+
+        Parameters
+        ----------
+        sort : bool, default True
+            if True, will lexicographically sort values, if False skips
+            sorting. Result ordering when ``sort=False`` is not defined.
+
+        Returns
+        -------
+        modes : Series (sorted)
+        """
+        # TODO: Add option for bins like value_counts()
+        from pandas.core.algorithms import mode
+        return mode(self)
+
     def unique(self):
         """
         Return array of unique values in the Series. Significantly faster than


Test patch:
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -10050,6 +10050,59 @@ def wrapper(x):
             self.assert_(np.isnan(r0).all())
             self.assert_(np.isnan(r1).all())
 
+    def test_mode(self):
+        df = pd.DataFrame({"A": [12, 12, 11, 12, 19, 11],
+                           "B": [10, 10, 10, np.nan, 3, 4],
+                           "C": [8, 8, 8, 9, 9, 9],
+                           "D": range(6),
+                           "E": [8, 8, 1, 1, 3, 3]})
+        assert_frame_equal(df[["A"]].mode(),
+                           pd.DataFrame({"A": [12]}))
+        assert_frame_equal(df[["D"]].mode(),
+                           pd.DataFrame(pd.Series([], dtype="int64"),
+                                        columns=["D"]))
+        assert_frame_equal(df[["E"]].mode(),
+                           pd.DataFrame(pd.Series([1, 3, 8], dtype="int64"),
+                                        columns=["E"]))
+        assert_frame_equal(df[["A", "B"]].mode(),
+                           pd.DataFrame({"A": [12], "B": [10.]}))
+        assert_frame_equal(df.mode(),
+                           pd.DataFrame({"A": [12, np.nan, np.nan],
+                                         "B": [10, np.nan, np.nan],
+                                         "C": [8, 9, np.nan],
+                                         "D": [np.nan, np.nan, np.nan],
+                                         "E": [1, 3, 8]}))
+
+        # outputs in sorted order
+        df["C"] = list(reversed(df["C"]))
+        print(df["C"])
+        print(df["C"].mode())
+        a, b = (df[["A", "B", "C"]].mode(),
+                           pd.DataFrame({"A": [12, np.nan],
+                                         "B": [10, np.nan],
+                                         "C": [8, 9]}))
+        print(a)
+        print(b)
+        assert_frame_equal(a, b)
+        # should work with heterogeneous types
+        df = pd.DataFrame({"A": range(6),
+                           "B": pd.date_range('2011', periods=6),
+                           "C": list('abcdef')})
+        exp = pd.DataFrame({"A": pd.Series([], dtype=df["A"].dtype),
+                            "B": pd.Series([], dtype=df["B"].dtype),
+                            "C": pd.Series([], dtype=df["C"].dtype)})
+        assert_frame_equal(df.mode(), exp)
+
+        # and also when not empty
+        df.loc[1, "A"] = 0
+        df.loc[4, "B"] = df.loc[3, "B"]
+        df.loc[5, "C"] = 'e'
+        exp = pd.DataFrame({"A": pd.Series([0], dtype=df["A"].dtype),
+                            "B": pd.Series([df.loc[3, "B"]], dtype=df["B"].dtype),
+                            "C": pd.Series(['e'], dtype=df["C"].dtype)})
+
+        assert_frame_equal(df.mode(), exp)
+
     def test_sum_corner(self):
         axis0 = self.empty.sum(0)
         axis1 = self.empty.sum(1)
diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py
--- a/pandas/tests/test_series.py
+++ b/pandas/tests/test_series.py
@@ -19,10 +19,8 @@
 from pandas.core.index import MultiIndex
 from pandas.tseries.index import Timestamp, DatetimeIndex
 import pandas.core.config as cf
-import pandas.core.series as smod
 import pandas.lib as lib
 
-import pandas.core.common as com
 import pandas.core.datetools as datetools
 import pandas.core.nanops as nanops
 
@@ -1721,6 +1719,35 @@ def test_median(self):
         int_ts = Series(np.ones(10, dtype=int), index=lrange(10))
         self.assertAlmostEqual(np.median(int_ts), int_ts.median())
 
+    def test_mode(self):
+        s = Series([12, 12, 11, 10, 19, 11])
+        exp = Series([11, 12])
+        assert_series_equal(s.mode(), exp)
+
+        assert_series_equal(Series([1, 2, 3]).mode(), Series([], dtype=int))
+
+        lst = [5] * 20 + [1] * 10 + [6] * 25
+        np.random.shuffle(lst)
+        s = Series(lst)
+        assert_series_equal(s.mode(), Series([6]))
+
+        s = Series([5] * 10)
+        assert_series_equal(s.mode(), Series([5]))
+
+        s = Series(lst)
+        s[0] = np.nan
+        assert_series_equal(s.mode(), Series([6], dtype=float))
+
+        s = Series(list('adfasbasfwewefwefweeeeasdfasnbam'))
+        assert_series_equal(s.mode(), Series(['e']))
+
+        s = Series(['2011-01-03', '2013-01-02', '1900-05-03'], dtype='M8[ns]')
+        assert_series_equal(s.mode(), Series([], dtype="M8[ns]"))
+        s = Series(['2011-01-03', '2013-01-02', '1900-05-03', '2011-01-03',
+                    '2013-01-02'], dtype='M8[ns]')
+        assert_series_equal(s.mode(), Series(['2011-01-03', '2013-01-02'],
+                                             dtype='M8[ns]'))
+
     def test_prod(self):
         self._check_stat_op('prod', np.prod)