tmplot 0.2.0__tar.gz → 0.4.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.
@@ -1,3 +1,4 @@
1
1
  include LICENSE
2
2
  include README.md
3
+ include CHANGELOG.md
3
4
  recursive-exclude tests *
tmplot-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: tmplot
3
+ Version: 0.4.0
4
+ Summary: Visualization of Topic Modeling Results
5
+ Author-email: Maksim Terpilovskii <maximtrp@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/maximtrp/tmplot
8
+ Project-URL: documentation, https://tmplot.readthedocs.io/
9
+ Keywords: data science,data analytics
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Classifier: Topic :: Text Processing :: General
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.23
21
+ Requires-Dist: pandas>=1.5
22
+ Requires-Dist: scikit-learn>=1.4
23
+ Requires-Dist: scipy>=1.9
24
+ Requires-Dist: altair>=5
25
+ Requires-Dist: ipywidgets>=8
26
+ Requires-Dist: IPython>=8
27
+ Requires-Dist: tqdm>=4.64
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Requires-Dist: coverage; extra == "test"
31
+ Requires-Dist: build; extra == "test"
32
+ Requires-Dist: twine; extra == "test"
33
+ Provides-Extra: models
34
+ Requires-Dist: tomotopy>=0.8.0; extra == "models"
35
+ Requires-Dist: gensim; extra == "models"
36
+ Requires-Dist: bitermplus; extra == "models"
37
+ Dynamic: license-file
38
+
39
+ # tmplot
40
+
41
+ [![Codacy coverage](https://img.shields.io/codacy/coverage/5939b1cf99bc4f9d91de11c0d3ff9e50)](https://app.codacy.com/gh/maximtrp/tmplot/coverage)
42
+ [![Codacy grade](https://img.shields.io/codacy/grade/5939b1cf99bc4f9d91de11c0d3ff9e50)](https://app.codacy.com/gh/maximtrp/tmplot)
43
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/maximtrp/tmplot/python-package.yml?label=tests)](https://github.com/maximtrp/tmplot/actions/workflows/python-package.yml)
44
+ [![Documentation Status](https://readthedocs.org/projects/tmplot/badge/?version=latest)](https://tmplot.readthedocs.io/en/latest/?badge=latest)
45
+ [![Downloads](https://static.pepy.tech/badge/tmplot)](https://pepy.tech/project/tmplot)
46
+ [![PyPI](https://img.shields.io/pypi/v/tmplot)](https://pypi.org/project/tmplot)
47
+ [![Issues](https://img.shields.io/github/issues/maximtrp/tmplot.svg)](https://github.com/maximtrp/tmplot/issues)
48
+
49
+ **tmplot** is a comprehensive Python package for **topic modeling analysis and visualization**. Built for data scientists and researchers, it provides powerful interactive reports and advanced analytics that extend beyond traditional LDAvis/pyLDAvis capabilities.
50
+
51
+ **Analyze** • **Visualize** • **Compare** multiple topic models with ease
52
+
53
+ ![Plots](https://raw.githubusercontent.com/maximtrp/tmplot/main/images/topics_terms_plots.png)
54
+
55
+ ## Key Features
56
+
57
+ ### Interactive Visualization
58
+
59
+ - **Topic scatter plots** with customizable coordinates and sizing
60
+ - **Term probability charts** with relevance weighting
61
+ - **Document analysis** showing top documents per topic
62
+ - **Interactive reports** with real-time parameter adjustment
63
+
64
+ ### Advanced Analytics
65
+
66
+ - **Topic stability analysis** across multiple model runs
67
+ - **Model comparison** with sophisticated distance metrics
68
+ - **Saliency calculations** for term importance
69
+ - **Entropy metrics** for model optimization
70
+
71
+ ### Model Support
72
+
73
+ - **[tomotopy](https://bab2min.github.io/tomotopy/)**: `LDAModel`, `LLDAModel`, `CTModel`, `DMRModel`, `HDPModel`, `PTModel`, `SLDAModel`, `GDMRModel`
74
+ - **[gensim](https://radimrehurek.com/gensim/)**: `LdaModel`, `LdaMulticore`
75
+ - **[bitermplus](https://github.com/maximtrp/bitermplus)**: `BTM`
76
+
77
+ ### Distance Metrics
78
+
79
+ - Kullback-Leibler (symmetric & non-symmetric)
80
+ - Jensen-Shannon divergence
81
+ - Jeffrey's divergence
82
+ - Hellinger & Bhattacharyya distances
83
+ - Total variation distance
84
+ - Jaccard index
85
+
86
+ ### Dimensionality Reduction
87
+
88
+ t-SNE, SpectralEmbedding, MDS, LocallyLinearEmbedding, Isomap
89
+
90
+ ## Donate
91
+
92
+ If you find this package useful, please consider donating any amount of money. This will help me spend more time on supporting open-source software.
93
+
94
+ <a href="https://www.buymeacoffee.com/maximtrp" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
95
+
96
+ ## Quick Start
97
+
98
+ ### Installation
99
+
100
+ ```bash
101
+ # From PyPI (recommended)
102
+ pip install tmplot
103
+
104
+ # Development version
105
+ pip install git+https://github.com/maximtrp/tmplot.git
106
+ ```
107
+
108
+ ### Basic Usage
109
+
110
+ ```python
111
+ import tmplot as tmp
112
+
113
+ # Load your topic model and documents
114
+ model = your_fitted_model # tomotopy, gensim, or bitermplus
115
+ docs = your_documents
116
+
117
+ # Create interactive report
118
+ tmp.report(model, docs=docs)
119
+
120
+ # Or create individual visualizations
121
+ coords = tmp.prepare_coords(model)
122
+ tmp.plot_scatter_topics(coords, size_col='size')
123
+ ```
124
+
125
+ ## Advanced Examples
126
+
127
+ ### Get Stable Topics
128
+
129
+ ```python
130
+ import tmplot as tmp
131
+
132
+ # Find stable topics across multiple models
133
+ models = [model1, model2, model3, model4]
134
+ closest_topics, distances = tmp.get_closest_topics(models)
135
+ stable_topics, stable_distances = tmp.get_stable_topics(closest_topics, distances)
136
+ ```
137
+
138
+ ### Analyze Model
139
+
140
+ ```python
141
+ # Calculate entropy for model selection
142
+ entropy_score = tmp.entropy(phi_matrix)
143
+
144
+ # Analyze topic stability
145
+ saliency = tmp.get_salient_terms(phi, theta)
146
+ ```
147
+
148
+ ### Visualize
149
+
150
+ ```python
151
+ # Create topic distance matrix with different metrics
152
+ topic_dists = tmp.get_topics_dist(phi, method='jsd')
153
+
154
+ # Generate coordinates with custom algorithm
155
+ coords = tmp.get_topics_scatter(topic_dists, theta, method='tsne')
156
+ tmp.plot_scatter_topics(coords, topic=3) # Highlight topic 3
157
+ ```
158
+
159
+ ## Documentation & Examples
160
+
161
+ - [Complete Tutorial](https://tmplot.readthedocs.io/en/latest/tutorial.html) - Step-by-step guide
162
+ - [API Reference](https://tmplot.readthedocs.io/) - Full documentation
163
+ - [Tutorial Notebook](docs/source/tutorial.ipynb) - Jupyter walkthrough
164
+
165
+ ## Requirements
166
+
167
+ **Core dependencies:** `numpy`, `scipy`, `scikit-learn`, `pandas`, `altair`, `ipywidgets`
168
+
169
+ **Optional models:** `tomotopy`, `gensim`, `bitermplus`
tmplot-0.4.0/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # tmplot
2
+
3
+ [![Codacy coverage](https://img.shields.io/codacy/coverage/5939b1cf99bc4f9d91de11c0d3ff9e50)](https://app.codacy.com/gh/maximtrp/tmplot/coverage)
4
+ [![Codacy grade](https://img.shields.io/codacy/grade/5939b1cf99bc4f9d91de11c0d3ff9e50)](https://app.codacy.com/gh/maximtrp/tmplot)
5
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/maximtrp/tmplot/python-package.yml?label=tests)](https://github.com/maximtrp/tmplot/actions/workflows/python-package.yml)
6
+ [![Documentation Status](https://readthedocs.org/projects/tmplot/badge/?version=latest)](https://tmplot.readthedocs.io/en/latest/?badge=latest)
7
+ [![Downloads](https://static.pepy.tech/badge/tmplot)](https://pepy.tech/project/tmplot)
8
+ [![PyPI](https://img.shields.io/pypi/v/tmplot)](https://pypi.org/project/tmplot)
9
+ [![Issues](https://img.shields.io/github/issues/maximtrp/tmplot.svg)](https://github.com/maximtrp/tmplot/issues)
10
+
11
+ **tmplot** is a comprehensive Python package for **topic modeling analysis and visualization**. Built for data scientists and researchers, it provides powerful interactive reports and advanced analytics that extend beyond traditional LDAvis/pyLDAvis capabilities.
12
+
13
+ **Analyze** • **Visualize** • **Compare** multiple topic models with ease
14
+
15
+ ![Plots](https://raw.githubusercontent.com/maximtrp/tmplot/main/images/topics_terms_plots.png)
16
+
17
+ ## Key Features
18
+
19
+ ### Interactive Visualization
20
+
21
+ - **Topic scatter plots** with customizable coordinates and sizing
22
+ - **Term probability charts** with relevance weighting
23
+ - **Document analysis** showing top documents per topic
24
+ - **Interactive reports** with real-time parameter adjustment
25
+
26
+ ### Advanced Analytics
27
+
28
+ - **Topic stability analysis** across multiple model runs
29
+ - **Model comparison** with sophisticated distance metrics
30
+ - **Saliency calculations** for term importance
31
+ - **Entropy metrics** for model optimization
32
+
33
+ ### Model Support
34
+
35
+ - **[tomotopy](https://bab2min.github.io/tomotopy/)**: `LDAModel`, `LLDAModel`, `CTModel`, `DMRModel`, `HDPModel`, `PTModel`, `SLDAModel`, `GDMRModel`
36
+ - **[gensim](https://radimrehurek.com/gensim/)**: `LdaModel`, `LdaMulticore`
37
+ - **[bitermplus](https://github.com/maximtrp/bitermplus)**: `BTM`
38
+
39
+ ### Distance Metrics
40
+
41
+ - Kullback-Leibler (symmetric & non-symmetric)
42
+ - Jensen-Shannon divergence
43
+ - Jeffrey's divergence
44
+ - Hellinger & Bhattacharyya distances
45
+ - Total variation distance
46
+ - Jaccard index
47
+
48
+ ### Dimensionality Reduction
49
+
50
+ t-SNE, SpectralEmbedding, MDS, LocallyLinearEmbedding, Isomap
51
+
52
+ ## Donate
53
+
54
+ If you find this package useful, please consider donating any amount of money. This will help me spend more time on supporting open-source software.
55
+
56
+ <a href="https://www.buymeacoffee.com/maximtrp" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
57
+
58
+ ## Quick Start
59
+
60
+ ### Installation
61
+
62
+ ```bash
63
+ # From PyPI (recommended)
64
+ pip install tmplot
65
+
66
+ # Development version
67
+ pip install git+https://github.com/maximtrp/tmplot.git
68
+ ```
69
+
70
+ ### Basic Usage
71
+
72
+ ```python
73
+ import tmplot as tmp
74
+
75
+ # Load your topic model and documents
76
+ model = your_fitted_model # tomotopy, gensim, or bitermplus
77
+ docs = your_documents
78
+
79
+ # Create interactive report
80
+ tmp.report(model, docs=docs)
81
+
82
+ # Or create individual visualizations
83
+ coords = tmp.prepare_coords(model)
84
+ tmp.plot_scatter_topics(coords, size_col='size')
85
+ ```
86
+
87
+ ## Advanced Examples
88
+
89
+ ### Get Stable Topics
90
+
91
+ ```python
92
+ import tmplot as tmp
93
+
94
+ # Find stable topics across multiple models
95
+ models = [model1, model2, model3, model4]
96
+ closest_topics, distances = tmp.get_closest_topics(models)
97
+ stable_topics, stable_distances = tmp.get_stable_topics(closest_topics, distances)
98
+ ```
99
+
100
+ ### Analyze Model
101
+
102
+ ```python
103
+ # Calculate entropy for model selection
104
+ entropy_score = tmp.entropy(phi_matrix)
105
+
106
+ # Analyze topic stability
107
+ saliency = tmp.get_salient_terms(phi, theta)
108
+ ```
109
+
110
+ ### Visualize
111
+
112
+ ```python
113
+ # Create topic distance matrix with different metrics
114
+ topic_dists = tmp.get_topics_dist(phi, method='jsd')
115
+
116
+ # Generate coordinates with custom algorithm
117
+ coords = tmp.get_topics_scatter(topic_dists, theta, method='tsne')
118
+ tmp.plot_scatter_topics(coords, topic=3) # Highlight topic 3
119
+ ```
120
+
121
+ ## Documentation & Examples
122
+
123
+ - [Complete Tutorial](https://tmplot.readthedocs.io/en/latest/tutorial.html) - Step-by-step guide
124
+ - [API Reference](https://tmplot.readthedocs.io/) - Full documentation
125
+ - [Tutorial Notebook](docs/source/tutorial.ipynb) - Jupyter walkthrough
126
+
127
+ ## Requirements
128
+
129
+ **Core dependencies:** `numpy`, `scipy`, `scikit-learn`, `pandas`, `altair`, `ipywidgets`
130
+
131
+ **Optional models:** `tomotopy`, `gensim`, `bitermplus`
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -7,8 +7,9 @@ name = "tmplot"
7
7
  dynamic = ["version"]
8
8
  description = "Visualization of Topic Modeling Results"
9
9
  readme = "README.md"
10
- requires-python = ">=3.8"
11
- license.file = "LICENSE"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
12
13
  authors = [
13
14
  { name = "Maksim Terpilovskii", email = "maximtrp@gmail.com" },
14
15
  ]
@@ -17,9 +18,7 @@ keywords = [
17
18
  "data analytics"
18
19
  ]
19
20
  classifiers = [
20
- "License :: OSI Approved :: MIT License",
21
21
  "Operating System :: OS Independent",
22
- "Programming Language :: Python :: 3.8",
23
22
  "Programming Language :: Python :: 3.9",
24
23
  "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
@@ -31,18 +30,18 @@ urls.homepage = "https://github.com/maximtrp/tmplot"
31
30
  urls.documentation = "https://tmplot.readthedocs.io/"
32
31
 
33
32
  dependencies = [
34
- "numpy",
35
- "seaborn",
36
- "pandas",
37
- "scikit-learn",
38
- "scipy",
39
- "altair",
40
- "ipywidgets",
41
- "tqdm",
33
+ "numpy>=1.23",
34
+ "pandas>=1.5",
35
+ "scikit-learn>=1.4",
36
+ "scipy>=1.9",
37
+ "altair>=5",
38
+ "ipywidgets>=8",
39
+ "IPython>=8",
40
+ "tqdm>=4.64",
42
41
  ]
43
42
 
44
43
  [project.optional-dependencies]
45
- test = ["pytest", "coverage"]
44
+ test = ["pytest", "coverage", "build", "twine"]
46
45
  models = ["tomotopy>=0.8.0", "gensim", "bitermplus"]
47
46
 
48
47
  [tool.setuptools.dynamic]
@@ -5,4 +5,4 @@ from ._stability import * # noqa: F401, F403
5
5
  from ._vis import * # noqa: F401, F403
6
6
  from ._metrics import * # noqa: F401, F403
7
7
 
8
- __version__ = '0.2.0'
8
+ __version__ = "0.4.0"
@@ -1,5 +1,6 @@
1
1
  __all__ = ["get_topics_dist", "get_topics_scatter", "get_top_topic_words"]
2
2
  from typing import Optional, Union, List
3
+ from inspect import signature
3
4
  from itertools import combinations
4
5
  from pandas import DataFrame, Index
5
6
  import numpy as np
@@ -15,37 +16,48 @@ from sklearn.manifold import (
15
16
  from ._helpers import calc_topics_marg_probs
16
17
 
17
18
 
19
+ EPSILON = 1e-64
20
+
21
+
22
+ def _positive_probabilities(values: np.ndarray) -> np.ndarray:
23
+ values = np.clip(np.asarray(values, dtype=float), EPSILON, None)
24
+ return values / values.sum()
25
+
26
+
18
27
  def _dist_klb(a1: np.ndarray, a2: np.ndarray):
19
- dist = kl_div(a1, a2)
20
- return dist[np.isfinite(dist)].sum()
28
+ return kl_div(_positive_probabilities(a1), _positive_probabilities(a2)).sum()
21
29
 
22
30
 
23
31
  def _dist_sklb(a1: np.ndarray, a2: np.ndarray):
24
- dist = kl_div(a1, a2) + kl_div(a1, a2)
25
- return dist[np.isfinite(dist)].sum()
32
+ a1_safe = _positive_probabilities(a1)
33
+ a2_safe = _positive_probabilities(a2)
34
+ return (kl_div(a1_safe, a2_safe) + kl_div(a2_safe, a1_safe)).sum()
26
35
 
27
36
 
28
37
  def _dist_jsd(a1: np.ndarray, a2: np.ndarray):
29
- dist = 0.5 * kl_div(a1, a2) + 0.5 * kl_div(a1, a2)
38
+ m = 0.5 * (a1 + a2)
39
+ dist = 0.5 * kl_div(a1, m) + 0.5 * kl_div(a2, m)
30
40
  return dist[np.isfinite(dist)].sum()
31
41
 
32
42
 
33
43
  def _dist_jef(a1: np.ndarray, a2: np.ndarray):
34
- vals = (a1 - a2) * (np.log(a1) - np.log(a2))
35
- vals[(vals <= 0) | ~np.isfinite(vals)] = 0.0
36
- return vals.sum()
44
+ a1_safe = _positive_probabilities(a1)
45
+ a2_safe = _positive_probabilities(a2)
46
+ return ((a1_safe - a2_safe) * (np.log(a1_safe) - np.log(a2_safe))).sum()
37
47
 
38
48
 
39
49
  def _dist_hel(a1: np.ndarray, a2: np.ndarray):
40
- a1[(a1 <= 0) | ~np.isfinite(a1)] = 1e-64
41
- a2[(a2 <= 0) | ~np.isfinite(a2)] = 1e-64
42
- hel_val = distance.euclidean(np.sqrt(a1), np.sqrt(a2)) / np.sqrt(2)
50
+ a1_safe = a1.copy()
51
+ a2_safe = a2.copy()
52
+ a1_safe[(a1_safe <= 0) | ~np.isfinite(a1_safe)] = EPSILON
53
+ a2_safe[(a2_safe <= 0) | ~np.isfinite(a2_safe)] = EPSILON
54
+ hel_val = distance.euclidean(np.sqrt(a1_safe), np.sqrt(a2_safe)) / np.sqrt(2)
43
55
  return hel_val
44
56
 
45
57
 
46
58
  def _dist_bhat(a1: np.ndarray, a2: np.ndarray):
47
59
  pq = a1 * a2
48
- pq[(pq <= 0) | ~np.isfinite(pq)] = 1e-64
60
+ pq[(pq <= 0) | ~np.isfinite(pq)] = EPSILON
49
61
  dist = -np.log(np.sum(np.sqrt(pq)))
50
62
  return dist
51
63
 
@@ -92,6 +104,14 @@ def get_topics_dist(
92
104
  Topics distances matrix.
93
105
  """
94
106
  phi_copy = np.array(phi)
107
+
108
+ if phi_copy.ndim != 2:
109
+ raise ValueError("phi must be a 2D array (words x topics)")
110
+ if np.any(phi_copy < 0):
111
+ raise ValueError("phi must contain non-negative probability values")
112
+ if not np.allclose(phi_copy.sum(axis=0), 1.0, atol=1e-6):
113
+ raise ValueError("phi columns must sum to 1 (probability distributions)")
114
+
95
115
  topics_num = phi_copy.shape[1]
96
116
  topics_pairs = combinations(range(topics_num), 2)
97
117
 
@@ -109,8 +129,12 @@ def get_topics_dist(
109
129
  "jac": _dist_jac,
110
130
  }
111
131
 
132
+ if method not in dist_funcs:
133
+ raise ValueError(
134
+ f"Unknown distance method {method!r}; choose from {sorted(dist_funcs)}"
135
+ )
136
+ _dist_func = dist_funcs[method]
112
137
  for i, j in topics_pairs:
113
- _dist_func = dist_funcs.get(method, "sklb")
114
138
  topics_dists[((i, j), (j, i))] = _dist_func(
115
139
  phi_copy[:, i], phi_copy[:, j], **kwargs
116
140
  )
@@ -118,6 +142,17 @@ def get_topics_dist(
118
142
  return topics_dists
119
143
 
120
144
 
145
+ def _classical_mds(distances: np.ndarray) -> np.ndarray:
146
+ count = distances.shape[0]
147
+ centering = np.eye(count) - np.ones((count, count)) / count
148
+ gram = -0.5 * centering @ (distances**2) @ centering
149
+ eigenvalues, eigenvectors = np.linalg.eigh(gram)
150
+ positive = eigenvalues > np.finfo(float).eps
151
+ if not positive.any():
152
+ return np.zeros((count, 1))
153
+ return eigenvectors[:, positive] * np.sqrt(eigenvalues[positive])
154
+
155
+
121
156
  def get_topics_scatter(
122
157
  topic_dists: np.ndarray,
123
158
  theta: np.ndarray,
@@ -149,11 +184,39 @@ def get_topics_scatter(
149
184
  DataFrame
150
185
  Topics scatter coordinates.
151
186
  """
152
- if not method_kws:
153
- method_kws = {"n_components": 2}
187
+ topic_dists = np.asarray(topic_dists, dtype=float)
188
+ theta = np.asarray(theta, dtype=float)
189
+ if topic_dists.ndim != 2 or topic_dists.shape[0] != topic_dists.shape[1]:
190
+ raise ValueError("topic_dists must be a square 2D matrix")
191
+ if not np.isfinite(topic_dists).all():
192
+ raise ValueError("topic_dists must contain only finite values")
193
+ if not np.allclose(topic_dists, topic_dists.T):
194
+ raise ValueError("topic_dists must be symmetric")
195
+ if theta.ndim != 2 or theta.shape[0] != topic_dists.shape[0]:
196
+ raise ValueError("theta topics dimension must match topic_dists")
197
+ if topic_dists.shape[0] < 2:
198
+ raise ValueError("at least two topics are required for scatter coordinates")
199
+
200
+ valid_methods = ["tsne", "sem", "mds", "lle", "ltsa", "isomap"]
201
+ if method not in valid_methods:
202
+ raise ValueError(
203
+ f"Unknown scatter method {method!r}; choose from {valid_methods}"
204
+ )
205
+
206
+ if topic_dists.shape[0] == 2:
207
+ half_distance = topic_dists[0, 1] / 2
208
+ coords = np.array([[-half_distance, 0.0], [half_distance, 0.0]])
209
+ topics_xy = DataFrame(coords, columns=Index(["x", "y"]))
210
+ topics_xy["topic"] = topics_xy.index.astype(int)
211
+ topics_xy["size"] = calc_topics_marg_probs(theta) * 100
212
+ return topics_xy
213
+
214
+ method_kws = dict(method_kws or {})
215
+ method_kws.setdefault("n_components", 2)
154
216
 
155
217
  if method == "tsne":
156
- method_kws.setdefault("init", "pca")
218
+ method_kws.setdefault("metric", "precomputed")
219
+ method_kws.setdefault("init", "random")
157
220
  method_kws.setdefault("learning_rate", "auto")
158
221
  method_kws.setdefault("perplexity", min(50, max(topic_dists.shape[0] // 2, 1)))
159
222
  transformer = TSNE(**method_kws)
@@ -161,24 +224,46 @@ def get_topics_scatter(
161
224
  elif method == "sem":
162
225
  method_kws.setdefault("affinity", "precomputed")
163
226
  transformer = SpectralEmbedding(**method_kws)
227
+ nonzero = topic_dists[topic_dists > 0]
228
+ scale = np.median(nonzero) if nonzero.size else 1.0
229
+ transform_input = np.exp(-((topic_dists / scale) ** 2))
230
+ np.fill_diagonal(transform_input, 1.0)
164
231
 
165
232
  elif method == "mds":
166
- method_kws.setdefault("dissimilarity", "precomputed")
233
+ mds_params = signature(MDS.__init__).parameters
234
+ if "metric_mds" in mds_params:
235
+ # scikit-learn >= 1.9 deprecated `dissimilarity` in favor of `metric`
236
+ method_kws.setdefault("metric", "precomputed")
237
+ else:
238
+ method_kws.setdefault("dissimilarity", "precomputed")
167
239
  method_kws.setdefault("normalized_stress", "auto")
240
+ method_kws.setdefault("n_init", 1)
241
+ if "init" in mds_params:
242
+ # `init` is a constructor argument only since scikit-learn 1.9
243
+ method_kws.setdefault("init", "random")
168
244
  transformer = MDS(**method_kws)
169
245
 
170
246
  elif method == "lle":
171
247
  method_kws["method"] = "standard"
248
+ method_kws.setdefault("n_neighbors", min(5, topic_dists.shape[0] - 1))
172
249
  transformer = LocallyLinearEmbedding(**method_kws)
250
+ transform_input = _classical_mds(topic_dists)
173
251
 
174
252
  elif method == "ltsa":
175
253
  method_kws["method"] = "ltsa"
254
+ method_kws.setdefault("n_neighbors", min(5, topic_dists.shape[0] - 1))
176
255
  transformer = LocallyLinearEmbedding(**method_kws)
256
+ transform_input = _classical_mds(topic_dists)
177
257
 
178
258
  elif method == "isomap":
259
+ method_kws.setdefault("metric", "precomputed")
260
+ method_kws.setdefault("n_neighbors", min(5, topic_dists.shape[0] - 1))
179
261
  transformer = Isomap(**method_kws)
180
262
 
181
- coords = transformer.fit_transform(topic_dists)
263
+ else:
264
+ raise AssertionError("validated scatter method was not handled")
265
+
266
+ coords = transformer.fit_transform(locals().get("transform_input", topic_dists))
182
267
 
183
268
  topics_xy = DataFrame(coords, columns=Index(["x", "y"]))
184
269
  topics_xy["topic"] = topics_xy.index.astype(int)
@@ -213,6 +298,7 @@ def get_top_topic_words(
213
298
  DataFrame
214
299
  Words with highest probabilities in all (or selected) topics.
215
300
  """
216
- return phi.loc[:, topics_idx or phi.columns].apply(
301
+ selected_topics = phi.columns if topics_idx is None else topics_idx
302
+ return phi.loc[:, selected_topics].apply(
217
303
  lambda x: x.sort_values(ascending=False).head(words_num).index, axis=0
218
304
  )