Skip to content

Commit d6eb71c

Browse files
jbrockmendelclaude
andcommitted
PERF: minor optimizations in _libs.algos
- unique_deltas: use malloc'd C array + memcpy instead of Python list to avoid per-element GIL interaction - is_monotonic: remove redundant assignments before break - rank_1d: skip get_rank_nan_fill_val + putmask when check_mask is False Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d2f3fcc commit d6eb71c

2 files changed

Lines changed: 20 additions & 8 deletions

File tree

doc/source/whatsnew/v3.1.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ Performance improvements
142142
- Performance improvement in :meth:`DataFrame.insert` when the number of blocks is small (:issue:`57641`)
143143
- Performance improvement in :meth:`DataFrame.loc` with non-unique masked index (:issue:`56759`)
144144
- Performance improvement in :meth:`DataFrame.query` and :meth:`DataFrame.eval` when the :class:`DataFrame` contains :class:`PeriodDtype` or :class:`IntervalDtype` columns (:issue:`35247`)
145+
- Performance improvement in :meth:`DataFrame.rank` and :meth:`Series.rank` by skipping unnecessary ``putmask`` for non-nullable dtypes (:issue:`64857`)
145146
- Performance improvement in :meth:`DataFrame.sort_values` with multiple numeric columns by avoiding unnecessary :class:`Categorical` conversion (:issue:`15389`)
146147
- Performance improvement in :meth:`DataFrame.to_stata` when writing object-dtype datetime columns with date formats that require year/month extraction (:issue:`64555`)
147148
- Performance improvement in :meth:`GroupBy.any` and :meth:`GroupBy.all` for boolean-dtype columns (:issue:`37850`)

pandas/_libs/algos.pyx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,25 +136,37 @@ cpdef ndarray[int64_t, ndim=1] unique_deltas(const int64_t[:] arr):
136136
An ordered ndarray[int64_t]
137137
"""
138138
cdef:
139-
Py_ssize_t i, n = len(arr)
139+
Py_ssize_t i, n = len(arr), num_uniques = 0
140140
int64_t val
141141
khiter_t k
142142
kh_int64_t *table
143143
int ret = 0
144-
list uniques = []
144+
int64_t *uniques = NULL
145145
ndarray[int64_t, ndim=1] result
146146

147147
table = kh_init_int64()
148148
kh_resize_int64(table, 10)
149+
150+
# n - 1 is the max possible number of unique deltas
151+
if n > 1:
152+
uniques = <int64_t*>malloc((n - 1) * sizeof(int64_t))
153+
if uniques is NULL:
154+
kh_destroy_int64(table)
155+
raise MemoryError()
156+
149157
for i in range(n - 1):
150158
val = arr[i + 1] - arr[i]
151159
k = kh_get_int64(table, val)
152160
if k == table.n_buckets:
153161
kh_put_int64(table, val, &ret)
154-
uniques.append(val)
162+
uniques[num_uniques] = val
163+
num_uniques += 1
155164
kh_destroy_int64(table)
156165

157-
result = np.array(uniques, dtype=np.int64)
166+
result = np.empty(num_uniques, dtype=np.int64)
167+
if num_uniques > 0:
168+
memcpy(cnp.PyArray_DATA(result), uniques, num_uniques * sizeof(int64_t))
169+
free(uniques)
158170
result.sort()
159171
return result
160172

@@ -859,8 +871,6 @@ def is_monotonic(const numeric_object_t[:] arr, bint timelike):
859871
is_monotonic_dec = 0
860872
break
861873
if not is_monotonic_inc and not is_monotonic_dec:
862-
is_monotonic_inc = 0
863-
is_monotonic_dec = 0
864874
break
865875
prev = cur
866876

@@ -1038,7 +1048,6 @@ def rank_1d(
10381048
# will flip the ordering to still end up with lowest rank.
10391049
# Symmetric logic applies to `na_option == 'bottom'`
10401050
nans_rank_highest = ascending ^ (na_option == "top")
1041-
nan_fill_val = get_rank_nan_fill_val(nans_rank_highest, <numeric_object_t>0)
10421051
if nans_rank_highest:
10431052
order = [masked_vals, mask]
10441053
else:
@@ -1047,7 +1056,9 @@ def rank_1d(
10471056
if check_labels:
10481057
order.append(labels)
10491058

1050-
np.putmask(masked_vals, mask, nan_fill_val)
1059+
if check_mask:
1060+
nan_fill_val = get_rank_nan_fill_val(nans_rank_highest, <numeric_object_t>0)
1061+
np.putmask(masked_vals, mask, nan_fill_val)
10511062
# putmask doesn't accept a memoryview, so we assign as a separate step
10521063
masked_vals_memview = masked_vals
10531064

0 commit comments

Comments
 (0)