From fa88f74c54b74ca97313674efd63e801cc99262f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 12:44:50 +0000 Subject: [PATCH 1/6] Iteration 30: pre-compute all 8 radix histograms in one scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 8 per-pass count loops (each reading n elements) with a single O(n) pre-scan that fills all 8×256 histogram buckets at once. Each element's lo and hi key words are read once and all 8 byte extractions are done in the same iteration, saving 7×n element reads from the count phases. The 8 scatter passes are unchanged; only the histogram accumulation is collapsed into one loop. Run: https://github.com/githubnext/tsessebe/actions/runs/25214403845 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/series.ts | 55 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/core/series.ts b/src/core/series.ts index 2410e13c..02fa6296 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -140,8 +140,13 @@ function pearsonCorrFromArrays( */ let _rxA: Uint32Array = new Uint32Array(0); let _rxB: Uint32Array = new Uint32Array(0); -/** 256-bucket histogram reused every pass (never reallocated). */ -const _rxCnt: Uint32Array = new Uint32Array(256); +/** + * Pre-computed histogram for all 8 radix passes (8 × 256 buckets). + * Layout: histo[pass * 256 + byte] = count of elements with that byte in that pass. + * A single O(n) scan fills all 8 histograms before any scatter pass runs, + * eliminating 7 redundant count loops vs the previous per-pass approach. + */ +const _rxHisto: Uint32Array = new Uint32Array(8 * 256); /** Pre-partition index buffers (grow lazily, never shrink). */ let _finBuf: Uint32Array = new Uint32Array(0); let _nanBuf: Uint32Array = new Uint32Array(0); @@ -805,30 +810,48 @@ export class Series { // _rxA is already initialised by the merged loop above. // AoS layout: srcBuf[i*3]=origIdx, srcBuf[i*3+1]=loKey, srcBuf[i*3+2]=hiKey. + // Pre-compute all 8 histograms in a single O(n) scan, saving 7 redundant + // count loops. _rxHisto[pass*256 + byte] = count of elements with that byte. + _rxHisto.fill(0); + for (let i = 0; i < finCount; i++) { + const si = i * 3; + const lo = srcBuf[si + 1]!; + const hi = srcBuf[si + 2]!; + let idx: number; + idx = 0 * 256 + (lo & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 1 * 256 + ((lo >>> 8) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 2 * 256 + ((lo >>> 16) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 3 * 256 + ((lo >>> 24) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 4 * 256 + (hi & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 5 * 256 + ((hi >>> 8) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 6 * 256 + ((hi >>> 16) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 7 * 256 + ((hi >>> 24) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + } + + // Convert each histogram to an exclusive prefix sum (cumulative offsets). + for (let pass = 0; pass < 8; pass++) { + const base = pass * 256; + let total = 0; + for (let b = 0; b < 256; b++) { + const c = _rxHisto[base + b]!; + _rxHisto[base + b] = total; + total = total + c; + } + } + let dstBuf = _rxB; for (let pass = 0; pass < 8; pass++) { - _rxCnt.fill(0); // keyOff: offset within the AoS triple for the key word this pass reads. // pass 0-3 use lo (offset 1); pass 4-7 use hi (offset 2). const keyOff = pass < 4 ? 1 : 2; const shift = (pass % 4) * 8; - for (let i = 0; i < finCount; i++) { - const bucket = (srcBuf[i * 3 + keyOff]! >>> shift) & 0xff; - const c = _rxCnt[bucket]!; - _rxCnt[bucket] = c + 1; - } - let total = 0; - for (let b = 0; b < 256; b++) { - const c = _rxCnt[b]!; - _rxCnt[b] = total; - total = total + c; - } + const histoBase = pass * 256; for (let i = 0; i < finCount; i++) { const si = i * 3; const bucket = (srcBuf[si + keyOff]! >>> shift) & 0xff; - const p = _rxCnt[bucket]!; - _rxCnt[bucket] = p + 1; + const p = _rxHisto[histoBase + bucket]!; + _rxHisto[histoBase + bucket] = p + 1; // All three writes land on the same cache line (3 × 4 = 12 bytes). const di = p * 3; dstBuf[di] = srcBuf[si]!; From 8a4d761f4134ab7e4f66ec32f06e24b08c5d0284 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 07:12:40 +0000 Subject: [PATCH 2/6] =?UTF-8?q?Iteration=20296:=20+hashPandasObject=20?= =?UTF-8?q?=E2=80=94=20FNV-1a=2064-bit=20hashing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run: https://github.com/githubnext/tsessebe/actions/runs/25139337654 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- playground/hash_pandas_object.html | 98 ++++++++++++ playground/index.html | 5 + src/index.ts | 2 + src/stats/hash_pandas_object.ts | 209 +++++++++++++++++++++++++ src/stats/index.ts | 2 + tests/stats/hash_pandas_object.test.ts | 209 +++++++++++++++++++++++++ 6 files changed, 525 insertions(+) create mode 100644 playground/hash_pandas_object.html create mode 100644 src/stats/hash_pandas_object.ts create mode 100644 tests/stats/hash_pandas_object.test.ts diff --git a/playground/hash_pandas_object.html b/playground/hash_pandas_object.html new file mode 100644 index 00000000..972d0fc1 --- /dev/null +++ b/playground/hash_pandas_object.html @@ -0,0 +1,98 @@ + + + + + + tsb · hashPandasObject + + + +

📦 hashPandasObject

+

+ Compute FNV-1a 64-bit hash values for each element of a + Series or each row of a DataFrame. + Mirrors pandas.util.hash_pandas_object. +

+ +

Series hashing

