|
| 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 — NaN-Ignoring Aggregates (nancumops)</title> |
| 7 | + <style> |
| 8 | + :root { |
| 9 | + --bg: #0d1117; --surface: #161b22; --border: #30363d; |
| 10 | + --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff; |
| 11 | + --green: #3fb950; --yellow: #d29922; --red: #f85149; |
| 12 | + } |
| 13 | + * { box-sizing: border-box; margin: 0; padding: 0; } |
| 14 | + body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; padding: 2rem; } |
| 15 | + h1 { font-size: 1.6rem; margin-bottom: 0.25rem; } |
| 16 | + .subtitle { color: var(--muted); margin-bottom: 2rem; } |
| 17 | + h2 { font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; } |
| 18 | + p { color: var(--muted); line-height: 1.6; margin-bottom: 0.75rem; } |
| 19 | + .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; } |
| 20 | + label { font-size: 0.85rem; color: var(--muted); display: block; margin-bottom: 0.25rem; } |
| 21 | + input, select { background: var(--bg); border: 1px solid var(--border); color: var(--text); |
| 22 | + border-radius: 4px; padding: 0.4rem 0.6rem; font-size: 0.95rem; width: 100%; margin-bottom: 0.75rem; } |
| 23 | + button { background: var(--accent); color: #0d1117; border: none; border-radius: 4px; |
| 24 | + padding: 0.5rem 1.25rem; font-size: 0.95rem; cursor: pointer; font-weight: 600; } |
| 25 | + button:hover { opacity: 0.85; } |
| 26 | + .output { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; |
| 27 | + padding: 0.75rem; font-family: monospace; font-size: 0.9rem; white-space: pre; min-height: 3rem; } |
| 28 | + .row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } |
| 29 | + table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } |
| 30 | + th { background: var(--bg); color: var(--muted); padding: 0.4rem 0.75rem; text-align: left; |
| 31 | + border-bottom: 1px solid var(--border); } |
| 32 | + td { padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--border); font-family: monospace; } |
| 33 | + code { background: var(--bg); padding: 0.1rem 0.35rem; border-radius: 3px; font-size: 0.88rem; } |
| 34 | + .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 3px; font-size: 0.78rem; |
| 35 | + font-weight: 600; margin-left: 0.5rem; } |
| 36 | + .badge-green { background: #1a3a1c; color: var(--green); } |
| 37 | + .badge-blue { background: #112233; color: var(--accent); } |
| 38 | + details { margin-bottom: 1rem; } |
| 39 | + summary { cursor: pointer; color: var(--accent); font-size: 0.9rem; } |
| 40 | + .py-equiv { background: #1a1000; border: 1px solid #3d2800; border-radius: 4px; |
| 41 | + padding: 0.75rem; font-family: monospace; font-size: 0.88rem; white-space: pre; color: #d29922; } |
| 42 | + </style> |
| 43 | +</head> |
| 44 | +<body> |
| 45 | + |
| 46 | +<h1>🔢 NaN-Ignoring Aggregates</h1> |
| 47 | +<p class="subtitle"> |
| 48 | + <code>nansum</code>, <code>nanmean</code>, <code>nanmedian</code>, <code>nanstd</code>, <code>nanvar</code>, |
| 49 | + <code>nanmin</code>, <code>nanmax</code>, <code>nanprod</code>, <code>nancount</code> |
| 50 | + — mirrors <code>numpy.nan*</code> functions in pandas workflows. |
| 51 | +</p> |
| 52 | + |
| 53 | +<!-- ── Live Calculator ─────────────────────────────────────────────── --> |
| 54 | +<div class="card"> |
| 55 | + <h2>🧮 Live Calculator</h2> |
| 56 | + <p>Enter a comma-separated list of numbers (use <code>NaN</code>, <code>null</code> for missing).</p> |
| 57 | + <label>Input values</label> |
| 58 | + <input id="inputValues" value="1, 2, NaN, null, 3, 5, NaN" /> |
| 59 | + <label>ddof (for std/var)</label> |
| 60 | + <select id="ddofSelect"> |
| 61 | + <option value="1">1 (sample — default)</option> |
| 62 | + <option value="0">0 (population)</option> |
| 63 | + <option value="2">2</option> |
| 64 | + </select> |
| 65 | + <button onclick="runCalc()">Compute All</button> |
| 66 | + <div id="calcOutput" class="output" style="margin-top:0.75rem;"></div> |
| 67 | +</div> |
| 68 | + |
| 69 | +<!-- ── Reference Table ────────────────────────────────────────────── --> |
| 70 | +<div class="card"> |
| 71 | + <h2>📖 Function Reference</h2> |
| 72 | + <table> |
| 73 | + <thead> |
| 74 | + <tr> |
| 75 | + <th>Function</th> |
| 76 | + <th>Description</th> |
| 77 | + <th>Empty/all-NaN returns</th> |
| 78 | + <th>pandas / numpy equivalent</th> |
| 79 | + </tr> |
| 80 | + </thead> |
| 81 | + <tbody> |
| 82 | + <tr><td><code>nancount(input)</code></td><td>Count of valid (non-NaN) numeric values</td><td><code>0</code></td><td><code>np.count_nonzero(~np.isnan(a))</code></td></tr> |
| 83 | + <tr><td><code>nansum(input)</code></td><td>Sum, ignoring NaN/null</td><td><code>0</code></td><td><code>np.nansum(a)</code></td></tr> |
| 84 | + <tr><td><code>nanmean(input)</code></td><td>Mean, ignoring NaN/null</td><td><code>NaN</code></td><td><code>np.nanmean(a)</code></td></tr> |
| 85 | + <tr><td><code>nanmedian(input)</code></td><td>Median, ignoring NaN/null</td><td><code>NaN</code></td><td><code>np.nanmedian(a)</code></td></tr> |
| 86 | + <tr><td><code>nanvar(input, {ddof})</code></td><td>Variance (ddof=1 default)</td><td><code>NaN</code></td><td><code>np.nanvar(a, ddof=1)</code></td></tr> |
| 87 | + <tr><td><code>nanstd(input, {ddof})</code></td><td>Std deviation (ddof=1 default)</td><td><code>NaN</code></td><td><code>np.nanstd(a, ddof=1)</code></td></tr> |
| 88 | + <tr><td><code>nanmin(input)</code></td><td>Minimum, ignoring NaN/null</td><td><code>NaN</code></td><td><code>np.nanmin(a)</code></td></tr> |
| 89 | + <tr><td><code>nanmax(input)</code></td><td>Maximum, ignoring NaN/null</td><td><code>NaN</code></td><td><code>np.nanmax(a)</code></td></tr> |
| 90 | + <tr><td><code>nanprod(input)</code></td><td>Product, ignoring NaN/null</td><td><code>1</code></td><td><code>np.nanprod(a)</code></td></tr> |
| 91 | + </tbody> |
| 92 | + </table> |
| 93 | +</div> |
| 94 | + |
| 95 | +<!-- ── Usage Examples ─────────────────────────────────────────────── --> |
| 96 | +<div class="card"> |
| 97 | + <h2>💡 Usage Examples</h2> |
| 98 | + |
| 99 | + <details open> |
| 100 | + <summary>Basic array usage</summary> |
| 101 | + <pre style="margin-top:0.5rem;padding:0.75rem;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.88rem;overflow-x:auto;"> |
| 102 | +import { nansum, nanmean, nanmedian, nanstd } from "tsb"; |
| 103 | + |
| 104 | +const data = [1, 2, NaN, null, 3, 5]; |
| 105 | + |
| 106 | +nansum(data); // 11 |
| 107 | +nanmean(data); // 2.75 |
| 108 | +nanmedian(data); // 2.5 |
| 109 | +nanstd(data); // 1.708... |
| 110 | +</pre> |
| 111 | + <div class="py-equiv"> |
| 112 | +# Python / pandas equivalent |
| 113 | +import numpy as np |
| 114 | + |
| 115 | +data = [1, 2, np.nan, np.nan, 3, 5] |
| 116 | + |
| 117 | +np.nansum(data) # 11.0 |
| 118 | +np.nanmean(data) # 2.75 |
| 119 | +np.nanmedian(data) # 2.5 |
| 120 | +np.nanstd(data, ddof=1) # 1.708... |
| 121 | + </div> |
| 122 | + </details> |
| 123 | + |
| 124 | + <details> |
| 125 | + <summary>Using with Series</summary> |
| 126 | + <pre style="margin-top:0.5rem;padding:0.75rem;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.88rem;overflow-x:auto;"> |
| 127 | +import { Series, nansum, nanmean, nancount } from "tsb"; |
| 128 | + |
| 129 | +const s = new Series({ data: [10, null, 30, NaN, 50] }); |
| 130 | + |
| 131 | +nancount(s); // 3 |
| 132 | +nansum(s); // 90 |
| 133 | +nanmean(s); // 30 |
| 134 | +</pre> |
| 135 | + <div class="py-equiv"> |
| 136 | +# Python / pandas equivalent |
| 137 | +import pandas as pd, numpy as np |
| 138 | + |
| 139 | +s = pd.Series([10, np.nan, 30, np.nan, 50]) |
| 140 | + |
| 141 | +s.count() # 3 |
| 142 | +s.sum() # 90.0 |
| 143 | +s.mean() # 30.0 |
| 144 | + </div> |
| 145 | + </details> |
| 146 | + |
| 147 | + <details> |
| 148 | + <summary>Variance and std with ddof</summary> |
| 149 | + <pre style="margin-top:0.5rem;padding:0.75rem;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.88rem;overflow-x:auto;"> |
| 150 | +import { nanvar, nanstd } from "tsb"; |
| 151 | + |
| 152 | +const xs = [2, 4, 4, 4, 5, 5, 7, 9]; |
| 153 | + |
| 154 | +// Sample (ddof=1, default) |
| 155 | +nanvar(xs); // ≈ 4.571 |
| 156 | +nanstd(xs); // ≈ 2.138 |
| 157 | + |
| 158 | +// Population (ddof=0) |
| 159 | +nanvar(xs, { ddof: 0 }); // 4.0 |
| 160 | +nanstd(xs, { ddof: 0 }); // 2.0 |
| 161 | +</pre> |
| 162 | + <div class="py-equiv"> |
| 163 | +# Python / pandas equivalent |
| 164 | +import numpy as np |
| 165 | + |
| 166 | +xs = [2, 4, 4, 4, 5, 5, 7, 9] |
| 167 | + |
| 168 | +np.nanvar(xs, ddof=1) # 4.571... |
| 169 | +np.nanstd(xs, ddof=1) # 2.138... |
| 170 | + |
| 171 | +np.nanvar(xs, ddof=0) # 4.0 |
| 172 | +np.nanstd(xs, ddof=0) # 2.0 |
| 173 | + </div> |
| 174 | + </details> |
| 175 | +</div> |
| 176 | + |
| 177 | +<!-- ── Comparison: skipna vs no-skipna ──────────────────────────────── --> |
| 178 | +<div class="card"> |
| 179 | + <h2>⚡ NaN Impact Demo</h2> |
| 180 | + <p>See how NaN values affect results with and without nan-ignoring functions.</p> |
| 181 | + <button onclick="runComparison()">Run Comparison</button> |
| 182 | + <div id="compOutput" class="output" style="margin-top:0.75rem;min-height:6rem;"></div> |
| 183 | +</div> |
| 184 | + |
| 185 | +<script type="module"> |
| 186 | + // ── inline implementation (mirrors src/stats/nancumops.ts) ────────── |
| 187 | + function toValues(input) { |
| 188 | + if (Array.isArray(input)) return input; |
| 189 | + return input.values ?? []; |
| 190 | + } |
| 191 | + function numericValues(input) { |
| 192 | + return toValues(input).filter(v => typeof v === "number" && !Number.isNaN(v)); |
| 193 | + } |
| 194 | + function sortedAsc(xs) { return [...xs].sort((a, b) => a - b); } |
| 195 | + |
| 196 | + function nancount(input) { return numericValues(input).length; } |
| 197 | + function nansum(input) { |
| 198 | + const xs = numericValues(input); |
| 199 | + return xs.length === 0 ? 0 : xs.reduce((s, x) => s + x, 0); |
| 200 | + } |
| 201 | + function nanmean(input) { |
| 202 | + const xs = numericValues(input); |
| 203 | + if (xs.length === 0) return NaN; |
| 204 | + return xs.reduce((s, x) => s + x, 0) / xs.length; |
| 205 | + } |
| 206 | + function nanmedian(input) { |
| 207 | + const xs = sortedAsc(numericValues(input)); |
| 208 | + const n = xs.length; |
| 209 | + if (n === 0) return NaN; |
| 210 | + const mid = Math.floor(n / 2); |
| 211 | + return n % 2 === 1 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2; |
| 212 | + } |
| 213 | + function nanvar(input, ddof = 1) { |
| 214 | + const xs = numericValues(input); |
| 215 | + const n = xs.length; |
| 216 | + if (n <= ddof) return NaN; |
| 217 | + const mean = xs.reduce((s, x) => s + x, 0) / n; |
| 218 | + return xs.reduce((ss, x) => ss + (x - mean) ** 2, 0) / (n - ddof); |
| 219 | + } |
| 220 | + function nanstd(input, ddof = 1) { return Math.sqrt(nanvar(input, ddof)); } |
| 221 | + function nanmin(input) { |
| 222 | + const xs = numericValues(input); |
| 223 | + return xs.length === 0 ? NaN : Math.min(...xs); |
| 224 | + } |
| 225 | + function nanmax(input) { |
| 226 | + const xs = numericValues(input); |
| 227 | + return xs.length === 0 ? NaN : Math.max(...xs); |
| 228 | + } |
| 229 | + function nanprod(input) { |
| 230 | + const xs = numericValues(input); |
| 231 | + return xs.length === 0 ? 1 : xs.reduce((p, x) => p * x, 1); |
| 232 | + } |
| 233 | + |
| 234 | + function parseInput(str) { |
| 235 | + return str.split(",").map(s => { |
| 236 | + const t = s.trim(); |
| 237 | + if (t === "null" || t === "undefined") return null; |
| 238 | + if (t === "NaN") return NaN; |
| 239 | + const n = Number(t); |
| 240 | + return Number.isNaN(n) ? null : n; |
| 241 | + }); |
| 242 | + } |
| 243 | + |
| 244 | + function fmt(n) { |
| 245 | + if (typeof n !== "number") return String(n); |
| 246 | + if (Number.isNaN(n)) return "NaN"; |
| 247 | + return Number.isInteger(n) ? String(n) : n.toPrecision(6); |
| 248 | + } |
| 249 | + |
| 250 | + window.runCalc = function() { |
| 251 | + const data = parseInput(document.getElementById("inputValues").value); |
| 252 | + const ddof = parseInt(document.getElementById("ddofSelect").value); |
| 253 | + const lines = [ |
| 254 | + `Input (parsed): [${data.map(v => v === null ? "null" : fmt(v)).join(", ")}]`, |
| 255 | + `nancount : ${nancount(data)}`, |
| 256 | + `nansum : ${fmt(nansum(data))}`, |
| 257 | + `nanmean : ${fmt(nanmean(data))}`, |
| 258 | + `nanmedian : ${fmt(nanmedian(data))}`, |
| 259 | + `nanvar : ${fmt(nanvar(data, ddof))} (ddof=${ddof})`, |
| 260 | + `nanstd : ${fmt(nanstd(data, ddof))} (ddof=${ddof})`, |
| 261 | + `nanmin : ${fmt(nanmin(data))}`, |
| 262 | + `nanmax : ${fmt(nanmax(data))}`, |
| 263 | + `nanprod : ${fmt(nanprod(data))}`, |
| 264 | + ]; |
| 265 | + document.getElementById("calcOutput").textContent = lines.join("\n"); |
| 266 | + }; |
| 267 | + |
| 268 | + window.runComparison = function() { |
| 269 | + const data = [1, 2, NaN, null, 3, 5, NaN, 4]; |
| 270 | + const cleanData = data.filter(v => typeof v === "number" && !Number.isNaN(v)); |
| 271 | + |
| 272 | + const lines = [ |
| 273 | + `Full array : [${data.map(v => v === null ? "null" : fmt(v)).join(", ")}]`, |
| 274 | + `Clean array: [${cleanData.join(", ")}]`, |
| 275 | + "", |
| 276 | + "Standard JS Math (without NaN handling):", |
| 277 | + ` [1,2,NaN,...].reduce(+) = ${[1,2,NaN,null,3,5,NaN,4].reduce((a,b) => a + b, 0)}`, |
| 278 | + ` Math.min(1,2,NaN,null,3,5) = ${Math.min(1,2,NaN,null,3,5)}`, |
| 279 | + "", |
| 280 | + "tsb nan-ignoring functions:", |
| 281 | + ` nansum = ${fmt(nansum(data))} (ignores NaN/null)`, |
| 282 | + ` nanmean = ${fmt(nanmean(data))} (ignores NaN/null)`, |
| 283 | + ` nanmin = ${fmt(nanmin(data))} (ignores NaN/null)`, |
| 284 | + ` nanmax = ${fmt(nanmax(data))} (ignores NaN/null)`, |
| 285 | + ` nancount = ${nancount(data)} / ${data.length} values are valid`, |
| 286 | + ]; |
| 287 | + document.getElementById("compOutput").textContent = lines.join("\n"); |
| 288 | + }; |
| 289 | + |
| 290 | + // run on load |
| 291 | + window.runCalc(); |
| 292 | +</script> |
| 293 | + |
| 294 | +</body> |
| 295 | +</html> |
0 commit comments