Skip to content

Commit 0c9121e

Browse files
Iteration 221: Add stats/quantile.ts — quantileSeries and quantileDataFrame
Implements pandas Series.quantile() and DataFrame.quantile() with full feature parity: - quantileSeries(series, options): number | Series<Scalar> - q: scalar or array of quantile levels in [0, 1] - interpolation: linear, lower, higher, midpoint, nearest - skipna: true (default) ignores null/NaN - quantileDataFrame(df, options): Series<Scalar> | DataFrame - axis=0 (default): per-column quantiles - axis=1: per-row quantiles - numericOnly: true (default) skips non-numeric columns - multi-q returns DataFrame; scalar q returns Series - 46 unit + 4 property-based tests (fast-check) - monotonicity, q=0/1 min/max invariants, lower<=linear<=higher - playground/quantile.html — 7 interactive demos Metric: 56 (+1 vs 55) Run: https://github.com/githubnext/tsessebe/actions/runs/24299797044 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 08728ce commit 0c9121e

5 files changed

Lines changed: 919 additions & 0 deletions

File tree

playground/quantile.html

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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 — quantile</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; background: #f9fafb; color: #1a1a2e; }
9+
h1 { color: #4f46e5; }
10+
h2 { color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 6px; }
11+
.demo { background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 16px 0; }
12+
pre { background: #1e1e2e; color: #cdd6f4; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 14px; }
13+
.output { background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; padding: 12px; font-family: monospace; white-space: pre; margin-top: 10px; }
14+
a { color: #4f46e5; }
15+
table { border-collapse: collapse; margin-top: 8px; }
16+
th, td { border: 1px solid #e5e7eb; padding: 6px 12px; text-align: right; }
17+
th { background: #f3f4f6; }
18+
</style>
19+
</head>
20+
<body>
21+
<p><a href="index.html">← tsb playground</a></p>
22+
<h1>📐 quantile</h1>
23+
<p>
24+
<strong><code>quantileSeries</code></strong> / <strong><code>quantileDataFrame</code></strong>
25+
compute quantile(s) / percentile(s), mirroring
26+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.Series.quantile.html"><code>Series.quantile()</code></a> and
27+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.quantile.html"><code>DataFrame.quantile()</code></a>.
28+
</p>
29+
<p>Equivalent Python: <code>series.quantile(q=0.5)</code> / <code>df.quantile(q=0.5)</code></p>
30+
31+
<h2>1 · Scalar quantile (median)</h2>
32+
<div class="demo">
33+
<pre>const s = new Series({ data: [1, 2, 3, 4, 5] });
34+
quantileSeries(s); // default q=0.5 → 3
35+
quantileSeries(s, { q: 0.25 }); // → 2
36+
quantileSeries(s, { q: 0.75 }); // → 4</pre>
37+
<div class="output" id="demo1"></div>
38+
</div>
39+
40+
<h2>2 · Multiple quantile levels</h2>
41+
<div class="demo">
42+
<pre>const s = new Series({ data: [1, 2, 3, 4, 5] });
43+
const q = quantileSeries(s, { q: [0.25, 0.5, 0.75] });
44+
// Series indexed by q-values: { 0.25: 2, 0.5: 3, 0.75: 4 }</pre>
45+
<div class="output" id="demo2"></div>
46+
</div>
47+
48+
<h2>3 · Interpolation methods</h2>
49+
<div class="demo">
50+
<pre>const s = new Series({ data: [0, 10] });
51+
// q=0.5 → position 0.5 between indices 0 and 1
52+
quantileSeries(s, { q: 0.5, interpolation: "linear" }); // 5
53+
quantileSeries(s, { q: 0.5, interpolation: "lower" }); // 0
54+
quantileSeries(s, { q: 0.5, interpolation: "higher" }); // 10
55+
quantileSeries(s, { q: 0.5, interpolation: "midpoint" }); // 5
56+
quantileSeries(s, { q: 0.5, interpolation: "nearest" }); // 0</pre>
57+
<div class="output" id="demo3"></div>
58+
</div>
59+
60+
<h2>4 · NaN handling (skipna=true by default)</h2>
61+
<div class="demo">
62+
<pre>const s = new Series({ data: [1, null, 3, NaN, 5] });
63+
quantileSeries(s, { q: 0.5 }); // ignores null/NaN → 3
64+
quantileSeries(s, { q: 0.5, skipna: false }); // NaN propagates → NaN</pre>
65+
<div class="output" id="demo4"></div>
66+
</div>
67+
68+
<h2>5 · DataFrame — axis=0 (per-column quantiles)</h2>
69+
<div class="demo">
70+
<pre>const df = DataFrame.fromColumns({ a: [1, 2, 3, 4], b: [10, 20, 30, 40] });
71+
quantileDataFrame(df, { q: 0.5 });
72+
// Series { a: 2.5, b: 25 }
73+
74+
quantileDataFrame(df, { q: [0.25, 0.5, 0.75] });
75+
// DataFrame 3×2: rows=[0.25, 0.5, 0.75], cols=[a, b]</pre>
76+
<div class="output" id="demo5"></div>
77+
</div>
78+
79+
<h2>6 · DataFrame — axis=1 (per-row quantiles)</h2>
80+
<div class="demo">
81+
<pre>const df = DataFrame.fromColumns({ a: [1, 2, 3], b: [3, 4, 5], c: [5, 6, 7] });
82+
quantileDataFrame(df, { axis: 1, q: 0.5 });
83+
// Series — median of each row: [3, 4, 5]</pre>
84+
<div class="output" id="demo6"></div>
85+
</div>
86+
87+
<h2>7 · Q=[0, 0.25, 0.5, 0.75, 1] summary table</h2>
88+
<div class="demo">
89+
<pre>const df = DataFrame.fromColumns({ score: [55, 70, 80, 88, 92, 95, 99] });
90+
quantileDataFrame(df, { q: [0, 0.25, 0.5, 0.75, 1] });
91+
// → summary statistics table</pre>
92+
<div id="demo7"></div>
93+
</div>
94+
95+
<script type="module">
96+
import { Series, DataFrame, quantileSeries, quantileDataFrame } from "./tsb.js";
97+
98+
function show(id, val) {
99+
document.getElementById(id).textContent = JSON.stringify(val, null, 2);
100+
}
101+
102+
// Demo 1
103+
{
104+
const s = new Series({ data: [1, 2, 3, 4, 5] });
105+
show("demo1", {
106+
"q=0.5 (default)": quantileSeries(s),
107+
"q=0.25": quantileSeries(s, { q: 0.25 }),
108+
"q=0.75": quantileSeries(s, { q: 0.75 }),
109+
});
110+
}
111+
112+
// Demo 2
113+
{
114+
const s = new Series({ data: [1, 2, 3, 4, 5] });
115+
const result = quantileSeries(s, { q: [0.25, 0.5, 0.75] });
116+
const idx = [...result.index.values];
117+
const vals = [...result.values];
118+
const obj = {};
119+
for (let i = 0; i < idx.length; i++) obj[idx[i]] = vals[i];
120+
show("demo2", obj);
121+
}
122+
123+
// Demo 3
124+
{
125+
const s = new Series({ data: [0, 10] });
126+
show("demo3", {
127+
linear: quantileSeries(s, { q: 0.5, interpolation: "linear" }),
128+
lower: quantileSeries(s, { q: 0.5, interpolation: "lower" }),
129+
higher: quantileSeries(s, { q: 0.5, interpolation: "higher" }),
130+
midpoint: quantileSeries(s, { q: 0.5, interpolation: "midpoint" }),
131+
nearest: quantileSeries(s, { q: 0.5, interpolation: "nearest" }),
132+
});
133+
}
134+
135+
// Demo 4
136+
{
137+
const s = new Series({ data: [1, null, 3, NaN, 5] });
138+
show("demo4", {
139+
"skipna=true": quantileSeries(s, { q: 0.5 }),
140+
"skipna=false": quantileSeries(s, { q: 0.5, skipna: false }),
141+
});
142+
}
143+
144+
// Demo 5
145+
{
146+
const df = DataFrame.fromColumns({ a: [1, 2, 3, 4], b: [10, 20, 30, 40] });
147+
const scalar = quantileDataFrame(df, { q: 0.5 });
148+
const multi = quantileDataFrame(df, { q: [0.25, 0.5, 0.75] });
149+
show("demo5", {
150+
"axis=0 single q=0.5": { a: [...scalar.col("a").values][0] ?? scalar.values[0], b: scalar.values[1] },
151+
"axis=0 multi-q shape": multi.shape,
152+
"axis=0 multi-q row 0.25 a": multi.col("a").values[0],
153+
"axis=0 multi-q row 0.5 a": multi.col("a").values[1],
154+
"axis=0 multi-q row 0.75 a": multi.col("a").values[2],
155+
});
156+
}
157+
158+
// Demo 6
159+
{
160+
const df = DataFrame.fromColumns({ a: [1, 2, 3], b: [3, 4, 5], c: [5, 6, 7] });
161+
const r = quantileDataFrame(df, { axis: 1, q: 0.5 });
162+
show("demo6", { "axis=1 q=0.5 per row": [...r.values] });
163+
}
164+
165+
// Demo 7 — summary table
166+
{
167+
const df = DataFrame.fromColumns({ score: [55, 70, 80, 88, 92, 95, 99] });
168+
const result = quantileDataFrame(df, { q: [0, 0.25, 0.5, 0.75, 1] });
169+
const table = document.createElement("table");
170+
const labels = ["min (q=0)", "Q1 (q=0.25)", "median (q=0.5)", "Q3 (q=0.75)", "max (q=1)"];
171+
const vals = [...result.col("score").values];
172+
let html = "<thead><tr><th>Statistic</th><th>score</th></tr></thead><tbody>";
173+
for (let i = 0; i < labels.length; i++) {
174+
html += `<tr><td>${labels[i]}</td><td>${vals[i]}</td></tr>`;
175+
}
176+
html += "</tbody>";
177+
table.innerHTML = html;
178+
document.getElementById("demo7").appendChild(table);
179+
}
180+
</script>
181+
</body>
182+
</html>

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,9 @@ export type {
235235
AnyAllSeriesOptions,
236236
AnyAllDataFrameOptions,
237237
} from "./stats/index.ts";
238+
export { quantileSeries, quantileDataFrame } from "./stats/index.ts";
239+
export type {
240+
QuantileInterpolation,
241+
QuantileSeriesOptions,
242+
QuantileDataFrameOptions,
243+
} from "./stats/index.ts";

src/stats/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,9 @@ export type {
153153
AnyAllSeriesOptions,
154154
AnyAllDataFrameOptions,
155155
} from "./nunique.ts";
156+
export { quantileSeries, quantileDataFrame } from "./quantile.ts";
157+
export type {
158+
QuantileInterpolation,
159+
QuantileSeriesOptions,
160+
QuantileDataFrameOptions,
161+
} from "./quantile.ts";

0 commit comments

Comments
 (0)