+
import { Series, hashPandasObject } from "tsb";
+
+const s = new Series({ data: ["apple", "banana", "apple"], index: [0, 1, 2] });
+const h = hashPandasObject(s, { index: false });
+
+// Same value → same hash
+console.log(h.iat(0) === h.iat(2)); // true  (both "apple")
+console.log(h.iat(0) === h.iat(1)); // false ("apple" ≠ "banana")
+
+ +

DataFrame row hashing

+
import { DataFrame, hashPandasObject } from "tsb";
+
+const df = new DataFrame({
+  id:   [1, 2, 3],
+  name: ["Alice", "Bob", "Alice"],
+  age:  [30, 25, 30],
+});
+
+const rowHashes = hashPandasObject(df, { index: false });
+// Rows 0 and 2 are identical → same hash
+console.log(rowHashes.iat(0) === rowHashes.iat(2)); // true
+console.log(rowHashes.iat(0) === rowHashes.iat(1)); // false
+
+ +

Deduplication with hashes

+
import { DataFrame, hashPandasObject } from "tsb";
+
+const df = new DataFrame({
+  a: [1, 2, 1, 3],
+  b: ["x", "y", "x", "z"],
+});
+
+const hashes = hashPandasObject(df, { index: false });
+const seen = new Set<number>();
+const uniqueRows: number[] = [];
+
+for (let i = 0; i < df.shape[0]; i++) {
+  const h = hashes.iat(i);
+  if (!seen.has(h)) {
+    seen.add(h);
+    uniqueRows.push(i);
+  }
+}
+// uniqueRows = [0, 1, 3]  — row 2 is a duplicate of row 0
+console.log(uniqueRows);
+
+ +

Controlling index inclusion

+
import { Series, hashPandasObject } from "tsb";
+
+const s = new Series({ data: [42, 42], index: ["a", "b"] });
+
+// index=true (default): different index → different hash
+const withIdx = hashPandasObject(s, { index: true });
+console.log(withIdx.iat(0) === withIdx.iat(1)); // false
+
+// index=false: only values matter
+const noIdx = hashPandasObject(s, { index: false });
+console.log(noIdx.iat(0) === noIdx.iat(1)); // true
+
+ +
+ Algorithm: FNV-1a 64-bit (Fowler–Noll–Vo), a fast non-cryptographic hash + chosen for its excellent avalanche properties on short inputs. Results are stored as + float64 numbers (the 64-bit bit-pattern cast via Number(BigInt)). +
+ +

← Back to tsb playground

+ + diff --git a/playground/index.html b/playground/index.html index 62e78e60..27ccedd2 100644 --- a/playground/index.html +++ b/playground/index.html @@ -454,6 +454,11 @@

dataFrameStyle(df) · highlightMax / highlightMin / highlightNull / highlightBetween · backgroundGradient / textGradient · barChart · format / formatIndex · apply / applymap / map · setCaption / setTableStyles / hide · toHtml / toLatex. Mirrors pandas.DataFrame.style (Styler).

✅ Complete
+
+

🔑 hashPandasObject — FNV-1a Hashing

+

hashPandasObject(s) · hashPandasObject(df) · index option. Mirrors pandas.util.hash_pandas_object. FNV-1a 64-bit per element or row.

