Skip to content

Commit f0178e1

Browse files
Iteration 138: Add toDictOriented/fromDictOriented + wideToLong
- src/core/to_from_dict.ts: toDictOriented (7 orients: dict/columns/list/series/split/tight/records/index) + fromDictOriented (4 orients: columns/index/split/tight). Full TypeScript overloads with exact return types per orient string. - src/reshape/wide_to_long.ts: wideToLong() mirrors pandas.wide_to_long — collapses stub-prefixed column groups into rows. Supports sep, suffix regex, multiple id cols, alphabetic suffixes, and numeric suffix sorting. - Updated barrel exports: src/core/index.ts, src/reshape/index.ts, src/index.ts - Added tests/core/to_from_dict.test.ts (28 unit + 3 property tests) - Added tests/reshape/wide_to_long.test.ts (16 unit + 2 property tests) - Added playground/to_from_dict.html and playground/wide_to_long.html Metric: 29 → 31 pandas_features_ported (+2) Run: https://github.com/githubnext/tsessebe/actions/runs/24170164603 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 56a5489 commit f0178e1

9 files changed

Lines changed: 1238 additions & 0 deletions

File tree

playground/to_from_dict.html

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tsb — toDictOriented / fromDictOriented</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; }
9+
h1 { color: #0066cc; }
10+
h2 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
11+
pre { background: #f6f8fa; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.9em; }
12+
code { font-family: 'Fira Code', 'Cascadia Code', monospace; }
13+
.note { background: #fffbea; border-left: 4px solid #f5a623; padding: 0.7rem 1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
14+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
15+
th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; }
16+
th { background: #f0f4f8; }
17+
a { color: #0066cc; }
18+
</style>
19+
</head>
20+
<body>
21+
<p><a href="index.html">← tsb playground</a></p>
22+
23+
<h1><code>toDictOriented</code> / <code>fromDictOriented</code></h1>
24+
<p>
25+
Convert a DataFrame to and from dictionary structures with flexible orientation — mirrors
26+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_dict.html" target="_blank">
27+
<code>pandas.DataFrame.to_dict(orient=...)</code></a> and
28+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.from_dict.html" target="_blank">
29+
<code>pandas.DataFrame.from_dict(orient=...)</code></a>.
30+
</p>
31+
32+
<h2>Supported orientations — <code>toDictOriented</code></h2>
33+
<table>
34+
<thead><tr><th>Orient</th><th>Return type</th><th>Description</th></tr></thead>
35+
<tbody>
36+
<tr><td><code>"dict"</code> / <code>"columns"</code></td><td><code>Record&lt;col, Record&lt;rowLabel, value&gt;&gt;</code></td><td>Nested column → row-label → value map</td></tr>
37+
<tr><td><code>"list"</code></td><td><code>Record&lt;col, value[]&gt;</code></td><td>Column name → array of values</td></tr>
38+
<tr><td><code>"series"</code></td><td><code>Record&lt;col, Series&gt;</code></td><td>Column name → Series object</td></tr>
39+
<tr><td><code>"split"</code></td><td><code>{ index, columns, data }</code></td><td>Serialisable split structure</td></tr>
40+
<tr><td><code>"tight"</code></td><td><code>{ index, columns, data, index_names, column_names }</code></td><td>Split plus axis-name metadata</td></tr>
41+
<tr><td><code>"records"</code></td><td><code>Record&lt;col, value&gt;[]</code></td><td>Array of row objects</td></tr>
42+
<tr><td><code>"index"</code></td><td><code>Record&lt;rowLabel, Record&lt;col, value&gt;&gt;</code></td><td>Row-label → column → value</td></tr>
43+
</tbody>
44+
</table>
45+
46+
<h2>Supported orientations — <code>fromDictOriented</code></h2>
47+
<table>
48+
<thead><tr><th>Orient</th><th>Input shape</th></tr></thead>
49+
<tbody>
50+
<tr><td><code>"columns"</code> (default)</td><td><code>{ col: value[] }</code></td></tr>
51+
<tr><td><code>"index"</code></td><td><code>{ rowLabel: { col: value } }</code></td></tr>
52+
<tr><td><code>"split"</code></td><td><code>{ index?, columns, data }</code></td></tr>
53+
<tr><td><code>"tight"</code></td><td>Same as <code>"split"</code>, extra fields ignored</td></tr>
54+
</tbody>
55+
</table>
56+
57+
<h2>Example — all orientations</h2>
58+
<pre><code>import { DataFrame } from "tsb";
59+
import { toDictOriented, fromDictOriented } from "tsb";
60+
61+
const df = DataFrame.fromColumns(
62+
{ name: ["Alice", "Bob"], score: [92, 85] },
63+
{ index: new Index(["r0", "r1"]) },
64+
);
65+
66+
// "dict" / "columns"
67+
toDictOriented(df, "dict");
68+
// { name: { r0: "Alice", r1: "Bob" }, score: { r0: 92, r1: 85 } }
69+
70+
// "list"
71+
toDictOriented(df, "list");
72+
// { name: ["Alice", "Bob"], score: [92, 85] }
73+
74+
// "records"
75+
toDictOriented(df, "records");
76+
// [ { name: "Alice", score: 92 }, { name: "Bob", score: 85 } ]
77+
78+
// "split"
79+
toDictOriented(df, "split");
80+
// { index: ["r0", "r1"], columns: ["name", "score"], data: [["Alice", 92], ["Bob", 85]] }
81+
82+
// "index"
83+
toDictOriented(df, "index");
84+
// { r0: { name: "Alice", score: 92 }, r1: { name: "Bob", score: 85 } }
85+
86+
// fromDictOriented — columns (default)
87+
fromDictOriented({ name: ["Alice", "Bob"], score: [92, 85] });
88+
89+
// fromDictOriented — index
90+
fromDictOriented(
91+
{ r0: { name: "Alice", score: 92 }, r1: { name: "Bob", score: 85 } },
92+
"index",
93+
);
94+
95+
// fromDictOriented — split (round-trip)
96+
const split = toDictOriented(df, "split");
97+
const df2 = fromDictOriented(split, "split");
98+
// df2 is equivalent to df
99+
</code></pre>
100+
101+
<h2>Missing values</h2>
102+
<div class="note">
103+
Missing values (<code>null</code> / <code>undefined</code>) are preserved as <code>null</code>
104+
in all orientations. When using <code>fromDictOriented</code> with <code>"index"</code>
105+
orientation, any column that is absent from a given row object is filled with <code>null</code>.
106+
</div>
107+
108+
<h2>Type signatures</h2>
109+
<pre><code>function toDictOriented(df: DataFrame, orient: "dict" | "columns"): Record&lt;string, Record&lt;string, Scalar&gt;&gt;;
110+
function toDictOriented(df: DataFrame, orient: "list"): Record&lt;string, Scalar[]&gt;;
111+
function toDictOriented(df: DataFrame, orient: "series"): Record&lt;string, Series&lt;Scalar&gt;&gt;;
112+
function toDictOriented(df: DataFrame, orient: "split"): DictSplit;
113+
function toDictOriented(df: DataFrame, orient: "tight"): DictTight;
114+
function toDictOriented(df: DataFrame, orient: "records"): Record&lt;string, Scalar&gt;[];
115+
function toDictOriented(df: DataFrame, orient: "index"): Record&lt;string, Record&lt;string, Scalar&gt;&gt;;
116+
117+
function fromDictOriented(data: Record&lt;string, readonly Scalar[]&gt;, orient?: "columns"): DataFrame;
118+
function fromDictOriented(data: Record&lt;string, Record&lt;string, Scalar&gt;&gt;, orient: "index"): DataFrame;
119+
function fromDictOriented(data: SplitInput, orient: "split" | "tight"): DataFrame;
120+
</code></pre>
121+
</body>
122+
</html>

playground/wide_to_long.html

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tsb — wideToLong</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; }
9+
h1 { color: #0066cc; }
10+
h2 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
11+
pre { background: #f6f8fa; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.9em; }
12+
code { font-family: 'Fira Code', 'Cascadia Code', monospace; }
13+
.note { background: #fffbea; border-left: 4px solid #f5a623; padding: 0.7rem 1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
14+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
15+
th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; }
16+
th { background: #f0f4f8; }
17+
a { color: #0066cc; }
18+
</style>
19+
</head>
20+
<body>
21+
<p><a href="index.html">← tsb playground</a></p>
22+
23+
<h1><code>wideToLong</code></h1>
24+
<p>
25+
Reshape a wide-format DataFrame to long format by collapsing stub-prefixed column
26+
groups into rows — mirrors
27+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.wide_to_long.html" target="_blank">
28+
<code>pandas.wide_to_long()</code></a>.
29+
</p>
30+
31+
<h2>Concept</h2>
32+
<p>
33+
Given a wide DataFrame where repeated measurements are spread across columns with a
34+
common stub prefix and a numeric (or other) suffix — e.g. <code>score_2021</code>,
35+
<code>score_2022</code><code>wideToLong</code> pivots those column groups into rows.
36+
One row per original row per unique suffix is produced.
37+
</p>
38+
39+
<h2>Example — numeric suffixes</h2>
40+
<pre><code>import { DataFrame } from "tsb";
41+
import { wideToLong } from "tsb";
42+
43+
const df = DataFrame.fromColumns({
44+
id: ["x", "y"],
45+
A1: [1, 2],
46+
A2: [3, 4],
47+
B1: [5, 6],
48+
B2: [7, 8],
49+
});
50+
51+
const long = wideToLong(df, ["A", "B"], "id", "num");
52+
53+
// long.columns.values → ["id", "num", "A", "B"]
54+
// long.shape → [4, 4]
55+
//
56+
// id num A B
57+
// x 1 1 5
58+
// y 1 2 6
59+
// x 2 3 7
60+
// y 2 4 8
61+
</code></pre>
62+
63+
<h2>Example — separator and custom suffix</h2>
64+
<pre><code>const df = DataFrame.fromColumns({
65+
country: ["US", "UK"],
66+
gdp_2020: [21e12, 2.7e12],
67+
gdp_2021: [23e12, 3.1e12],
68+
pop_2020: [331e6, 67e6],
69+
pop_2021: [332e6, 68e6],
70+
});
71+
72+
const long = wideToLong(df, ["gdp", "pop"], "country", "year", { sep: "_" });
73+
// long.shape → [4, 4] — 2 countries × 2 years
74+
// Columns: ["country", "year", "gdp", "pop"]
75+
</code></pre>
76+
77+
<h2>API reference</h2>
78+
<pre><code>function wideToLong(
79+
df: DataFrame,
80+
stubnames: string | string[],
81+
i: string | string[],
82+
j: string,
83+
options?: WideToLongOptions,
84+
): DataFrame;
85+
86+
interface WideToLongOptions {
87+
sep?: string; // separator between stub and suffix, default ""
88+
suffix?: string; // regex string matching suffix, default "\\d+"
89+
}
90+
</code></pre>
91+
92+
<h2>Parameters</h2>
93+
<table>
94+
<thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead>
95+
<tbody>
96+
<tr><td><code>df</code></td><td><code>DataFrame</code></td><td>Source DataFrame (not mutated)</td></tr>
97+
<tr><td><code>stubnames</code></td><td><code>string | string[]</code></td><td>Prefix(es) shared by the wide column groups</td></tr>
98+
<tr><td><code>i</code></td><td><code>string | string[]</code></td><td>Column(s) to keep as id variables (repeated per suffix)</td></tr>
99+
<tr><td><code>j</code></td><td><code>string</code></td><td>Name of the new column holding the suffix values</td></tr>
100+
<tr><td><code>options.sep</code></td><td><code>string</code></td><td>Separator between stub and suffix (default: <code>""</code>)</td></tr>
101+
<tr><td><code>options.suffix</code></td><td><code>string</code></td><td>Regex string matching the suffix (default: <code>"\\d+"</code>)</td></tr>
102+
</tbody>
103+
</table>
104+
105+
<h2>Output layout</h2>
106+
<div class="note">
107+
Output columns are always ordered: <strong>id cols</strong>, <strong>j</strong>, <strong>stub cols</strong>
108+
(in the same order the stubs were passed). Suffixes are sorted numerically when they are all
109+
integers, otherwise lexicographically. Wide columns that are absent from the DataFrame are
110+
filled with <code>null</code>.
111+
</div>
112+
</body>
113+
</html>

src/core/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ export { MultiIndex } from "./multi_index.ts";
1717
export type { MultiIndexOptions } from "./multi_index.ts";
1818
export { insertColumn, popColumn, reorderColumns, moveColumn, dataFrameFromPairs } from "./insert_pop.ts";
1919
export type { PopResult } from "./insert_pop.ts";
20+
export { toDictOriented, fromDictOriented } from "./to_from_dict.ts";
21+
export type {
22+
ToDictOrient,
23+
FromDictOrient,
24+
DictSplit,
25+
DictTight,
26+
SplitInput,
27+
} from "./to_from_dict.ts";

0 commit comments

Comments
 (0)