Polars plugin for stable hashing functionality
See the codeThis plugin provides stable hashing functionality across different polars versions.
π Documentation β every expression, its input and output types, and its arguments.
import polars as pl
import polars_hash as plh
df = pl.DataFrame({
"foo":["hello_world"]
})
result = df.select(plh.col('foo').chash.sha2_256())
print(result)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β foo β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ‘
β 35072c1ae546350e0bfa7ab11d49dc6f129e72ccd57ec7eb671225bbd197c8f1 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
df = pl.DataFrame({
"foo":["hello_world"]
})
result = df.select(plh.col('foo').nchash.wyhash())
print(result)
ββββββββββββββββββββββββ
β foo β
β --- β
β u64 β
ββββββββββββββββββββββββ‘
β 16737367591072095403 β
ββββββββββββββββββββββββ
result = df.select(plh.col('foo').nchash.farmhash64())
print(result)
ββββββββββββββββββββββββ
β foo β
β --- β
β u64 β
ββββββββββββββββββββββββ‘
β 15605398435621216523 β
ββββββββββββββββββββββββ
result = df.select(plh.col('foo').nchash.farmhash32())
print(result)
ββββββββββββββ
β foo β
β --- β
β u32 β
ββββββββββββββ‘
β 1719156559 β
ββββββββββββββ
result = df.select(plh.col('foo').nchash.cityhash128())
print(result)
βββββββββββββββββββββββββββββββββββββββββββ
β foo β
β --- β
β u128 β
βββββββββββββββββββββββββββββββββββββββββββ‘
β 133423608296839006301901834072762183026 β
βββββββββββββββββββββββββββββββββββββββββββ
result = df.select(plh.col('foo').nchash.gxhash64())
print(result)
βββββββββββββββββββββββ
β foo β
β --- β
β u64 β
βββββββββββββββββββββββ‘
β 2180020304351407825 β
βββββββββββββββββββββββ
cityhash32() and cityhash64() return the values printed above for farmhash32()
and farmhash64(). That is expected: FarmHash reuses CityHash for short input, and
hello_world is 11 bytes. See
the CityHash reference.
The GxHash expressions need a CPU with AES instructions and have no software fallback.
Every x86, x86-64 and aarch64 wheel is built for them; there are no linux-armv7 or
linux-ppc64le wheels from 0.8.0 on, because GxHash cannot be built for either. See
the GxHash reference.
bytes writes a value as its own bytes, so that a hasher reads the value itself and
not a string form of it. Each type keeps its own width: Int32 makes 4 bytes and
Float64 makes 8. Utf8 and Binary have no byte order of their own and pass
through unchanged.
df = pl.DataFrame({"literal": [1]}, schema={"literal": pl.Int32})
result = df.select(
plh.col('literal').bytes.to_le().alias('le'),
plh.col('literal').bytes.to_be().alias('be'),
)
print(result)
βββββββββββββββββββββββ¬ββββββββββββββββββββββ
β le β be β
β --- β --- β
β binary β binary β
βββββββββββββββββββββββͺββββββββββββββββββββββ‘
β b"\x01\x00\x00\x00" β b"\x00\x00\x00\x01" β
βββββββββββββββββββββββ΄ββββββββββββββββββββββ
Send the result to a hasher for a hash of the value and not of its text:
plh.col("id").cast(pl.Int64).bytes.to_le().nchash.murmur32()
df = pl.DataFrame(
{"coord": [{"longitude": -120.6623, "latitude": 35.3003}]},
schema={
"coord": pl.Struct(
[pl.Field("longitude", pl.Float64), pl.Field("latitude", pl.Float64)]
),
},
)
df.with_columns(
plh.col('coord').geohash.from_coords().alias('geohash')
)
shape: (1, 2)
βββββββββββββββββββββββ¬βββββββββββββββ
β coord β geohash β
β --- β --- β
β struct[2] β str β
βββββββββββββββββββββββͺβββββββββββββββ‘
β {-120.6623,35.3003} β 9q60y60rhsgg β
βββββββββββββββββββββββ΄βββββββββββββββ
pl.select(pl.lit('9q60y60rhs').geohash.to_coords().alias('coordinates'))
shape: (1, 1)
βββββββββββββββββββββββββ
β coordinates β
β --- β
β struct[2] β
βββββββββββββββββββββββββ‘
β {-120.6623,35.300298} β
βββββββββββββββββββββββββ
df = pl.DataFrame(
{"coord": [{"longitude": -120.6623, "latitude": 35.3003}]},
schema={
"coord": pl.Struct(
[pl.Field("longitude", pl.Float64), pl.Field("latitude", pl.Float64)]
),
},
)
df.with_columns(
plh.col('coord').h3.from_coords().alias('h3')
)
shape: (1, 2)
βββββββββββββββββββββββ¬ββββββββββββββββββ
β coord β h3 β
β --- β --- β
β struct[2] β str β
βββββββββββββββββββββββͺββββββββββββββββββ‘
β {-120.6623,35.3003} β 8c29adc423821ff β
βββββββββββββββββββββββ΄ββββββββββββββββββ
Bins timestamps into variable-precision sliding windows of time, so rows that fall in the same window share a hash. Timestamps must lie between 1970-01-01 and 2098-01-01. A higher precision means a shorter window: 10 covers about 4 seconds, 8 about 4 minutes.
Precision may be 1 to 32, but past about 18 the hash stops changing for present-day timestamps and the extra characters are padding. The exact point depends on the date: timestamps close to 1970 keep splitting to about 21, far-future ones run out sooner.
from datetime import datetime
df = pl.DataFrame({"datetime": [datetime(2017, 2, 21, 20, 15, 13)]})
df.with_columns(
plh.col('datetime').timehash.from_datetime().alias('timehash')
)
shape: (1, 2)
βββββββββββββββββββββββ¬βββββββββββββ
β datetime β timehash β
β --- β --- β
β datetime[ΞΌs] β str β
βββββββββββββββββββββββͺβββββββββββββ‘
β 2017-02-21 20:15:13 β afcccc0e1b β
βββββββββββββββββββββββ΄βββββββββββββ
pl.select(pl.lit('afcccc0e1b').timehash.to_datetime().alias('datetime'))
shape: (1, 1)
ββββββββββββββββββββββββββββββββββ
β datetime β
β --- β
β datetime[ΞΌs, UTC] β
ββββββββββββββββββββββββββββββββββ‘
β 2017-02-21 20:15:11.292315 UTC β
ββββββββββββββββββββββββββββββββββ
pl.select(pl.lit('afcccc0e1b').timehash.neighbors().alias('neighbors'))
shape: (1, 1)
βββββββββββββββββββββββββββββββ
β neighbors β
β --- β
β struct[2] β
βββββββββββββββββββββββββββββββ‘
β {"afcccc0e1a","afcccc0e1c"} β
βββββββββββββββββββββββββββββββ
uuidhash makes UUID version 5 values (RFC 4122). A v5 UUID is a SHA-1 digest of a
namespace UUID and a name, so the same namespace and the same name always give the same
UUID. A null input gives null.
df = pl.DataFrame({"literal": ["hello", None, "world"]})
df.select(plh.col('literal').uuidhash.uuid5())
shape: (3, 1)
ββββββββββββββββββββββββββββββββββββββββ
β literal β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββ‘
β 9342d47a-1bab-5709-9869-c840b2eac501 β
β null β
β b3a4c24e-f57a-5448-b81b-a643f6768036 β
ββββββββββββββββββββββββββββββββββββββββ
pl.select(pl.lit('https://example.com').uuidhash.uuid5('url').alias('uuid'))
shape: (1, 1)
ββββββββββββββββββββββββββββββββββββββββ
β uuid β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββ‘
β 4fd35a71-71ef-5a55-a9d9-aa75c889a6d0 β
ββββββββββββββββββββββββββββββββββββββββ
The namespace is "dns", "url", "oid", "x500", or a custom UUID of your own.
Give a separator value. Without one, ("ab", "c") and ("a", "bc") make the same
string and therefore the same digest.
df = pl.DataFrame({"foo": ["hello_world"], "bar": ["today"]})
result = df.select(plh.concat_str("foo", "bar", separator="|").chash.sha2_256())
To hash a row of any column type, and not only strings, use hash_rows below.
hash_rows gives each row bytes that no other row can make, for all column types.
Any hasher then reads those bytes.
df = pl.DataFrame(
{"foo": ["hello_world"], "bar": [42], "baz": [[1, 2, 3]], "qux": [{"a": 1}]}
)
df.select(plh.hash_rows(pl.all()).chash.sha2_256())
shape: (1, 1)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β foo β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ‘
β 9055866af8d3c113e0a8fdb729ce8e6fa67ed5f6f51efa8235a588e88ea972f4 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The encoder reads the meaning of a value, not the polars storage of it. An Int32 and
the Int64 next to it make the same hash. A Datetime in milliseconds and the same
time in nanoseconds also make the same hash, and a Categorical makes the hash of its
string. The encoder does not read the column names. Therefore a new name keeps the
hash, but a new order does not. The
reference
gives all the rules and the byte layout of version 1, which does not change.
Python
67.4%
Rust
31.1%
Makefile
1.5%
Polars plugin for stable hashing functionality
See the codeThis plugin provides stable hashing functionality across different polars versions.
π Documentation β every expression, its input and output types, and its arguments.
import polars as pl
import polars_hash as plh
df = pl.DataFrame({
"foo":["hello_world"]
})
result = df.select(plh.col('foo').chash.sha2_256())
print(result)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β foo β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ‘
β 35072c1ae546350e0bfa7ab11d49dc6f129e72ccd57ec7eb671225bbd197c8f1 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
df = pl.DataFrame({
"foo":["hello_world"]
})
result = df.select(plh.col('foo').nchash.wyhash())
print(result)
ββββββββββββββββββββββββ
β foo β
β --- β
β u64 β
ββββββββββββββββββββββββ‘
β 16737367591072095403 β
ββββββββββββββββββββββββ
result = df.select(plh.col('foo').nchash.farmhash64())
print(result)
ββββββββββββββββββββββββ
β foo β
β --- β
β u64 β
ββββββββββββββββββββββββ‘
β 15605398435621216523 β
ββββββββββββββββββββββββ
result = df.select(plh.col('foo').nchash.farmhash32())
print(result)
ββββββββββββββ
β foo β
β --- β
β u32 β
ββββββββββββββ‘
β 1719156559 β
ββββββββββββββ
result = df.select(plh.col('foo').nchash.cityhash128())
print(result)
βββββββββββββββββββββββββββββββββββββββββββ
β foo β
β --- β
β u128 β
βββββββββββββββββββββββββββββββββββββββββββ‘
β 133423608296839006301901834072762183026 β
βββββββββββββββββββββββββββββββββββββββββββ
result = df.select(plh.col('foo').nchash.gxhash64())
print(result)
βββββββββββββββββββββββ
β foo β
β --- β
β u64 β
βββββββββββββββββββββββ‘
β 2180020304351407825 β
βββββββββββββββββββββββ
cityhash32() and cityhash64() return the values printed above for farmhash32()
and farmhash64(). That is expected: FarmHash reuses CityHash for short input, and
hello_world is 11 bytes. See
the CityHash reference.
The GxHash expressions need a CPU with AES instructions and have no software fallback.
Every x86, x86-64 and aarch64 wheel is built for them; there are no linux-armv7 or
linux-ppc64le wheels from 0.8.0 on, because GxHash cannot be built for either. See
the GxHash reference.
bytes writes a value as its own bytes, so that a hasher reads the value itself and
not a string form of it. Each type keeps its own width: Int32 makes 4 bytes and
Float64 makes 8. Utf8 and Binary have no byte order of their own and pass
through unchanged.
df = pl.DataFrame({"literal": [1]}, schema={"literal": pl.Int32})
result = df.select(
plh.col('literal').bytes.to_le().alias('le'),
plh.col('literal').bytes.to_be().alias('be'),
)
print(result)
βββββββββββββββββββββββ¬ββββββββββββββββββββββ
β le β be β
β --- β --- β
β binary β binary β
βββββββββββββββββββββββͺββββββββββββββββββββββ‘
β b"\x01\x00\x00\x00" β b"\x00\x00\x00\x01" β
βββββββββββββββββββββββ΄ββββββββββββββββββββββ
Send the result to a hasher for a hash of the value and not of its text:
plh.col("id").cast(pl.Int64).bytes.to_le().nchash.murmur32()
df = pl.DataFrame(
{"coord": [{"longitude": -120.6623, "latitude": 35.3003}]},
schema={
"coord": pl.Struct(
[pl.Field("longitude", pl.Float64), pl.Field("latitude", pl.Float64)]
),
},
)
df.with_columns(
plh.col('coord').geohash.from_coords().alias('geohash')
)
shape: (1, 2)
βββββββββββββββββββββββ¬βββββββββββββββ
β coord β geohash β
β --- β --- β
β struct[2] β str β
βββββββββββββββββββββββͺβββββββββββββββ‘
β {-120.6623,35.3003} β 9q60y60rhsgg β
βββββββββββββββββββββββ΄βββββββββββββββ
pl.select(pl.lit('9q60y60rhs').geohash.to_coords().alias('coordinates'))
shape: (1, 1)
βββββββββββββββββββββββββ
β coordinates β
β --- β
β struct[2] β
βββββββββββββββββββββββββ‘
β {-120.6623,35.300298} β
βββββββββββββββββββββββββ
df = pl.DataFrame(
{"coord": [{"longitude": -120.6623, "latitude": 35.3003}]},
schema={
"coord": pl.Struct(
[pl.Field("longitude", pl.Float64), pl.Field("latitude", pl.Float64)]
),
},
)
df.with_columns(
plh.col('coord').h3.from_coords().alias('h3')
)
shape: (1, 2)
βββββββββββββββββββββββ¬ββββββββββββββββββ
β coord β h3 β
β --- β --- β
β struct[2] β str β
βββββββββββββββββββββββͺββββββββββββββββββ‘
β {-120.6623,35.3003} β 8c29adc423821ff β
βββββββββββββββββββββββ΄ββββββββββββββββββ
Bins timestamps into variable-precision sliding windows of time, so rows that fall in the same window share a hash. Timestamps must lie between 1970-01-01 and 2098-01-01. A higher precision means a shorter window: 10 covers about 4 seconds, 8 about 4 minutes.
Precision may be 1 to 32, but past about 18 the hash stops changing for present-day timestamps and the extra characters are padding. The exact point depends on the date: timestamps close to 1970 keep splitting to about 21, far-future ones run out sooner.
from datetime import datetime
df = pl.DataFrame({"datetime": [datetime(2017, 2, 21, 20, 15, 13)]})
df.with_columns(
plh.col('datetime').timehash.from_datetime().alias('timehash')
)
shape: (1, 2)
βββββββββββββββββββββββ¬βββββββββββββ
β datetime β timehash β
β --- β --- β
β datetime[ΞΌs] β str β
βββββββββββββββββββββββͺβββββββββββββ‘
β 2017-02-21 20:15:13 β afcccc0e1b β
βββββββββββββββββββββββ΄βββββββββββββ
pl.select(pl.lit('afcccc0e1b').timehash.to_datetime().alias('datetime'))
shape: (1, 1)
ββββββββββββββββββββββββββββββββββ
β datetime β
β --- β
β datetime[ΞΌs, UTC] β
ββββββββββββββββββββββββββββββββββ‘
β 2017-02-21 20:15:11.292315 UTC β
ββββββββββββββββββββββββββββββββββ
pl.select(pl.lit('afcccc0e1b').timehash.neighbors().alias('neighbors'))
shape: (1, 1)
βββββββββββββββββββββββββββββββ
β neighbors β
β --- β
β struct[2] β
βββββββββββββββββββββββββββββββ‘
β {"afcccc0e1a","afcccc0e1c"} β
βββββββββββββββββββββββββββββββ
uuidhash makes UUID version 5 values (RFC 4122). A v5 UUID is a SHA-1 digest of a
namespace UUID and a name, so the same namespace and the same name always give the same
UUID. A null input gives null.
df = pl.DataFrame({"literal": ["hello", None, "world"]})
df.select(plh.col('literal').uuidhash.uuid5())
shape: (3, 1)
ββββββββββββββββββββββββββββββββββββββββ
β literal β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββ‘
β 9342d47a-1bab-5709-9869-c840b2eac501 β
β null β
β b3a4c24e-f57a-5448-b81b-a643f6768036 β
ββββββββββββββββββββββββββββββββββββββββ
pl.select(pl.lit('https://example.com').uuidhash.uuid5('url').alias('uuid'))
shape: (1, 1)
ββββββββββββββββββββββββββββββββββββββββ
β uuid β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββ‘
β 4fd35a71-71ef-5a55-a9d9-aa75c889a6d0 β
ββββββββββββββββββββββββββββββββββββββββ
The namespace is "dns", "url", "oid", "x500", or a custom UUID of your own.
Give a separator value. Without one, ("ab", "c") and ("a", "bc") make the same
string and therefore the same digest.
df = pl.DataFrame({"foo": ["hello_world"], "bar": ["today"]})
result = df.select(plh.concat_str("foo", "bar", separator="|").chash.sha2_256())
To hash a row of any column type, and not only strings, use hash_rows below.
hash_rows gives each row bytes that no other row can make, for all column types.
Any hasher then reads those bytes.
df = pl.DataFrame(
{"foo": ["hello_world"], "bar": [42], "baz": [[1, 2, 3]], "qux": [{"a": 1}]}
)
df.select(plh.hash_rows(pl.all()).chash.sha2_256())
shape: (1, 1)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β foo β
β --- β
β str β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ‘
β 9055866af8d3c113e0a8fdb729ce8e6fa67ed5f6f51efa8235a588e88ea972f4 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The encoder reads the meaning of a value, not the polars storage of it. An Int32 and
the Int64 next to it make the same hash. A Datetime in milliseconds and the same
time in nanoseconds also make the same hash, and a Categorical makes the hash of its
string. The encoder does not read the column names. Therefore a new name keeps the
hash, but a new order does not. The
reference
gives all the rules and the byte layout of version 1, which does not change.
Python
67.4%
Rust
31.1%
Makefile
1.5%