Skip to content

Commit b51f879

Browse files
committed
DOC, TST, BUG: Improve uint64 core/algos behavior
1) duplicated() Updates documentation to describe the "values" parameter in the signature, adds tests for uint64, and refactors to use duplicated_uint64. 2) mode() Updates documentation to describe the "values" parameter in the signature, adds tests for uint64, and reactors to use mode_uint64. 3) unique() Uses UInt64HashTable to patch a uint64 overflow bug analogous to that seen in Series.unique (patched in pandas-devgh-14915). 4) Types API Introduces "is_signed_integer_dtype" and "is_unsigned _integer_dtype" to the public API. Used in refactoring/ patching of 1-3.
1 parent 1678f14 commit b51f879

File tree

7 files changed

+105
-11
lines changed

7 files changed

+105
-11
lines changed

doc/source/whatsnew/v0.20.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -289,5 +289,6 @@ Bug Fixes
289289

290290

291291
- Bug in ``Series.unique()`` in which unsigned 64-bit integers were causing overflow (:issue:`14721`)
292+
- Bug in ``pd.unique()`` in which unsigned 64-bit integers were causing overflow (:issue:`14915`)
292293
- Require at least 0.23 version of cython to avoid problems with character encodings (:issue:`14699`)
293294
- Bug in converting object elements of array-like objects to unsigned 64-bit integers (:issue:`4471`)

pandas/api/tests/test_api.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,10 @@ class TestTypes(Base, tm.TestCase):
153153
'is_floating_dtype', 'is_int64_dtype', 'is_integer',
154154
'is_integer_dtype', 'is_number', 'is_numeric_dtype',
155155
'is_object_dtype', 'is_scalar', 'is_sparse',
156-
'is_string_dtype',
156+
'is_string_dtype', 'is_signed_integer_dtype',
157157
'is_timedelta64_dtype', 'is_timedelta64_ns_dtype',
158-
'is_period', 'is_period_dtype',
159-
'is_re', 'is_re_compilable',
158+
'is_unsigned_integer_dtype', 'is_period',
159+
'is_period_dtype', 'is_re', 'is_re_compilable',
160160
'is_dict_like', 'is_iterator',
161161
'is_list_like', 'is_hashable',
162162
'is_named_tuple', 'is_sequence',

pandas/core/algorithms.py