+
✅ Complete
+
diff --git a/src/index.ts b/src/index.ts index 27a15d16..5770bb34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -683,3 +683,5 @@ export type { GradientOptions, BarOptions, } from "./stats/index.ts"; +export { hashPandasObject } from "./stats/index.ts"; +export type { HashPandasObjectOptions } from "./stats/index.ts"; diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts new file mode 100644 index 00000000..43561c63 --- /dev/null +++ b/src/stats/hash_pandas_object.ts @@ -0,0 +1,209 @@ +/** + * hash_pandas_object — FNV-1a 64-bit hashes for Series and DataFrame. + * + * Mirrors `pandas.util.hash_pandas_object`, which returns a `Series` of + * `uint64` hash values — one per element (for a Series input) or one per row + * (for a DataFrame input). + * + * Implementation uses FNV-1a 64-bit (Fowler–Noll–Vo) running on JavaScript + * `BigInt` arithmetic. The result values are stored as `float64` (the only + * numeric type available in the tsb dtype system) by converting the `uint64` + * bit-pattern to `number` via `Number(bigint)`. For hash-equality checks this + * is fine because every `uint64` value that differs will also differ as a + * `float64` in the range 0 – 2**64-1 that we use. + * + * @example + * ```ts + * import { Series, DataFrame, hashPandasObject } from "tsb"; + * + * const s = new Series({ data: [1, 2, 3], index: ["a", "b", "c"] }); + * const h = hashPandasObject(s); + * // h is a Series with hash values; equal inputs ⇒ equal hashes + * + * const df = new DataFrame({ a: [1, 2], b: ["x", "y"] }); + * const hr = hashPandasObject(df); + * // hr has one hash per row + * ``` + * + * @module + */ + +import type { Scalar } from "../types.ts"; +import { DataFrame } from "../core/frame.ts"; +import { Series } from "../core/series.ts"; + +// ─── FNV-1a 64-bit constants ────────────────────────────────────────────────── + +const FNV_PRIME = BigInt("0x00000100000001B3"); +const FNV_OFFSET = BigInt("0xcbf29ce484222325"); +const MASK64 = (BigInt(1) << BigInt(64)) - BigInt(1); + +/** Hash a single byte into the running FNV-1a state. */ +function fnvByte(hash: bigint, byte: number): bigint { + return ((hash ^ BigInt(byte)) * FNV_PRIME) & MASK64; +} + +/** Hash an arbitrary string (UTF-8 bytes) into the FNV state. */ +function fnvString(hash: bigint, s: string): bigint { + for (let i = 0; i < s.length; i++) { + let code = s.charCodeAt(i); + // Encode as UTF-8 bytes + if (code < 0x80) { + hash = fnvByte(hash, code); + } else if (code < 0x800) { + hash = fnvByte(hash, 0xc0 | (code >> 6)); + hash = fnvByte(hash, 0x80 | (code & 0x3f)); + } else { + hash = fnvByte(hash, 0xe0 | (code >> 12)); + hash = fnvByte(hash, 0x80 | ((code >> 6) & 0x3f)); + hash = fnvByte(hash, 0x80 | (code & 0x3f)); + } + } + return hash; +} + +/** Hash a single scalar value into the FNV state. */ +function fnvScalar(hash: bigint, val: Scalar): bigint { + if (val === null || val === undefined) { + // encode as a sentinel byte sequence + return fnvByte(fnvByte(hash, 0xfe), 0xfe); + } + if (typeof val === "boolean") { + return fnvByte(hash, val ? 1 : 0); + } + if (typeof val === "number") { + if (Number.isNaN(val)) { + return fnvByte(fnvByte(hash, 0xfd), 0xfd); + } + // Encode as little-endian 8-byte IEEE 754 + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, val, true); + const bytes = new Uint8Array(buf); + for (let i = 0; i < 8; i++) { + hash = fnvByte(hash, bytes[i]!); + } + return hash; + } + if (typeof val === "bigint") { + return fnvString(hash, val.toString()); + } + if (val instanceof Date) { + return fnvString(hash, String(val.getTime())); + } + // string or timedelta-like — stringify + return fnvString(hash, String(val)); +} + +// ─── Options ────────────────────────────────────────────────────────────────── + +/** Options for {@link hashPandasObject}. */ +export interface HashPandasObjectOptions { + /** + * Whether to include the index in the hash. Default `true`. + * + * When `false`, two Series with different indexes but identical values will + * produce the same hash values. + */ + index?: boolean; +} + +// ─── Series overload ────────────────────────────────────────────────────────── + +/** + * Return a `Series` of FNV-1a 64-bit hash values for each element + * of `s`. The result index matches `s.index`. + * + * Mirrors `pandas.util.hash_pandas_object` for a `Series` input. + * + * @param obj - A `Series` to hash. + * @param options - Optional settings (see {@link HashPandasObjectOptions}). + * @returns A `Series` of hash values. + * + * @example + * ```ts + * const s = new Series({ data: ["a", "b", "a"], index: [0, 1, 2] }); + * const h = hashPandasObject(s); + * h.iat(0) === h.iat(2); // true — same value → same hash + * h.iat(0) !== h.iat(1); // true (with overwhelming probability) + * ``` + */ +export function hashPandasObject( + obj: Series, + options?: HashPandasObjectOptions, +): Series; + +/** + * Return a `Series` of FNV-1a 64-bit row-hashes for each row of `df`. + * The result index matches `df.index`. + * + * Mirrors `pandas.util.hash_pandas_object` for a `DataFrame` input. + * + * @param obj - A `DataFrame` to hash. + * @param options - Optional settings (see {@link HashPandasObjectOptions}). + * @returns A `Series` of row hash values. + * + * @example + * ```ts + * const df = new DataFrame({ a: [1, 2], b: ["x", "y"] }); + * const h = hashPandasObject(df); + * // h.iat(0) is the hash of row 0; h.iat(1) is the hash of row 1 + * ``` + */ +export function hashPandasObject( + obj: DataFrame, + options?: HashPandasObjectOptions, +): Series; + +export function hashPandasObject( + obj: Series | DataFrame, + options: HashPandasObjectOptions = {}, +): Series { + const includeIndex = options.index !== false; + + if (obj instanceof Series) { + return _hashSeries(obj, includeIndex); + } + return _hashDataFrame(obj, includeIndex); +} + +// ─── internal helpers ───────────────────────────────────────────────────────── + +function _hashSeries(s: Series, includeIndex: boolean): Series { + const n = s.index.size; + const hashes: number[] = []; + + for (let i = 0; i < n; i++) { + let h = FNV_OFFSET; + if (includeIndex) { + h = fnvScalar(h, s.index.at(i) as Scalar); + // separator byte between index and value + h = fnvByte(h, 0xff); + } + h = fnvScalar(h, s.iat(i)); + hashes.push(Number(h)); + } + + return new Series({ data: hashes, index: s.index, dtype: "float64" }); +} + +function _hashDataFrame(df: DataFrame, includeIndex: boolean): Series { + const [nRows] = df.shape; + const colNames = df.columns.values as readonly string[]; + const hashes: number[] = []; + + for (let i = 0; i < nRows; i++) { + let h = FNV_OFFSET; + if (includeIndex) { + h = fnvScalar(h, df.index.at(i) as Scalar); + h = fnvByte(h, 0xff); + } + for (const name of colNames) { + const s = df.col(name); + h = fnvScalar(h, s.iat(i)); + h = fnvByte(h, 0xfe); // column separator + } + hashes.push(Number(h)); + } + + return new Series({ data: hashes, index: df.index, dtype: "float64" }); +} diff --git a/src/stats/index.ts b/src/stats/index.ts index 454582aa..2dd26e63 100644 --- a/src/stats/index.ts +++ b/src/stats/index.ts @@ -501,3 +501,5 @@ export type { GradientOptions, BarOptions, } from "./style.ts"; +export { hashPandasObject } from "./hash_pandas_object.ts"; +export type { HashPandasObjectOptions } from "./hash_pandas_object.ts"; diff --git a/tests/stats/hash_pandas_object.test.ts b/tests/stats/hash_pandas_object.test.ts new file mode 100644 index 00000000..3e26e4cf --- /dev/null +++ b/tests/stats/hash_pandas_object.test.ts @@ -0,0 +1,209 @@ +/** + * Tests for hashPandasObject — FNV-1a 64-bit hashing of Series and DataFrame. + */ + +import { describe, expect, it } from "bun:test"; +import fc from "fast-check"; +import { DataFrame, Series, hashPandasObject } from "../../src/index.ts"; + +// ─── Series hashing ─────────────────────────────────────────────────────────── + +describe("hashPandasObject — Series", () => { + it("returns a Series of the same length", () => { + const s = new Series({ data: [1, 2, 3] }); + const h = hashPandasObject(s); + expect(h.index.size).toBe(3); + }); + + it("returns numeric hash values", () => { + const s = new Series({ data: ["a", "b", "c"] }); + const h = hashPandasObject(s); + for (let i = 0; i < 3; i++) { + expect(typeof h.iat(i)).toBe("number"); + } + }); + + it("equal values → equal hashes (index=true)", () => { + const s = new Series({ data: ["x", "y", "x"], index: [0, 1, 2] }); + const h = hashPandasObject(s); + // index differs (0 vs 2), so hashes must differ + expect(h.iat(0)).not.toBe(h.iat(2)); + }); + + it("equal values + equal index → equal hashes", () => { + const s1 = new Series({ data: [42], index: ["k"] }); + const s2 = new Series({ data: [42], index: ["k"] }); + const h1 = hashPandasObject(s1); + const h2 = hashPandasObject(s2); + expect(h1.iat(0)).toBe(h2.iat(0)); + }); + + it("different values → different hashes (with overwhelming probability)", () => { + const s = new Series({ data: [1, 2, 3, 4, 5], index: [0, 1, 2, 3, 4] }); + const h = hashPandasObject(s); + const unique = new Set(); + for (let i = 0; i < 5; i++) { + unique.add(h.iat(i)); + } + expect(unique.size).toBe(5); + }); + + it("index=false: same value + different index → same hash", () => { + const s = new Series({ data: ["hello", "hello"], index: ["a", "b"] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("index=false: different values → different hashes", () => { + const s = new Series({ data: ["hello", "world"] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).not.toBe(h.iat(1)); + }); + + it("preserves the original index on the result", () => { + const s = new Series({ data: [10, 20], index: ["x", "y"] }); + const h = hashPandasObject(s); + expect(h.index.at(0)).toBe("x"); + expect(h.index.at(1)).toBe("y"); + }); + + it("handles null values", () => { + const s = new Series({ data: [null, null], index: [0, 1] }); + const h = hashPandasObject(s, { index: false }); + // Same null → same hash + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("null ≠ zero hash", () => { + const sNull = new Series({ data: [null], index: [0] }); + const sZero = new Series({ data: [0], index: [0] }); + const hNull = hashPandasObject(sNull, { index: false }); + const hZero = hashPandasObject(sZero, { index: false }); + expect(hNull.iat(0)).not.toBe(hZero.iat(0)); + }); + + it("handles boolean values", () => { + const s = new Series({ data: [true, false, true] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).toBe(h.iat(2)); // true === true + expect(h.iat(0)).not.toBe(h.iat(1)); // true ≠ false + }); + + it("handles Date values", () => { + const d1 = new Date("2024-01-01"); + const d2 = new Date("2024-01-02"); + const s = new Series({ data: [d1, d1, d2] }); + const h = hashPandasObject(s, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + expect(h.iat(0)).not.toBe(h.iat(2)); + }); + + it("handles empty Series", () => { + const s = new Series({ data: [] }); + const h = hashPandasObject(s); + expect(h.index.size).toBe(0); + }); + + it("property: deterministic — same input same hash", () => { + fc.assert( + fc.property(fc.array(fc.oneof(fc.integer(), fc.string(), fc.boolean()), { maxLength: 20 }), (arr) => { + const s1 = new Series({ data: arr }); + const s2 = new Series({ data: arr }); + const h1 = hashPandasObject(s1, { index: false }); + const h2 = hashPandasObject(s2, { index: false }); + for (let i = 0; i < arr.length; i++) { + if (h1.iat(i) !== h2.iat(i)) { + return false; + } + } + return true; + }), + ); + }); +}); + +// ─── DataFrame hashing ──────────────────────────────────────────────────────── + +describe("hashPandasObject — DataFrame", () => { + it("returns a Series with one hash per row", () => { + const df = DataFrame.fromColumns({ a: [1, 2, 3], b: ["x", "y", "z"] }); + const h = hashPandasObject(df); + expect(h.index.size).toBe(3); + }); + + it("returns numeric hashes", () => { + const df = DataFrame.fromColumns({ a: [1, 2], b: [3, 4] }); + const h = hashPandasObject(df); + expect(typeof h.iat(0)).toBe("number"); + expect(typeof h.iat(1)).toBe("number"); + }); + + it("identical rows → same hash (index=false)", () => { + const df = DataFrame.fromColumns({ a: [1, 1], b: ["x", "x"] }); + const h = hashPandasObject(df, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("different rows → different hashes", () => { + const df = DataFrame.fromColumns({ a: [1, 2], b: ["x", "y"] }); + const h = hashPandasObject(df, { index: false }); + expect(h.iat(0)).not.toBe(h.iat(1)); + }); + + it("preserves df.index on result", () => { + const df = DataFrame.fromColumns({ a: [10, 20] }, { index: ["r0", "r1"] }); + const h = hashPandasObject(df); + expect(h.index.at(0)).toBe("r0"); + expect(h.index.at(1)).toBe("r1"); + }); + + it("index=true: same data, different index → different hashes", () => { + const df = DataFrame.fromColumns({ a: [1, 1] }, { index: [0, 1] }); + const h = hashPandasObject(df, { index: true }); + expect(h.iat(0)).not.toBe(h.iat(1)); + }); + + it("handles null values in rows", () => { + const df = DataFrame.fromColumns({ a: [null, null] }); + const h = hashPandasObject(df, { index: false }); + expect(h.iat(0)).toBe(h.iat(1)); + }); + + it("handles empty DataFrame", () => { + const df = DataFrame.fromColumns({}); + const h = hashPandasObject(df); + expect(h.index.size).toBe(0); + }); + + it("column order matters", () => { + // { a:[1], b:[2] } ≠ { b:[2], a:[1] } — different column order → different row hashes + const df1 = DataFrame.fromColumns({ a: [1], b: [2] }); + const df2 = DataFrame.fromColumns({ b: [2], a: [1] }); + const h1 = hashPandasObject(df1, { index: false }); + const h2 = hashPandasObject(df2, { index: false }); + // Column order is reflected in the hash + expect(h1.iat(0)).not.toBe(h2.iat(0)); + }); + + it("property: deterministic for DataFrames", () => { + fc.assert( + fc.property( + fc.array(fc.record({ a: fc.integer(), b: fc.string() }), { minLength: 1, maxLength: 10 }), + (rows) => { + const aVals = rows.map((r) => r.a); + const bVals = rows.map((r) => r.b); + const df1 = DataFrame.fromColumns({ a: aVals, b: bVals }); + const df2 = DataFrame.fromColumns({ a: aVals, b: bVals }); + const h1 = hashPandasObject(df1, { index: false }); + const h2 = hashPandasObject(df2, { index: false }); + for (let i = 0; i < rows.length; i++) { + if (h1.iat(i) !== h2.iat(i)) { + return false; + } + } + return true; + }, + ), + ); + }); +}); From b54cbc58d39d31e3a1843a7e4c041bcdefb4e527 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 07:12:42 +0000 Subject: [PATCH 3/6] Fix TS2322: use Dtype.float64 instead of string literal in hash_pandas_object Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/stats/hash_pandas_object.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts index 43561c63..632a4f36 100644 --- a/src/stats/hash_pandas_object.ts +++ b/src/stats/hash_pandas_object.ts @@ -31,6 +31,7 @@ import type { Scalar } from "../types.ts"; import { DataFrame } from "../core/frame.ts"; import { Series } from "../core/series.ts"; +import { Dtype } from "../core/dtype.ts"; // ─── FNV-1a 64-bit constants ────────────────────────────────────────────────── @@ -183,7 +184,7 @@ function _hashSeries(s: Series, includeIndex: boolean): Series { hashes.push(Number(h)); } - return new Series({ data: hashes, index: s.index, dtype: "float64" }); + return new Series({ data: hashes, index: s.index, dtype: Dtype.float64 }); } function _hashDataFrame(df: DataFrame, includeIndex: boolean): Series { @@ -205,5 +206,5 @@ function _hashDataFrame(df: DataFrame, includeIndex: boolean): Series { hashes.push(Number(h)); } - return new Series({ data: hashes, index: df.index, dtype: "float64" }); + return new Series({ data: hashes, index: df.index, dtype: Dtype.float64 }); } From 5ce0789c7c7d6e440ce05bb0956140939635b31f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 22:50:19 +0000 Subject: [PATCH 4/6] Merge main into autoloop/tsb-perf-evolve, resolve add/add conflicts Use main's playground/hash_pandas_object.html (passes conformance tests). Accept PR's src/stats/hash_pandas_object.ts and tests unchanged. New benchmark files from main are included. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- benchmarks/pandas/bench_compare.py | 28 +++ benchmarks/pandas/bench_update.py | 30 +++ benchmarks/pandas/bench_xs.py | 24 ++ benchmarks/tsb/bench_compare.ts | 30 +++ benchmarks/tsb/bench_update.ts | 29 +++ benchmarks/tsb/bench_xs.ts | 30 +++ playground/hash_pandas_object.html | 371 +++++++++++++++++++++++++---- 7 files changed, 492 insertions(+), 50 deletions(-) create mode 100644 benchmarks/pandas/bench_compare.py create mode 100644 benchmarks/pandas/bench_update.py create mode 100644 benchmarks/pandas/bench_xs.py create mode 100644 benchmarks/tsb/bench_compare.ts create mode 100644 benchmarks/tsb/bench_update.ts create mode 100644 benchmarks/tsb/bench_xs.ts diff --git a/benchmarks/pandas/bench_compare.py b/benchmarks/pandas/bench_compare.py new file mode 100644 index 00000000..6124844a --- /dev/null +++ b/benchmarks/pandas/bench_compare.py @@ -0,0 +1,28 @@ +import pandas as pd +import json +import time + +N = 100_000 +data = [i % 1000 for i in range(N)] +s = pd.Series(data, dtype=float) + +# Warm-up +for _ in range(20): + s.eq(500) + s.lt(300) + s.ge(700) + +iterations = 300 +start = time.perf_counter() +for _ in range(iterations): + s.eq(500) + s.lt(300) + s.ge(700) +total_ms = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "compare", + "mean_ms": total_ms / iterations, + "iterations": iterations, + "total_ms": total_ms, +})) diff --git a/benchmarks/pandas/bench_update.py b/benchmarks/pandas/bench_update.py new file mode 100644 index 00000000..b4381027 --- /dev/null +++ b/benchmarks/pandas/bench_update.py @@ -0,0 +1,30 @@ +import pandas as pd +import numpy as np +import json +import time + +N = 100_000 +data = list(range(N)) +other_data = [i * 10 if i % 3 == 0 else None for i in range(N)] + +s = pd.Series(data, dtype=float) +o = pd.Series(other_data, dtype=float) + +# Warm-up +for _ in range(20): + sc = s.copy() + sc.update(o) + +iterations = 200 +start = time.perf_counter() +for _ in range(iterations): + sc = s.copy() + sc.update(o) +total_ms = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "update", + "mean_ms": total_ms / iterations, + "iterations": iterations, + "total_ms": total_ms, +})) diff --git a/benchmarks/pandas/bench_xs.py b/benchmarks/pandas/bench_xs.py new file mode 100644 index 00000000..a6c3c6fc --- /dev/null +++ b/benchmarks/pandas/bench_xs.py @@ -0,0 +1,24 @@ +import pandas as pd +import json +import time + +N = 100_000 +index = [str(i) for i in range(N)] +df = pd.DataFrame({"a": range(N), "b": [i * 2 for i in range(N)]}, index=index) + +# Warm-up +for i in range(100): + df.xs("500") + +iterations = 10_000 +start = time.perf_counter() +for i in range(iterations): + df.xs(str(i % N)) +total_ms = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "xs", + "mean_ms": total_ms / iterations, + "iterations": iterations, + "total_ms": total_ms, +})) diff --git a/benchmarks/tsb/bench_compare.ts b/benchmarks/tsb/bench_compare.ts new file mode 100644 index 00000000..b2d8caf1 --- /dev/null +++ b/benchmarks/tsb/bench_compare.ts @@ -0,0 +1,30 @@ +import { Series, seriesEq, seriesLt, seriesGe } from "../../src/index.ts"; + +const N = 100_000; +const data = Float64Array.from({ length: N }, (_, i) => i % 1000); +const s = new Series({ data }); + +// Warm-up +for (let i = 0; i < 20; i++) { + seriesEq(s, 500); + seriesLt(s, 300); + seriesGe(s, 700); +} + +const iterations = 300; +const start = performance.now(); +for (let i = 0; i < iterations; i++) { + seriesEq(s, 500); + seriesLt(s, 300); + seriesGe(s, 700); +} +const total_ms = performance.now() - start; + +console.log( + JSON.stringify({ + function: "compare", + mean_ms: total_ms / iterations, + iterations, + total_ms, + }), +); diff --git a/benchmarks/tsb/bench_update.ts b/benchmarks/tsb/bench_update.ts new file mode 100644 index 00000000..d06e560a --- /dev/null +++ b/benchmarks/tsb/bench_update.ts @@ -0,0 +1,29 @@ +import { Series, seriesUpdate } from "../../src/index.ts"; + +const N = 100_000; +const data = Float64Array.from({ length: N }, (_, i) => i); +const other = Float64Array.from({ length: N }, (_, i) => (i % 3 === 0 ? i * 10 : null as unknown as number)); + +const s = new Series({ data }); +const o = new Series({ data: other }); + +// Warm-up +for (let i = 0; i < 20; i++) { + seriesUpdate(s, o); +} + +const iterations = 200; +const start = performance.now(); +for (let i = 0; i < iterations; i++) { + seriesUpdate(s, o); +} +const total_ms = performance.now() - start; + +console.log( + JSON.stringify({ + function: "update", + mean_ms: total_ms / iterations, + iterations, + total_ms, + }), +); diff --git a/benchmarks/tsb/bench_xs.ts b/benchmarks/tsb/bench_xs.ts new file mode 100644 index 00000000..f28ab7a0 --- /dev/null +++ b/benchmarks/tsb/bench_xs.ts @@ -0,0 +1,30 @@ +import { DataFrame, xsDataFrame } from "../../src/index.ts"; + +const N = 100_000; +const rows = Array.from({ length: N }, (_, i) => i); +const a = Float64Array.from(rows); +const b = Float64Array.from(rows.map((x) => x * 2)); +const index = rows.map(String); + +const df = DataFrame.fromColumns({ a, b }, { index }); + +// Warm-up +for (let i = 0; i < 100; i++) { + xsDataFrame(df, "500"); +} + +const iterations = 10_000; +const start = performance.now(); +for (let i = 0; i < iterations; i++) { + xsDataFrame(df, String(i % N)); +} +const total_ms = performance.now() - start; + +console.log( + JSON.stringify({ + function: "xs", + mean_ms: total_ms / iterations, + iterations, + total_ms, + }), +); diff --git a/playground/hash_pandas_object.html b/playground/hash_pandas_object.html index 972d0fc1..134212d7 100644 --- a/playground/hash_pandas_object.html +++ b/playground/hash_pandas_object.html @@ -1,42 +1,248 @@ - + - - - - tsb · hashPandasObject - - - -

