Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 40 additions & 17 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,18 @@ on:
type: boolean
default: false
pull_request:
types: [opened, reopened, synchronize, closed]
paths:
- docs/**.rst
- docs/conf.py
- docs/make.bat
- docs/Makefile
- docs/requirements.txt
- docs/_static/**.css
- src/khisto/**.py
- docs/**
- pyproject.toml
- src/khisto/**/*.py
- .github/workflows/docs.yaml
push:
tags: ['*']
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
# Keep default workflow permissions read-only; deployment jobs elevate explicitly.
permissions:
contents: read
id-token: write
packages: read
pages: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest
# queued. However, do NOT cancel in-progress runs as we want to allow these production deployments
# to complete.
Expand All @@ -34,6 +29,7 @@ concurrency:
cancel-in-progress: false
jobs:
build:
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-22.04
steps:
- name: Checkout khisto-python
Expand Down Expand Up @@ -61,7 +57,34 @@ jobs:
with:
name: khisto-python-docs
path: ./docs/_build/html/
# Publish to GH pages on Git tag push
preview:
if: >-
always() &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does always() add here, as it is always true? I would consider removing it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always() permet d’exécuter ce job lorsque build est skipped, notamment à la fermeture d’une PR. Sans lui, GitHub Actions applique implicitement success() et ignore preview, empêchant ainsi la suppression de l’aperçu. Je propose donc de le conserver.

github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
(github.event.action == 'closed' || needs.build.result == 'success')
needs: build
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout khisto-python
uses: actions/checkout@v4
- name: Download the docs artifact
if: github.event.action != 'closed'
uses: actions/download-artifact@v4
with:
name: khisto-python-docs
path: ./docs/_build/html
- name: Deploy PR preview
uses: rossjrw/pr-preview-action@v1
with:
source-dir: ./docs/_build/html
preview-branch: gh-pages
wait-for-pages-deployment: true
qr-code: false
# Publish production docs to GitHub Pages on manual request.
publish:
if: github.event_name == 'workflow_dispatch' && inputs.deploy-gh-pages == true
needs: build
Expand All @@ -77,9 +100,9 @@ jobs:
name: khisto-python-docs
path: ./docs/_build/html
- name: Deploy web site
run: |
# Install tool for pushing to GH pages
pip install ghp-import

# Push built site directory contents to GH pages
ghp-import -m "Deployed ${GITHUB_SHA:0:7}" --push --force --no-jekyll ./docs/_build/html/
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: ./docs/_build/html
branch: gh-pages
clean-exclude: pr-preview
force: false
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ plt.show()

## Development

