From aaab11ece36f5e028c57f942fcb1a5eb6d0cb5ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:52:47 +0000 Subject: [PATCH 1/4] Initial plan From 7d8e3af8dfe37950dea2c065084aef0a65d16dc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:00:17 +0000 Subject: [PATCH 2/4] tests(xs): fill pandas-parity gaps for level/dropLevel options Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/c6aeff19-4fd3-44f0-bd1a-26e491895e29 Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- tests/stats/xs.test.ts | 224 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/tests/stats/xs.test.ts b/tests/stats/xs.test.ts index 6d79a631..b7cda62b 100644 --- a/tests/stats/xs.test.ts +++ b/tests/stats/xs.test.ts @@ -215,6 +215,230 @@ describe("xsSeries (MultiIndex)", () => { // ─── property tests ─────────────────────────────────────────────────────────── +// ─── pandas-parity: named & negative level lookup ──────────────────────────── +// +// pandas.DataFrame.xs / Series.xs accept `level=` as either an integer +// (positional, supports negatives) or a string (level name). The +// implementation in src/stats/xs.ts supports both via resolveLevel(); the +// existing tests only covered positive integer levels. + +describe("xsDataFrame (MultiIndex) — pandas-parity level lookup", () => { + const mi = MultiIndex.fromTuples( + [ + ["A", 1], + ["A", 2], + ["B", 1], + ["B", 2], + ], + { names: ["letter", "num"] }, + ); + const df = new DataFrame( + new Map([ + ["val", new Series({ data: [10, 20, 30, 40], index: mi as unknown as Index })], + ]), + mi as unknown as Index, + ["val"], + ); + + test("named level lookup matches numeric level lookup (outer)", () => { + const byName = xsDataFrame(df, "A", { level: "letter" }) as DataFrame; + const byNum = xsDataFrame(df, "A", { level: 0 }) as DataFrame; + expect(byName.get("val")?.values).toEqual(byNum.get("val")?.values); + expect(byName.get("val")?.values).toEqual([10, 20]); + }); + + test("named level lookup matches numeric level lookup (inner)", () => { + const byName = xsDataFrame(df, 1, { level: "num" }) as DataFrame; + const byNum = xsDataFrame(df, 1, { level: 1 }) as DataFrame; + expect(byName.get("val")?.values).toEqual(byNum.get("val")?.values); + expect(byName.get("val")?.values).toEqual([10, 30]); + }); + + test("negative level index resolves from the end (-1 → last level)", () => { + const r = xsDataFrame(df, 1, { level: -1 }) as DataFrame; + expect(r.get("val")?.values).toEqual([10, 30]); + }); + + test("throws on out-of-range positive level number", () => { + expect(() => xsDataFrame(df, "A", { level: 5 })).toThrow(/out of range/); + }); + + test("throws on out-of-range negative level number", () => { + expect(() => xsDataFrame(df, "A", { level: -5 })).toThrow(/out of range/); + }); + + test("throws on unknown level name", () => { + expect(() => xsDataFrame(df, "A", { level: "missing" })).toThrow(/Level name/); + }); +}); + +// ─── pandas-parity: dropLevel=false ────────────────────────────────────────── +// +// pandas xs has `drop_level=True` as its default. When `drop_level=False` +// the matched level must remain in the result index (so the caller can see +// what was matched). This was implemented but had no test coverage. + +describe("xsDataFrame (MultiIndex) — dropLevel option", () => { + const mi = MultiIndex.fromTuples( + [ + ["A", 1], + ["A", 2], + ["B", 1], + ["B", 2], + ], + { names: ["letter", "num"] }, + ); + const df = new DataFrame( + new Map([ + ["val", new Series({ data: [10, 20, 30, 40], index: mi as unknown as Index })], + ]), + mi as unknown as Index, + ["val"], + ); + + test("dropLevel=true (default) reduces a 2-level MultiIndex to a flat Index", () => { + const r = xsDataFrame(df, "A") as DataFrame; + expect(r.index).not.toBeInstanceOf(MultiIndex); + expect(r.index.values).toEqual([1, 2]); + }); + + test("dropLevel=false preserves the matched level (still a MultiIndex with both names)", () => { + const r = xsDataFrame(df, "A", { dropLevel: false }) as DataFrame; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.nlevels).toBe(2); + expect(ri.names).toEqual(["letter", "num"]); + expect(ri.at(0)).toEqual(["A", 1]); + expect(ri.at(1)).toEqual(["A", 2]); + expect(r.get("val")?.values).toEqual([10, 20]); + }); + + test("dropLevel=false on inner-level xs preserves both levels", () => { + const r = xsDataFrame(df, 1, { level: "num", dropLevel: false }) as DataFrame; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.names).toEqual(["letter", "num"]); + expect(ri.at(0)).toEqual(["A", 1]); + expect(ri.at(1)).toEqual(["B", 1]); + expect(r.get("val")?.values).toEqual([10, 30]); + }); +}); + +// ─── pandas-parity: 3-level MultiIndex ─────────────────────────────────────── +// +// pandas tests xs against MultiIndexes deeper than 2 levels. The reduction +// logic (droplevel of the matched level) needs to leave the remaining levels +// (and their names) in the right order — only 2-level cases were covered. + +describe("xsDataFrame — 3-level MultiIndex", () => { + const mi3 = MultiIndex.fromTuples( + [ + ["A", "x", 1], + ["A", "x", 2], + ["A", "y", 1], + ["B", "x", 1], + ], + { names: ["L0", "L1", "L2"] }, + ); + const df3 = new DataFrame( + new Map([ + ["v", new Series({ data: [1, 2, 3, 4], index: mi3 as unknown as Index })], + ]), + mi3 as unknown as Index, + ["v"], + ); + + test("xs at outer level drops L0, leaves a 2-level MultiIndex named [L1, L2]", () => { + const r = xsDataFrame(df3, "A") as DataFrame; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.nlevels).toBe(2); + expect(ri.names).toEqual(["L1", "L2"]); + expect(r.get("v")?.values).toEqual([1, 2, 3]); + }); + + test("xs at middle level drops L1, leaves a 2-level MultiIndex named [L0, L2]", () => { + const r = xsDataFrame(df3, "x", { level: "L1" }) as DataFrame; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.nlevels).toBe(2); + expect(ri.names).toEqual(["L0", "L2"]); + expect(r.get("v")?.values).toEqual([1, 2, 4]); + }); + + test("xs at innermost level drops L2, leaves a 2-level MultiIndex named [L0, L1]", () => { + const r = xsDataFrame(df3, 1, { level: "L2" }) as DataFrame; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.names).toEqual(["L0", "L1"]); + expect(r.get("v")?.values).toEqual([1, 3, 4]); + }); + + test("xs at outer level with dropLevel=false keeps all three levels", () => { + const r = xsDataFrame(df3, "A", { dropLevel: false }) as DataFrame; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.nlevels).toBe(3); + expect(ri.names).toEqual(["L0", "L1", "L2"]); + }); +}); + +// ─── pandas-parity: xsSeries level / dropLevel options ─────────────────────── + +describe("xsSeries (MultiIndex) — pandas-parity level & dropLevel", () => { + const s = new Series({ + data: [100, 200, 300], + index: MultiIndex.fromTuples( + [ + ["X", 1], + ["X", 2], + ["Y", 1], + ], + { names: ["g", "n"] }, + ) as unknown as Index, + }); + + test("named level lookup matches numeric level lookup", () => { + const byName = xsSeries(s, 1, { level: "n" }) as Series; + const byNum = xsSeries(s, 1, { level: 1 }) as Series; + expect(byName.values).toEqual(byNum.values); + expect(byName.values).toEqual([100, 300]); + }); + + test("negative level index resolves from the end", () => { + const r = xsSeries(s, 1, { level: -1 }) as Series; + expect(r.values).toEqual([100, 300]); + }); + + test("dropLevel=false keeps result as a MultiIndex preserving both levels", () => { + const r = xsSeries(s, "X", { dropLevel: false }) as Series; + expect(r.index).toBeInstanceOf(MultiIndex); + const ri = r.index as unknown as MultiIndex; + expect(ri.nlevels).toBe(2); + expect(ri.names).toEqual(["g", "n"]); + expect(r.values).toEqual([100, 200]); + }); + + test("throws on unknown level name", () => { + expect(() => xsSeries(s, "X", { level: "missing" })).toThrow(/Level name/); + }); + + test("throws on out-of-range level number", () => { + expect(() => xsSeries(s, "X", { level: 9 })).toThrow(/out of range/); + }); + + test("Series name is preserved when xs returns a Series (multi-match)", () => { + const dup = new Series({ + data: [1, 2, 3, 4], + index: ["a", "b", "a", "c"], + name: "mycol", + }); + const r = xsSeries(dup, "a") as Series; + expect(r.name).toBe("mycol"); + expect(r.values).toEqual([1, 3]); + }); +}); + describe("xsDataFrame property tests", () => { test("axis=1 returns the exact column Series", () => { fc.assert( From 5762d4374e44e3ab53dd7e94236528d509cdf4ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 17:25:19 +0000 Subject: [PATCH 3/4] fix(lint): resolve useLiteralKeys and noExcessiveCognitiveComplexity errors - Replace string-literal property access rec["size"], rec["at"], rec["getLoc"] with dot notation in src/core/frame.ts - Refactor applyNearest in src/core/reindex.ts into three smaller helpers (buildLeftNearest, buildRightNearest, pickNearest) to bring cognitive complexity below the Biome limit of 15 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/frame.ts | 6 ++-- src/core/reindex.ts | 74 ++++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/core/frame.ts b/src/core/frame.ts index ddb641a1..5b7d441c 100644 --- a/src/core/frame.ts +++ b/src/core/frame.ts @@ -779,9 +779,9 @@ function isIndexLike(v: unknown): v is Index