📦 hashPandasObject

+ + + + tsb — hashPandasObject Playground + + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

📦 hashPandasObject — Interactive Playground

+

+ hashPandasObject(obj) computes FNV-1a 64-bit hash values for each element + of a Series or each row of a DataFrame — mirroring + pandas.util.hash_pandas_object.
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser. +

+ + +
+

1 · Series hashing

- Compute FNV-1a 64-bit hash values for each element of a - Series or each row of a DataFrame. - Mirrors pandas.util.hash_pandas_object. + Hash each element of a Series. Identical values produce identical hashes; + pass { index: false } to ignore the index label when computing the hash.

- -

Series hashing

-
import { Series, hashPandasObject } from "tsb";
+    
+
+ TypeScript +
+ + +
+
+
import { Series, hashPandasObject } from "tsb";
 
 const s = new Series({ data: ["apple", "banana", "apple"], index: [0, 1, 2] });
 const h = hashPandasObject(s, { index: false });
 
 // Same value → same hash
-console.log(h.iat(0) === h.iat(2)); // true  (both "apple")
-console.log(h.iat(0) === h.iat(1)); // false ("apple" ≠ "banana")
-
+console.log("apple===apple:", h.iat(0) === h.iat(2)); // true +console.log("apple===banana:", h.iat(0) === h.iat(1)); // false +console.log("hashes:", [...h.values]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
-