+30-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from pandas import compat, lib, tslib, _np_version_under1p8
1010
from pandas.types.cast import _maybe_promote
1111
from pandas.types.generic import ABCSeries, ABCIndex
12-
from pandas.types.common import (is_integer_dtype,
12+
from pandas.types.common import (is_unsigned_integer_dtype,
13+
is_signed_integer_dtype,
14+
is_integer_dtype,
1315
is_int64_dtype,
1416
is_categorical_dtype,
1517
is_extension_type,
@@ -490,12 +492,14 @@ def _value_counts_arraylike(values, dropna=True):
490492

491493
def duplicated(values, keep='first'):
492494
"""
493-
Return boolean ndarray denoting duplicate values
495+
Return boolean ndarray denoting duplicate values.
494496
495497
.. versionadded:: 0.19.0
496498
497499
Parameters
498500
----------
501+
values : ndarray-like
502+
Array over which to check for duplicate values.
499503
keep : {'first', 'last', False}, default 'first'
500504
- ``first`` : Mark duplicates as ``True`` except for the first
501505
occurrence.
@@ -521,9 +525,12 @@ def duplicated(values, keep='first'):
521525
elif isinstance(values, (ABCSeries, ABCIndex)):
522526
values = values.values
523527

524-
if is_integer_dtype(dtype):
528+
if is_signed_integer_dtype(dtype):
525529
values = _ensure_int64(values)
526530
duplicated = htable.duplicated_int64(values, keep=keep)
531+
elif is_unsigned_integer_dtype(dtype):
532+
values = _ensure_uint64(values)
533+
duplicated = htable.duplicated_uint64(values, keep=keep)
527534
elif is_float_dtype(dtype):
528535
values = _ensure_float64(values)
529536
duplicated = htable.duplicated_float64(values, keep=keep)
@@ -535,7 +542,19 @@ def duplicated(values, keep='first'):
535542

536543

537544
def mode(values):
538-
"""Returns the mode or mode(s) of the passed Series or ndarray (sorted)"""
545+
"""
546+
Returns the mode(s) of an array.
547+
548+
Parameters
549+
----------
550+
values : array-like
551+
Array over which to check for duplicate values.
552+
553+
Returns
554+
-------
555+
mode : Series
556+
"""
557+
539558
# must sort because hash order isn't necessarily defined.
540559
from pandas.core.series import Series
541560

@@ -547,10 +566,12 @@ def mode(values):
547566
constructor = Series
548567

549568
dtype = values.dtype
550-
if is_integer_dtype(values):
569+
if is_signed_integer_dtype(values):
551570
values = _ensure_int64(values)
552571
result = constructor(sorted(htable.mode_int64(values)), dtype=dtype)
553-
572+
elif is_unsigned_integer_dtype(values):
573+
values = _ensure_uint64(values)
574+
result = constructor(sorted(htable.mode_uint64(values)), dtype=dtype)
554575
elif issubclass(values.dtype.type, (np.datetime64, np.timedelta64)):
555576
dtype = values.dtype
556577
values = values.view(np.int64)
@@ -893,8 +914,10 @@ def _hashtable_algo(f, values, return_dtype=None):
893914
dtype = values.dtype
894915
if is_float_dtype(dtype):
895916
return f(htable.Float64HashTable, _ensure_float64)
896-
elif is_integer_dtype(dtype):
917+
elif is_signed_integer_dtype(dtype):
897918
return f(htable.Int64HashTable, _ensure_int64)
919+
elif is_unsigned_integer_dtype(dtype):
920+
return f(htable.UInt64HashTable, _ensure_uint64)
898921
elif is_datetime64_dtype(dtype):
899922
return_dtype = return_dtype or 'M8[ns]'
900923
return f(htable.Int64HashTable, _ensure_int64).view(return_dtype)

pandas/hashtable.pyx

+35
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,41 @@ def mode_int64(int64_t[:] values):
234234
return modes[:j + 1]
235235

236236

237+
@cython.wraparound(False)
238+
@cython.boundscheck(False)
239+
def mode_uint64(uint64_t[:] values):
240+
cdef:
241+
int count, max_count = 2
242+
int j = -1 # so you can do +=
243+
int k
244+
kh_uint64_t *table
245+
ndarray[uint64_t] modes
246+
247+
table = kh_init_uint64()
248+
249+
build_count_table_uint64(values, table, 0)
250+
251+
modes = np.empty(table.n_buckets, dtype=np.uint64)
252+
253+
with nogil:
254+
for k in range(table.n_buckets):
255+
if kh_exist_uint64(table, k):
256+
count = table.vals[k]
257+
258+
if count == max_count:
259+
j += 1
260+
elif count > max_count:
261+
max_count = count
262+
j = 0
263+
else:
264+
continue
265+
modes[j] = table.keys[k]
266+
267+
kh_destroy_uint64(table)
268+
269+
return modes[:j + 1]
270+
271+
237272
@cython.wraparound(False)
238273
@cython.boundscheck(False)
239274
def duplicated_object(ndarray[object] values, object keep='first'):

pandas/tests/test_algos.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,11 @@ def test_timedelta64_dtype_array_returned(self):
365365
tm.assert_numpy_array_equal(result, expected)
366366
self.assertEqual(result.dtype, expected.dtype)
367367

368+
def test_uint64_overflow(self):
369+
s = pd.Series([1, 2, 2**63, 2**63], dtype=np.uint64)
370+
exp = np.array([1, 2, 2**63], dtype=np.uint64)
371+
tm.assert_numpy_array_equal(algos.unique(s), exp)
372+
368373

369374
class TestIsin(tm.TestCase):
370375
_multiprocess_can_split_ = True
@@ -672,7 +677,9 @@ def test_numeric_object_likes(self):
672677
np.array([1 + 1j, 2 + 2j, 1 + 1j, 5 + 5j, 3 + 3j,
673678
2 + 2j, 4 + 4j, 1 + 1j, 5 + 5j, 6 + 6j]),
674679
np.array(['a', 'b', 'a', 'e', 'c',
675-
'b', 'd', 'a', 'e', 'f'], dtype=object)]
680+
'b', 'd', 'a', 'e', 'f'], dtype=object),
681+
np.array([1, 2**63, 1, 3**5, 10,
682+
2**63, 39, 1, 3**5, 7], dtype=np.uint64)]
676683

677684
exp_first = np.array([False, False, True, False, False,
678685
True, False, True, True, False])
@@ -1202,6 +1209,20 @@ def test_int64_add_overflow():
12021209
b_mask=np.array([False, True]))
12031210

12041211

1212+
def test_mode_uint64():
1213+
s = Series([1, 2**63, 2**63], dtype=np.uint64)
1214+
exp = Series([2**63], dtype=np.uint64)
1215+
1216+
tm.assert_series_equal(algos.mode(s), exp)
1217+
tm.assert_series_equal(s.mode(), exp)
1218+
1219+
s = Series([1, 2**63], dtype=np.uint64)
1220+
exp = Series([], dtype=np.uint64)
1221+
1222+
tm.assert_series_equal(algos.mode(s), exp)
1223+
tm.assert_series_equal(s.mode(), exp)
1224+
1225+
12051226
if __name__ == '__main__':
12061227
import nose
12071228
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],

pandas/types/api.py

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
is_floating_dtype,
4545
is_bool_dtype,
4646
is_complex_dtype,
47+
is_signed_integer_dtype,
48+
is_unsigned_integer_dtype,
4749

4850
# like
4951
is_re,

pandas/types/common.py

+12
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ def is_integer_dtype(arr_or_dtype):
155155
not issubclass(tipo, (np.datetime64, np.timedelta64)))
156156

157157

158+
def is_signed_integer_dtype(arr_or_dtype):
159+
tipo = _get_dtype_type(arr_or_dtype)
160+
return (issubclass(tipo, np.signedinteger) and
161+
not issubclass(tipo, (np.datetime64, np.timedelta64)))
162+
163+
164+
def is_unsigned_integer_dtype(arr_or_dtype):
165+
tipo = _get_dtype_type(arr_or_dtype)
166+
return (issubclass(tipo, np.unsignedinteger) and
167+
not issubclass(tipo, (np.datetime64, np.timedelta64)))
168+
169+
158170
def is_int64_dtype(arr_or_dtype):
159171
tipo = _get_dtype_type(arr_or_dtype)
160172
return issubclass(tipo, np.int64)

0 commit comments

Comments
 (0)