python-som 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {python_som-0.2.0 → python_som-0.3.0}/.gitignore +7 -1
  2. python_som-0.3.0/CHANGELOG.md +179 -0
  3. python_som-0.3.0/PKG-INFO +174 -0
  4. python_som-0.3.0/README.md +125 -0
  5. python_som-0.3.0/docs/api.md +26 -0
  6. python_som-0.3.0/docs/assets/iris.png +0 -0
  7. python_som-0.3.0/docs/changelog.md +1 -0
  8. python_som-0.3.0/docs/getting-started.md +138 -0
  9. python_som-0.3.0/docs/index.md +1 -0
  10. python_som-0.3.0/docs/neighborhood-functions.md +135 -0
  11. python_som-0.3.0/docs/training-modes.md +90 -0
  12. python_som-0.3.0/examples/iris.py +84 -0
  13. python_som-0.3.0/pyproject.toml +169 -0
  14. python_som-0.3.0/src/python_som/__init__.py +58 -0
  15. python_som-0.3.0/src/python_som/_decay.py +68 -0
  16. python_som-0.3.0/src/python_som/_distance.py +26 -0
  17. python_som-0.3.0/src/python_som/_neighborhood.py +218 -0
  18. python_som-0.3.0/src/python_som/_som.py +649 -0
  19. python_som-0.3.0/src/python_som/py.typed +0 -0
  20. python_som-0.3.0/tests/__init__.py +5 -0
  21. python_som-0.3.0/tests/conftest.py +79 -0
  22. python_som-0.3.0/tests/test_analysis.py +134 -0
  23. python_som-0.3.0/tests/test_construction.py +148 -0
  24. python_som-0.3.0/tests/test_decay.py +80 -0
  25. python_som-0.3.0/tests/test_end_to_end.py +114 -0
  26. python_som-0.3.0/tests/test_io_types.py +80 -0
  27. python_som-0.3.0/tests/test_neighborhood.py +332 -0
  28. python_som-0.3.0/tests/test_packaging.py +103 -0
  29. python_som-0.3.0/tests/test_training.py +291 -0
  30. python_som-0.3.0/tests/test_weight_init.py +125 -0
  31. python_som-0.2.0/CHANGELOG.md +0 -85
  32. python_som-0.2.0/PKG-INFO +0 -174
  33. python_som-0.2.0/README.md +0 -148
  34. python_som-0.2.0/pyproject.toml +0 -39
  35. python_som-0.2.0/python_som/__init__.py +0 -944
  36. python_som-0.2.0/requirements.txt +0 -3
  37. python_som-0.2.0/test.py +0 -74
  38. python_som-0.2.0/test_iris.ipynb +0 -408
  39. python_som-0.2.0/test_output_iris.png +0 -0
  40. python_som-0.2.0/tests/test_neighborhood.py +0 -290
  41. {python_som-0.2.0 → python_som-0.3.0}/LICENSE +0 -0
@@ -129,4 +129,10 @@ dmypy.json
129
129
  .pyre/
130
130
 
131
131
  # .dump directory
