Skip to content

Commit 7796dff

Browse files
committed
ENH: Implement mode(dropna=False)
1 parent 54470f3 commit 7796dff

File tree

8 files changed

+102
-27
lines changed

8 files changed

+102
-27
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ Other Enhancements
442442
- Updated ``to_gbq`` and ``read_gbq`` signature and documentation to reflect changes from
443443
the Pandas-GBQ library version 0.4.0. Adds intersphinx mapping to Pandas-GBQ
444444
library. (:issue:`20564`)
445+
- :func:`Series.mode` and :func:`DataFrame.mode` now support the ``dropna`` parameter which can be used to specify whether NaN/NaT values should be considered (:issue:`17534`)
445446

446447
.. _whatsnew_0230.api_breaking:
447448

pandas/_libs/hashtable_func_helper.pxi.in

+6-6
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ def ismember_{{dtype}}({{scalar}}[:] arr, {{scalar}}[:] values, bint hasnans=0):
288288
{{py:
289289

290290
# dtype, ctype, table_type, npy_dtype
291-
dtypes = [('int64', 'int64_t', 'int64', 'int64'),
291+
dtypes = [('float64', 'float64_t', 'float64', 'float64'),
292+
('int64', 'int64_t', 'int64', 'int64'),
292293
('uint64', 'uint64_t', 'uint64', 'uint64'),
293294
('object', 'object', 'pymap', 'object_')]
294295
}}
@@ -302,11 +303,11 @@ dtypes = [('int64', 'int64_t', 'int64', 'int64'),
302303
{{if dtype == 'object'}}
303304

304305

305-
def mode_{{dtype}}(ndarray[{{ctype}}] values):
306+
def mode_{{dtype}}(ndarray[{{ctype}}] values, bint dropna):
306307
{{else}}
307308

308309

309-
def mode_{{dtype}}({{ctype}}[:] values):
310+
def mode_{{dtype}}({{ctype}}[:] values, bint dropna):
310311
{{endif}}
311312
cdef:
312313
int count, max_count = 1
@@ -317,9 +318,9 @@ def mode_{{dtype}}({{ctype}}[:] values):
317318

318319
table = kh_init_{{table_type}}()
319320
{{if dtype == 'object'}}
320-
build_count_table_{{dtype}}(values, table, 1)
321+
build_count_table_{{dtype}}(values, table, dropna)
321322
{{else}}
322-
build_count_table_{{dtype}}(values, table, 0)
323+
build_count_table_{{dtype}}(values, table, dropna)
323324
{{endif}}
324325

325326
modes = np.empty(table.n_buckets, dtype=np.{{npy_dtype}})
@@ -329,7 +330,6 @@ def mode_{{dtype}}({{ctype}}[:] values):
329330
for k in range(table.n_buckets):
330331
if kh_exist_{{table_type}}(table, k):
331332
count = table.vals[k]
332-
333333
if count == max_count:
334334
j += 1
335335
elif count > max_count:

pandas/core/algorithms.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
is_bool_dtype, needs_i8_conversion,
2626
is_datetimetz,
2727
is_datetime64_any_dtype, is_datetime64tz_dtype,
28-
is_timedelta64_dtype, is_interval_dtype,
29-
is_scalar, is_list_like,
28+
is_timedelta64_dtype, is_datetimelike,
29+
is_interval_dtype, is_scalar, is_list_like,
3030
_ensure_platform_int, _ensure_object,
3131
_ensure_float64, _ensure_uint64,
3232
_ensure_int64)
@@ -791,14 +791,16 @@ def duplicated(values, keep='first'):
791791
return f(values, keep=keep)
792792

793793

794-
def mode(values):
794+
def mode(values, dropna=True):
795795
"""
796796
Returns the mode(s) of an array.
797797
798798
Parameters
799799
----------
800800
values : array-like
801801
Array over which to check for duplicate values.
802+
dropna : boolean, default True
803+
Don't consider counts of NaN/NaT.
802804
803805
Returns
804806
-------
@@ -811,20 +813,18 @@ def mode(values):
811813

812814
# categorical is a fast-path
813815
if is_categorical_dtype(values):
814-
815816
if isinstance(values, Series):
816-
return Series(values.values.mode(), name=values.name)
817-
return values.mode()
817+
return Series(values.values.mode(dropna=dropna), name=values.name)
818+
return values.mode(dropna=dropna)
818819

819-
values, dtype, ndtype = _ensure_data(values)
820+
if dropna and is_datetimelike(values):
821+
mask = values.isnull()
822+
values = values[~mask]
820823

821-
# TODO: this should support float64
822-
if ndtype not in ['int64', 'uint64', 'object']:
823-
ndtype = 'object'
824-
values = _ensure_object(values)
824+
values, dtype, ndtype = _ensure_data(values)
825825

826826
f = getattr(htable, "mode_{dtype}".format(dtype=ndtype))
827-
result = f(values)
827+
result = f(values, dropna=dropna)
828828
try:
829829
result = np.sort(result)
830830
except TypeError as e:

pandas/core/arrays/categorical.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -2044,20 +2044,28 @@ def max(self, numeric_only=None, **kwargs):
20442044
else:
20452045
return self.categories[pointer]
20462046

2047-
def mode(self):
2047+
def mode(self, dropna=True):
20482048
"""
20492049
Returns the mode(s) of the Categorical.
20502050
20512051
Always returns `Categorical` even if only one value.
20522052
2053+
Parameters
2054+
----------
2055+
dropna : boolean, default True
2056+
Don't consider counts of NaN/NaT.
2057+
20532058
Returns
20542059
-------
20552060
modes : `Categorical` (sorted)
20562061
"""
20572062

20582063
import pandas._libs.hashtable as htable
2059-
good = self._codes != -1
2060-
values = sorted(htable.mode_int64(_ensure_int64(self._codes[good])))
2064+
values = self._codes
2065+
if dropna:
2066+
good = self._codes != -1
2067+
values = self._codes[good]
2068+
values = sorted(htable.mode_int64(_ensure_int64(values), dropna))
20612069
result = self._constructor(values=values, categories=self.categories,
20622070
ordered=self.ordered, fastpath=True)
20632071
return result

pandas/core/frame.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -6992,7 +6992,7 @@ def _get_agg_axis(self, axis_num):
69926992
else:
69936993
raise ValueError('Axis must be 0 or 1 (got %r)' % axis_num)
69946994

6995-
def mode(self, axis=0, numeric_only=False):
6995+
def mode(self, axis=0, numeric_only=False, dropna=True):
69966996
"""
69976997
Gets the mode(s) of each element along the axis selected. Adds a row
69986998
for each mode per label, fills in gaps with nan.
@@ -7010,6 +7010,8 @@ def mode(self, axis=0, numeric_only=False):
70107010
* 1 or 'columns' : get mode of each row
70117011
numeric_only : boolean, default False
70127012
if True, only apply to numeric columns
7013+
dropna : boolean, default True
7014+
Don't consider counts of NaN/NaT.
70137015
70147016
Returns
70157017
-------
@@ -7026,7 +7028,7 @@ def mode(self, axis=0, numeric_only=False):
70267028
data = self if not numeric_only else self._get_numeric_data()
70277029

70287030
def f(s):
7029-
return s.mode()
7031+
return s.mode(dropna=dropna)
70307032

70317033
return data.apply(f, axis=axis)
70327034

pandas/core/series.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1420,17 +1420,22 @@ def count(self, level=None):
14201420
return self._constructor(out, index=lev,
14211421
dtype='int64').__finalize__(self)
14221422

1423-
def mode(self):
1423+
def mode(self, dropna=True):
14241424
"""Return the mode(s) of the dataset.
14251425
14261426
Always returns Series even if only one value is returned.
14271427
1428+
Parameters
1429+
-------
1430+
dropna : boolean, default True
1431+
Don't consider counts of NaN/NaT.
1432+
14281433
Returns
14291434
-------
14301435
modes : Series (sorted)
14311436
"""
14321437
# TODO: Add option for bins like value_counts()
1433-
return algorithms.mode(self)
1438+
return algorithms.mode(self, dropna=dropna)
14341439

14351440
def unique(self):
14361441
"""

pandas/tests/frame/test_analytics.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from pandas.compat import lrange, product, PY35
1616
from pandas import (compat, isna, notna, DataFrame, Series,
1717
MultiIndex, date_range, Timestamp, Categorical,
18-
_np_version_under1p12, _np_version_under1p15)
18+
_np_version_under1p12, _np_version_under1p15,
19+
to_datetime, to_timedelta)
1920
import pandas as pd
2021
import pandas.core.nanops as nanops
2122
import pandas.core.algorithms as algorithms
@@ -889,6 +890,40 @@ def test_mode(self):
889890
dtype=df["C"].dtype)})
890891
tm.assert_frame_equal(df.mode(), exp)
891892

893+
def test_mode_dropna(self):
894+
# GH 17534
895+
# Test the dropna=False parameter for mode
896+
897+
df = pd.DataFrame({"A": [1, np.nan, np.nan, np.nan],
898+
"B": [np.nan, np.nan, 'a', np.nan],
899+
"C": Categorical([np.nan, np.nan, 'a', np.nan]),
900+
"D": to_datetime(['NaT', '2000-1-2', 'NaT', 'NaT']),
901+
"E": to_timedelta(['1 days', 'nan', 'nan', 'nan']),
902+
"F": [1, 1, np.nan, np.nan],
903+
"G": [np.nan, np.nan, 'a', 'a'],
904+
"H": Categorical(['a', np.nan, 'a', np.nan]),
905+
"I": to_datetime(['2000-1-2', '2000-1-2',
906+
'NaT', 'NaT']),
907+
"J": to_timedelta(['1 days', 'nan',
908+
'1 days', 'nan'])})
909+
910+
result = df.loc[:, 'A':'E'].mode(dropna=False)
911+
expected = pd.DataFrame({'A': [np.nan],
912+
'B': np.array([np.nan], dtype=object),
913+
'C': Categorical([np.nan], categories=['a']),
914+
'D': [pd.NaT],
915+
'E': to_timedelta([pd.NaT])})
916+
tm.assert_frame_equal(result, expected)
917+
918+
result = df.loc[:, 'F':'J'].mode(dropna=False)
919+
expected = pd.DataFrame({'F': [1, np.nan],
920+
'G': [np.nan, 'a'],
921+
'H': Categorical([np.nan, 'a'],
922+
categories=['a']),
923+
'I': to_datetime(['NaT', '2000-1-2']),
924+
'J': to_timedelta(['nan', '1 days'])})
925+
tm.assert_frame_equal(result, expected)
926+
892927
def test_operators_timedelta64(self):
893928
from datetime import timedelta
894929
df = DataFrame(dict(A=date_range('2012-1-1', periods=3, freq='D'),

pandas/tests/series/test_analytics.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from pandas import (Series, Categorical, DataFrame, isna, notna,
1414
bdate_range, date_range, _np_version_under1p10,
15-
CategoricalIndex)
15+
CategoricalIndex, to_datetime, to_timedelta)
1616
from pandas.core.index import MultiIndex
1717
from pandas.core.indexes.datetimes import Timestamp
1818
from pandas.core.indexes.timedeltas import Timedelta
@@ -321,6 +321,30 @@ def test_mode(self):
321321
exp = Series(exp, dtype='category')
322322
tm.assert_series_equal(Series(c).mode(), exp)
323323

324+
@pytest.mark.parametrize('values, expected', [
325+
([np.nan, np.nan, 1], [np.nan]),
326+
([np.nan, 1], [1, np.nan]),
327+
([np.nan, np.nan, 'a'], np.array([np.nan], dtype=object)),
328+
([np.nan, 'a'], [np.nan, 'a']),
329+
(Categorical([np.nan, np.nan, 'a']),
330+
Categorical([np.nan], categories=['a'])),
331+
(Categorical([np.nan, 'a']),
332+
Categorical([np.nan, 'a'], categories=['a'])),
333+
(Categorical([np.nan, np.nan, 1]),
334+
Categorical([np.nan], categories=[1])),
335+
(to_datetime(['NaT', '2000-1-2', 'NaT']), [pd.NaT]),
336+
(to_datetime(['NaT', '2000-1-2']), to_datetime(['NaT', '2000-1-2'])),
337+
(to_timedelta(['1 days', 'nan', 'nan']), to_timedelta(['NaT'])),
338+
(to_timedelta(['1 days', 'nan']), to_timedelta(['nan', '1 days']))
339+
])
340+
def test_mode_dropna(self, values, expected):
341+
# GH 17534
342+
# Test the dropna=False parameter for mode
343+
344+
result = Series(values).mode(dropna=False)
345+
expected = Series(expected)
346+
tm.assert_series_equal(result, expected)
347+
324348
def test_prod(self):
325349
self._check_stat_op('prod', np.prod)
326350

0 commit comments

Comments
 (0)