DataFrame row hashing

-
import { DataFrame, hashPandasObject } from "tsb";
+  
+  
+

2 · DataFrame row hashing

+

+ Hash each row of a DataFrame. Rows with identical values across all columns + produce the same hash, making this useful for deduplication and change detection. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, hashPandasObject } from "tsb";
 
 const df = new DataFrame({
   id:   [1, 2, 3],
@@ -46,12 +252,29 @@ 

DataFrame row hashing

const rowHashes = hashPandasObject(df, { index: false }); // Rows 0 and 2 are identical → same hash -console.log(rowHashes.iat(0) === rowHashes.iat(2)); // true -console.log(rowHashes.iat(0) === rowHashes.iat(1)); // false -
+console.log("row0===row2:", rowHashes.iat(0) === rowHashes.iat(2)); // true +console.log("row0===row1:", rowHashes.iat(0) === rowHashes.iat(1)); // false
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+ -

Deduplication with hashes

-
import { DataFrame, hashPandasObject } from "tsb";
+  
+  
+

3 · Deduplication with hashes

+

+ Use row hashes to find unique rows efficiently — a common pattern when + duplicated() is too slow on large DataFrames. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame, hashPandasObject } from "tsb";
 
 const df = new DataFrame({
   a: [1, 2, 1, 3],
@@ -59,10 +282,10 @@ 

Deduplication with hashes

}); const hashes = hashPandasObject(df, { index: false }); -const seen = new Set<number>(); +const seen = new Set(); const uniqueRows: number[] = []; -for (let i = 0; i < df.shape[0]; i++) { +for (let i = 0; i < df.shape[0]; i++) { const h = hashes.iat(i); if (!seen.has(h)) { seen.add(h); @@ -70,29 +293,77 @@

Deduplication with hashes

} } // uniqueRows = [0, 1, 3] — row 2 is a duplicate of row 0 -console.log(uniqueRows); -
+console.log("unique row indices:", uniqueRows);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+ + -

Controlling index inclusion

-
import { Series, hashPandasObject } from "tsb";
+  
+  
+

4 · Controlling index inclusion

+

+ By default (index: true), the index label is mixed into the hash. + Set index: false to hash only the values. +

+
+
+ TypeScript +
+ + +
+
+
import { Series, hashPandasObject } from "tsb";
 
 const s = new Series({ data: [42, 42], index: ["a", "b"] });
 
 // index=true (default): different index → different hash
 const withIdx = hashPandasObject(s, { index: true });
-console.log(withIdx.iat(0) === withIdx.iat(1)); // false
+console.log("index=true, iat(0)===iat(1):", withIdx.iat(0) === withIdx.iat(1)); // false
 
 // index=false: only values matter
 const noIdx = hashPandasObject(s, { index: false });
-console.log(noIdx.iat(0) === noIdx.iat(1)); // true
-
+console.log("index=false, iat(0)===iat(1):", noIdx.iat(0) === noIdx.iat(1)); // true
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+ + + + +
+

🧪 Scratch Pad

+

Write your own hashPandasObject code below. All exports from tsb are available.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+
import { Series, DataFrame, hashPandasObject } from "tsb";
+
+// Try it! Hash a Series of numbers.
+const nums = new Series({ data: [10, 20, 10, 30] });
+const hashes = hashPandasObject(nums, { index: false });
 
-    
- Algorithm: FNV-1a 64-bit (Fowler–Noll–Vo), a fast non-cryptographic hash - chosen for its excellent avalanche properties on short inputs. Results are stored as - float64 numbers (the 64-bit bit-pattern cast via Number(BigInt)). +console.log("10===10:", hashes.iat(0) === hashes.iat(2)); +console.log("10===20:", hashes.iat(0) === hashes.iat(1)); +console.log("all hashes:", [...hashes.values]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+ + -

← Back to tsb playground

- + + + From 7380f1bbd73b8d63d40cb4eaf9c81eb8d73f177e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 22:05:48 +0000 Subject: [PATCH 5/6] Fix lint errors: noParameterAssign and formatting in hash_pandas_object.ts and series.ts Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/5edd497e-686c-4369-a2c2-60753b5c8261 Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- src/core/series.ts | 24 ++++++++++----- src/stats/hash_pandas_object.ts | 41 ++++++++++++-------------- tests/stats/hash_pandas_object.test.ts | 25 +++++++++------- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/core/series.ts b/src/core/series.ts index 02fa6296..46d3a7cb 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -818,14 +818,22 @@ export class Series { const lo = srcBuf[si + 1]!; const hi = srcBuf[si + 2]!; let idx: number; - idx = 0 * 256 + (lo & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 1 * 256 + ((lo >>> 8) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 2 * 256 + ((lo >>> 16) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 3 * 256 + ((lo >>> 24) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 4 * 256 + (hi & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 5 * 256 + ((hi >>> 8) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 6 * 256 + ((hi >>> 16) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; - idx = 7 * 256 + ((hi >>> 24) & 0xff); _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 0 * 256 + (lo & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 1 * 256 + ((lo >>> 8) & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 2 * 256 + ((lo >>> 16) & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 3 * 256 + ((lo >>> 24) & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 4 * 256 + (hi & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 5 * 256 + ((hi >>> 8) & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 6 * 256 + ((hi >>> 16) & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; + idx = 7 * 256 + ((hi >>> 24) & 0xff); + _rxHisto[idx] = _rxHisto[idx]! + 1; } // Convert each histogram to an exclusive prefix sum (cumulative offsets). diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts index 632a4f36..a10d4bd3 100644 --- a/src/stats/hash_pandas_object.ts +++ b/src/stats/hash_pandas_object.ts @@ -28,10 +28,10 @@ * @module */ -import type { Scalar } from "../types.ts"; -import { DataFrame } from "../core/frame.ts"; -import { Series } from "../core/series.ts"; import { Dtype } from "../core/dtype.ts"; +import type { DataFrame } from "../core/frame.ts"; +import { Series } from "../core/series.ts"; +import type { Scalar } from "../types.ts"; // ─── FNV-1a 64-bit constants ────────────────────────────────────────────────── @@ -45,22 +45,23 @@ function fnvByte(hash: bigint, byte: number): bigint { } /** Hash an arbitrary string (UTF-8 bytes) into the FNV state. */ -function fnvString(hash: bigint, s: string): bigint { +function fnvString(initialHash: bigint, s: string): bigint { + let h = initialHash; for (let i = 0; i < s.length; i++) { - let code = s.charCodeAt(i); + const code = s.charCodeAt(i); // Encode as UTF-8 bytes if (code < 0x80) { - hash = fnvByte(hash, code); + h = fnvByte(h, code); } else if (code < 0x800) { - hash = fnvByte(hash, 0xc0 | (code >> 6)); - hash = fnvByte(hash, 0x80 | (code & 0x3f)); + h = fnvByte(h, 0xc0 | (code >> 6)); + h = fnvByte(h, 0x80 | (code & 0x3f)); } else { - hash = fnvByte(hash, 0xe0 | (code >> 12)); - hash = fnvByte(hash, 0x80 | ((code >> 6) & 0x3f)); - hash = fnvByte(hash, 0x80 | (code & 0x3f)); + h = fnvByte(h, 0xe0 | (code >> 12)); + h = fnvByte(h, 0x80 | ((code >> 6) & 0x3f)); + h = fnvByte(h, 0x80 | (code & 0x3f)); } } - return hash; + return h; } /** Hash a single scalar value into the FNV state. */ @@ -80,10 +81,12 @@ function fnvScalar(hash: bigint, val: Scalar): bigint { const buf = new ArrayBuffer(8); new DataView(buf).setFloat64(0, val, true); const bytes = new Uint8Array(buf); + let h = hash; for (let i = 0; i < 8; i++) { - hash = fnvByte(hash, bytes[i]!); + const b = bytes[i]; + h = fnvByte(h, b === undefined ? 0 : b); } - return hash; + return h; } if (typeof val === "bigint") { return fnvString(hash, val.toString()); @@ -128,10 +131,7 @@ export interface HashPandasObjectOptions { * h.iat(0) !== h.iat(1); // true (with overwhelming probability) * ``` */ -export function hashPandasObject( - obj: Series, - options?: HashPandasObjectOptions, -): Series; +export function hashPandasObject(obj: Series, options?: HashPandasObjectOptions): Series; /** * Return a `Series` of FNV-1a 64-bit row-hashes for each row of `df`. @@ -150,10 +150,7 @@ export function hashPandasObject( * // h.iat(0) is the hash of row 0; h.iat(1) is the hash of row 1 * ``` */ -export function hashPandasObject( - obj: DataFrame, - options?: HashPandasObjectOptions, -): Series; +export function hashPandasObject(obj: DataFrame, options?: HashPandasObjectOptions): Series; export function hashPandasObject( obj: Series | DataFrame, diff --git a/tests/stats/hash_pandas_object.test.ts b/tests/stats/hash_pandas_object.test.ts index 3e26e4cf..7c0d0f67 100644 --- a/tests/stats/hash_pandas_object.test.ts +++ b/tests/stats/hash_pandas_object.test.ts @@ -106,18 +106,21 @@ describe("hashPandasObject — Series", () => { it("property: deterministic — same input same hash", () => { fc.assert( - fc.property(fc.array(fc.oneof(fc.integer(), fc.string(), fc.boolean()), { maxLength: 20 }), (arr) => { - const s1 = new Series({ data: arr }); - const s2 = new Series({ data: arr }); - const h1 = hashPandasObject(s1, { index: false }); - const h2 = hashPandasObject(s2, { index: false }); - for (let i = 0; i < arr.length; i++) { - if (h1.iat(i) !== h2.iat(i)) { - return false; + fc.property( + fc.array(fc.oneof(fc.integer(), fc.string(), fc.boolean()), { maxLength: 20 }), + (arr) => { + const s1 = new Series({ data: arr }); + const s2 = new Series({ data: arr }); + const h1 = hashPandasObject(s1, { index: false }); + const h2 = hashPandasObject(s2, { index: false }); + for (let i = 0; i < arr.length; i++) { + if (h1.iat(i) !== h2.iat(i)) { + return false; + } } - } - return true; - }), + return true; + }, + ), ); }); }); From 5c20b6f8b7d25a86f49d4e16394bf4207f78f7ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 22:06:43 +0000 Subject: [PATCH 6/6] Use DataView.getUint8() to avoid unnecessary undefined fallback in fnvScalar Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/5edd497e-686c-4369-a2c2-60753b5c8261 Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- src/stats/hash_pandas_object.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/stats/hash_pandas_object.ts b/src/stats/hash_pandas_object.ts index a10d4bd3..9f88ba51 100644 --- a/src/stats/hash_pandas_object.ts +++ b/src/stats/hash_pandas_object.ts @@ -79,12 +79,11 @@ function fnvScalar(hash: bigint, val: Scalar): bigint { } // Encode as little-endian 8-byte IEEE 754 const buf = new ArrayBuffer(8); - new DataView(buf).setFloat64(0, val, true); - const bytes = new Uint8Array(buf); + const view = new DataView(buf); + view.setFloat64(0, val, true); let h = hash; for (let i = 0; i < 8; i++) { - const b = bytes[i]; - h = fnvByte(h, b === undefined ? 0 : b); + h = fnvByte(h, view.getUint8(i)); } return h; }