132
- /.dump
132
+ /.dump
133
+
134
+ # Claude Code working files. CLAUDE.md holds this maintainer's agent conventions rather than
135
+ # repository policy, so it stays local instead of being imposed on contributors.
136
+ /CLAUDE.md
137
+ /CLAUDE.local.md
138
+ /.claude/
@@ -0,0 +1,179 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
6
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-07-29
9
+
10
+ Modernizes the packaging and corrects four methodology defects. **Numerical results differ from
11
+ 0.2.0.** The changes are listed with the passage of Kohonen (2013) each one follows.
12
+
13
+ ### Changed, and results differ
14
+
15
+ - **Seeds no longer reproduce 0.1.x/0.2.0 results.** The random generator is now a per-instance
16
+ `np.random.Generator`. Previously the constructor called `np.random.seed`, which reseeded NumPy's
17
+ *global* generator as a side effect of building a SOM, disturbing the whole host program. The
18
+ side effect is gone, but `random_seed=42` now yields a different map.
19
+
20
+ To reproduce figures made with an earlier version, pin `python-som==0.2.0`.
21
+
22
+ - Batch training no longer destroys models whose neighborhood contains no data. `_train_batch`
23
+ built its result from a zeroed array and assigned it wholesale, so every model it did not touch
24
+ became the zero vector. On a 30x30 map with 20 samples and a small radius, that wiped 282 of 900
25
+ models in a single step, and drove the quantization error from 0.0 to 0.196. Untouched models now
26
+ keep their previous value, and the denominator is compared against a tolerance rather than exact
27
+ zero. (Kohonen Eq. 8.)
28
+
29
+ - Linear initialization now spans the principal-component plane. It used
30
+ `pca.explained_variance_`, the two eigen*values*, where it needed `pca.components_`, the
31
+ eigen*vectors*. Every model came out as a scalar broadcast across all features: a constant vector
32
+ on the all-ones diagonal, rank 1 rather than a plane. PCA is also now fitted on the raw data
33
+ instead of standardized data, so the models live in the same space as the inputs they are compared
34
+ against during training. (Kohonen Section 4.3.)
35
+
36
+ - `random` training now samples i.i.d. with replacement. It previously drew with
37
+ `replace=(n_iteration > len(data))`, so it was a random permutation whenever the iteration count
38
+ did not exceed the sample count, and independent draws only beyond it. The character of the
39
+ sampling therefore depended on how many iterations you asked for. It is now uniformly i.i.d.,
40
+ which is the stochastic approximation of Robbins and Monro (1951) that Kohonen cites in
41
+ Section 4.1.
42
+
43
+ - `sequential` training honours `n_iteration`. It iterated the dataset exactly once regardless
44
+ of the requested count, so asking for 500 iterations over 30 samples ran 30 steps while the decay
45
+ functions still used 500 as their horizon, leaving the learning rate almost unchanged. It now
46
+ cycles the dataset until the requested number of steps has run.
47
+
48
+ - The automatic map-size ratio uses `sqrt(lambda_1 / lambda_2)` rather than the raw eigenvalue
49
+ ratio, rounds instead of floor-dividing, and clamps to at least 1 so a side can no longer come out
50
+ as zero. Kohonen Section 3.5 asks for side lengths matching "the lengths of the two largest
51
+ principal components"; a component is a unit direction, so its length in the data is the standard
52
+ deviation. For Iris with `x=20`, `y` moves from 6 to 11.
53
+
54
+ - The neighborhood radius is floored at the new `min_neighborhood_radius` parameter, default
55
+ 0.5. Kohonen Section 4.2: "the final value of sigma shall not go to zero, because otherwise the
56
+ process loses its ordering power. It should always remain, say, above half of the grid spacing."
57
+ This also stops `linear_decay` from reaching a division by zero at its horizon.
58
+
59
+ - `get_shape()` returns `tuple[int, int]` rather than NumPy integers, so
60
+ `plt.figure(figsize=som.get_shape())` works without the `float()` workaround the old example
61
+ needed.
62
+
63
+ ### Added
64
+
65
+ - `mexican_hat` is accepted as an alias for `mexicanhat`.
66
+ - A test suite, where there was none: 190 tests at 100% coverage, including one regression test
67
+ per defect above carrying its measured numbers, and property-based tests via Hypothesis over
68
+ generated grid sizes, radii and centres.
69
+ - Type annotations are now exported, via a `py.typed` marker. The library was already annotated,
70
+ but without the marker PEP 561 requires downstream type checkers to ignore it.
71
+ - Documentation site at <https://andremsouza.github.io/python-som/>, built with Material for
72
+ MkDocs, including the derivations behind the neighborhood functions.
73
+ - Continuous integration across Python 3.10 to 3.13, with ruff, mypy `--strict`, coverage
74
+ gating, and a build that installs the wheel into a clean environment and imports it.
75
+ - Releases publish through PyPI Trusted Publishing, so no long-lived API token exists.
76
+
77
+ ### Performance
78
+
79
+ - Batch training is about 30x faster. The nested Python loop over every pair of nodes is
80
+ replaced by a NumPy contraction. Measured on a 20x20 map with 150 samples over 5 iterations: 1.89 s
81
+ to 0.06 s. A regression test asserts the new result matches the old formulation to 1e-12.
82
+
83
+ ### Internal
84
+
85
+ - The package moves to a `src/` layout and splits into `_som`, `_neighborhood`, `_decay` and
86
+ `_distance`. `import python_som; python_som.SOM(...)` is unchanged, and the pre-0.3.0 private names
87
+ (`_asymptotic_decay`, `_euclidean_distance`, and the rest) still resolve.
88
+ - `test_iris.ipynb` is removed; its narrative moved into the documentation. `SOM_atualizado.py`, an
89
+ untracked experimental variant that the sdist had been sweeping up, is gone.
90
+ - `print` calls become `logging`; `verbose=True` still drives the tqdm progress bar.
91
+
92
+ ### Fixed
93
+
94
+ - The `bubble` neighborhood is documented as using the Chebyshev metric, so its region is a
95
+ square rather than a disc. The behavior is unchanged and follows Vrieze's appendix pseudo-code
96
+ (`b = MAX(ABS(i - w_i), ABS(j - w_j))`), but it was previously unstated, and it means the bubble is
97
+ *not* isotropic under the Euclidean metric. The test suite shipped in 0.2.0 asserted that it was;
98
+ that assertion passed only because no counterexample exists on a grid that small. The smallest is
99
+ at radius `sqrt(50)`, where `(5, 5)` is inside a `sigma = 5` square and `(7, 1)` is outside.
100
+
101
+ ## [0.2.0] - 2026-07-28
102
+
103
+ ### Added
104
+
105
+ - Mexican hat neighborhood function, selectable with `neighborhood_function='mexicanhat'`
106
+ (contributed by [@Arne49](https://github.com/Arne49) in
107
+ [#2](https://github.com/andremsouza/python-som/pull/2)).
108
+
109
+ Implemented as the isotropic 2-D form `(1 - u) * exp(-u)` over `u = sqdist(c, i) / (2 * sigma^2)`,
110
+ normalized so that `h(c, c) = 1`. It crosses zero at a radius of `sqrt(2) * sigma` and reaches its
111
+ minimum of `-exp(-2)` at a radius of `2 * sigma`.
112
+
113
+ Note that a neighborhood function must depend on the grid distance alone, not on the two axis
114
+ offsets separately, per `sqdist(c, i)` in Kohonen (2013) Eq. (5) and the "lateral distance"
115
+ abscissa of Vrieze (1995) Fig. 3. The gaussian happens to be separable into a product of per-axis
116
+ terms, but that is a property of the exponential rather than of neighborhood functions in general.
117
+
118
+ - `## Neighborhood functions` section in the README, comparing all three and documenting the
119
+ batch-mode restriction below.
120
+ - Test suite for the neighborhood functions, asserting closed-form analytic properties rather than
121
+ golden values.
122
+
123
+ ### Fixed
124
+
125
+ - Cyclic (toroidal) neighborhoods were only correct in one direction. The fold-back handled
126
+ offsets above `+length/2` but left those below `-length/2` alone, so a winner in the upper half of
127
+ an axis did not wrap. On a 10x10 toroidal map with sigma=1, a winner at row 0 gave `h[9] = 0.6065`
128
+ while a winner at row 9 gave `h[0] = 0.0` instead of the same 0.6065. Now uses the minimum-image
129
+ convention, which folds both tails.
130
+
131
+ This changes results for any map built with `cyclic_x=True` or `cyclic_y=True`.
132
+
133
+ - A neighborhood radius of zero divided by zero and produced `inf`/`nan` weights instead of raising.
134
+ `_gaussian` and `_mexicanhat` now reject a non-finite or non-positive radius; `_bubble` still
135
+ accepts a radius of zero, which selects the winner alone.
136
+
137
+ - An unknown `neighborhood_function` raised a bare `KeyError`. It now raises `ValueError` listing the
138
+ valid names, matching the behavior of `weight_initialization`.
139
+
140
+ ### Changed
141
+
142
+ - `requires-python` is now `>=3.10`. The declared floor of `>=3.9` was never accurate: the module
143
+ uses PEP 604 unions (`int | None`) in runtime-evaluated annotations without
144
+ `from __future__ import annotations`, so importing it on Python 3.9 raises `TypeError`. Python 3.9
145
+ reached end of life in October 2025. Installers will now decline the package on 3.9 rather than
146
+ install something that cannot be imported.
147
+
148
+ - `train(mode='batch')` combined with `neighborhood_function='mexicanhat'` now raises `ValueError`.
149
+ The batch update of Kohonen (2013) Eq. (8) is a weighted mean whose denominator `sum_j n_j * h_ji`
150
+ is only well defined for a non-negative neighborhood function. The mexican hat is signed by
151
+ construction, so the denominator can vanish or turn negative: on a 12x12 grid, 49 of 144
152
+ denominators come out negative, which inverts or explodes the update. This is a property of the
153
+ function itself rather than of this implementation. Use `mode='random'` or `mode='sequential'`.
154
+
155
+ ### Packaging
156
+
157
+ - Restored the `cli` extra (`pip install python-som[cli]`, which pulls in `tqdm` for training
158
+ progress bars). This extra shipped in 0.1.3 on PyPI but was never committed to the repository, so
159
+ releasing from the repository as-is would have silently removed it.
160
+
161
+ ### Note on 0.1.3
162
+
163
+ There is no changelog entry for 0.1.3 because it was published to PyPI from an uncommitted working
164
+ tree and has no corresponding commit. Its `python_som/__init__.py` is byte-identical to the 0.1.2
165
+ tag; the only differences are the version string and the `cli` extra restored above.
166
+
167
+ ## [0.1.3] - 2025-01-14
168
+
169
+ Published to PyPI only. See the note above.
170
+
171
+ ## [0.1.2] - 2025-01-14
172
+
173
+ Earlier releases predate this changelog. See the
174
+ [commit history](https://github.com/andremsouza/python-som/commits/master) for details.
175
+
176
+ [0.3.0]: https://github.com/andremsouza/python-som/releases/tag/v0.3.0
177
+ [0.2.0]: https://github.com/andremsouza/python-som/releases/tag/v0.2.0
178
+ [0.1.3]: https://pypi.org/project/python-som/0.1.3/
179
+ [0.1.2]: https://pypi.org/project/python-som/0.1.2/
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-som
3
+ Version: 0.3.0
4
+ Summary: Python implementation of the Self-Organizing Map
5
+ Project-URL: Homepage, https://github.com/andremsouza/python-som
6
+ Project-URL: Documentation, https://andremsouza.github.io/python-som/
7
+ Project-URL: Changelog, https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/andremsouza/python-som/issues
9
+ Author-email: André Moreira Souza <msouza.andre@hotmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: pandas>=2.0
28
+ Requires-Dist: scikit-learn>=1.3
29
+ Provides-Extra: cli
30
+ Requires-Dist: tqdm>=4.66; extra == 'cli'
31
+ Provides-Extra: dev
32
+ Requires-Dist: hypothesis==6.163.0; extra == 'dev'
33
+ Requires-Dist: mypy==2.3.0; extra == 'dev'
34
+ Requires-Dist: pandas-stubs==3.0.3.260530; (python_version >= '3.11') and extra == 'dev'
35
+ Requires-Dist: pre-commit==4.6.1; extra == 'dev'
36
+ Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
38
+ Requires-Dist: ruff==0.16.0; extra == 'dev'
39
+ Requires-Dist: tomli==2.4.1; (python_version < '3.11') and extra == 'dev'
40
+ Requires-Dist: twine==7.0.0; extra == 'dev'
41
+ Requires-Dist: types-tqdm==4.69.0.20260728; extra == 'dev'
42
+ Provides-Extra: docs
43
+ Requires-Dist: mkdocs-material==9.7.7; extra == 'docs'
44
+ Requires-Dist: mkdocstrings-python==2.0.5; extra == 'docs'
45
+ Provides-Extra: examples
46
+ Requires-Dist: matplotlib>=3.8; extra == 'examples'
47
+ Requires-Dist: seaborn>=0.13; extra == 'examples'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # python-som
51
+
52
+ [![CI](https://github.com/andremsouza/python-som/actions/workflows/ci.yml/badge.svg)](https://github.com/andremsouza/python-som/actions/workflows/ci.yml)
53
+ [![PyPI](https://img.shields.io/pypi/v/python-som.svg)](https://pypi.org/project/python-som/)
54
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-som.svg)](https://pypi.org/project/python-som/)
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/andremsouza/python-som/blob/master/LICENSE)
56
+
57
+ Implementation of Kohonen's 2-D self-organizing map, built on NumPy, with scikit-learn for PCA and
58
+ standardization. Accepts NumPy arrays, pandas DataFrames and plain lists.
59
+
60
+ [Documentation](https://andremsouza.github.io/python-som/) ·
61
+ [Changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md)
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install python-som # requires Python 3.10+
67
+ pip install "python-som[cli]" # adds tqdm progress bars
68
+ ```
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ import numpy as np
74
+ import python_som
75
+
76
+ rng = np.random.default_rng(0)
77
+ data = rng.normal(size=(150, 4))
78
+
79
+ som = python_som.SOM(x=20, y=None, input_len=4, data=data, random_seed=42)
80
+ som.weight_initialization(mode="linear", data=data)
81
+ error = som.train(data, n_iteration=len(data), mode="batch")
82
+
83
+ umatrix = som.distance_matrix()
84
+ winner = som.winner(data[0])
85
+ ```
86
+
87
+ A full worked example with plots is in [examples/iris.py](https://github.com/andremsouza/python-som/blob/master/examples/iris.py) and in the
88
+ [getting-started guide](https://andremsouza.github.io/python-som/getting-started/).
89
+
90
+ ![U-matrix of a SOM trained on Iris](https://raw.githubusercontent.com/andremsouza/python-som/master/docs/assets/iris.png)
91
+
92
+ ## Features
93
+
94
+ * Stepwise and batch training
95
+ * Random, random-sampling and linear (PCA) weight initialization
96
+ * Automatic selection of the map size ratio, from PCA
97
+ * Cyclic arrays, for toroidal maps
98
+ * Gaussian, bubble and Mexican hat neighborhood functions
99
+ * Custom decay functions
100
+ * Visualization support: U-matrix, activation matrix
101
+ * Supervised labelling, via the label map
102
+ * Fully type-annotated, with a `py.typed` marker
103
+
104
+ ## Neighborhood functions
105
+
106
+ All three are functions of the distance between two nodes in the grid, `sqdist(c, i)` in Eq. (5) of
107
+ Kohonen (2013).
108
+
109
+ | Name | Shape | Notes |
110
+ | --- | --- | --- |
111
+ | `'gaussian'` | `exp(-r² / 2σ²)` | Strictly positive, monotonically decreasing. The default. |
112
+ | `'bubble'` | `1` for `max(dx, dy) ≤ σ`, else `0` | The truncated inner lobe of the Mexican hat. Uses the Chebyshev metric, so the region is a square. |
113
+ | `'mexicanhat'` | `(1 - u)·exp(-u)`, `u = r² / 2σ²` | Excitatory near the winner, inhibitory beyond it. Zero at `r = √2·σ`, minimum `-e⁻²` at `r = 2σ`. |
114
+
115
+ The Mexican hat takes negative values, so it **cannot be used with `mode='batch'`**: the batch
116
+ update of Kohonen Eq. (8) is a weighted mean whose denominator is not sign-definite for a signed
117
+ neighborhood function. Use `mode='random'` or `mode='sequential'`; `mode='batch'` raises a
118
+ `ValueError`.
119
+
120
+ See [Neighborhood functions](https://andremsouza.github.io/python-som/neighborhood-functions/) for
121
+ the derivations, including why the Mexican hat is not an outer product of two 1-D wavelets.
122
+
123
+ ## Upgrading
124
+
125
+ 0.3.0 corrects several methodology defects, so numerical results are not comparable with earlier
126
+ versions. In particular **`random_seed` no longer reproduces pre-0.3.0 maps**: the generator is now
127
+ per-instance rather than a call to `np.random.seed` on NumPy's global state. To reproduce figures
128
+ made with an older version, pin `python-som==0.2.0`.
129
+
130
+ Each change and the passage of Kohonen (2013) behind it is in the
131
+ [changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md).
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ uv sync --all-extras
137
+ uv run pytest --cov # tests and coverage
138
+ uv run ruff check . # lint
139
+ uv run ruff format --check . # formatting
140
+ uv run mypy # type-check
141
+ uv run mkdocs serve # docs, locally
142
+ pre-commit install # optional, run the gates on commit
143
+ ```
144
+
145
+ If you use the SonarQube for IDE (SonarLint) VS Code extension, it will also apply Sonar's Python
146
+ rules locally; the ruff configuration is set up to cover most of the same ground.
147
+
148
+ ## References
149
+
150
+ Based on:
151
+
152
+ Teuvo Kohonen,
153
+ Essentials of the self-organizing map,
154
+ Neural Networks,
155
+ Volume 37,
156
+ 2013,
157
+ Pages 52-65,
158
+ ISSN 0893-6080,
159
+ <https://doi.org/10.1016/j.neunet.2012.09.018>
160
+
161
+ The Mexican hat neighborhood follows the lateral-interaction formulation in:
162
+
163
+ O. J. Vrieze,
164
+ Kohonen network,
165
+ in: Artificial Neural Networks: An Introduction to ANN Theory and Practice,
166
+ Lecture Notes in Computer Science, Volume 931,
167
+ Springer, Berlin, Heidelberg,
168
+ 1995,
169
+ Pages 83-100,
170
+ <https://doi.org/10.1007/BFb0027024>
171
+
172
+ ## License
173
+
174
+ MIT. See [LICENSE](https://github.com/andremsouza/python-som/blob/master/LICENSE).
@@ -0,0 +1,125 @@
1
+ # python-som
2
+
3
+ [![CI](https://github.com/andremsouza/python-som/actions/workflows/ci.yml/badge.svg)](https://github.com/andremsouza/python-som/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/python-som.svg)](https://pypi.org/project/python-som/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-som.svg)](https://pypi.org/project/python-som/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/andremsouza/python-som/blob/master/LICENSE)
7
+
8
+ Implementation of Kohonen's 2-D self-organizing map, built on NumPy, with scikit-learn for PCA and
9
+ standardization. Accepts NumPy arrays, pandas DataFrames and plain lists.
10
+
11
+ [Documentation](https://andremsouza.github.io/python-som/) ·
12
+ [Changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md)
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install python-som # requires Python 3.10+
18
+ pip install "python-som[cli]" # adds tqdm progress bars
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```python
24
+ import numpy as np
25
+ import python_som
26
+
27
+ rng = np.random.default_rng(0)
28
+ data = rng.normal(size=(150, 4))
29
+
30
+ som = python_som.SOM(x=20, y=None, input_len=4, data=data, random_seed=42)
31
+ som.weight_initialization(mode="linear", data=data)
32
+ error = som.train(data, n_iteration=len(data), mode="batch")
33
+
34
+ umatrix = som.distance_matrix()
35
+ winner = som.winner(data[0])
36
+ ```
37
+
38
+ A full worked example with plots is in [examples/iris.py](https://github.com/andremsouza/python-som/blob/master/examples/iris.py) and in the
39
+ [getting-started guide](https://andremsouza.github.io/python-som/getting-started/).
40
+
41
+ ![U-matrix of a SOM trained on Iris](https://raw.githubusercontent.com/andremsouza/python-som/master/docs/assets/iris.png)
42
+
43
+ ## Features
44
+
45
+ * Stepwise and batch training
46
+ * Random, random-sampling and linear (PCA) weight initialization
47
+ * Automatic selection of the map size ratio, from PCA
48
+ * Cyclic arrays, for toroidal maps
49
+ * Gaussian, bubble and Mexican hat neighborhood functions
50
+ * Custom decay functions
51
+ * Visualization support: U-matrix, activation matrix
52
+ * Supervised labelling, via the label map
53
+ * Fully type-annotated, with a `py.typed` marker
54
+
55
+ ## Neighborhood functions
56
+
57
+ All three are functions of the distance between two nodes in the grid, `sqdist(c, i)` in Eq. (5) of
58
+ Kohonen (2013).
59
+
60
+ | Name | Shape | Notes |
61
+ | --- | --- | --- |
62
+ | `'gaussian'` | `exp(-r² / 2σ²)` | Strictly positive, monotonically decreasing. The default. |
63
+ | `'bubble'` | `1` for `max(dx, dy) ≤ σ`, else `0` | The truncated inner lobe of the Mexican hat. Uses the Chebyshev metric, so the region is a square. |
64
+ | `'mexicanhat'` | `(1 - u)·exp(-u)`, `u = r² / 2σ²` | Excitatory near the winner, inhibitory beyond it. Zero at `r = √2·σ`, minimum `-e⁻²` at `r = 2σ`. |
65
+
66
+ The Mexican hat takes negative values, so it **cannot be used with `mode='batch'`**: the batch
67
+ update of Kohonen Eq. (8) is a weighted mean whose denominator is not sign-definite for a signed
68
+ neighborhood function. Use `mode='random'` or `mode='sequential'`; `mode='batch'` raises a
69
+ `ValueError`.
70
+
71
+ See [Neighborhood functions](https://andremsouza.github.io/python-som/neighborhood-functions/) for
72
+ the derivations, including why the Mexican hat is not an outer product of two 1-D wavelets.
73
+
74
+ ## Upgrading
75
+
76
+ 0.3.0 corrects several methodology defects, so numerical results are not comparable with earlier
77
+ versions. In particular **`random_seed` no longer reproduces pre-0.3.0 maps**: the generator is now
78
+ per-instance rather than a call to `np.random.seed` on NumPy's global state. To reproduce figures
79
+ made with an older version, pin `python-som==0.2.0`.
80
+
81
+ Each change and the passage of Kohonen (2013) behind it is in the
82
+ [changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md).
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ uv sync --all-extras
88
+ uv run pytest --cov # tests and coverage
89
+ uv run ruff check . # lint
90
+ uv run ruff format --check . # formatting
91
+ uv run mypy # type-check
92
+ uv run mkdocs serve # docs, locally
93
+ pre-commit install # optional, run the gates on commit
94
+ ```
95
+
96
+ If you use the SonarQube for IDE (SonarLint) VS Code extension, it will also apply Sonar's Python
97
+ rules locally; the ruff configuration is set up to cover most of the same ground.
98
+
99
+ ## References
100
+
101
+ Based on:
102
+
103
+ Teuvo Kohonen,
104
+ Essentials of the self-organizing map,
105
+ Neural Networks,
106
+ Volume 37,
107
+ 2013,
108
+ Pages 52-65,
109
+ ISSN 0893-6080,
110
+ <https://doi.org/10.1016/j.neunet.2012.09.018>
111
+
112
+ The Mexican hat neighborhood follows the lateral-interaction formulation in:
113
+
114
+ O. J. Vrieze,
115
+ Kohonen network,
116
+ in: Artificial Neural Networks: An Introduction to ANN Theory and Practice,
117
+ Lecture Notes in Computer Science, Volume 931,
118
+ Springer, Berlin, Heidelberg,
119
+ 1995,
120
+ Pages 83-100,
121
+ <https://doi.org/10.1007/BFb0027024>
122
+
123
+ ## License
124
+
125
+ MIT. See [LICENSE](https://github.com/andremsouza/python-som/blob/master/LICENSE).
@@ -0,0 +1,26 @@
1
+ # API reference
2
+
3
+ ::: python_som
4
+ options:
5
+ members:
6
+ - SOM
7
+
8
+ ## Neighborhood functions
9
+
10
+ ::: python_som._neighborhood
11
+ options:
12
+ members:
13
+ - gaussian
14
+ - bubble
15
+ - mexican_hat
16
+ - axis_offsets
17
+ - squared_grid_distance
18
+ - resolve
19
+
20
+ ## Decay functions
21
+
22
+ ::: python_som._decay
23
+
24
+ ## Distance functions
25
+
26
+ ::: python_som._distance
Binary file
@@ -0,0 +1 @@
1
+ --8<-- "CHANGELOG.md"
@@ -0,0 +1,138 @@
1
+ # Getting started
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pip install python-som
7
+
8
+ # with tqdm progress bars during training
9
+ pip install "python-som[cli]"
10
+ ```
11
+
12
+ Requires Python 3.10 or later.
13
+
14
+ ## A complete example
15
+
16
+ This walks through the Iris dataset end to end. The runnable version is
17
+ [`examples/iris.py`](https://github.com/andremsouza/python-som/blob/master/examples/iris.py).
18
+
19
+ ### Load the data
20
+
21
+ ```python
22
+ import numpy as np
23
+ import seaborn as sns
24
+
25
+ iris = sns.load_dataset("iris")
26
+ target = iris.iloc[:, -1].to_numpy()
27
+ features = iris.iloc[:, :-1].to_numpy()
28
+ ```
29
+
30
+ ### Build the map
31
+
32
+ ```python
33
+ import python_som
34
+
35
+ som = python_som.SOM(
36
+ x=20,
37
+ y=None, # chosen from the data by PCA
38
+ input_len=features.shape[1],
39
+ learning_rate=0.5,
40
+ neighborhood_radius=1.0,
41
+ neighborhood_function="gaussian",
42
+ cyclic_x=True,
43
+ cyclic_y=True,
44
+ data=features, # required when a dimension is None
45
+ random_seed=42,
46
+ )
47
+ print(som.get_shape())
48
+ ```
49
+
50
+ Leaving one dimension as `None` asks the library to pick it. Kohonen Section 3.5 advises matching
51
+ the side lengths to *"the lengths of the two largest principal components"*; since a component is a
52
+ unit direction, its length in the data is the standard deviation, so the ratio is
53
+ $\sqrt{\lambda_1/\lambda_2}$.
54
+
55
+ ### Initialize the models
56
+
57
+ ```python
58
+ som.weight_initialization(mode="linear", data=features)
59
+ ```
60
+
61
+ Linear initialization is the one to prefer. Kohonen Section 4.3: *"much faster and convergence
62
+ follow if the initial values are selected as a regular, two-dimensional sequence of vectors taken
63
+ along a hyperplane spanned by the two largest principal components"*. It is also the only
64
+ deterministic initializer.
65
+
66
+ The alternatives are `mode="random"` (draws from a distribution) and `mode="sample"` (copies random
67
+ rows of your data).
68
+
69
+ ### Train
70
+
71
+ ```python
72
+ error = som.train(features, n_iteration=len(features), mode="batch", verbose=True)
73
+ print(f"quantization error: {error:.4f}")
74
+ ```
75
+
76
+ See [Training modes](training-modes.md) for how the three modes differ and why batch is
77
+ recommended.
78
+
79
+ ### Inspect the result
80
+
81
+ ```python
82
+ umatrix = som.distance_matrix().T # the U-matrix
83
+ activations = som.activation_matrix(features) # samples per node
84
+ labels = som.label_map(features, target) # label frequencies per node
85
+ ```
86
+
87
+ ### Plot it
88
+
89
+ ```python
90
+ import matplotlib.pyplot as plt
91
+
92
+ plt.figure(figsize=som.get_shape())
93
+ plt.pcolor(umatrix, cmap="bone_r")
94
+
95
+ markers, colors = ["o", "s", "D"], ["C0", "C1", "C2"]
96
+ codes = {name: i for i, name in enumerate(np.unique(target))}
97
+ for sample, label in zip(features, target):
98
+ w = som.winner(sample)
99
+ plt.plot(
100
+ w[0] + 0.5,
101
+ w[1] + 0.5,
102
+ markers[codes[label]],
103
+ markerfacecolor="None",
104
+ markeredgecolor=colors[codes[label]],
105
+ markersize=12,
106
+ markeredgewidth=2,
107
+ )
108
+ plt.axis((0, som.get_shape()[0], 0, som.get_shape()[1]))
109
+ plt.savefig("iris.png")
110
+ ```
111
+
112
+ ![U-matrix of a SOM trained on Iris](assets/iris.png)
113
+
114
+ Darker regions of the U-matrix mean neighbouring models are far apart, so they read as boundaries
115
+ between clusters.
116
+
117
+ ## Progress output
118
+
119
+ `verbose=True` shows a tqdm progress bar when `python-som[cli]` is installed. It also logs on the
120
+ `python_som` logger at INFO level, which is off by default the way any library logger is; enable it
121
+ with:
122
+
123
+ ```python
124
+ import logging
125
+
126
+ logging.basicConfig(level=logging.INFO)
127
+ ```
128
+
129
+ ## Reproducibility
130
+
131
+ ```python
132
+ som = python_som.SOM(x=10, y=10, input_len=4, random_seed=42)
133
+ print(som.get_random_seed())
134
+ ```
135
+
136
+ Same seed, same map. The generator belongs to the instance, so building a SOM will not disturb
137
+ NumPy's global random state. Note that seeds do not reproduce across the 0.3.0 boundary; see the
138
+ warning in [Training modes](training-modes.md#reproducibility).
@@ -0,0 +1 @@
1
+ --8<-- "README.md"