On Windows, install [Visual Studio Build Tools 2022](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
with the **Desktop development with C++** workload before running the commands
below.

```bash
# Clone repository
git clone https://github.com/KhiopsML/khisto-python.git
Expand Down
47 changes: 47 additions & 0 deletions docs/_static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,53 @@ img.sidebar-logo {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important;
}

@media (prefers-reduced-motion: reduce) {
.sd-card {
transition: none;
}
}

.histogram-guide-card {
border-color: #4170d8 !important;
background: #4170d8 !important;
color: #ffffff !important;
}

.histogram-guide-card .sd-card-title,
.histogram-guide-card .sd-card-text {
color: #ffffff;
}

[data-theme="dark"] .histogram-guide-card {
border-color: #6699ff !important;
background: #6699ff !important;
color: #000000 !important;
}

[data-theme="dark"] .histogram-guide-card .sd-card-title,
[data-theme="dark"] .histogram-guide-card .sd-card-text {
color: #000000;
}

.histogram-guide-card:hover {
border-color: #f16e00 !important;
}

[data-theme="dark"] .histogram-guide-card:hover {
border-color: #ff7900 !important;
}

.histogram-guide-card:focus-within {
outline: 3px solid #000000;
outline-offset: 2px;
box-shadow: 0 0 0 2px #ffffff !important;
}

[data-theme="dark"] .histogram-guide-card:focus-within {
outline-color: #ffffff;
box-shadow: 0 0 0 2px #000000 !important;
}

/* Gallery images */
.sd-card img {
border-radius: 4px;
Expand Down
7 changes: 5 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@

html_theme = 'furo'
html_theme_options = {
"source_repository": "https://github.com/KhiopsML/khisto-python/",
"source_branch": "main",
"source_directory": "docs/",
"light_css_variables": {
"color-brand-primary": "#FF7900",
"color-brand-content": "#F16E00",
Expand All @@ -80,7 +83,7 @@
"color-highlighted-background": "#FFD200",
"color-admonition-title--note": "#FF7900",
"color-admonition-title-background--note": "#FFF0E2",
"font-stack": "Helvetica Neue, Helvetica, sans-serif",
"font-stack": "Helvetica Neue, Helvetica, Arial, sans-serif",
},
"dark_css_variables": {
"color-brand-primary": "#FF7900",
Expand All @@ -90,7 +93,7 @@
"color-highlighted-background": "#FFD200",
"color-admonition-title--note": "#FF7900",
"color-admonition-title-background--note": "#CC6100",
"font-stack": "Helvetica Neue, Helvetica, sans-serif",
"font-stack": "Helvetica Neue, Helvetica, Arial, sans-serif",
},
# Sets the Github Icon (the SVG is embedded, copied from furo's repo)
"footer_icons": [
Expand Down
27 changes: 27 additions & 0 deletions docs/counts_vs_density.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Counts vs Density
=================

Both columns show the same 500 observations with two different binning schemes.
With equal-width bins, counts and density have the same shape. With variable-width
bins, raw counts favor wider intervals and can distort the visual comparison.

.. image:: images/counts-vs-density.png

With equal-width bins, taller bars indicate a greater probability of falling
within that interval. When bin widths vary, height alone is not enough:
probability is represented by the bar's area, calculated as density multiplied
by bin width.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a detailed introduction to density and variable-width bins
->
Note that variable-width bins are ideal for unbalanced distributions and outliers, as shown in [Histograms - Khiops].

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@ElouenGinat ElouenGinat Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image I added back this section. I had a reviewer telling me he liked it (it's maybe a little too long... What are your recommandations?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L'utilisateur est clairement prioritaire; il faut donc garder ce tableau.

Par contre, corriger:
"Counts divided by 𝑁
, or density"
=>
"Counts divided by 𝑁
, or density times bin width"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Note that variable-width bins are ideal for unbalanced distributions and outliers,
as shown in `Histograms - Khiops <https://khiops.org/learn/histograms/>`_.

Reading variable-width histograms
---------------------------------

.. csv-table::
:header: "Question", "Use", "What to compare"
:widths: 35, 35, 30

"How many observations?", "Counts (``density=False``)", "Bar heights"
"What fraction of observations?", "Counts divided by :math:`N`, or density", "Bar areas"
"Where are values concentrated?", "Density (``density=True``, the Khisto default)", "Bar heights"
75 changes: 44 additions & 31 deletions docs/demo.ipynb

Large diffs are not rendered by default.

Binary file added docs/images/counts-vs-density.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Khisto — Histograms that fit your data
Get started
-----------

.. admonition:: Density by default
:class: important

Unlike NumPy and Matplotlib, Khisto uses density by default.
:doc:`Learn how to read variable-width histograms <counts_vs_density>`.

.. div:: install-cmd

.. code-block:: bash
Expand Down Expand Up @@ -74,6 +80,23 @@ Get started

A runnable notebook tour covering all features.

.. grid:: 1 1 2 2
:gutter: 3
:class-container: sd-mt-3

.. grid-item-card:: :octicon:`law;1.5em` Counts or density?
:link: counts_vs_density
:link-type: doc

See why counts mislead when bins have different widths.

.. grid-item-card:: :octicon:`info;1.5em` Histograms - Khiops
:link: https://khiops.org/learn/histograms/
:link-type: url
:class-card: histogram-guide-card

Go further, from the simplest histogram to the most complex.

.. toctree::
:maxdepth: 2
:caption: API Reference
Expand All @@ -89,3 +112,4 @@ Get started
:hidden:

Demo <demo>
Counts vs density <counts_vs_density>
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ ignore = [
[tool.scikit-build]
wheel.py-api = "py3" # We specify full compatibility with all Python 3 releases
cmake.version = "CMakeLists.txt" # Read CMake minimal version from file
cmake.args = ["-G", "Ninja"] # Generator specification and build configuration

# CMake variables
# Build khisto binary in Release mode for better performance, as it is not intended for debugging
Expand All @@ -143,7 +142,7 @@ cmake.define.BUILD_LEX_YACC = false
cmake.define.BUILD_JARS = false
cmake.define.GENERATE_VIEWS = false
cmake.define.C11 = true
ninja.make-fallback = false
ninja.make-fallback = true
build.targets = ["khisto"] # Build only khisto
install.components = ["KHISTO"] # Only install KHISTO component in wheel

Expand Down
85 changes: 84 additions & 1 deletion scripts/generate_distribution_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from khisto.matplotlib import hist


SEED = 42
SAMPLE_SIZE = 10_000
PARETO_SHAPE = 3.0
Expand Down Expand Up @@ -42,6 +41,86 @@ def save_gaussian_figure(data: np.ndarray, output_path: Path) -> None:
plt.close(fig)


def save_counts_density_comparison_figure(output_path: Path) -> None:
equal_edges = np.array([0, 10, 20, 30, 40])
equal_counts = np.array([90, 110, 100, 200])
variable_edges = np.array([0, 30, 40])
variable_counts = np.array([300, 200])
total_count = equal_counts.sum()

fig, axes = plt.subplots(2, 2, figsize=(9, 5.5), sharex=True)
configurations = (
(equal_edges, equal_counts, "Without Khisto\nEqual-width bins"),
(variable_edges, variable_counts, "With Khisto\nVariable-width bins"),
)
for column, (edges, counts, title) in enumerate(configurations):
widths = np.diff(edges)
densities = counts / (total_count * widths)
colors = ["darkorange" if edge >= 30 else "steelblue" for edge in edges[:-1]]
count_bars = axes[0, column].bar(
edges[:-1], counts, width=widths, align="edge", color=colors,
edgecolor="white",
)
density_bars = axes[1, column].bar(
edges[:-1], densities, width=widths, align="edge", color=colors,
edgecolor="white",
)
axes[0, column].bar_label(count_bars, labels=[str(count) for count in counts], padding=3)
axes[1, column].bar_label(
density_bars,
labels=[f"{density:.3f}" for density in densities],
padding=3,
)
axes[0, column].set_title(title)
for row in range(2):
axes[row, column].set_xticks([0, 10, 20, 30, 40])
axes[row, column].tick_params(axis="x", labelbottom=True)
axes[row, column].set_xlabel("Value")
axes[0, column].set_ylim(0, 350)
axes[1, column].set_ylim(0, 0.048)

axes[0, 0].set_ylabel("Count")
axes[1, 0].set_ylabel("Density")
axes[0, 0].text(
15,
165,
"Small local fluctuation",
ha="center",
)
axes[0, 1].text(
15,
245,
"Counts apply to the whole interval;\ndo not rely on rectangle area",
color="white",
ha="center",
va="center",
weight="bold",
fontsize=9,
)
axes[1, 1].text(
15,
0.012,
"Fluctuation smoothed out\nin the wider bin",
color="white",
ha="center",
va="center",
weight="bold",
)
axes[1, 1].annotate(
"bin 30–40 is twice \nas dense as bin 0-30",
xy=(35, 0.034),
xytext=(15, 0.034),
arrowprops={"arrowstyle": "->", "color": "#333333"},
ha="center",
va="center",
weight="bold",
)
fig.suptitle("Same 500 observations, two binning choices")
fig.tight_layout()
fig.savefig(output_path, dpi=180)
plt.close(fig)


def save_pareto_figure(data: np.ndarray, output_path: Path) -> None:
fig, ax = plt.subplots(figsize=(7, 4.5))
hist(data, density=True, ax=ax, color="darkorange", edgecolor="white", linewidth=0.8)
Expand All @@ -64,6 +143,9 @@ def main() -> None:
pareto_data = generate_pareto_data()

save_gaussian_figure(gaussian_data, images_dir / "gaussian-quick-start.png")
save_counts_density_comparison_figure(
images_dir / "counts-vs-density.png"
)
save_pareto_figure(pareto_data, images_dir / "pareto-quick-start.png")

print(f"Gaussian sample: size={gaussian_data.size}, mean={gaussian_data.mean():.4f}, std={gaussian_data.std():.4f}")
Expand All @@ -72,6 +154,7 @@ def main() -> None:
f"size={pareto_data.size}, min={pareto_data.min():.4f}, max={pareto_data.max():.4f}, shape={PARETO_SHAPE:.1f}"
)
print(f"Wrote {images_dir / 'gaussian-quick-start.png'}")
print(f"Wrote {images_dir / 'counts-vs-density.png'}")
print(f"Wrote {images_dir / 'pareto-quick-start.png'}")


Expand Down
Loading