Skip to content

Commit 179391d

Browse files
committed
fix: implement alpha test feedback. Make density=True by default for hist. rework hist implmentation to be identical to matplotlib. rework the demo notebook to be simpler. Add better errors in backend.
1 parent ff62264 commit 179391d

8 files changed

Lines changed: 388 additions & 475 deletions

File tree

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Khisto is a Python library for creating histograms using the **Khiops optimal bi
1212

1313
Documentation is available at **[khiops.github.io/khisto-python](https://khiopsml.github.io/khisto-python/)**.
1414

15+
| Standard Gaussian | Heavy-tailed Pareto |
16+
| --- | --- |
17+
| ![Adaptive Gaussian histogram](docs/images/gaussian-quick-start.png) | ![Adaptive Pareto histogram](docs/images/pareto-quick-start.png) |
18+
1519
## Installation
1620

1721
```bash
@@ -26,17 +30,26 @@ pip install "khisto[matplotlib]"
2630

2731
## Quick Start
2832

29-
### NumPy-like API
30-
3133
```python
3234
import numpy as np
33-
from khisto import histogram
35+
import matplotlib.pyplot as plt
36+
from khisto.matplotlib import hist
37+
38+
# Generate 10,000 samples from a Pareto distribution
39+
long_tail_data = np.random.pareto(3, size=10000)
40+
41+
# Plot an adaptive histogram on logarithmic axes.
42+
n, bins, patches = hist(long_tail_data, density=True)
43+
plt.xscale("symlog")
44+
plt.yscale("log")
45+
plt.show()
3446

35-
# Generate 10,000 samples from a standard Gaussian distribution.
36-
data = np.random.normal(0, 1, 10000)
47+
# Generate 10,000 samples from a Normal distribution
48+
normal_data = np.random.normal(size=10000)
3749

38-
# Compute optimal histogram (drop-in replacement for np.histogram)
39-
hist, bin_edges = histogram(data)
50+
# Plot an adaptive histogram
51+
n, bins, patches = hist(normal_data, density=True)
52+
plt.show()
4053
```
4154

4255
## Development

docs/demo.ipynb

Lines changed: 293 additions & 316 deletions
Large diffs are not rendered by default.

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ Get started
8080
:hidden:
8181

8282
Histograms <array/histogram/index>
83-
Core <core/index>
8483
Matplotlib <matplotlib/index>
84+
Core <core/index>
8585

8686
.. toctree::
8787
:maxdepth: 2

sandbox/khisto_demo.ipynb

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
131131
"\u001b[31mValueError\u001b[39m Traceback (most recent call last)",
132132
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m khisto \u001b[38;5;28;01mimport\u001b[39;00m matplotlib\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m matplotlib.hist([data, [\u001b[32m1\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m3\u001b[39m], [\u001b[32m2\u001b[39m,\u001b[32m2\u001b[39m,\u001b[32m2\u001b[39m,\u001b[32m2\u001b[39m]], max_bins=\u001b[32m20\u001b[39m, alpha=\u001b[32m0.5\u001b[39m)\n",
133-
"\u001b[36mFile \u001b[39m\u001b[32m~/python/khisto-python/src/khisto/matplotlib/hist.py:122\u001b[39m, in \u001b[36mhist\u001b[39m\u001b[34m(x, range, max_bins, density, cumulative, histtype, orientation, log, color, label, ax, edgecolor, linewidth, alpha, **kwargs)\u001b[39m\n\u001b[32m 119\u001b[39m ax = plt.gca()\n\u001b[32m 121\u001b[39m \u001b[38;5;66;03m# Compute histogram using khisto\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m122\u001b[39m hist_values, bin_edges = \u001b[30;43mkhisto_histogram\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 123\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mx\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mrange\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mrange\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mmax_bins\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mmax_bins\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdensity\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mdensity\u001b[39;49m\n\u001b[32m 124\u001b[39m \u001b[30;43m\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 125\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m cumulative_mode != \u001b[32m0\u001b[39m:\n\u001b[32m 126\u001b[39m hist_values = _apply_cumulative(\n\u001b[32m 127\u001b[39m hist_values,\n\u001b[32m 128\u001b[39m bin_edges,\n\u001b[32m 129\u001b[39m density=density,\n\u001b[32m 130\u001b[39m reverse=cumulative_mode < \u001b[32m0\u001b[39m,\n\u001b[32m 131\u001b[39m )\n",
133+
"\u001b[36mFile \u001b[39m\u001b[32m~/python/khisto-python/src/khisto/matplotlib/hist.py:122\u001b[39m, in \u001b[36mhist\u001b[39m\u001b[34m(x, range, max_bins, density, cumulative, histtype, orientation, log, color, label, ax, edgecolor, linewidth, alpha, **kwargs)\u001b[39m\n\u001b[32m 119\u001b[39m ax = plt.gca()\n\u001b[32m 121\u001b[39m \u001b[38;5;66;03m# Compute histogram using khisto\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m122\u001b[39m hist_values, bin_edges = \u001b[30;43mkhisto_histogram\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mx\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mrange\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mrange\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mmax_bins\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mmax_bins\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdensity\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mdensity\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 123\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m cumulative_mode != \u001b[32m0\u001b[39m:\n\u001b[32m 124\u001b[39m hist_values = _apply_cumulative(\n\u001b[32m 125\u001b[39m hist_values,\n\u001b[32m 126\u001b[39m bin_edges,\n\u001b[32m 127\u001b[39m density=density,\n\u001b[32m 128\u001b[39m reverse=cumulative_mode < \u001b[32m0\u001b[39m,\n\u001b[32m 129\u001b[39m )\n",
134134
"\u001b[36mFile \u001b[39m\u001b[32m~/python/khisto-python/src/khisto/array/histogram/api.py:107\u001b[39m, in \u001b[36mhistogram\u001b[39m\u001b[34m(a, range, max_bins, density)\u001b[39m\n\u001b[32m 53\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mhistogram\u001b[39m(\n\u001b[32m 54\u001b[39m a: ArrayLike,\n\u001b[32m 55\u001b[39m \u001b[38;5;28mrange\u001b[39m: Optional[\u001b[38;5;28mtuple\u001b[39m[\u001b[38;5;28mfloat\u001b[39m, \u001b[38;5;28mfloat\u001b[39m]] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 56\u001b[39m max_bins: Optional[\u001b[38;5;28mint\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 57\u001b[39m density: \u001b[38;5;28mbool\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[32m 58\u001b[39m ) -> \u001b[38;5;28mtuple\u001b[39m[NDArray[np.float64], NDArray[np.float64]]:\n\u001b[32m 59\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Compute an optimal histogram using the Khiops binning algorithm.\u001b[39;00m\n\u001b[32m 60\u001b[39m \n\u001b[32m 61\u001b[39m \u001b[33;03m Parameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 105\u001b[39m \u001b[33;03m Analysis, 180:0-0, 2023.\u001b[39;00m\n\u001b[32m 106\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m107\u001b[39m arr = \u001b[30;43mnp\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43masarray\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43ma\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mdtype\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mnp\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mfloat64\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 109\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m arr.ndim != \u001b[32m1\u001b[39m:\n\u001b[32m 110\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 111\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mExpected 1-D array, got \u001b[39m\u001b[38;5;132;01m{\u001b[39;00marr.ndim\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m-D array instead. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 112\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mReshape your data or flatten it before calling histogram.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 113\u001b[39m )\n",
135135
"\u001b[31mValueError\u001b[39m: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (3,) + inhomogeneous part."
136136
]
@@ -153,7 +153,7 @@
153153
},
154154
{
155155
"cell_type": "code",
156-
"execution_count": 5,
156+
"execution_count": null,
157157
"id": "23c19584",
158158
"metadata": {},
159159
"outputs": [
@@ -174,7 +174,7 @@
174174
" <a list of 3 BarContainer objects>)"
175175
]
176176
},
177-
"execution_count": 5,
177+
"execution_count": 6,
178178
"metadata": {},
179179
"output_type": "execute_result"
180180
},
@@ -195,7 +195,7 @@
195195
},
196196
{
197197
"cell_type": "code",
198-
"execution_count": 6,
198+
"execution_count": null,
199199
"id": "2749c664",
200200
"metadata": {},
201201
"outputs": [
@@ -219,7 +219,7 @@
219219
},
220220
{
221221
"cell_type": "code",
222-
"execution_count": 7,
222+
"execution_count": null,
223223
"id": "1cca2392",
224224
"metadata": {},
225225
"outputs": [
@@ -241,7 +241,7 @@
241241
},
242242
{
243243
"cell_type": "code",
244-
"execution_count": 8,
244+
"execution_count": null,
245245
"id": "2ad6d7e5",
246246
"metadata": {},
247247
"outputs": [
@@ -273,7 +273,7 @@
273273
},
274274
{
275275
"cell_type": "code",
276-
"execution_count": 9,
276+
"execution_count": 5,
277277
"id": "b6c4ea8c",
278278
"metadata": {},
279279
"outputs": [
@@ -297,6 +297,7 @@
297297
],
298298
"source": [
299299
"from khisto.matplotlib import hist\n",
300+
"from khisto.matplotlib.hist import _hist\n",
300301
"\n",
301302
"# Basic histogram plot\n",
302303
"fig, ax = plt.subplots(figsize=(8, 5))\n",
@@ -311,7 +312,7 @@
311312
},
312313
{
313314
"cell_type": "code",
314-
"execution_count": 12,
315+
"execution_count": 6,
315316
"id": "09479225",
316317
"metadata": {},
317318
"outputs": [
@@ -339,7 +340,7 @@
339340
},
340341
{
341342
"cell_type": "code",
342-
"execution_count": 16,
343+
"execution_count": 8,
343344
"id": "6c89bf07",
344345
"metadata": {},
345346
"outputs": [
@@ -366,7 +367,7 @@
366367
},
367368
{
368369
"cell_type": "code",
369-
"execution_count": 17,
370+
"execution_count": 12,
370371
"id": "25d8d0e5",
371372
"metadata": {},
372373
"outputs": [
@@ -403,7 +404,7 @@
403404
},
404405
{
405406
"cell_type": "code",
406-
"execution_count": 18,
407+
"execution_count": 15,
407408
"id": "d985437b",
408409
"metadata": {},
409410
"outputs": [
@@ -487,7 +488,7 @@
487488
},
488489
{
489490
"cell_type": "code",
490-
"execution_count": 19,
491+
"execution_count": 18,
491492
"id": "51179a02",
492493
"metadata": {},
493494
"outputs": [
@@ -521,7 +522,7 @@
521522
},
522523
{
523524
"cell_type": "code",
524-
"execution_count": 20,
525+
"execution_count": 21,
525526
"id": "e9bbabc9",
526527
"metadata": {},
527528
"outputs": [
@@ -558,7 +559,7 @@
558559
},
559560
{
560561
"cell_type": "code",
561-
"execution_count": 21,
562+
"execution_count": 22,
562563
"id": "1190f8aa",
563564
"metadata": {},
564565
"outputs": [
@@ -594,7 +595,7 @@
594595
},
595596
{
596597
"cell_type": "code",
597-
"execution_count": 22,
598+
"execution_count": 23,
598599
"id": "bf2ba150",
599600
"metadata": {},
600601
"outputs": [

src/khisto/core/backend.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,14 @@ def _process_histogram_file(file_path: Path) -> list[HistogramResult]:
235235
]
236236

237237

238-
def compute_histograms(x: np.ndarray) -> list[HistogramResult]:
238+
def compute_histograms(x: NDArray[np.float64]) -> list[HistogramResult]:
239239
"""Compute optimal histogram of an array using khisto CLI binary input.
240240
241241
Parameters
242242
----------
243-
x : np.ndarray
244-
Array of numeric values.
243+
x : NDArray[np.float64]
244+
Array of numeric values. Only 1-dimensional arrays are supported.
245+
Missing values (NaN) are filtered out.
245246
246247
Returns
247248
-------
@@ -257,10 +258,14 @@ def compute_histograms(x: np.ndarray) -> list[HistogramResult]:
257258
If input array is empty after filtering.
258259
"""
259260
x = np.asarray(x, dtype=np.float64)
261+
262+
if len(x) == 0:
263+
raise ValueError("Input array is empty")
264+
260265
x = x[~np.isnan(x)]
261266

262267
if len(x) == 0:
263-
raise ValueError("Input array is empty after filtering")
268+
raise ValueError("Input array is empty after filtering missing values")
264269

265270
# Use delete=False so the files are closed before the subprocess reads them.
266271
# On Windows, files keep an exclusive lock while open, whence,

0 commit comments

Comments
 (0)