1"""
2data hash pandas / numpy objects
3"""
4
5from __future__ import annotations
6
7import itertools
8from typing import TYPE_CHECKING
9
10import numpy as np
11
12from pandas._libs.hashing import hash_object_array
13
14from pandas.core.dtypes.common import is_list_like
15from pandas.core.dtypes.dtypes import CategoricalDtype
16from pandas.core.dtypes.generic import (
17 ABCDataFrame,
18 ABCExtensionArray,
19 ABCIndex,
20 ABCMultiIndex,
21 ABCSeries,
22)
23
24if TYPE_CHECKING:
25 from collections.abc import (
26 Hashable,
27 Iterable,
28 Iterator,
29 )
30
31 from pandas._typing import (
32 ArrayLike,
33 npt,
34 )
35
36 from pandas import (
37 DataFrame,
38 Index,
39 MultiIndex,
40 Series,
41 )
42
43
44# 16 byte long hashing key
45_default_hash_key = "0123456789123456"
46
47
48def combine_hash_arrays(
49 arrays: Iterator[np.ndarray], num_items: int
50) -> npt.NDArray[np.uint64]:
51 """
52 Parameters
53 ----------
54 arrays : Iterator[np.ndarray]
55 num_items : int
56
57 Returns
58 -------
59 np.ndarray[uint64]
60
61 Should be the same as CPython's tupleobject.c
62 """
63 try:
64 first = next(arrays)
65 except StopIteration:
66 return np.array([], dtype=np.uint64)
67
68 arrays = itertools.chain([first], arrays)
69
70 mult = np.uint64(1000003)
71 out = np.zeros_like(first) + np.uint64(0x345678)
72 last_i = 0
73 for i, a in enumerate(arrays):
74 inverse_i = num_items - i
75 out ^= a
76 out *= mult
77 mult += np.uint64(82520 + inverse_i + inverse_i)
78 last_i = i
79 assert last_i + 1 == num_items, "Fed in wrong num_items"
80 out += np.uint64(97531)
81 return out
82
83
84def hash_pandas_object(
85 obj: Index | DataFrame | Series,
86 index: bool = True,
87 encoding: str = "utf8",
88 hash_key: str | None = _default_hash_key,
89 categorize: bool = True,
90) -> Series:
91 """
92 Return a data hash of the Index/Series/DataFrame.
93
94 The hash is computed element-wise using the underlying data values,
95 and optionally includes the index when hashing a Series or DataFrame.
96
97 Parameters
98 ----------
99 obj : Index, Series, or DataFrame
100 The pandas object to hash.
101 index : bool, default True
102 Include the index in the hash (if Series/DataFrame).
103 encoding : str, default 'utf8'
104 Encoding for data & key when strings.
105 hash_key : str, default _default_hash_key
106 Hash_key for string key to encode.
107 categorize : bool, default True
108 Whether to first categorize object arrays before hashing. This is more
109 efficient when the array contains duplicate values.
110
111 Returns
112 -------
113 Series of uint64
114 Same length as the object.
115
116 See Also
117 --------
118 util.hash_array : Return a hash of the given array.
119 util.hash_tuples : Hash a MultiIndex or listlike-of-tuples efficiently.
120
121 Examples
122 --------
123 >>> pd.util.hash_pandas_object(pd.Series([1, 2, 3]))
124 0 14639053686158035780
125 1 3869563279212530728
126 2 393322362522515241
127 dtype: uint64
128 """
129 from pandas import Series
130
131 if hash_key is None:
132 hash_key = _default_hash_key
133
134 if isinstance(obj, ABCMultiIndex):
135 return Series(hash_tuples(obj, encoding, hash_key), dtype="uint64", copy=False)
136
137 elif isinstance(obj, ABCIndex):
138 h = hash_array(obj._values, encoding, hash_key, categorize).astype(
139 "uint64", copy=False
140 )
141 ser = Series(h, index=obj, dtype="uint64", copy=False)
142
143 elif isinstance(obj, ABCSeries):
144 h = hash_array(obj._values, encoding, hash_key, categorize).astype(
145 "uint64", copy=False
146 )
147 if index:
148 index_iter = (
149 hash_pandas_object(
150 obj.index,
151 index=False,
152 encoding=encoding,
153 hash_key=hash_key,
154 categorize=categorize,
155 )._values
156 for _ in [None]
157 )
158 arrays = itertools.chain([h], index_iter)
159 h = combine_hash_arrays(arrays, 2)
160
161 ser = Series(h, index=obj.index, dtype="uint64", copy=False)
162
163 elif isinstance(obj, ABCDataFrame):
164 hashes = (
165 hash_array(series._values, encoding, hash_key, categorize)
166 for _, series in obj.items()
167 )
168 num_items = len(obj.columns)
169 if index:
170 index_hash_generator = (
171 hash_pandas_object(
172 obj.index,
173 index=False,
174 encoding=encoding,
175 hash_key=hash_key,
176 categorize=categorize,
177 )._values
178 for _ in [None]
179 )
180 num_items += 1
181
182 # keep `hashes` specifically a generator to keep mypy happy
183 _hashes = itertools.chain(hashes, index_hash_generator)
184 hashes = (x for x in _hashes)
185 h = combine_hash_arrays(hashes, num_items)
186
187 ser = Series(h, index=obj.index, dtype="uint64", copy=False)
188 else:
189 raise TypeError(f"Unexpected type for hashing {type(obj)}")
190
191 return ser
192
193
194def hash_tuples(
195 vals: MultiIndex | Iterable[tuple[Hashable, ...]],
196 encoding: str = "utf8",
197 hash_key: str = _default_hash_key,
198) -> npt.NDArray[np.uint64]:
199 """
200 Hash a MultiIndex / listlike-of-tuples efficiently.
201
202 Parameters
203 ----------
204 vals : MultiIndex or listlike-of-tuples
205 encoding : str, default 'utf8'
206 hash_key : str, default _default_hash_key
207
208 Returns
209 -------
210 ndarray[np.uint64] of hashed values
211 """
212 if not is_list_like(vals):
213 raise TypeError("must be convertible to a list-of-tuples")
214
215 from pandas import (
216 Categorical,
217 MultiIndex,
218 )
219
220 if not isinstance(vals, ABCMultiIndex):
221 mi = MultiIndex.from_tuples(vals)
222 else:
223 mi = vals
224
225 # create a list-of-Categoricals
226 cat_vals = [
227 Categorical._simple_new(
228 mi.codes[level],
229 CategoricalDtype(categories=mi.levels[level], ordered=False),
230 )
231 for level in range(mi.nlevels)
232 ]
233
234 # hash the list-of-ndarrays
235 hashes = (
236 cat._hash_pandas_object(encoding=encoding, hash_key=hash_key, categorize=False)
237 for cat in cat_vals
238 )
239 h = combine_hash_arrays(hashes, len(cat_vals))
240
241 return h
242
243
244def hash_array(
245 vals: ArrayLike,
246 encoding: str = "utf8",
247 hash_key: str = _default_hash_key,
248 categorize: bool = True,
249) -> npt.NDArray[np.uint64]:
250 """
251 Given a 1d array, return an array of deterministic integers.
252
253 Parameters
254 ----------
255 vals : ndarray or ExtensionArray
256 The input array to hash.
257 encoding : str, default 'utf8'
258 Encoding for data & key when strings.
259 hash_key : str, default _default_hash_key
260 Hash_key for string key to encode.
261 categorize : bool, default True
262 Whether to first categorize object arrays before hashing. This is more
263 efficient when the array contains duplicate values.
264
265 Returns
266 -------
267 ndarray[np.uint64, ndim=1]
268 Hashed values, same length as the vals.
269
270 See Also
271 --------
272 util.hash_pandas_object : Return a data hash of the Index/Series/DataFrame.
273 util.hash_tuples : Hash a MultiIndex / listlike-of-tuples efficiently.
274
275 Examples
276 --------
277 >>> pd.util.hash_array(np.array([1, 2, 3]))
278 array([ 6238072747940578789, 15839785061582574730, 2185194620014831856],
279 dtype=uint64)
280 """
281 if not hasattr(vals, "dtype"):
282 raise TypeError("must pass an ndarray-like")
283
284 if isinstance(vals, ABCExtensionArray):
285 return vals._hash_pandas_object(
286 encoding=encoding, hash_key=hash_key, categorize=categorize
287 )
288
289 if not isinstance(vals, np.ndarray):
290 # GH#42003
291 raise TypeError(
292 "hash_array requires np.ndarray or ExtensionArray, not "
293 f"{type(vals).__name__}. Use hash_pandas_object instead."
294 )
295
296 return _hash_ndarray(vals, encoding, hash_key, categorize)
297
298
299def _hash_ndarray(
300 vals: np.ndarray,
301 encoding: str = "utf8",
302 hash_key: str = _default_hash_key,
303 categorize: bool = True,
304) -> npt.NDArray[np.uint64]:
305 """
306 See hash_array.__doc__.
307 """
308 dtype = vals.dtype
309
310 # _hash_ndarray only takes 64-bit values, so handle 128-bit by parts
311 if np.issubdtype(dtype, np.complex128):
312 hash_real = _hash_ndarray(vals.real, encoding, hash_key, categorize)
313 hash_imag = _hash_ndarray(vals.imag, encoding, hash_key, categorize)
314 return hash_real + 23 * hash_imag
315
316 # First, turn whatever array this is into unsigned 64-bit ints, if we can
317 # manage it.
318 if dtype == bool:
319 vals = vals.astype("u8")
320 elif issubclass(dtype.type, (np.datetime64, np.timedelta64)):
321 vals = vals.view("i8").astype("u8", copy=False)
322 elif issubclass(dtype.type, np.number) and dtype.itemsize <= 8:
323 vals = vals.view(f"u{vals.dtype.itemsize}").astype("u8")
324 else:
325 # With repeated values, its MUCH faster to categorize object dtypes,
326 # then hash and rename categories. We allow skipping the categorization
327 # when the values are known/likely to be unique.
328 if categorize:
329 from pandas import (
330 Categorical,
331 Index,
332 factorize,
333 )
334
335 codes, categories = factorize(vals, sort=False)
336 tdtype = CategoricalDtype(
337 categories=Index(categories, copy=False), ordered=False
338 )
339 cat = Categorical._simple_new(codes, tdtype)
340 return cat._hash_pandas_object(
341 encoding=encoding, hash_key=hash_key, categorize=False
342 )
343
344 try:
345 vals = hash_object_array(vals, hash_key, encoding)
346 except TypeError:
347 # we have mixed types
348 vals = hash_object_array(
349 vals.astype(str).astype(object), hash_key, encoding
350 )
351
352 # Then, redistribute these 64-bit ints within the space of 64-bit ints
353 vals ^= vals >> 30
354 vals *= np.uint64(0xBF58476D1CE4E5B9)
355 vals ^= vals >> 27
356 vals *= np.uint64(0x94D049BB133111EB)
357 vals ^= vals >> 31
358 # error: Incompatible return value type (got "Any | ndarray[tuple[int, ...],
359 # dtype[signedinteger[Any]]]", expected "ndarray[tuple[int, ...],
360 # dtype[unsignedinteger[_64Bit]]]")
361 return vals # type: ignore[return-value]