diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e60776e..aafb17c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,3 +28,9 @@ jobs: - name: Run linter run: uv run ruff check . + + # The pre-commit hook only helps people who installed it. This is the gate: + # the root README is outside MkDocs, so nothing else notices it drifting + # from the package. + - name: Check the estimator tables match the package + run: uv run --no-sync python scripts/sync_readme_tables.py --check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b6ca98c..3473d2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,17 @@ repos: hooks: - id: black + - repo: local + hooks: + # The root README never passes through MkDocs, so no plugin can keep its + # estimator tables in step with the docs. CI runs the same check. + - id: sync-readme-tables + name: estimator tables are current + entry: uv run python scripts/sync_readme_tables.py --check + language: system + pass_filenames: false + files: ^(README\.md|docs/README\.md|scripts/estimators\.py|ikpykit/.*/__init__\.py)$ + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: diff --git a/README.md b/README.md index a55dfb1..53b7b8c 100644 --- a/README.md +++ b/README.md @@ -82,74 +82,76 @@ clf.predict([[0.1, 0.3], [0, 0.7], [90, 85]]) ## Implemented Algorithms + + #### Summary -| Algorithms | Kernel Similarity | Anomaly Detection | Clustering | Change Detection | -| --------------- | ------------------------------ | -------------------------- | -------------------- | ---------------- | -| Point Data | IsoKernel (AAAI'19, SIGKDD'18) | IForest (ICDM'08, TKDD'12) | IDKC (IS'23) | | -| | | INNE (CIJ'18) | PSKC (TKDE'23) | | -| | | IDKD (TKDE'22) | IKAHC (PRJ'23) | | -| Graph Data | IsoGraphKernel (AAAI'21) | IKGOD (SIAM'23) | | | -| Group Data | IsodisKernel (SIGKDD'20) | IKGAD (TKDE'22) | | | -| Stream Data | | | StreaKHC (SIGKDD'22) | ICID (JAIR'24) | -| Time Series | | IKTOD (VLDB'22) | | | -| Trajectory Data | | IKAT (JAIR'24) | TIDKC (ICDM'23) | | +| Algorithms | Kernel Similarity | Anomaly Detection | Clustering | Change Detection | +| --- | --- | --- | --- | --- | +| Point Data | IsoKernel (AAAI'19, SIGKDD'18) | IForest (ICDM'08, TKDD'12)
INNE (CIJ'18)
IDKD (TKDE'22) | IDKC (IS'23)
PSKC (TKDE'23)
IKAHC (PR'23) | | +| Graph Data | IsoGraphKernel (AAAI'21) | IKGOD (SIAM'23) | | | +| Group Data | IsoDisKernel (SIGKDD'20) | IKGAD (TKDE'22) | | | +| Stream Data | | | STREAMKHC (SIGKDD'22) | ICID (JAIR'24) | +| Time Series | | IKTOD (VLDB'22) | | | +| Trajectory Data | | IKAT (JAIR'24) | TIDKC (ICDM'23) | | + +**(i) Isolation Kernel**: -**(i) Isolation Kernel** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IsoDisKernel](https://isolationkernel.github.io/ikpykit/latest/api/kernel/isodiskernel.html) | Isolation Distribution Kernel | Distribution similarity calculating | SIGKDD2020 | +| [IsoKernel](https://isolationkernel.github.io/ikpykit/latest/api/kernel/isokernel.html) | Isolation Kernel | IK feature mapping and similarity calculating | AAAI2019, SIGKDD2018 | -| Abbr | Algorithm | Application | Publication | -| ---------------------------------------------------------------------------------------------- | ----------------------------- | --------------------------------------------- | -------------------- | -| [IsoKernel](https://isolationkernel.github.io/ikpykit/latest/api/kernel/isolation_kernel.html) | Isolation Kernel | IK feature mapping and similarity calculating | AAAI2019, SIGKDD2018 | -| [IsoDisKernel](https://isolationkernel.github.io/ikpykit/latest/api/kernel/isolation_dis_kernel.html) | Isolation Distribution Kernel | Distribution similarity calculating | SIGKDD2020 | +**(ii) Point Anomaly Detection**: -**(ii) Point Anomaly detection** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IDKD](https://isolationkernel.github.io/ikpykit/latest/api/anomaly/idkd.html) | Isolation Distributional Kernel for point anomaly detections | Anomaly Detection | TKDE2022 | +| [INNE](https://isolationkernel.github.io/ikpykit/latest/api/anomaly/inne.html) | Isolation-based anomaly detection using nearest-neighbor ensembles | Anomaly Detection | CIJ2018 | +| [IForest](https://isolationkernel.github.io/ikpykit/latest/api/anomaly/iforest.html) | Isolation forest | Anomaly Detection | ICDM2008, TKDD2012 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------- | ------------------ | -| [IForest](https://isolationkernel.github.io/ikpykit/latest/api/anomaly/iforest.html) | Isolation forest | Anomaly Detection | ICDM2008, TKDD2022 | -| [INNE](https://isolationkernel.github.io/ikpykit/latest/api/anomaly/inne.html) | Isolation-based anomaly detection using nearest-neighbor ensembles | Anomaly Detection | CIJ2018 | -| [IDKD](https://isolationkernel.github.io/ikpykit/latest/api/anomaly/idkd.html) | Isolation Distributional Kernel for point anomaly detections | Anomaly Detection | TKDE2022 | +**(iii) Point Clustering**: -**(iii) Point Clustering** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IDKC](https://isolationkernel.github.io/ikpykit/latest/api/cluster/idkc.html) | Kernel-based Clustering via Isolation Distributional Kernel | Point Clustering | IS2023 | +| [IKAHC](https://isolationkernel.github.io/ikpykit/latest/api/cluster/ikahc.html) | Isolation Kernel for Agglomerative Hierarchical Clustering | Hierarchical Clustering | PR2023 | +| [PSKC](https://isolationkernel.github.io/ikpykit/latest/api/cluster/pskc.html) | Point-set Kernel Clustering | Point Clustering | TKDE2023 | -| Abbr | Algorithm | Application | Publication | -| ------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------- | ----------- | -| [IDKC](https://isolationkernel.github.io/ikpykit/latest/api/cluster/idkc.html) | Kernel-based Clustering via Isolation Distributional Kernel. | Point Clustering | IS2023 | -| [PSKC](https://isolationkernel.github.io/ikpykit/latest/api/cluster/pskc.html) | Point-set Kernel Clustering | Point Clustering | TKDE2023 | -| [IKAHC](https://isolationkernel.github.io/ikpykit/latest/api/cluster/ikahc.html) | Isolation Kernel for Agglomerative Hierarchical Clustering | Hierarchical Clustering | PR2023 | +**(iv) Graph Mining**: -**(IV) Graph Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKGOD](https://isolationkernel.github.io/ikpykit/latest/api/graph/ikgod.html) | Subgraph Centralization: A Necessary Step for Graph Anomaly Detection | Graph Anomaly Detection | SIAM2023 | +| [IsoGraphKernel](https://isolationkernel.github.io/ikpykit/latest/api/graph/isographkernel.html) | Isolation Graph Kernel | Graph IK embedding and similarity calculating | AAAI2021 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------- | ----------- | -| [IKGOD](https://isolationkernel.github.io/ikpykit/latest/api/graph/ikgod.html) | Subgraph Centralization: A Necessary Step for Graph Anomaly Detection. | Graph Anomaly Detection | SIAM2023 | -| [IsoGraphKernel](https://isolationkernel.github.io/ikpykit/latest/api/graph/IsoGraphKernel.html) | Isolation Graph Kernel | Graph IK embedding and similarity calculating | AAAI2021 | +**(v) Group Mining**: -**(V) Group Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKGAD](https://isolationkernel.github.io/ikpykit/latest/api/group/ikgad.html) | Isolation Distributional Kernel for group anomaly detections | Group Anomaly Detection | TKDE2022 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------- | ----------- | -| [IKGAD](https://isolationkernel.github.io/ikpykit/latest/api/group/ikgad.html) | Isolation Distributional Kernel for group anomaly detections | Group Anomaly Detection | TKDE2022 | +**(vi) Stream Mining**: -**(VI) Stream Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [ICID](https://isolationkernel.github.io/ikpykit/latest/api/stream/icid.html) | Detecting change intervals with isolation distributional kernel | Change Intervals Detection | JAIR2024 | +| [STREAMKHC](https://isolationkernel.github.io/ikpykit/latest/api/stream/streamkhc.html) | Streaming Hierarchical Clustering Based on Point-Set Kernel | Online Hierarchical Clustering | SIGKDD2022 | -| Abbr | Algorithm | Application | Publication | -| ------------------------------------------------------------------------------ | --------------------------------------------------------------- | ------------------------------ | ----------- | -| [StreaKHC](https://isolationkernel.github.io/ikpykit/latest/api/stream/streakhc.html) | Isolation Distribution Kernel for Trajectory Anomaly Detections | Online Hierarchical Clustering | SIGKDD2022 | -| [ICID](https://isolationkernel.github.io/ikpykit/latest/api/stream/icid.html) | Detecting change intervals with isolation distributional kernel | Change Intervals Detection | JAIR2024 | +**(vii) Trajectory Mining**: -**(VII) Trajectory Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKAT](https://isolationkernel.github.io/ikpykit/latest/api/trajectory/ikat.html) | Isolation Distribution Kernel for Trajectory Anomaly Detections | Trajectory Anomaly Detection | JAIR2024 | +| [TIDKC](https://isolationkernel.github.io/ikpykit/latest/api/trajectory/tidkc.html) | Distribution-based Trajectory Clustering | Trajectory Clustering | ICDM2023 | -| Abbr | Algorithm | Application | Publication | -| ---------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------- | ----------- | -| [TIDKC](https://isolationkernel.github.io/ikpykit/latest/api/trajectory/tidkc.html) | Distribution-based Tajectory Clustering | Trajectory Clustering | ICDM2023 | -| [IKAT](https://isolationkernel.github.io/ikpykit/latest/api/trajectory/ikat.html) | Isolation Distribution Kernel for Trajectory Anomaly Detections | Trajectory Anomaly Detection | JAIR2024 | +**(viii) Time Series Mining**: -**(VIII) Time Series** +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKTOD](https://isolationkernel.github.io/ikpykit/latest/api/timeseries/iktod.html) | Isolation distribution kernel for Time Series Anomaly Detection | Anomaly Detection | VLDB2022 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------- | ----------- | -| [IKTOD](https://isolationkernel.github.io/ikpykit/latest/api/time_series/iktod.html) | Isolation distribution kernel for Time Series Anomaly Detection | Anomaly detection | VLDB2022 | + --- diff --git a/docs/README.md b/docs/README.md index f24f0ff..b8cf912 100644 --- a/docs/README.md +++ b/docs/README.md @@ -87,74 +87,76 @@ clf.predict([[0.1, 0.3], [0, 0.7], [90, 85]]) ## Implemented Algorithms + + #### Summary -| Algorithms | Kernel Similarity | Anomaly Detection | Clustering | Change Detection | -| --------------- | ------------------------------ | -------------------------- | -------------------- | ---------------- | -| Point Data | IsoKernel (AAAI'19, SIGKDD'18) | IForest (ICDM'08, TKDD'12) | IDKC (IS'23) | | -| | | INNE (CIJ'18) | PSKC (TKDE'23) | | -| | | IDKD (TKDE'22) | IKAHC (PRJ'23) | | -| Graph Data | IsoGraphKernel (AAAI'21) | IKGOD (SIAM'23) | | | -| Group Data | IsodisKernel (SIGKDD'20) | IKGAD (TKDE'22) | | | -| Stream Data | | | StreaKHC (SIGKDD'22) | ICID (JAIR'24) | -| Time Series | | IKTOD (VLDB'22) | | | -| Trajectory Data | | IKAT (JAIR'24) | TIDKC (ICDM'23) | | +| Algorithms | Kernel Similarity | Anomaly Detection | Clustering | Change Detection | +| --- | --- | --- | --- | --- | +| Point Data | IsoKernel (AAAI'19, SIGKDD'18) | IForest (ICDM'08, TKDD'12)
INNE (CIJ'18)
IDKD (TKDE'22) | IDKC (IS'23)
PSKC (TKDE'23)
IKAHC (PR'23) | | +| Graph Data | IsoGraphKernel (AAAI'21) | IKGOD (SIAM'23) | | | +| Group Data | IsoDisKernel (SIGKDD'20) | IKGAD (TKDE'22) | | | +| Stream Data | | | STREAMKHC (SIGKDD'22) | ICID (JAIR'24) | +| Time Series | | IKTOD (VLDB'22) | | | +| Trajectory Data | | IKAT (JAIR'24) | TIDKC (ICDM'23) | | + +**(i) Isolation Kernel**: -**(i) Isolation Kernel** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IsoDisKernel](./api/kernel/isodiskernel.md) | Isolation Distribution Kernel | Distribution similarity calculating | SIGKDD2020 | +| [IsoKernel](./api/kernel/isokernel.md) | Isolation Kernel | IK feature mapping and similarity calculating | AAAI2019, SIGKDD2018 | -| Abbr | Algorithm | Application | Publication | -| ---------------------------------------------------------------------------------------------- | ----------------------------- | --------------------------------------------- | -------------------- | -| [IsoKernel](./api/kernel/isokernel.md) | Isolation Kernel | IK feature mapping and similarity calculating | AAAI2019, SIGKDD2018 | -| [IsoDisKernel](./api/kernel/isodiskernel.md) | Isolation Distribution Kernel | Distribution similarity calculating | SIGKDD2020 | +**(ii) Point Anomaly Detection**: -**(ii) Point Anomaly detection** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IDKD](./api/anomaly/idkd.md) | Isolation Distributional Kernel for point anomaly detections | Anomaly Detection | TKDE2022 | +| [INNE](./api/anomaly/inne.md) | Isolation-based anomaly detection using nearest-neighbor ensembles | Anomaly Detection | CIJ2018 | +| [IForest](./api/anomaly/iforest.md) | Isolation forest | Anomaly Detection | ICDM2008, TKDD2012 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------- | ------------------ | -| [IForest](./api/anomaly/iforest.md) | Isolation forest | Anomaly Detection | ICDM2008, TKDD2022 | -| [INNE](./api/anomaly/inne.md) | Isolation-based anomaly detection using nearest-neighbor ensembles | Anomaly Detection | CIJ2018 | -| [IDKD](./api/anomaly/idkd.md) | Isolation Distributional Kernel for point anomaly detections | Anomaly Detection | TKDE2022 | +**(iii) Point Clustering**: -**(iii) Point Clustering** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IDKC](./api/cluster/idkc.md) | Kernel-based Clustering via Isolation Distributional Kernel | Point Clustering | IS2023 | +| [IKAHC](./api/cluster/ikahc.md) | Isolation Kernel for Agglomerative Hierarchical Clustering | Hierarchical Clustering | PR2023 | +| [PSKC](./api/cluster/pskc.md) | Point-set Kernel Clustering | Point Clustering | TKDE2023 | -| Abbr | Algorithm | Application | Publication | -| ------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------- | ----------- | -| [IDKC](./api/cluster/idkc.md) | Kernel-based Clustering via Isolation Distributional Kernel. | Point Clustering | IS2023 | -| [PSKC](./api/cluster/pskc.md) | Point-set Kernel Clustering | Point Clustering | TKDE2023 | -| [IKAHC](./api/cluster/ikahc.md) | Isolation Kernel for Agglomerative Hierarchical Clustering | Hierarchical Clustering | PR2023 | +**(iv) Graph Mining**: -**(IV) Graph Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKGOD](./api/graph/ikgod.md) | Subgraph Centralization: A Necessary Step for Graph Anomaly Detection | Graph Anomaly Detection | SIAM2023 | +| [IsoGraphKernel](./api/graph/isographkernel.md) | Isolation Graph Kernel | Graph IK embedding and similarity calculating | AAAI2021 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------- | ----------- | -| [IKGOD](./api/graph/ikgod.md) | Subgraph Centralization: A Necessary Step for Graph Anomaly Detection. | Graph Anomaly Detection | SIAM2023 | -| [IsoGraphKernel](./api/graph/isographkernel.md) | Isolation Graph Kernel | Graph IK embedding and similarity calculating | AAAI2021 | +**(v) Group Mining**: -**(V) Group Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKGAD](./api/group/ikgad.md) | Isolation Distributional Kernel for group anomaly detections | Group Anomaly Detection | TKDE2022 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------- | ----------- | -| [IKGAD](./api/group/ikgad.md) | Isolation Distributional Kernel for group anomaly detections | Group Anomaly Detection | TKDE2022 | +**(vi) Stream Mining**: -**(VI) Stream Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [ICID](./api/stream/icid.md) | Detecting change intervals with isolation distributional kernel | Change Intervals Detection | JAIR2024 | +| [STREAMKHC](./api/stream/streamkhc.md) | Streaming Hierarchical Clustering Based on Point-Set Kernel | Online Hierarchical Clustering | SIGKDD2022 | -| Abbr | Algorithm | Application | Publication | -| ------------------------------------------------------------------------------ | --------------------------------------------------------------- | ------------------------------ | ----------- | -| [StreaKHC](./api/stream/streamkhc.md) | Isolation Distribution Kernel for Trajectory Anomaly Detections | Online Hierarchical Clustering | SIGKDD2022 | -| [ICID](./api/stream/icid.md) | Detecting change intervals with isolation distributional kernel | Change Intervals Detection | JAIR2024 | +**(vii) Trajectory Mining**: -**(VII) Trajectory Data** : +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKAT](./api/trajectory/ikat.md) | Isolation Distribution Kernel for Trajectory Anomaly Detections | Trajectory Anomaly Detection | JAIR2024 | +| [TIDKC](./api/trajectory/tidkc.md) | Distribution-based Trajectory Clustering | Trajectory Clustering | ICDM2023 | -| Abbr | Algorithm | Application | Publication | -| ---------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------- | ----------- | -| [TIDKC](./api/trajectory/tidkc.md) | Distribution-based Tajectory Clustering | Trajectory Clustering | ICDM2023 | -| [IKAT](./api/trajectory/ikat.md) | Isolation Distribution Kernel for Trajectory Anomaly Detections | Trajectory Anomaly Detection | JAIR2024 | +**(viii) Time Series Mining**: -**(VIII) Time Series** +| Abbr | Algorithm | Application | Publication | +| --- | --- | --- | --- | +| [IKTOD](./api/timeseries/iktod.md) | Isolation distribution kernel for Time Series Anomaly Detection | Anomaly Detection | VLDB2022 | -| Abbr | Algorithm | Application | Publication | -| ----------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------- | ----------- | -| [IKTOD](./api/timeseries/iktod.md) | Isolation distribution kernel for Time Series Anomaly Detection | Anomaly detection | VLDB2022 | + --- diff --git a/scripts/estimators.py b/scripts/estimators.py new file mode 100644 index 0000000..04dff0a --- /dev/null +++ b/scripts/estimators.py @@ -0,0 +1,319 @@ +"""The single description of what IKPyKit implements. + +The estimator tables existed in three places that had drifted apart: the root +``README.md``, ``docs/README.md`` and, since the API reference was generated, +``api/index.md``. This module holds the parts that cannot be read off the code +and renders every one of those tables from them, so adding an estimator means +editing one entry rather than three tables. + +What the code already states is not repeated here. The class list comes from the +``__all__`` of each module, and the one-line description from the first +paragraph of its docstring. +""" + +from __future__ import annotations + +import importlib +import inspect +import re +from collections.abc import Callable +from dataclasses import dataclass + +# Longest description to put in a page's front matter. Social cards and search +# engines both cut off around here, so a longer one is only truncated elsewhere. +DESCRIPTION_LIMIT = 160 + +API_ROOT = "api" + + +@dataclass(frozen=True) +class Section: + """A group of estimators shown under one heading.""" + + title: str + module: str + subsections: tuple[Section, ...] = () + # Dataset loaders belong in the API reference but not in a table of + # algorithms, so a section can opt out of the README tables. + in_readme: bool = True + + +SECTIONS = ( + Section("Isolation Kernel", "ikpykit.kernel"), + Section("Point Anomaly Detection", "ikpykit.anomaly"), + Section("Point Clustering", "ikpykit.cluster"), + Section("Graph Mining", "ikpykit.graph"), + Section("Group Mining", "ikpykit.group"), + Section("Stream Mining", "ikpykit.stream"), + Section( + "Trajectory Mining", + "ikpykit.trajectory", + subsections=( + Section("DataLoader", "ikpykit.trajectory.dataloader", in_readme=False), + ), + ), + Section("Time Series Mining", "ikpykit.timeseries"), +) + + +@dataclass(frozen=True) +class Estimator: + """What the tables say about an estimator that the code does not. + + ``data_type`` and ``capability`` place it in the summary matrix; they are the + row and the column it appears in. + """ + + algorithm: str + application: str + publications: tuple[str, ...] + data_type: str + capability: str + + +# Every public class outside a section marked `in_readme=False` needs an entry +# here. `check_complete` enforces that, so a new estimator cannot quietly end up +# missing from the tables the way it used to. +ESTIMATORS = { + "IsoKernel": Estimator( + algorithm="Isolation Kernel", + application="IK feature mapping and similarity calculating", + publications=("AAAI2019", "SIGKDD2018"), + data_type="Point Data", + capability="Kernel Similarity", + ), + "IsoDisKernel": Estimator( + algorithm="Isolation Distribution Kernel", + application="Distribution similarity calculating", + publications=("SIGKDD2020",), + data_type="Group Data", + capability="Kernel Similarity", + ), + "IForest": Estimator( + algorithm="Isolation forest", + application="Anomaly Detection", + publications=("ICDM2008", "TKDD2012"), + data_type="Point Data", + capability="Anomaly Detection", + ), + "INNE": Estimator( + algorithm="Isolation-based anomaly detection using nearest-neighbor ensembles", + application="Anomaly Detection", + publications=("CIJ2018",), + data_type="Point Data", + capability="Anomaly Detection", + ), + "IDKD": Estimator( + algorithm="Isolation Distributional Kernel for point anomaly detections", + application="Anomaly Detection", + publications=("TKDE2022",), + data_type="Point Data", + capability="Anomaly Detection", + ), + "IDKC": Estimator( + algorithm="Kernel-based Clustering via Isolation Distributional Kernel", + application="Point Clustering", + publications=("IS2023",), + data_type="Point Data", + capability="Clustering", + ), + "PSKC": Estimator( + algorithm="Point-set Kernel Clustering", + application="Point Clustering", + publications=("TKDE2023",), + data_type="Point Data", + capability="Clustering", + ), + "IKAHC": Estimator( + algorithm="Isolation Kernel for Agglomerative Hierarchical Clustering", + application="Hierarchical Clustering", + publications=("PR2023",), + data_type="Point Data", + capability="Clustering", + ), + "IKGOD": Estimator( + algorithm="Subgraph Centralization: A Necessary Step for Graph Anomaly Detection", + application="Graph Anomaly Detection", + publications=("SIAM2023",), + data_type="Graph Data", + capability="Anomaly Detection", + ), + "IsoGraphKernel": Estimator( + algorithm="Isolation Graph Kernel", + application="Graph IK embedding and similarity calculating", + publications=("AAAI2021",), + data_type="Graph Data", + capability="Kernel Similarity", + ), + "IKGAD": Estimator( + algorithm="Isolation Distributional Kernel for group anomaly detections", + application="Group Anomaly Detection", + publications=("TKDE2022",), + data_type="Group Data", + capability="Anomaly Detection", + ), + "ICID": Estimator( + algorithm="Detecting change intervals with isolation distributional kernel", + application="Change Intervals Detection", + publications=("JAIR2024",), + data_type="Stream Data", + capability="Change Detection", + ), + "STREAMKHC": Estimator( + algorithm="Streaming Hierarchical Clustering Based on Point-Set Kernel", + application="Online Hierarchical Clustering", + publications=("SIGKDD2022",), + data_type="Stream Data", + capability="Clustering", + ), + "IKAT": Estimator( + algorithm="Isolation Distribution Kernel for Trajectory Anomaly Detections", + application="Trajectory Anomaly Detection", + publications=("JAIR2024",), + data_type="Trajectory Data", + capability="Anomaly Detection", + ), + "TIDKC": Estimator( + algorithm="Distribution-based Trajectory Clustering", + application="Trajectory Clustering", + publications=("ICDM2023",), + data_type="Trajectory Data", + capability="Clustering", + ), + "IKTOD": Estimator( + algorithm="Isolation distribution kernel for Time Series Anomaly Detection", + application="Anomaly Detection", + publications=("VLDB2022",), + data_type="Time Series", + capability="Anomaly Detection", + ), +} + +MATRIX_ROWS = ( + "Point Data", + "Graph Data", + "Group Data", + "Stream Data", + "Time Series", + "Trajectory Data", +) +MATRIX_COLUMNS = ( + "Kernel Similarity", + "Anomaly Detection", + "Clustering", + "Change Detection", +) + +ROMAN = ("i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x") + + +def summarize_text(text: str) -> str: + """Collapse text to a single line, keeping whole sentences within the limit. + + Text longer than the limit is cut at the last sentence that still fits, + rather than mid-word. + """ + line = " ".join(text.split()) + if len(line) <= DESCRIPTION_LIMIT: + return line + + kept = "" + for sentence in re.findall(r"[^.]*\.(?:\s|$)", line): + if len(kept) + len(sentence) > DESCRIPTION_LIMIT: + break + kept += sentence + # A first sentence that is itself over the limit leaves nothing to keep. + return kept.strip() or line[:DESCRIPTION_LIMIT].rstrip() + + +def summarize(obj: object) -> str: + """Return the first paragraph of an object's docstring as a single line.""" + doc = inspect.getdoc(obj) or "" + return summarize_text(doc.split("\n\n")[0]) + + +def page_path(module: str, name: str) -> str: + """Return the doc path for a class, mirroring its location in the package.""" + package = module.removeprefix("ikpykit.").replace(".", "/") + return f"{package}/{name.lower()}.md" + + +def collect(section: Section) -> list[tuple[str, str, str]]: + """Return (module, name, description) for every public class in a section.""" + module = importlib.import_module(section.module) + return [ + (section.module, name, summarize(getattr(module, name))) + for name in module.__all__ + ] + + +def readme_sections() -> list[tuple[Section, list[tuple[str, str, str]]]]: + """Return the sections that appear in the README tables, with their classes.""" + return [(s, collect(s)) for s in SECTIONS if s.in_readme] + + +def check_complete() -> None: + """Fail if an estimator has no table entry, or an entry has no estimator.""" + public = {name for _, entries in readme_sections() for _, name, _ in entries} + missing = sorted(public - ESTIMATORS.keys()) + if missing: + raise SystemExit( + f"No entry in ESTIMATORS for {', '.join(missing)}. Add one in " + f"{__file__} so the tables include it." + ) + extra = sorted(ESTIMATORS.keys() - public) + if extra: + raise SystemExit( + f"ESTIMATORS has entries for {', '.join(extra)}, which no module " + f"exports. Remove them from {__file__}." + ) + + +def short_publication(publication: str) -> str: + """Shorten a publication for the summary matrix: AAAI2019 -> AAAI'19.""" + return re.sub(r"^(\D+)\d{2}(\d{2})$", r"\1'\2", publication) + + +def render_summary_matrix() -> list[str]: + """Render the matrix of which estimators cover which task on which data.""" + cells: dict[tuple[str, str], list[str]] = {} + for name, meta in ESTIMATORS.items(): + short = ", ".join(short_publication(p) for p in meta.publications) + cells.setdefault((meta.data_type, meta.capability), []).append( + f"{name} ({short})" + ) + + lines = [ + "| Algorithms | " + " | ".join(MATRIX_COLUMNS) + " |", + "| --- | " + " | ".join("---" for _ in MATRIX_COLUMNS) + " |", + ] + for row in MATRIX_ROWS: + # Several estimators can share a cell;
keeps them in one row rather + # than spilling into blank rows underneath. + filled = ["
".join(cells.get((row, col), [])) for col in MATRIX_COLUMNS] + lines.append(f"| {row} | " + " | ".join(filled) + " |") + return lines + + +def render_tables(link: Callable[[str, str], str]) -> str: + """Render the whole generated block, linking each class through `link`. + + `link` takes the module and the class name and returns a URL, which differs + between the two READMEs: one is read on GitHub, the other inside the site. + """ + lines = ["#### Summary", "", *render_summary_matrix(), ""] + for number, (section, entries) in zip(ROMAN, readme_sections(), strict=False): + lines += [ + f"**({number}) {section.title}**:", + "", + "| Abbr | Algorithm | Application | Publication |", + "| --- | --- | --- | --- |", + ] + for module, name, _ in entries: + meta = ESTIMATORS[name] + lines.append( + f"| [{name}]({link(module, name)}) | {meta.algorithm} | " + f"{meta.application} | {', '.join(meta.publications)} |" + ) + lines.append("") + return "\n".join(lines).rstrip() diff --git a/scripts/gen_api_pages.py b/scripts/gen_api_pages.py index db31de9..51d6f53 100644 --- a/scripts/gen_api_pages.py +++ b/scripts/gen_api_pages.py @@ -2,92 +2,38 @@ Every public estimator used to need a one-line stub under ``docs/api/`` and a matching entry in the ``nav`` of ``mkdocs.yml``. Both were easy to forget, and a -missing one failed silently: the class simply had no page. The package already -states what is public, through the ``__all__`` of each module, so that is what -this script reads. - -The only thing it cannot derive is the section names below, which are editorial, -and their order, which is meaningful. Adding an estimator to a module that is -already listed needs no change here. +missing one failed silently: the class simply had no page. What is public, and +how each class describes itself, is read from the package by estimators.py. Run by mkdocs-gen-files during the build; the pages exist only in the built site. """ from __future__ import annotations -import importlib -import inspect import json -import re +import pathlib +import sys import textwrap -from dataclasses import dataclass, field import mkdocs_gen_files from mkdocs.structure.files import InclusionLevel -# Longest description to put in front matter. Social cards and search engines -# both cut off around here, so a longer one is only truncated elsewhere. -DESCRIPTION_LIMIT = 160 - -API_ROOT = "api" - - -@dataclass(frozen=True) -class Section: - """A group of estimators shown under one heading in the navigation.""" - - title: str - module: str - subsections: tuple[Section, ...] = field(default_factory=tuple) - - -SECTIONS = ( - Section("Isolation Kernel", "ikpykit.kernel"), - Section("Point Anomaly Detection", "ikpykit.anomaly"), - Section("Point Clustering", "ikpykit.cluster"), - Section("Graph Mining", "ikpykit.graph"), - Section("Group Mining", "ikpykit.group"), - Section("Stream Mining", "ikpykit.stream"), - Section( - "Trajectory Mining", - "ikpykit.trajectory", - subsections=(Section("DataLoader", "ikpykit.trajectory.dataloader"),), - ), - Section("Time Series Mining", "ikpykit.timeseries"), +# mkdocs-gen-files runs this through runpy, which does not put the script's own +# directory on the path the way running it as a program would. +sys.path.insert(0, str(pathlib.Path(__file__).parent)) + +from estimators import ( # noqa: E402 + API_ROOT, + ESTIMATORS, + SECTIONS, + Section, + check_complete, + collect, + page_path, + summarize_text, ) -def summarize_text(text: str) -> str: - """Collapse text to a single line, keeping whole sentences within the limit. - - Text longer than the limit is cut at the last sentence that still fits, - rather than mid-word. - """ - line = " ".join(text.split()) - if len(line) <= DESCRIPTION_LIMIT: - return line - - kept = "" - for sentence in re.findall(r"[^.]*\.(?:\s|$)", line): - if len(kept) + len(sentence) > DESCRIPTION_LIMIT: - break - kept += sentence - # A first sentence that is itself over the limit leaves nothing to keep. - return kept.strip() or line[:DESCRIPTION_LIMIT].rstrip() - - -def summarize(obj: object) -> str: - """Return the first paragraph of an object's docstring as a single line.""" - doc = inspect.getdoc(obj) or "" - return summarize_text(doc.split("\n\n")[0]) - - -def page_path(module: str, name: str) -> str: - """Return the doc path for a class, mirroring its location in the package.""" - package = module.removeprefix("ikpykit.").replace(".", "/") - return f"{package}/{name.lower()}.md" - - def write_page(module: str, name: str, description: str) -> None: """Write the stub that mkdocstrings expands into the rendered class page.""" path = page_path(module, name) @@ -99,16 +45,6 @@ def write_page(module: str, name: str, description: str) -> None: page.write(f"::: {module}.{name}\n") -def collect(section: Section) -> list[tuple[str, str, str]]: - """Return (module, name, description) for every public class in a section.""" - module = importlib.import_module(section.module) - entries = [] - for name in module.__all__: - obj = getattr(module, name) - entries.append((section.module, name, summarize(obj))) - return entries - - def render_nav(section: Section, depth: int = 0) -> list[str]: """Render one section of the literate-nav SUMMARY, subsections first.""" indent = " " * depth @@ -121,15 +57,25 @@ def render_nav(section: Section, depth: int = 0) -> list[str]: def render_overview(section: Section, level: int = 2) -> list[str]: - """Render one section of the overview page as a table of its estimators.""" + """Render one section of the overview page as a table of its estimators. + + The publication column only exists for estimators the READMEs list, so + dataset loaders get the two-column form. + """ + entries = collect(section) + described = all(name in ESTIMATORS for _, name, _ in entries) + header = ["Estimator", "Description"] + (["Publication"] if described else []) lines = [ f"{'#' * level} {section.title}", "", - "| Estimator | Description |", - "| --- | --- |", + "| " + " | ".join(header) + " |", + "| " + " | ".join("---" for _ in header) + " |", ] - for module, name, description in collect(section): - lines.append(f"| [{name}]({page_path(module, name)}) | {description} |") + for module, name, description in entries: + row = [f"[{name}]({page_path(module, name)})", description] + if described: + row.append(", ".join(ESTIMATORS[name].publications)) + lines.append("| " + " | ".join(row) + " |") lines.append("") for subsection in section.subsections: lines += render_overview(subsection, level + 1) @@ -137,6 +83,8 @@ def render_overview(section: Section, level: int = 2) -> list[str]: def main() -> None: + check_complete() + for section in SECTIONS: for subsection in (section, *section.subsections): for module, name, description in collect(subsection): diff --git a/scripts/sync_readme_tables.py b/scripts/sync_readme_tables.py new file mode 100644 index 0000000..b72465c --- /dev/null +++ b/scripts/sync_readme_tables.py @@ -0,0 +1,99 @@ +"""Write the estimator tables into both READMEs, or check that they are current. + +The root README is read on GitHub and never passes through MkDocs, so a plugin +cannot reach it. Instead both files carry the tables between markers and this +script fills them in from scripts/estimators.py. Run with --check by pre-commit, +which fails if either file has fallen behind. + + python scripts/sync_readme_tables.py # rewrite both READMEs + python scripts/sync_readme_tables.py --check # exit 1 if out of date +""" + +from __future__ import annotations + +import argparse +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).parent)) + +from estimators import API_ROOT, check_complete, page_path, render_tables # noqa: E402 + +BEGIN = "" +END = "" + +SITE = "https://isolationkernel.github.io/ikpykit/latest" + + +def site_link(module: str, name: str) -> str: + """Absolute link for the root README, which is read outside the site.""" + return f"{SITE}/{API_ROOT}/{page_path(module, name)}".replace(".md", ".html") + + +def docs_link(module: str, name: str) -> str: + """Relative link for docs/README.md, which MkDocs resolves and checks.""" + return f"./{API_ROOT}/{page_path(module, name)}" + + +TARGETS = { + pathlib.Path("README.md"): site_link, + pathlib.Path("docs/README.md"): docs_link, +} + + +def bounds(text: str, path: pathlib.Path) -> tuple[int, int]: + start, end = text.find(BEGIN), text.find(END) + if start == -1 or end == -1: + raise SystemExit(f"{path}: missing the {BEGIN} / {END} markers.") + return start, end + + +def normalize(block: str) -> str: + """Collapse the whitespace used to align table pipes. + + Editors and Markdown formatters pad table cells so the pipes line up, which + is how these tables were kept before. Comparing normalized forms means a + padded table still counts as up to date, rather than this script and a + formatter overwriting each other on every commit. + """ + return "\n".join( + " ".join(line.split()) for line in block.splitlines() if line.strip() + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="do not write; exit non-zero if a README is out of date", + ) + args = parser.parse_args() + + check_complete() + + stale = [] + for path, link in TARGETS.items(): + current = path.read_text() + start, end = bounds(current, path) + body = render_tables(link) + if normalize(current[start + len(BEGIN) : end]) == normalize(body): + continue + if args.check: + stale.append(path) + else: + path.write_text(f"{current[:start]}{BEGIN}\n\n{body}\n\n{current[end:]}") + print(f"updated {path}") + + if stale: + names = ", ".join(str(p) for p in stale) + print( + f"{names} out of date. Run `python {__file__}` to regenerate.", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())